Skip to content

Commit 2d3d650

Browse files
authored
feat(leiosnotify): report response delivery result (#1879)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 84405b1 commit 2d3d650

9 files changed

Lines changed: 516 additions & 10 deletions

File tree

muxer/muxer.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ func (m *Muxer) RegisterProtocol(
223223
if !ok {
224224
return
225225
}
226-
if err := m.Send(msg); err != nil {
226+
err := m.Send(msg)
227+
msg.reportDelivery(err)
228+
if err != nil {
227229
m.sendError(err)
228230
return
229231
}

muxer/muxer_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"time"
2828

2929
"github.com/blinklabs-io/gouroboros/muxer"
30+
"github.com/stretchr/testify/require"
3031
"go.uber.org/goleak"
3132
)
3233

@@ -38,6 +39,14 @@ type mockConn struct {
3839
mu sync.Mutex
3940
}
4041

42+
type failingWriteConn struct {
43+
*mockConn
44+
}
45+
46+
func (*failingWriteConn) Write([]byte) (int, error) {
47+
return 0, errors.New("test: forced write failure")
48+
}
49+
4150
func newMockConn() *mockConn {
4251
return &mockConn{
4352
readBuf: &bytes.Buffer{},
@@ -408,6 +417,54 @@ func TestMuxerSendReceive(t *testing.T) {
408417
}
409418
}
410419

420+
func TestMuxerReportsSegmentDelivery(t *testing.T) {
421+
defer goleak.VerifyNone(t)
422+
423+
tests := []struct {
424+
name string
425+
conn net.Conn
426+
wantError bool
427+
}{
428+
{
429+
name: "success",
430+
conn: newMockConn(),
431+
},
432+
{
433+
name: "write failure",
434+
conn: &failingWriteConn{mockConn: newMockConn()},
435+
wantError: true,
436+
},
437+
}
438+
for _, test := range tests {
439+
t.Run(test.name, func(t *testing.T) {
440+
m := muxer.New(test.conn)
441+
defer m.Stop()
442+
sendChan, _, _ := m.RegisterProtocol(
443+
0x01,
444+
muxer.ProtocolRoleInitiator,
445+
)
446+
m.Start()
447+
448+
deliveryChan := make(chan error, 1)
449+
segment := muxer.NewSegment(0x01, []byte("test"), false)
450+
require.NotNil(t, segment)
451+
segment.SetDeliveryChan(deliveryChan)
452+
sendChan <- segment
453+
454+
select {
455+
case err := <-deliveryChan:
456+
if test.wantError {
457+
require.Error(t, err)
458+
} else {
459+
require.NoError(t, err)
460+
}
461+
case <-time.After(time.Second):
462+
t.Fatal("timed out waiting for segment delivery result")
463+
}
464+
})
465+
}
466+
}
467+
411468
// TestDiffusionModes tests different diffusion modes
412469
func TestDiffusionModes(t *testing.T) {
413470
defer goleak.VerifyNone(t)

muxer/segment.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,25 @@ type SegmentHeader struct {
4040
// the actual payload
4141
type Segment struct {
4242
SegmentHeader
43-
Payload []byte
43+
Payload []byte
44+
deliveryChan chan<- error
45+
}
46+
47+
// SetDeliveryChan registers a channel that receives the result of writing the
48+
// segment to the underlying connection. The channel must be buffered so muxer
49+
// shutdown cannot block on an abandoned receiver.
50+
func (s *Segment) SetDeliveryChan(deliveryChan chan<- error) {
51+
s.deliveryChan = deliveryChan
52+
}
53+
54+
func (s *Segment) reportDelivery(err error) {
55+
if s.deliveryChan == nil {
56+
return
57+
}
58+
select {
59+
case s.deliveryChan <- err:
60+
default:
61+
}
4462
}
4563

4664
// NewSegment returns a new Segment given a protocol ID, payload bytes, and whether the segment

protocol/leiosnotify/client_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,21 +204,32 @@ func TestConfig(t *testing.T) {
204204

205205
// Test config with options
206206
requestNextCalled := false
207+
responseSentCalled := false
207208

208209
cfg = NewConfig(
209210
WithTimeout(120*time.Second),
210211
WithRequestNextFunc(func(ctx CallbackContext) (protocol.Message, error) {
211212
requestNextCalled = true
212213
return nil, nil
213214
}),
215+
WithResponseSentFunc(func(
216+
CallbackContext,
217+
protocol.Message,
218+
error,
219+
) {
220+
responseSentCalled = true
221+
}),
214222
)
215223

216224
assert.Equal(t, 120*time.Second, cfg.Timeout)
217225
assert.NotNil(t, cfg.RequestNextFunc)
226+
assert.NotNil(t, cfg.ResponseSentFunc)
218227

219228
// Test that callback can be invoked
220229
_, _ = cfg.RequestNextFunc(CallbackContext{})
221230
assert.True(t, requestNextCalled)
231+
cfg.ResponseSentFunc(CallbackContext{}, nil, nil)
232+
assert.True(t, responseSentCalled)
222233
}
223234

224235
func TestProtocolConstants(t *testing.T) {

protocol/leiosnotify/leiosnotify.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type LeiosNotify struct {
8181
type Config struct {
8282
NotificationFunc NotificationFunc
8383
PipelineLimit int
84+
ResponseSentFunc ResponseSentFunc
8485
RequestNextFunc RequestNextFunc
8586
Timeout time.Duration
8687
}
@@ -100,6 +101,7 @@ type CallbackContext struct {
100101
// Callback function types
101102
type (
102103
RequestNextFunc func(CallbackContext) (protocol.Message, error)
104+
ResponseSentFunc func(CallbackContext, protocol.Message, error)
103105
NotificationFunc func(CallbackContext, protocol.Message) error
104106
)
105107

@@ -189,6 +191,18 @@ func WithRequestNextFunc(
189191
}
190192
}
191193

194+
// WithResponseSentFunc registers a callback invoked after the server attempts
195+
// to send a response returned by RequestNextFunc. The callback receives the
196+
// send result so notification sources can commit or release delivery
197+
// reservations without advancing them before transport delivery.
198+
func WithResponseSentFunc(
199+
responseSentFunc ResponseSentFunc,
200+
) LeiosNotifyOptionFunc {
201+
return func(c *Config) {
202+
c.ResponseSentFunc = responseSentFunc
203+
}
204+
}
205+
192206
func WithTimeout(timeout time.Duration) LeiosNotifyOptionFunc {
193207
return func(c *Config) {
194208
c.Timeout = timeout

protocol/leiosnotify/server.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,11 @@ func (s *Server) handleRequestNext() error {
119119
"received leios-notify NotificationRequestNext message but callback returned nil",
120120
)
121121
}
122-
if err := s.SendMessage(resp); err != nil {
123-
return err
122+
sendErr := s.SendMessageAndWait(resp)
123+
if s.config.ResponseSentFunc != nil {
124+
s.config.ResponseSentFunc(s.callbackContext, resp, sendErr)
124125
}
125-
return nil
126+
return sendErr
126127
}
127128

128129
func (s *Server) handleDone() {

protocol/leiosnotify/server_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ func writeLeiosNotifyTestSegment(
6666
require.NoError(t, err)
6767
}
6868

69+
type leiosNotifyFailWriteConn struct {
70+
net.Conn
71+
}
72+
73+
var errLeiosNotifyTestWrite = errors.New(
74+
"test: forced transport write failure",
75+
)
76+
77+
func (leiosNotifyFailWriteConn) Write([]byte) (int, error) {
78+
return 0, errLeiosNotifyTestWrite
79+
}
80+
6981
func TestNewServer(t *testing.T) {
7082
connId := connection.ConnectionId{
7183
LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
@@ -112,6 +124,169 @@ func TestHandleRequestNext_CallbackIsCalled(t *testing.T) {
112124
assert.True(t, called, "expected RequestNextFunc to be called")
113125
}
114126

127+
func TestHandleRequestNextReportsSuccessfulSend(t *testing.T) {
128+
connId := connection.ConnectionId{
129+
LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
130+
RemoteAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
131+
}
132+
connA, connB := net.Pipe()
133+
defer connA.Close()
134+
defer connB.Close()
135+
m := muxer.New(connA)
136+
defer m.Stop()
137+
138+
response := NewMsgBlockAnnouncement(cbor.RawMessage{0x82, 0x01, 0x02})
139+
type sendResult struct {
140+
msg protocol.Message
141+
err error
142+
}
143+
resultCh := make(chan sendResult, 1)
144+
cfg := NewConfig(
145+
WithRequestNextFunc(func(CallbackContext) (protocol.Message, error) {
146+
return response, nil
147+
}),
148+
WithResponseSentFunc(func(
149+
_ CallbackContext,
150+
msg protocol.Message,
151+
err error,
152+
) {
153+
resultCh <- sendResult{msg: msg, err: err}
154+
}),
155+
)
156+
server := NewServer(protocol.ProtocolOptions{
157+
ConnectionId: connId,
158+
Muxer: m,
159+
}, &cfg)
160+
server.Start()
161+
defer server.Protocol.Stop()
162+
m.Start()
163+
164+
requestData, err := cbor.Encode(NewMsgNotificationRequestNext())
165+
require.NoError(t, err)
166+
writeLeiosNotifyTestSegment(
167+
t,
168+
connB,
169+
muxer.NewSegment(ProtocolId, requestData, false),
170+
)
171+
require.NoError(t, connB.SetReadDeadline(time.Now().Add(time.Second)))
172+
_, err = readLeiosNotifyTestSegment(t, connB)
173+
require.NoError(t, err)
174+
175+
select {
176+
case result := <-resultCh:
177+
require.Same(t, response, result.msg)
178+
require.NoError(t, result.err)
179+
case <-time.After(time.Second):
180+
t.Fatal("timed out waiting for response send callback")
181+
}
182+
}
183+
184+
func TestHandleRequestNextReportsFailedSend(t *testing.T) {
185+
connId := connection.ConnectionId{
186+
LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
187+
RemoteAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
188+
}
189+
resultCh := make(chan error, 1)
190+
connA, connB := net.Pipe()
191+
defer connA.Close()
192+
defer connB.Close()
193+
m := muxer.New(leiosNotifyFailWriteConn{Conn: connA})
194+
defer m.Stop()
195+
cfg := NewConfig(
196+
WithRequestNextFunc(func(CallbackContext) (protocol.Message, error) {
197+
return NewMsgBlockAnnouncement(cbor.RawMessage{0x81, 0x01}), nil
198+
}),
199+
WithResponseSentFunc(func(
200+
_ CallbackContext,
201+
_ protocol.Message,
202+
err error,
203+
) {
204+
resultCh <- err
205+
}),
206+
)
207+
server := NewServer(protocol.ProtocolOptions{
208+
ConnectionId: connId,
209+
Muxer: m,
210+
}, &cfg)
211+
server.Start()
212+
defer server.Protocol.Stop()
213+
m.Start()
214+
215+
requestData, err := cbor.Encode(NewMsgNotificationRequestNext())
216+
require.NoError(t, err)
217+
writeLeiosNotifyTestSegment(
218+
t,
219+
connB,
220+
muxer.NewSegment(ProtocolId, requestData, false),
221+
)
222+
223+
select {
224+
case sendErr := <-resultCh:
225+
require.ErrorIs(t, sendErr, errLeiosNotifyTestWrite)
226+
case <-time.After(time.Second):
227+
t.Fatal("timed out waiting for failed response send callback")
228+
}
229+
}
230+
231+
func TestHandleRequestNextReportsShutdownBeforeDelivery(t *testing.T) {
232+
connId := connection.ConnectionId{
233+
LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
234+
RemoteAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
235+
}
236+
connA, connB := net.Pipe()
237+
defer connA.Close()
238+
defer connB.Close()
239+
m := muxer.New(connA)
240+
defer m.Stop()
241+
242+
requestCalled := make(chan struct{})
243+
releaseRequest := make(chan struct{})
244+
resultCh := make(chan error, 1)
245+
cfg := NewConfig(
246+
WithRequestNextFunc(func(CallbackContext) (protocol.Message, error) {
247+
close(requestCalled)
248+
// Keep the response from being queued until shutdown is observable.
249+
<-releaseRequest
250+
return NewMsgBlockAnnouncement(cbor.RawMessage{0x81, 0x01}), nil
251+
}),
252+
WithResponseSentFunc(func(
253+
_ CallbackContext,
254+
_ protocol.Message,
255+
err error,
256+
) {
257+
resultCh <- err
258+
}),
259+
)
260+
server := NewServer(protocol.ProtocolOptions{
261+
ConnectionId: connId,
262+
Muxer: m,
263+
}, &cfg)
264+
server.Start()
265+
m.Start()
266+
267+
requestData, err := cbor.Encode(NewMsgNotificationRequestNext())
268+
require.NoError(t, err)
269+
writeLeiosNotifyTestSegment(
270+
t,
271+
connB,
272+
muxer.NewSegment(ProtocolId, requestData, false),
273+
)
274+
select {
275+
case <-requestCalled:
276+
case <-time.After(time.Second):
277+
t.Fatal("timed out waiting for request callback")
278+
}
279+
server.Protocol.Stop()
280+
close(releaseRequest)
281+
282+
select {
283+
case sendErr := <-resultCh:
284+
require.ErrorIs(t, sendErr, protocol.ErrProtocolShuttingDown)
285+
case <-time.After(time.Second):
286+
t.Fatal("timed out waiting for shutdown delivery callback")
287+
}
288+
}
289+
115290
func TestHandleRequestNext_NilCallback(t *testing.T) {
116291
connId := connection.ConnectionId{
117292
LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},

0 commit comments

Comments
 (0)