detached 1f9680c

refactor(broker): relay raw envelopes through one core stream port

The broker core admits BrokerCoreService connections whose streams carry opaque session envelopes end to end. Typed preview and observation decode through an injected SessionCodec, validated in serviceConfig. daemonmux logical connections carry raw envelopes only; the client keeps its typed BrokerService.
commit
1f9680c645f9bb0aa17e9f8b62aa277f13404ebb
author
brice · · 2026-09-23 16:24 UTC
parent
4e0fee9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
36 files · +2540 -505
M internal/adapters/brokeripc/helpers_test.go +39 -17
@@ -1,6 +1,7 @@
11 package brokeripc
22
33 import (
4+ "bytes"
45 "context"
56 "encoding/binary"
67 "errors"
@@ -15,6 +16,7 @@ import (
1516 "github.com/stretchr/testify/require"
1617
1718 "github.com/bnema/vev/internal/adapters/brokerwire"
19+ "github.com/bnema/vev/internal/adapters/sessionwire"
1820 "github.com/bnema/vev/internal/domain"
1921 "github.com/bnema/vev/internal/ports"
2022 "github.com/bnema/vev/internal/protocol"
@@ -72,7 +74,7 @@ func attachmentRequest(stream ports.BrokerStreamID, admission ports.BrokerStream
7274
7375 // nextStreamID reads one fresh identity from the service's only allocator,
7476 // exactly as the composed supervisor and broker operations do.
75-func nextStreamID(t *testing.T, service ports.BrokerService) ports.BrokerStreamID {
77+func nextStreamID(t *testing.T, service ports.BrokerSession) ports.BrokerStreamID {
7678 t.Helper()
7779 stream, err := service.NextStreamID()
7880 require.NoError(t, err)
@@ -87,14 +89,14 @@ const membershipProbeEndpoint = "probe@host:22"
8789 // membership round trip after a stream-local or subscriber-local event: the
8890 // probe adds one host through the same delegated path a real caller uses and
8991 // asserts the registration comes back for exactly the requested endpoint.
90-func requireConnectionDelegates(t *testing.T, service ports.BrokerService) {
92+func requireConnectionDelegates(t *testing.T, service ports.BrokerSession) {
9193 t.Helper()
9294 require.NoError(t, connectionDelegates(context.Background(), service))
9395 }
9496
9597 // connectionDelegates is the error-returning form of requireConnectionDelegates,
9698 // safe to call from a test goroutine where FailNow is not.
97-func connectionDelegates(ctx context.Context, service ports.BrokerService) error {
99+func connectionDelegates(ctx context.Context, service ports.BrokerSession) error {
98100 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
99101 defer cancel()
100102 registration, err := service.AddHost(ctx, membershipProbeEndpoint, testPolicy())
@@ -284,22 +286,38 @@ func (s *fakeSubscription) notify() {
284286 type fakeLogicalConn struct {
285287 fromClient chan protocol.ClientMessage
286288 toClient chan protocol.ServerMessage
287- done chan struct{}
288- once sync.Once
289- mu sync.Mutex
290- err error
291- closed bool
289+ // preamble carries the canned session preamble acceptance: the raw relay
290+ // forwards the client's preamble request like any other envelope.
291+ preamble chan []byte
292+ done chan struct{}
293+ once sync.Once
294+ mu sync.Mutex
295+ err error
296+ closed bool
292297 }
293298
294299 func newFakeLogicalConn() *fakeLogicalConn {
295300 return &fakeLogicalConn{
296301 fromClient: make(chan protocol.ClientMessage, 64),
297302 toClient: make(chan protocol.ServerMessage, 64),
303+ preamble: make(chan []byte, 1),
298304 done: make(chan struct{}),
299305 }
300306 }
301307
302-func (c *fakeLogicalConn) SendClient(message protocol.ClientMessage) error {
308+func (c *fakeLogicalConn) SendEnvelope(payload []byte) error {
309+ if request, err := sessionwire.EncodePreambleRequestForTest(); err == nil && bytes.Equal(payload, request) {
310+ response, err := sessionwire.EncodePreambleResponseForTest()
311+ if err != nil {
312+ return err
313+ }
314+ c.preamble <- response
315+ return nil
316+ }
317+ message, err := sessionwire.DecodeClientEnvelope(payload)
318+ if err != nil {
319+ return err
320+ }
303321 select {
304322 case c.fromClient <- message:
305323 return nil
@@ -308,20 +326,24 @@ func (c *fakeLogicalConn) SendClient(message protocol.ClientMessage) error {
308326 }
309327 }
310328
311-func (c *fakeLogicalConn) ReceiveServer() (protocol.ServerMessage, error) {
329+func (c *fakeLogicalConn) RecvEnvelope() ([]byte, error) {
312330 // A message already queued when Close ran must still be observed first: a
313331 // real carriage never reports the connection done before a message the peer
314332 // sent ahead of an orderly close has been delivered. Draining non-blocking
315333 // before the select keeps that ordering deterministic instead of racing a
316334 // buffered send against an already-closed done channel.
317335 select {
336+ case response := <-c.preamble:
337+ return response, nil
318338 case message := <-c.toClient:
319- return message, nil
339+ return sessionwire.EncodeServerMessage(message)
320340 default:
321341 }
322342 select {
343+ case response := <-c.preamble:
344+ return response, nil
323345 case message := <-c.toClient:
324- return message, nil
346+ return sessionwire.EncodeServerMessage(message)
325347 case <-c.done:
326348 if err := c.Err(); err != nil {
327349 return nil, err
@@ -487,7 +509,7 @@ func (c *fakeCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.BrokerPre
487509 return newFakePreviewSubscription(), nil
488510 }
489511
490-func (c *fakeCore) OpenStream(_ context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
512+func (c *fakeCore) OpenEnvelopeStream(_ context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
491513 c.mu.Lock()
492514 defer c.mu.Unlock()
493515 if c.openErr != nil {
@@ -721,7 +743,7 @@ func newFakeAuthority(epoch ports.BrokerEpoch, snapshot ports.BrokerSnapshot) *f
721743 return &fakeAuthority{hub: newSnapshotHub(snapshot), epoch: epoch}
722744 }
723745
724-func (a *fakeAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {
746+func (a *fakeAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
725747 if err := ctx.Err(); err != nil {
726748 return nil, err
727749 }
@@ -770,10 +792,10 @@ func startEndpoint(t *testing.T, cfg Config) *endpoint {
770792 }
771793
772794 // accept returns the next admitted session.
773-func (e *endpoint) accept() ports.BrokerService {
795+func (e *endpoint) accept() ports.BrokerCoreService {
774796 e.t.Helper()
775797 type result struct {
776- service ports.BrokerService
798+ service ports.BrokerCoreService
777799 err error
778800 }
779801 out := make(chan result, 1)
@@ -807,7 +829,7 @@ func (e *endpoint) dialWith(cfg Config) ports.BrokerService {
807829 }
808830
809831 // pair dials one client and accepts its session.
810-func (e *endpoint) pair() (ports.BrokerService, ports.BrokerService) {
832+func (e *endpoint) pair() (ports.BrokerService, ports.BrokerCoreService) {
811833 e.t.Helper()
812834 client := e.dial()
813835 session := e.accept()
@@ -1,6 +1,7 @@
1 package brokeripc1 package brokeripc
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "encoding/binary"6 "encoding/binary"
6 "errors"7 "errors"
@@ -15,6 +16,7 @@ import (
15 "github.com/stretchr/testify/require"16 "github.com/stretchr/testify/require"
16 17
17 "github.com/bnema/vev/internal/adapters/brokerwire"18 "github.com/bnema/vev/internal/adapters/brokerwire"
19+ "github.com/bnema/vev/internal/adapters/sessionwire"
18 "github.com/bnema/vev/internal/domain"20 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/ports"21 "github.com/bnema/vev/internal/ports"
20 "github.com/bnema/vev/internal/protocol"22 "github.com/bnema/vev/internal/protocol"
@@ -72,7 +74,7 @@ func attachmentRequest(stream ports.BrokerStreamID, admission ports.BrokerStream
72 74
73 // nextStreamID reads one fresh identity from the service's only allocator,75 // nextStreamID reads one fresh identity from the service's only allocator,
74 // exactly as the composed supervisor and broker operations do.76 // exactly as the composed supervisor and broker operations do.
75-func nextStreamID(t *testing.T, service ports.BrokerService) ports.BrokerStreamID {77+func nextStreamID(t *testing.T, service ports.BrokerSession) ports.BrokerStreamID {
76 t.Helper()78 t.Helper()
77 stream, err := service.NextStreamID()79 stream, err := service.NextStreamID()
78 require.NoError(t, err)80 require.NoError(t, err)
@@ -87,14 +89,14 @@ const membershipProbeEndpoint = "probe@host:22"
87 // membership round trip after a stream-local or subscriber-local event: the89 // membership round trip after a stream-local or subscriber-local event: the
88 // probe adds one host through the same delegated path a real caller uses and90 // probe adds one host through the same delegated path a real caller uses and
89 // asserts the registration comes back for exactly the requested endpoint.91 // asserts the registration comes back for exactly the requested endpoint.
90-func requireConnectionDelegates(t *testing.T, service ports.BrokerService) {92+func requireConnectionDelegates(t *testing.T, service ports.BrokerSession) {
91 t.Helper()93 t.Helper()
92 require.NoError(t, connectionDelegates(context.Background(), service))94 require.NoError(t, connectionDelegates(context.Background(), service))
93 }95 }
94 96
95 // connectionDelegates is the error-returning form of requireConnectionDelegates,97 // connectionDelegates is the error-returning form of requireConnectionDelegates,
96 // safe to call from a test goroutine where FailNow is not.98 // safe to call from a test goroutine where FailNow is not.
97-func connectionDelegates(ctx context.Context, service ports.BrokerService) error {99+func connectionDelegates(ctx context.Context, service ports.BrokerSession) error {
98 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)100 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
99 defer cancel()101 defer cancel()
100 registration, err := service.AddHost(ctx, membershipProbeEndpoint, testPolicy())102 registration, err := service.AddHost(ctx, membershipProbeEndpoint, testPolicy())
@@ -284,22 +286,38 @@ func (s *fakeSubscription) notify() {
284 type fakeLogicalConn struct {286 type fakeLogicalConn struct {
285 fromClient chan protocol.ClientMessage287 fromClient chan protocol.ClientMessage
286 toClient chan protocol.ServerMessage288 toClient chan protocol.ServerMessage
287- done chan struct{}289+ // preamble carries the canned session preamble acceptance: the raw relay
288- once sync.Once290+ // forwards the client's preamble request like any other envelope.
289- mu sync.Mutex291+ preamble chan []byte
290- err error292+ done chan struct{}
291- closed bool293+ once sync.Once
294+ mu sync.Mutex
295+ err error
296+ closed bool
292 }297 }
293 298
294 func newFakeLogicalConn() *fakeLogicalConn {299 func newFakeLogicalConn() *fakeLogicalConn {
295 return &fakeLogicalConn{300 return &fakeLogicalConn{
296 fromClient: make(chan protocol.ClientMessage, 64),301 fromClient: make(chan protocol.ClientMessage, 64),
297 toClient: make(chan protocol.ServerMessage, 64),302 toClient: make(chan protocol.ServerMessage, 64),
303+ preamble: make(chan []byte, 1),
298 done: make(chan struct{}),304 done: make(chan struct{}),
299 }305 }
300 }306 }
301 307
302-func (c *fakeLogicalConn) SendClient(message protocol.ClientMessage) error {308+func (c *fakeLogicalConn) SendEnvelope(payload []byte) error {
309+ if request, err := sessionwire.EncodePreambleRequestForTest(); err == nil && bytes.Equal(payload, request) {
310+ response, err := sessionwire.EncodePreambleResponseForTest()
311+ if err != nil {
312+ return err
313+ }
314+ c.preamble <- response
315+ return nil
316+ }
317+ message, err := sessionwire.DecodeClientEnvelope(payload)
318+ if err != nil {
319+ return err
320+ }
303 select {321 select {
304 case c.fromClient <- message:322 case c.fromClient <- message:
305 return nil323 return nil
@@ -308,20 +326,24 @@ func (c *fakeLogicalConn) SendClient(message protocol.ClientMessage) error {
308 }326 }
309 }327 }
310 328
311-func (c *fakeLogicalConn) ReceiveServer() (protocol.ServerMessage, error) {329+func (c *fakeLogicalConn) RecvEnvelope() ([]byte, error) {
312 // A message already queued when Close ran must still be observed first: a330 // A message already queued when Close ran must still be observed first: a
313 // real carriage never reports the connection done before a message the peer331 // real carriage never reports the connection done before a message the peer
314 // sent ahead of an orderly close has been delivered. Draining non-blocking332 // sent ahead of an orderly close has been delivered. Draining non-blocking
315 // before the select keeps that ordering deterministic instead of racing a333 // before the select keeps that ordering deterministic instead of racing a
316 // buffered send against an already-closed done channel.334 // buffered send against an already-closed done channel.
317 select {335 select {
336+ case response := <-c.preamble:
337+ return response, nil
318 case message := <-c.toClient:338 case message := <-c.toClient:
319- return message, nil339+ return sessionwire.EncodeServerMessage(message)
320 default:340 default:
321 }341 }
322 select {342 select {
343+ case response := <-c.preamble:
344+ return response, nil
323 case message := <-c.toClient:345 case message := <-c.toClient:
324- return message, nil346+ return sessionwire.EncodeServerMessage(message)
325 case <-c.done:347 case <-c.done:
326 if err := c.Err(); err != nil {348 if err := c.Err(); err != nil {
327 return nil, err349 return nil, err
@@ -487,7 +509,7 @@ func (c *fakeCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.BrokerPre
487 return newFakePreviewSubscription(), nil509 return newFakePreviewSubscription(), nil
488 }510 }
489 511
490-func (c *fakeCore) OpenStream(_ context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {512+func (c *fakeCore) OpenEnvelopeStream(_ context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
491 c.mu.Lock()513 c.mu.Lock()
492 defer c.mu.Unlock()514 defer c.mu.Unlock()
493 if c.openErr != nil {515 if c.openErr != nil {
@@ -721,7 +743,7 @@ func newFakeAuthority(epoch ports.BrokerEpoch, snapshot ports.BrokerSnapshot) *f
721 return &fakeAuthority{hub: newSnapshotHub(snapshot), epoch: epoch}743 return &fakeAuthority{hub: newSnapshotHub(snapshot), epoch: epoch}
722 }744 }
723 745
724-func (a *fakeAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {746+func (a *fakeAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
725 if err := ctx.Err(); err != nil {747 if err := ctx.Err(); err != nil {
726 return nil, err748 return nil, err
727 }749 }
@@ -770,10 +792,10 @@ func startEndpoint(t *testing.T, cfg Config) *endpoint {
770 }792 }
771 793
772 // accept returns the next admitted session.794 // accept returns the next admitted session.
773-func (e *endpoint) accept() ports.BrokerService {795+func (e *endpoint) accept() ports.BrokerCoreService {
774 e.t.Helper()796 e.t.Helper()
775 type result struct {797 type result struct {
776- service ports.BrokerService798+ service ports.BrokerCoreService
777 err error799 err error
778 }800 }
779 out := make(chan result, 1)801 out := make(chan result, 1)
@@ -807,7 +829,7 @@ func (e *endpoint) dialWith(cfg Config) ports.BrokerService {
807 }829 }
808 830
809 // pair dials one client and accepts its session.831 // pair dials one client and accepts its session.
810-func (e *endpoint) pair() (ports.BrokerService, ports.BrokerService) {832+func (e *endpoint) pair() (ports.BrokerService, ports.BrokerCoreService) {
811 e.t.Helper()833 e.t.Helper()
812 client := e.dial()834 client := e.dial()
813 session := e.accept()835 session := e.accept()
M internal/adapters/brokeripc/listen_test.go +3 -3
@@ -27,7 +27,7 @@ type gatedAuthority struct {
2727 once sync.Once
2828 }
2929
30-func (a *gatedAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {
30+func (a *gatedAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
3131 a.once.Do(func() { close(a.entered) })
3232 <-a.gate
3333 return a.inner.AdmitClient(ctx)
@@ -41,7 +41,7 @@ type ctxAuthority struct {
4141 once sync.Once
4242 }
4343
44-func (a *ctxAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {
44+func (a *ctxAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
4545 a.once.Do(func() { close(a.entered) })
4646 <-ctx.Done()
4747 return nil, ctx.Err()
@@ -429,7 +429,7 @@ func TestStalledHandshakeDoesNotBlockOtherClients(t *testing.T) {
429429 func TestConcurrentDialAndClose(t *testing.T) {
430430 e := startEndpoint(t, Config{MaxClients: 8, HandshakeTimeout: 2 * time.Second})
431431
432- sessions := make(chan ports.BrokerService, 8)
432+ sessions := make(chan ports.BrokerCoreService, 8)
433433 go func() {
434434 defer close(sessions)
435435 for i := 0; i < cap(sessions); i++ {
@@ -27,7 +27,7 @@ type gatedAuthority struct {
27 once sync.Once27 once sync.Once
28 }28 }
29 29
30-func (a *gatedAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {30+func (a *gatedAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
31 a.once.Do(func() { close(a.entered) })31 a.once.Do(func() { close(a.entered) })
32 <-a.gate32 <-a.gate
33 return a.inner.AdmitClient(ctx)33 return a.inner.AdmitClient(ctx)
@@ -41,7 +41,7 @@ type ctxAuthority struct {
41 once sync.Once41 once sync.Once
42 }42 }
43 43
44-func (a *ctxAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {44+func (a *ctxAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
45 a.once.Do(func() { close(a.entered) })45 a.once.Do(func() { close(a.entered) })
46 <-ctx.Done()46 <-ctx.Done()
47 return nil, ctx.Err()47 return nil, ctx.Err()
@@ -429,7 +429,7 @@ func TestStalledHandshakeDoesNotBlockOtherClients(t *testing.T) {
429 func TestConcurrentDialAndClose(t *testing.T) {429 func TestConcurrentDialAndClose(t *testing.T) {
430 e := startEndpoint(t, Config{MaxClients: 8, HandshakeTimeout: 2 * time.Second})430 e := startEndpoint(t, Config{MaxClients: 8, HandshakeTimeout: 2 * time.Second})
431 431
432- sessions := make(chan ports.BrokerService, 8)432+ sessions := make(chan ports.BrokerCoreService, 8)
433 go func() {433 go func() {
434 defer close(sessions)434 defer close(sessions)
435 for i := 0; i < cap(sessions); i++ {435 for i := 0; i < cap(sessions); i++ {
M internal/adapters/brokeripc/preview_lifecycle_test.go +2 -2
@@ -57,7 +57,7 @@ func awaitPreviewChanged(t *testing.T, sub ports.BrokerPreviewSubscription) port
5757 // serverPreviewGeneration returns the server session's current preview
5858 // generation: it observes the same replacement bookkeeping startPreview
5959 // maintains, so a test can prove which generation the server holds current.
60-func serverPreviewGeneration(session ports.BrokerService) ports.BrokerPreviewGeneration {
60+func serverPreviewGeneration(session ports.BrokerCoreService) ports.BrokerPreviewGeneration {
6161 server, ok := session.(*serverSession)
6262 if !ok {
6363 return 0
@@ -68,7 +68,7 @@ func serverPreviewGeneration(session ports.BrokerService) ports.BrokerPreviewGen
6868 }
6969
7070 // serverPreviewOf returns the server session's current preview subscription.
71-func serverPreviewOf(session ports.BrokerService) ports.BrokerPreviewSubscription {
71+func serverPreviewOf(session ports.BrokerCoreService) ports.BrokerPreviewSubscription {
7272 server, ok := session.(*serverSession)
7373 if !ok {
7474 return nil
@@ -57,7 +57,7 @@ func awaitPreviewChanged(t *testing.T, sub ports.BrokerPreviewSubscription) port
57 // serverPreviewGeneration returns the server session's current preview57 // serverPreviewGeneration returns the server session's current preview
58 // generation: it observes the same replacement bookkeeping startPreview58 // generation: it observes the same replacement bookkeeping startPreview
59 // maintains, so a test can prove which generation the server holds current.59 // maintains, so a test can prove which generation the server holds current.
60-func serverPreviewGeneration(session ports.BrokerService) ports.BrokerPreviewGeneration {60+func serverPreviewGeneration(session ports.BrokerCoreService) ports.BrokerPreviewGeneration {
61 server, ok := session.(*serverSession)61 server, ok := session.(*serverSession)
62 if !ok {62 if !ok {
63 return 063 return 0
@@ -68,7 +68,7 @@ func serverPreviewGeneration(session ports.BrokerService) ports.BrokerPreviewGen
68 }68 }
69 69
70 // serverPreviewOf returns the server session's current preview subscription.70 // serverPreviewOf returns the server session's current preview subscription.
71-func serverPreviewOf(session ports.BrokerService) ports.BrokerPreviewSubscription {71+func serverPreviewOf(session ports.BrokerCoreService) ports.BrokerPreviewSubscription {
72 server, ok := session.(*serverSession)72 server, ok := session.(*serverSession)
73 if !ok {73 if !ok {
74 return nil74 return nil
A internal/adapters/brokeripc/raw_stream_test.go +55 -0
@@ -0,0 +1,55 @@
1+package brokeripc
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "io"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/require"
11+)
12+
13+type rawRelayCore struct {
14+ received chan []byte
15+ done chan struct{}
16+}
17+
18+func (c *rawRelayCore) SendEnvelope(payload []byte) error {
19+ c.received <- append([]byte(nil), payload...)
20+ return nil
21+}
22+func (c *rawRelayCore) RecvEnvelope() ([]byte, error) { <-c.done; return nil, io.EOF }
23+func (c *rawRelayCore) Done() <-chan struct{} { return c.done }
24+func (c *rawRelayCore) Err() error { return nil }
25+func (c *rawRelayCore) Close() error {
26+ select {
27+ case <-c.done:
28+ default:
29+ close(c.done)
30+ }
31+ return nil
32+}
33+
34+func TestServerStreamRelaysOpaqueEnvelopes(t *testing.T) {
35+ core := &rawRelayCore{received: make(chan []byte, 1), done: make(chan struct{})}
36+ session := &serverSession{ctx: context.Background(), cfg: Config{}.withDefaults()}
37+ stream, err := newServerStream(session, 1, core)
38+ require.NoError(t, err)
39+ session.wg.Add(1)
40+ go stream.relayClient()
41+ payload := []byte{0xff, 0xfe, 0xfd, 0x80}
42+ frame := make([]byte, 4+len(payload))
43+ binary.BigEndian.PutUint32(frame[:4], uint32(len(payload)))
44+ copy(frame[4:], payload)
45+ require.NoError(t, stream.deliver(frame[:2]))
46+ require.NoError(t, stream.deliver(frame[2:]))
47+ select {
48+ case got := <-core.received:
49+ require.Equal(t, payload, got)
50+ case <-time.After(5 * time.Second):
51+ t.Fatal("opaque envelope not relayed")
52+ }
53+ stream.close(nil)
54+ session.wg.Wait()
55+}
@@ -0,0 +1,55 @@
1+package brokeripc
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "io"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/require"
11+)
12+
13+type rawRelayCore struct {
14+ received chan []byte
15+ done chan struct{}
16+}
17+
18+func (c *rawRelayCore) SendEnvelope(payload []byte) error {
19+ c.received <- append([]byte(nil), payload...)
20+ return nil
21+}
22+func (c *rawRelayCore) RecvEnvelope() ([]byte, error) { <-c.done; return nil, io.EOF }
23+func (c *rawRelayCore) Done() <-chan struct{} { return c.done }
24+func (c *rawRelayCore) Err() error { return nil }
25+func (c *rawRelayCore) Close() error {
26+ select {
27+ case <-c.done:
28+ default:
29+ close(c.done)
30+ }
31+ return nil
32+}
33+
34+func TestServerStreamRelaysOpaqueEnvelopes(t *testing.T) {
35+ core := &rawRelayCore{received: make(chan []byte, 1), done: make(chan struct{})}
36+ session := &serverSession{ctx: context.Background(), cfg: Config{}.withDefaults()}
37+ stream, err := newServerStream(session, 1, core)
38+ require.NoError(t, err)
39+ session.wg.Add(1)
40+ go stream.relayClient()
41+ payload := []byte{0xff, 0xfe, 0xfd, 0x80}
42+ frame := make([]byte, 4+len(payload))
43+ binary.BigEndian.PutUint32(frame[:4], uint32(len(payload)))
44+ copy(frame[4:], payload)
45+ require.NoError(t, stream.deliver(frame[:2]))
46+ require.NoError(t, stream.deliver(frame[2:]))
47+ select {
48+ case got := <-core.received:
49+ require.Equal(t, payload, got)
50+ case <-time.After(5 * time.Second):
51+ t.Fatal("opaque envelope not relayed")
52+ }
53+ stream.close(nil)
54+ session.wg.Wait()
55+}
M internal/adapters/brokeripc/server.go +6 -6
@@ -281,7 +281,7 @@ func (l *listener) classifyAdmissionFailure(err error) error {
281281 // credential, or an unsupported build) or a client that fails the preamble or
282282 // admission is isolated: that peer is dropped and the listener keeps serving.
283283 // The most recent refusal is available to diagnostics through refusal.
284-func (l *listener) Accept() (ports.BrokerService, error) {
284+func (l *listener) Accept() (ports.BrokerCoreService, error) {
285285 if l == nil {
286286 return nil, ErrListenerClosed
287287 }
@@ -466,7 +466,7 @@ type serverSession struct {
466466 epoch ports.BrokerEpoch
467467 scope brokerwire.Scope
468468 conn *brokerwire.Connection
469- core ports.BrokerService
469+ core ports.BrokerCoreService
470470 transport wire.BoundedTransport
471471 ceilings brokerwire.Ceilings
472472 cfg Config
@@ -513,7 +513,7 @@ type serverSession struct {
513513 done chan struct{}
514514 }
515515
516-var _ ports.BrokerService = (*serverSession)(nil)
516+var _ ports.BrokerCoreService = (*serverSession)(nil)
517517
518518 // registrationDeadline returns the absolute instant by which an accepted client
519519 // must send its Register: the accept-time handshake deadline that already
@@ -535,7 +535,7 @@ func registrationDeadline(ctx context.Context) time.Time {
535535 // registerDeadline is the absolute instant by which the client's Register must
536536 // arrive (the accept-time handshake deadline); the zero time disables the bound
537537 // and exists only for direct construction outside the listener.
538-func newServerSession(epoch ports.BrokerEpoch, transport wire.BoundedTransport, ceilings brokerwire.Ceilings, core ports.BrokerService, cfg Config, release func(), registerDeadline time.Time) (*serverSession, error) {
538+func newServerSession(epoch ports.BrokerEpoch, transport wire.BoundedTransport, ceilings brokerwire.Ceilings, core ports.BrokerCoreService, cfg Config, release func(), registerDeadline time.Time) (*serverSession, error) {
539539 if epoch == 0 || transport == nil || core == nil {
540540 return nil, ErrConfig
541541 }
@@ -983,14 +983,14 @@ func (s *serverSession) SubscribePreview(request ports.BrokerPreviewRequest) (po
983983 // allocated (NextStreamID) and is transmitted unchanged: this listener never
984984 // re-allocates a wire identity, whether the request arrives over the wire or
985985 // through this in-process delegate.
986-func (s *serverSession) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
986+func (s *serverSession) OpenEnvelopeStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
987987 if err := s.checkRequestScope(request.Epoch, request.Connection); err != nil {
988988 return nil, err
989989 }
990990 scoped := request
991991 scoped.Epoch = s.epoch
992992 scoped.Connection = s.scope.Connection
993- return s.core.OpenStream(ctx, scoped)
993+ return s.core.OpenEnvelopeStream(ctx, scoped)
994994 }
995995
996996 // CloseStream retires one scope-checked stream. An absent connection identity is
@@ -281,7 +281,7 @@ func (l *listener) classifyAdmissionFailure(err error) error {
281 // credential, or an unsupported build) or a client that fails the preamble or281 // credential, or an unsupported build) or a client that fails the preamble or
282 // admission is isolated: that peer is dropped and the listener keeps serving.282 // admission is isolated: that peer is dropped and the listener keeps serving.
283 // The most recent refusal is available to diagnostics through refusal.283 // The most recent refusal is available to diagnostics through refusal.
284-func (l *listener) Accept() (ports.BrokerService, error) {284+func (l *listener) Accept() (ports.BrokerCoreService, error) {
285 if l == nil {285 if l == nil {
286 return nil, ErrListenerClosed286 return nil, ErrListenerClosed
287 }287 }
@@ -466,7 +466,7 @@ type serverSession struct {
466 epoch ports.BrokerEpoch466 epoch ports.BrokerEpoch
467 scope brokerwire.Scope467 scope brokerwire.Scope
468 conn *brokerwire.Connection468 conn *brokerwire.Connection
469- core ports.BrokerService469+ core ports.BrokerCoreService
470 transport wire.BoundedTransport470 transport wire.BoundedTransport
471 ceilings brokerwire.Ceilings471 ceilings brokerwire.Ceilings
472 cfg Config472 cfg Config
@@ -513,7 +513,7 @@ type serverSession struct {
513 done chan struct{}513 done chan struct{}
514 }514 }
515 515
516-var _ ports.BrokerService = (*serverSession)(nil)516+var _ ports.BrokerCoreService = (*serverSession)(nil)
517 517
518 // registrationDeadline returns the absolute instant by which an accepted client518 // registrationDeadline returns the absolute instant by which an accepted client
519 // must send its Register: the accept-time handshake deadline that already519 // must send its Register: the accept-time handshake deadline that already
@@ -535,7 +535,7 @@ func registrationDeadline(ctx context.Context) time.Time {
535 // registerDeadline is the absolute instant by which the client's Register must535 // registerDeadline is the absolute instant by which the client's Register must
536 // arrive (the accept-time handshake deadline); the zero time disables the bound536 // arrive (the accept-time handshake deadline); the zero time disables the bound
537 // and exists only for direct construction outside the listener.537 // and exists only for direct construction outside the listener.
538-func newServerSession(epoch ports.BrokerEpoch, transport wire.BoundedTransport, ceilings brokerwire.Ceilings, core ports.BrokerService, cfg Config, release func(), registerDeadline time.Time) (*serverSession, error) {538+func newServerSession(epoch ports.BrokerEpoch, transport wire.BoundedTransport, ceilings brokerwire.Ceilings, core ports.BrokerCoreService, cfg Config, release func(), registerDeadline time.Time) (*serverSession, error) {
539 if epoch == 0 || transport == nil || core == nil {539 if epoch == 0 || transport == nil || core == nil {
540 return nil, ErrConfig540 return nil, ErrConfig
541 }541 }
@@ -983,14 +983,14 @@ func (s *serverSession) SubscribePreview(request ports.BrokerPreviewRequest) (po
983 // allocated (NextStreamID) and is transmitted unchanged: this listener never983 // allocated (NextStreamID) and is transmitted unchanged: this listener never
984 // re-allocates a wire identity, whether the request arrives over the wire or984 // re-allocates a wire identity, whether the request arrives over the wire or
985 // through this in-process delegate.985 // through this in-process delegate.
986-func (s *serverSession) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {986+func (s *serverSession) OpenEnvelopeStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
987 if err := s.checkRequestScope(request.Epoch, request.Connection); err != nil {987 if err := s.checkRequestScope(request.Epoch, request.Connection); err != nil {
988 return nil, err988 return nil, err
989 }989 }
990 scoped := request990 scoped := request
991 scoped.Epoch = s.epoch991 scoped.Epoch = s.epoch
992 scoped.Connection = s.scope.Connection992 scoped.Connection = s.scope.Connection
993- return s.core.OpenStream(ctx, scoped)993+ return s.core.OpenEnvelopeStream(ctx, scoped)
994 }994 }
995 995
996 // CloseStream retires one scope-checked stream. An absent connection identity is996 // CloseStream retires one scope-checked stream. An absent connection identity is
M internal/adapters/brokeripc/session_test.go +1 -1
@@ -110,7 +110,7 @@ func TestSessionCloseStreamFillsConnectionScope(t *testing.T) {
110110
111111 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
112112 defer cancel()
113- opened, err := session.OpenStream(ctx, openRequest(1))
113+ opened, err := session.OpenEnvelopeStream(ctx, openRequest(1))
114114 require.NoError(t, err)
115115 require.NotNil(t, opened)
116116
@@ -110,7 +110,7 @@ func TestSessionCloseStreamFillsConnectionScope(t *testing.T) {
110 110
111 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)111 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
112 defer cancel()112 defer cancel()
113- opened, err := session.OpenStream(ctx, openRequest(1))113+ opened, err := session.OpenEnvelopeStream(ctx, openRequest(1))
114 require.NoError(t, err)114 require.NoError(t, err)
115 require.NotNil(t, opened)115 require.NotNil(t, opened)
116 116
M internal/adapters/brokeripc/stream.go +23 -29
@@ -9,16 +9,14 @@ import (
99 "github.com/bnema/vev/internal/adapters/brokerwire"
1010 "github.com/bnema/vev/internal/adapters/sessionwire"
1111 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol/wire"
1213 )
1314
1415 // Logical stream bridging.
1516 //
16-// One brokerwire logical stream carries the typed session protocol between a
17-// client and the daemon behind the broker. The broker terminates both hops: the
18-// client's session connection is built by sessionwire over the stream's private
19-// bounded byte pipe, and the admitted broker core hands back its own typed
20-// connection to the daemon. Two relay goroutines carry typed messages between
21-// them, so no session byte is ever reinterpreted here.
17+// One brokerwire logical stream carries the session protocol between client
18+// and daemon. The broker relays complete opaque envelope payloads through
19+// bounded framed carriages; neither preamble nor session message is decoded.
2220 //
2321 // Identity and cancellation stay per stream: a stream the core loses, a stream
2422 // whose local consumer stalled, and a stream the peer closed each settle alone,
@@ -29,10 +27,9 @@ import (
2927 type serverStream struct {
3028 session *serverSession
3129 id ports.BrokerStreamID
32- core ports.BrokerLogicalConnection
30+ core ports.BrokerEnvelopeStream
3331 pipe *streamPipe
3432 carriage *carriage
35- wire ports.ServerConnection
3633
3734 mu sync.Mutex
3835 closed bool
@@ -40,17 +37,13 @@ type serverStream struct {
4037 }
4138
4239 // newServerStream builds one bridged stream over the admitted core connection.
43-func newServerStream(s *serverSession, id ports.BrokerStreamID, core ports.BrokerLogicalConnection) (*serverStream, error) {
40+func newServerStream(s *serverSession, id ports.BrokerStreamID, core ports.BrokerEnvelopeStream) (*serverStream, error) {
4441 if s == nil || core == nil || id == 0 {
4542 return nil, ErrConfig
4643 }
4744 st := &serverStream{session: s, id: id, core: core, done: make(chan struct{})}
4845 st.pipe = newStreamPipe(s.cfg.StreamInboundChunks, s.cfg.StreamInboundBytes, st.sendChunk)
4946 st.carriage = newCarriage(st.pipe)
50- st.wire = sessionwire.NewServerConnection(st.carriage)
51- if st.wire == nil {
52- return nil, ErrConfig
53- }
5447 return st, nil
5548 }
5649
@@ -85,34 +78,36 @@ func (st *serverStream) start() {
8578 go st.watchCore()
8679 }
8780
88-// relayClient carries client-to-daemon messages from the wire session
89-// connection to the admitted core connection.
81+// relayClient carries complete client envelopes without decoding.
9082 func (st *serverStream) relayClient() {
9183 defer st.session.wg.Done()
9284 for {
93- message, err := st.wire.ReceiveClient()
85+ envelope, err := st.carriage.RecvBounded(wire.AbsoluteEnvelopeLimit)
9486 if err != nil {
9587 st.fail(cancelOrCause(st.session.ctx, streamFailure(err)))
9688 return
9789 }
98- if err := st.core.SendClient(message); err != nil {
99- st.fail(err)
90+ if err := st.core.SendEnvelope(envelope.Payload); err != nil {
91+ st.fail(coreFailure(st.core, err))
10092 return
10193 }
10294 }
10395 }
10496
105-// relayServer carries daemon-to-client messages from the admitted core
106-// connection to the wire session connection.
97+// relayServer carries complete daemon envelopes without decoding.
10798 func (st *serverStream) relayServer() {
10899 defer st.session.wg.Done()
109100 for {
110- message, err := st.core.ReceiveServer()
101+ payload, err := st.core.RecvEnvelope()
111102 if err != nil {
112103 st.fail(coreFailure(st.core, err))
113104 return
114105 }
115- if err := st.wire.SendServer(message); err != nil {
106+ if len(payload) == 0 || len(payload) > wire.AbsoluteEnvelopeLimit {
107+ st.fail(ErrProtocol)
108+ return
109+ }
110+ if err := st.carriage.Send(wire.Envelope{Payload: payload}); err != nil {
116111 st.fail(streamFailure(err))
117112 return
118113 }
@@ -121,10 +116,10 @@ func (st *serverStream) relayServer() {
121116
122117 // watchCore publishes the core connection's terminal outcome even when no read
123118 // is in flight. It settles the stream itself only for a failure: an orderly
124-// core end (Err()==nil) is left to relayServer, whose own core.ReceiveServer()
119+// core end (Err()==nil) is left to relayServer, whose own core.RecvEnvelope()
125120 // call observes the same terminal outcome right after forwarding any reply
126121 // that was already in flight, and settles the stream from there. Settling here
127-// on every Done would race relayServer's in-flight wire.SendServer and could
122+// on every Done would race relayServer's in-flight carriage.Send and could
128123 // close the carriage while a received reply is still being written out to the
129124 // client, discarding it.
130125 func (st *serverStream) watchCore() {
@@ -147,8 +142,7 @@ func (st *serverStream) fail(err error) {
147142 }
148143
149144 // close releases the stream exactly once: the pipe unblocks a stalled reader,
150-// the wire session connection releases the carriage, and the core connection is
151-// closed. It reports whether this call was the one that settled the stream.
145+// the carriage joins its writer, and the core connection closes.
152146 func (st *serverStream) close(cause error) bool {
153147 if st == nil {
154148 return false
@@ -161,7 +155,7 @@ func (st *serverStream) close(cause error) bool {
161155 st.closed = true
162156 st.mu.Unlock()
163157 st.pipe.closeWith(cause)
164- _ = st.wire.Close()
158+ _ = st.carriage.Close()
165159 _ = st.core.Close()
166160 close(st.done)
167161 return true
@@ -178,7 +172,7 @@ func streamFailure(err error) error {
178172
179173 // coreFailure classifies one core connection outcome. The core's typed terminal
180174 // error wins; an orderly end is a peer close.
181-func coreFailure(core ports.BrokerLogicalConnection, err error) error {
175+func coreFailure(core ports.BrokerEnvelopeStream, err error) error {
182176 if err == nil && core != nil {
183177 err = core.Err()
184178 }
@@ -221,7 +215,7 @@ func (s *serverSession) startStream(m brokerwire.OpenStream) {
221215 s.wg.Add(1)
222216 go func() {
223217 defer s.wg.Done()
224- core, err := s.core.OpenStream(s.ctx, request)
218+ core, err := s.core.OpenEnvelopeStream(s.ctx, request)
225219 if err != nil {
226220 _, _ = s.conn.CloseStream(m.Stream)
227221 if sendErr := s.refuseStream(m.Stream, err); sendErr != nil {
@@ -9,16 +9,14 @@ import (
9 "github.com/bnema/vev/internal/adapters/brokerwire"9 "github.com/bnema/vev/internal/adapters/brokerwire"
10 "github.com/bnema/vev/internal/adapters/sessionwire"10 "github.com/bnema/vev/internal/adapters/sessionwire"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol/wire"
12 )13 )
13 14
14 // Logical stream bridging.15 // Logical stream bridging.
15 //16 //
16-// One brokerwire logical stream carries the typed session protocol between a17+// One brokerwire logical stream carries the session protocol between client
17-// client and the daemon behind the broker. The broker terminates both hops: the18+// and daemon. The broker relays complete opaque envelope payloads through
18-// client's session connection is built by sessionwire over the stream's private19+// bounded framed carriages; neither preamble nor session message is decoded.
19-// bounded byte pipe, and the admitted broker core hands back its own typed
20-// connection to the daemon. Two relay goroutines carry typed messages between
21-// them, so no session byte is ever reinterpreted here.
22 //20 //
23 // Identity and cancellation stay per stream: a stream the core loses, a stream21 // Identity and cancellation stay per stream: a stream the core loses, a stream
24 // whose local consumer stalled, and a stream the peer closed each settle alone,22 // whose local consumer stalled, and a stream the peer closed each settle alone,
@@ -29,10 +27,9 @@ import (
29 type serverStream struct {27 type serverStream struct {
30 session *serverSession28 session *serverSession
31 id ports.BrokerStreamID29 id ports.BrokerStreamID
32- core ports.BrokerLogicalConnection30+ core ports.BrokerEnvelopeStream
33 pipe *streamPipe31 pipe *streamPipe
34 carriage *carriage32 carriage *carriage
35- wire ports.ServerConnection
36 33
37 mu sync.Mutex34 mu sync.Mutex
38 closed bool35 closed bool
@@ -40,17 +37,13 @@ type serverStream struct {
40 }37 }
41 38
42 // newServerStream builds one bridged stream over the admitted core connection.39 // newServerStream builds one bridged stream over the admitted core connection.
43-func newServerStream(s *serverSession, id ports.BrokerStreamID, core ports.BrokerLogicalConnection) (*serverStream, error) {40+func newServerStream(s *serverSession, id ports.BrokerStreamID, core ports.BrokerEnvelopeStream) (*serverStream, error) {
44 if s == nil || core == nil || id == 0 {41 if s == nil || core == nil || id == 0 {
45 return nil, ErrConfig42 return nil, ErrConfig
46 }43 }
47 st := &serverStream{session: s, id: id, core: core, done: make(chan struct{})}44 st := &serverStream{session: s, id: id, core: core, done: make(chan struct{})}
48 st.pipe = newStreamPipe(s.cfg.StreamInboundChunks, s.cfg.StreamInboundBytes, st.sendChunk)45 st.pipe = newStreamPipe(s.cfg.StreamInboundChunks, s.cfg.StreamInboundBytes, st.sendChunk)
49 st.carriage = newCarriage(st.pipe)46 st.carriage = newCarriage(st.pipe)
50- st.wire = sessionwire.NewServerConnection(st.carriage)
51- if st.wire == nil {
52- return nil, ErrConfig
53- }
54 return st, nil47 return st, nil
55 }48 }
56 49
@@ -85,34 +78,36 @@ func (st *serverStream) start() {
85 go st.watchCore()78 go st.watchCore()
86 }79 }
87 80
88-// relayClient carries client-to-daemon messages from the wire session81+// relayClient carries complete client envelopes without decoding.
89-// connection to the admitted core connection.
90 func (st *serverStream) relayClient() {82 func (st *serverStream) relayClient() {
91 defer st.session.wg.Done()83 defer st.session.wg.Done()
92 for {84 for {
93- message, err := st.wire.ReceiveClient()85+ envelope, err := st.carriage.RecvBounded(wire.AbsoluteEnvelopeLimit)
94 if err != nil {86 if err != nil {
95 st.fail(cancelOrCause(st.session.ctx, streamFailure(err)))87 st.fail(cancelOrCause(st.session.ctx, streamFailure(err)))
96 return88 return
97 }89 }
98- if err := st.core.SendClient(message); err != nil {90+ if err := st.core.SendEnvelope(envelope.Payload); err != nil {
99- st.fail(err)91+ st.fail(coreFailure(st.core, err))
100 return92 return
101 }93 }
102 }94 }
103 }95 }
104 96
105-// relayServer carries daemon-to-client messages from the admitted core97+// relayServer carries complete daemon envelopes without decoding.
106-// connection to the wire session connection.
107 func (st *serverStream) relayServer() {98 func (st *serverStream) relayServer() {
108 defer st.session.wg.Done()99 defer st.session.wg.Done()
109 for {100 for {
110- message, err := st.core.ReceiveServer()101+ payload, err := st.core.RecvEnvelope()
111 if err != nil {102 if err != nil {
112 st.fail(coreFailure(st.core, err))103 st.fail(coreFailure(st.core, err))
113 return104 return
114 }105 }
115- if err := st.wire.SendServer(message); err != nil {106+ if len(payload) == 0 || len(payload) > wire.AbsoluteEnvelopeLimit {
107+ st.fail(ErrProtocol)
108+ return
109+ }
110+ if err := st.carriage.Send(wire.Envelope{Payload: payload}); err != nil {
116 st.fail(streamFailure(err))111 st.fail(streamFailure(err))
117 return112 return
118 }113 }
@@ -121,10 +116,10 @@ func (st *serverStream) relayServer() {
121 116
122 // watchCore publishes the core connection's terminal outcome even when no read117 // watchCore publishes the core connection's terminal outcome even when no read
123 // is in flight. It settles the stream itself only for a failure: an orderly118 // is in flight. It settles the stream itself only for a failure: an orderly
124-// core end (Err()==nil) is left to relayServer, whose own core.ReceiveServer()119+// core end (Err()==nil) is left to relayServer, whose own core.RecvEnvelope()
125 // call observes the same terminal outcome right after forwarding any reply120 // call observes the same terminal outcome right after forwarding any reply
126 // that was already in flight, and settles the stream from there. Settling here121 // that was already in flight, and settles the stream from there. Settling here
127-// on every Done would race relayServer's in-flight wire.SendServer and could122+// on every Done would race relayServer's in-flight carriage.Send and could
128 // close the carriage while a received reply is still being written out to the123 // close the carriage while a received reply is still being written out to the
129 // client, discarding it.124 // client, discarding it.
130 func (st *serverStream) watchCore() {125 func (st *serverStream) watchCore() {
@@ -147,8 +142,7 @@ func (st *serverStream) fail(err error) {
147 }142 }
148 143
149 // close releases the stream exactly once: the pipe unblocks a stalled reader,144 // close releases the stream exactly once: the pipe unblocks a stalled reader,
150-// the wire session connection releases the carriage, and the core connection is145+// the carriage joins its writer, and the core connection closes.
151-// closed. It reports whether this call was the one that settled the stream.
152 func (st *serverStream) close(cause error) bool {146 func (st *serverStream) close(cause error) bool {
153 if st == nil {147 if st == nil {
154 return false148 return false
@@ -161,7 +155,7 @@ func (st *serverStream) close(cause error) bool {
161 st.closed = true155 st.closed = true
162 st.mu.Unlock()156 st.mu.Unlock()
163 st.pipe.closeWith(cause)157 st.pipe.closeWith(cause)
164- _ = st.wire.Close()158+ _ = st.carriage.Close()
165 _ = st.core.Close()159 _ = st.core.Close()
166 close(st.done)160 close(st.done)
167 return true161 return true
@@ -178,7 +172,7 @@ func streamFailure(err error) error {
178 172
179 // coreFailure classifies one core connection outcome. The core's typed terminal173 // coreFailure classifies one core connection outcome. The core's typed terminal
180 // error wins; an orderly end is a peer close.174 // error wins; an orderly end is a peer close.
181-func coreFailure(core ports.BrokerLogicalConnection, err error) error {175+func coreFailure(core ports.BrokerEnvelopeStream, err error) error {
182 if err == nil && core != nil {176 if err == nil && core != nil {
183 err = core.Err()177 err = core.Err()
184 }178 }
@@ -221,7 +215,7 @@ func (s *serverSession) startStream(m brokerwire.OpenStream) {
221 s.wg.Add(1)215 s.wg.Add(1)
222 go func() {216 go func() {
223 defer s.wg.Done()217 defer s.wg.Done()
224- core, err := s.core.OpenStream(s.ctx, request)218+ core, err := s.core.OpenEnvelopeStream(s.ctx, request)
225 if err != nil {219 if err != nil {
226 _, _ = s.conn.CloseStream(m.Stream)220 _, _ = s.conn.CloseStream(m.Stream)
227 if sendErr := s.refuseStream(m.Stream, err); sendErr != nil {221 if sendErr := s.refuseStream(m.Stream, err); sendErr != nil {
M internal/adapters/daemonmux/connector.go +1 -1
@@ -305,7 +305,7 @@ func (c *PhysicalConnection) Policy() ports.BrokerPolicy {
305305 // the LogicalConnector. A policy that differs in any field is refused as a
306306 // conflicting policy without touching the carriage, so a physical connection
307307 // can never multiplex a stream under a policy it did not negotiate.
308-func (c *PhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
308+func (c *PhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
309309 if c == nil || c.logical == nil {
310310 return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ErrLogicalConfig}
311311 }
@@ -305,7 +305,7 @@ func (c *PhysicalConnection) Policy() ports.BrokerPolicy {
305 // the LogicalConnector. A policy that differs in any field is refused as a305 // the LogicalConnector. A policy that differs in any field is refused as a
306 // conflicting policy without touching the carriage, so a physical connection306 // conflicting policy without touching the carriage, so a physical connection
307 // can never multiplex a stream under a policy it did not negotiate.307 // can never multiplex a stream under a policy it did not negotiate.
308-func (c *PhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {308+func (c *PhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
309 if c == nil || c.logical == nil {309 if c == nil || c.logical == nil {
310 return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ErrLogicalConfig}310 return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ErrLogicalConfig}
311 }311 }
M internal/adapters/daemonmux/connector_test.go +4 -4
@@ -292,7 +292,7 @@ func TestEndpointConnectorExposesAuthorityAndPolicyGuard(t *testing.T) {
292292 require.Equal(t, domain.RemoteFailureNone, physical.FailureKind())
293293
294294 // A mismatched request policy is refused before the wire is touched.
295- _, err = physical.OpenStream(context.Background(), muxOpenRequest(1, alternatePolicy(binding.Policy())))
295+ _, err = openTyped(physical, context.Background(), muxOpenRequest(1, alternatePolicy(binding.Policy())))
296296 require.Error(t, err)
297297 var typed ports.BrokerError
298298 require.ErrorAs(t, err, &typed)
@@ -300,7 +300,7 @@ func TestEndpointConnectorExposesAuthorityAndPolicyGuard(t *testing.T) {
300300 require.Zero(t, server.acceptedCount(), "a policy-mismatched open is never admitted")
301301
302302 // The matching open is admitted and usable over the real carriage.
303- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
303+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
304304 require.NoError(t, err)
305305 server.awaitAccepted(t, 1)
306306 require.NoError(t, logical.SendClient(protocol.Ping{}))
@@ -334,7 +334,7 @@ func TestEndpointConnectorDetachesSetupContext(t *testing.T) {
334334 cancel()
335335 // The setup context ended; the pooled connection must still work.
336336 require.False(t, channelClosed(physical.Done()))
337- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
337+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
338338 require.NoError(t, err)
339339 server.awaitAccepted(t, 1)
340340 require.NoError(t, logical.SendClient(protocol.Ping{}))
@@ -360,7 +360,7 @@ func TestPhysicalDoneBeforeLogicalLoss(t *testing.T) {
360360 t.Cleanup(func() { _ = physical.Close() })
361361 require.NoError(t, server.awaitAdoption(t))
362362
363- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
363+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
364364 require.NoError(t, err)
365365 server.awaitAccepted(t, 1)
366366 require.False(t, channelClosed(physical.Done()))
@@ -292,7 +292,7 @@ func TestEndpointConnectorExposesAuthorityAndPolicyGuard(t *testing.T) {
292 require.Equal(t, domain.RemoteFailureNone, physical.FailureKind())292 require.Equal(t, domain.RemoteFailureNone, physical.FailureKind())
293 293
294 // A mismatched request policy is refused before the wire is touched.294 // A mismatched request policy is refused before the wire is touched.
295- _, err = physical.OpenStream(context.Background(), muxOpenRequest(1, alternatePolicy(binding.Policy())))295+ _, err = openTyped(physical, context.Background(), muxOpenRequest(1, alternatePolicy(binding.Policy())))
296 require.Error(t, err)296 require.Error(t, err)
297 var typed ports.BrokerError297 var typed ports.BrokerError
298 require.ErrorAs(t, err, &typed)298 require.ErrorAs(t, err, &typed)
@@ -300,7 +300,7 @@ func TestEndpointConnectorExposesAuthorityAndPolicyGuard(t *testing.T) {
300 require.Zero(t, server.acceptedCount(), "a policy-mismatched open is never admitted")300 require.Zero(t, server.acceptedCount(), "a policy-mismatched open is never admitted")
301 301
302 // The matching open is admitted and usable over the real carriage.302 // The matching open is admitted and usable over the real carriage.
303- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))303+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
304 require.NoError(t, err)304 require.NoError(t, err)
305 server.awaitAccepted(t, 1)305 server.awaitAccepted(t, 1)
306 require.NoError(t, logical.SendClient(protocol.Ping{}))306 require.NoError(t, logical.SendClient(protocol.Ping{}))
@@ -334,7 +334,7 @@ func TestEndpointConnectorDetachesSetupContext(t *testing.T) {
334 cancel()334 cancel()
335 // The setup context ended; the pooled connection must still work.335 // The setup context ended; the pooled connection must still work.
336 require.False(t, channelClosed(physical.Done()))336 require.False(t, channelClosed(physical.Done()))
337- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))337+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
338 require.NoError(t, err)338 require.NoError(t, err)
339 server.awaitAccepted(t, 1)339 server.awaitAccepted(t, 1)
340 require.NoError(t, logical.SendClient(protocol.Ping{}))340 require.NoError(t, logical.SendClient(protocol.Ping{}))
@@ -360,7 +360,7 @@ func TestPhysicalDoneBeforeLogicalLoss(t *testing.T) {
360 t.Cleanup(func() { _ = physical.Close() })360 t.Cleanup(func() { _ = physical.Close() })
361 require.NoError(t, server.awaitAdoption(t))361 require.NoError(t, server.awaitAdoption(t))
362 362
363- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))363+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
364 require.NoError(t, err)364 require.NoError(t, err)
365 server.awaitAccepted(t, 1)365 server.awaitAccepted(t, 1)
366 require.False(t, channelClosed(physical.Done()))366 require.False(t, channelClosed(physical.Done()))
M internal/adapters/daemonmux/listener_test.go +2 -1
@@ -285,8 +285,9 @@ func servePongs(conn ports.ServerConnection) error {
285285 }
286286
287287 // listenerRoundTrip proves one typed exchange on a logical connection.
288-func listenerRoundTrip(t *testing.T, conn ports.ClientConnection) {
288+func listenerRoundTrip(t *testing.T, stream ports.BrokerEnvelopeStream) {
289289 t.Helper()
290+ conn := asTyped(stream)
290291 require.NoError(t, conn.SendClient(protocol.Ping{}))
291292 message, err := conn.ReceiveServer()
292293 require.NoError(t, err)
@@ -285,8 +285,9 @@ func servePongs(conn ports.ServerConnection) error {
285 }285 }
286 286
287 // listenerRoundTrip proves one typed exchange on a logical connection.287 // listenerRoundTrip proves one typed exchange on a logical connection.
288-func listenerRoundTrip(t *testing.T, conn ports.ClientConnection) {288+func listenerRoundTrip(t *testing.T, stream ports.BrokerEnvelopeStream) {
289 t.Helper()289 t.Helper()
290+ conn := asTyped(stream)
290 require.NoError(t, conn.SendClient(protocol.Ping{}))291 require.NoError(t, conn.SendClient(protocol.Ping{}))
291 message, err := conn.ReceiveServer()292 message, err := conn.ReceiveServer()
292 require.NoError(t, err)293 require.NoError(t, err)
M internal/adapters/daemonmux/logical_conn.go +32 -93
@@ -1,12 +1,8 @@
1-// Typed logical connections over one physical Pump.
1+// Logical connections over one physical Pump.
22 //
3-// A LogicalConnection is one independently cancellable typed session stream
4-// multiplexed over one physical daemonmux Pump. It implements
5-// ports.BrokerLogicalConnection by embedding the typed ports.ClientConnection
6-// that sessionwire builds on top of the mux stream, so the typed session
7-// protocol (4-byte framed Protobuf envelopes, the session preamble, and every
8-// semantic conversion) is reused verbatim: daemonmux never reinterprets a
9-// session message.
3+// A LogicalConnection carries complete raw session envelopes across the broker,
4+// without decoding them. Typed preview and observation are adapted by their
5+// consumers, never by this mux connection.
106 //
117 // The carriage between the two is a private, ordered, bounded byte stream
128 // (muxStreamPipe). Its read side pulls already-queued chunks from the pump
@@ -24,10 +20,6 @@
2420 // resets exactly that stream (one mux Reset) and leaves every sibling and the
2521 // physical connection untouched. Done, Err, and a physical-loss
2622 // ports.BrokerStreamLost are published once and stay stable afterwards.
27-//
28-// The narrow HandshakePlumbing seam exposes the accepted absolute 15 s
29-// handshake deadline and a completion hook from the wrapped session
30-// connection without changing ports or the daemon.
3123 package daemonmux
3224
3325 import (
@@ -35,13 +27,10 @@ import (
3527 "errors"
3628 "io"
3729 "sync"
38- "time"
3930
40- "github.com/bnema/vev/internal/adapters/sessionwire"
4131 "github.com/bnema/vev/internal/adapters/streamframe"
4232 "github.com/bnema/vev/internal/domain"
4333 "github.com/bnema/vev/internal/ports"
44- "github.com/bnema/vev/internal/protocol"
4534 "github.com/bnema/vev/internal/protocol/wire"
4635 )
4736
@@ -241,13 +230,9 @@ func discardQueuedInbound(pump *Pump, physical PhysicalStreamID) {
241230 }
242231 }
243232
244-// LogicalConnection is one typed client-side logical stream over a physical
245-// Pump. It implements ports.BrokerLogicalConnection: the embedded typed
246-// ports.ClientConnection carries the session protocol, while Close, Done, and
247-// Err add the stream lifecycle. It is safe for concurrent use.
233+// LogicalConnection is one client-side logical stream over a physical Pump.
234+// It implements the raw envelope port; Close, Done and Err manage its lifetime.
248235 type LogicalConnection struct {
249- ports.ClientConnection
250-
251236 pump *Pump
252237 ref StreamRef
253238 pipe *muxStreamPipe
@@ -258,22 +243,16 @@ type LogicalConnection struct {
258243 // keeps its exact cause even after the record is evicted.
259244 cause *terminalState
260245
261- terminal *terminalState
262- // plumbing is the wrapped session connection's handshake seam, resolved
263- // once at construction; nil when the wrapped connection lacks it.
264- plumbing HandshakePlumbing
265- // deadline is the exact absolute handshake deadline of the wrapped session
266- // connection, resolved once at construction.
267- deadline time.Time
246+ terminal *terminalState
268247 watchDone chan struct{}
269248 closeOnce sync.Once
270249 closeErr error
271250 }
272251
273-var _ ports.BrokerLogicalConnection = (*LogicalConnection)(nil)
252+var _ ports.BrokerEnvelopeStream = (*LogicalConnection)(nil)
274253
275-// newLogicalConnection builds the typed session channel over one confirmed
276-// open stream and starts the terminal watcher. It never blocks. cause is the
254+// newLogicalConnection builds the framed carriage over one confirmed open
255+// stream and starts the terminal watcher. It never blocks. cause is the
277256 // stream's retained terminal authority, captured at open so the connection
278257 // reports its exact outcome independent of the engine's bounded record.
279258 func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{}, cause *terminalState) *LogicalConnection {
@@ -281,21 +260,14 @@ func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{},
281260 pipe := newMuxStreamPipe(pump, ref.Physical, limit, streamDone)
282261 raw := newMuxTransport(pipe)
283262 connection := &LogicalConnection{
284- ClientConnection: sessionwire.NewClientConnection(raw),
285- pump: pump,
286- ref: ref,
287- pipe: pipe,
288- raw: raw,
289- stream: streamDone,
290- cause: cause,
291- terminal: newTerminalState(),
292- watchDone: make(chan struct{}),
293- }
294- if plumbing, ok := connection.ClientConnection.(HandshakePlumbing); ok {
295- connection.plumbing = plumbing
296- connection.deadline = plumbing.HandshakeDeadline()
297- } else {
298- connection.deadline = time.Now().Add(protocol.HandshakeTimeout)
263+ pump: pump,
264+ ref: ref,
265+ pipe: pipe,
266+ raw: raw,
267+ stream: streamDone,
268+ cause: cause,
269+ terminal: newTerminalState(),
270+ watchDone: make(chan struct{}),
299271 }
300272 go connection.watch()
301273 return connection
@@ -304,25 +276,6 @@ func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{},
304276 // Ref returns the complete stream reference recorded at admission.
305277 func (c *LogicalConnection) Ref() StreamRef { return c.ref }
306278
307-// HandshakeDeadline returns the accepted absolute local deadline of the
308-// session handshake, plumbing the exact 15 s deadline the wrapped session
309-// connection started with. It is resolved once at construction and stable for
310-// the connection's lifetime.
311-func (c *LogicalConnection) HandshakeDeadline() time.Time { return c.deadline }
312-
313-// OnHandshakeComplete registers fn to run exactly once when the session
314-// handshake completes, plumbing the wrapped session connection's completion
315-// hook. Registering after completion runs fn immediately.
316-func (c *LogicalConnection) OnHandshakeComplete(fn func()) {
317- if c.plumbing != nil {
318- c.plumbing.OnHandshakeComplete(fn)
319- return
320- }
321- if fn != nil {
322- fn()
323- }
324-}
325-
326279 // Done returns the logical terminal channel independent of reads. It closes
327280 // exactly once, before Err becomes stable.
328281 func (c *LogicalConnection) Done() <-chan struct{} { return c.terminal.Done() }
@@ -335,39 +288,26 @@ func (c *LogicalConnection) Err() error { return c.terminal.Err() }
335288 // FailureKind classifies Err. It is None while open and for an orderly Close.
336289 func (c *LogicalConnection) FailureKind() domain.RemoteFailureKind { return c.terminal.FailureKind() }
337290
338-// SendClient sends one typed client message over the logical stream. It
339-// classifies a terminal transport failure and otherwise returns the session
340-// error verbatim.
341-func (c *LogicalConnection) SendClient(message protocol.ClientMessage) error {
291+func (c *LogicalConnection) SendEnvelope(payload []byte) error {
342292 if c.settled() {
343293 return c.terminalError()
344294 }
345- if err := c.ClientConnection.SendClient(message); err != nil {
346- if c.settled() {
347- return c.terminalError()
348- }
349- return err
295+ if len(payload) > wire.AbsoluteEnvelopeLimit {
296+ return streamframe.ErrTooLarge
350297 }
351- return nil
298+ return c.raw.Send(wire.Envelope{Payload: payload})
352299 }
353300
354-// ReceiveServer receives one typed server message. An orderly end of the inner
355-// stream returns io.EOF; any other inner-stream error is treated as a
356-// malformed session frame: exactly this stream is reset towards the peer, its
357-// terminal outcome is published, and the error is returned.
358-func (c *LogicalConnection) ReceiveServer() (protocol.ServerMessage, error) {
359- if c.settled() {
360- return nil, c.terminalError()
361- }
362- message, err := c.ClientConnection.ReceiveServer()
363- if err == nil {
364- return message, nil
365- }
366- if errors.Is(err, io.EOF) {
367- c.syncFromEngine()
368- return nil, c.terminalError()
301+func (c *LogicalConnection) RecvEnvelope() ([]byte, error) {
302+ envelope, err := c.raw.Recv()
303+ if err != nil {
304+ if errors.Is(err, io.EOF) {
305+ c.syncFromEngine()
306+ return nil, c.terminalError()
307+ }
308+ return nil, c.abortMalformed(err)
369309 }
370- return nil, c.abortMalformed(err)
310+ return envelope.Payload, nil
371311 }
372312
373313 // Close independently closes the logical stream: it discards the inbound
@@ -660,8 +600,7 @@ func (s *muxStreamPipe) Close() error {
660600 }
661601
662602 // muxTransport adapts the ordered byte stream to wire.Transport: streamframe
663-// owns the 4-byte length framing, and sessionwire owns the typed conversion on
664-// top. Closing it closes the byte stream and joins streamframe's writer.
603+// owns the 4-byte length framing, and sessionwire owns any typed conversion at the consuming endpoint. Closing it closes the byte stream and joins streamframe's writer.
665604 type muxTransport struct {
666605 pipe *muxStreamPipe
667606 framer *streamframe.Framer
@@ -1,12 +1,8 @@
1-// Typed logical connections over one physical Pump.1+// Logical connections over one physical Pump.
2 //2 //
3-// A LogicalConnection is one independently cancellable typed session stream3+// A LogicalConnection carries complete raw session envelopes across the broker,
4-// multiplexed over one physical daemonmux Pump. It implements4+// without decoding them. Typed preview and observation are adapted by their
5-// ports.BrokerLogicalConnection by embedding the typed ports.ClientConnection5+// consumers, never by this mux connection.
6-// that sessionwire builds on top of the mux stream, so the typed session
7-// protocol (4-byte framed Protobuf envelopes, the session preamble, and every
8-// semantic conversion) is reused verbatim: daemonmux never reinterprets a
9-// session message.
10 //6 //
11 // The carriage between the two is a private, ordered, bounded byte stream7 // The carriage between the two is a private, ordered, bounded byte stream
12 // (muxStreamPipe). Its read side pulls already-queued chunks from the pump8 // (muxStreamPipe). Its read side pulls already-queued chunks from the pump
@@ -24,10 +20,6 @@
24 // resets exactly that stream (one mux Reset) and leaves every sibling and the20 // resets exactly that stream (one mux Reset) and leaves every sibling and the
25 // physical connection untouched. Done, Err, and a physical-loss21 // physical connection untouched. Done, Err, and a physical-loss
26 // ports.BrokerStreamLost are published once and stay stable afterwards.22 // ports.BrokerStreamLost are published once and stay stable afterwards.
27-//
28-// The narrow HandshakePlumbing seam exposes the accepted absolute 15 s
29-// handshake deadline and a completion hook from the wrapped session
30-// connection without changing ports or the daemon.
31 package daemonmux23 package daemonmux
32 24
33 import (25 import (
@@ -35,13 +27,10 @@ import (
35 "errors"27 "errors"
36 "io"28 "io"
37 "sync"29 "sync"
38- "time"
39 30
40- "github.com/bnema/vev/internal/adapters/sessionwire"
41 "github.com/bnema/vev/internal/adapters/streamframe"31 "github.com/bnema/vev/internal/adapters/streamframe"
42 "github.com/bnema/vev/internal/domain"32 "github.com/bnema/vev/internal/domain"
43 "github.com/bnema/vev/internal/ports"33 "github.com/bnema/vev/internal/ports"
44- "github.com/bnema/vev/internal/protocol"
45 "github.com/bnema/vev/internal/protocol/wire"34 "github.com/bnema/vev/internal/protocol/wire"
46 )35 )
47 36
@@ -241,13 +230,9 @@ func discardQueuedInbound(pump *Pump, physical PhysicalStreamID) {
241 }230 }
242 }231 }
243 232
244-// LogicalConnection is one typed client-side logical stream over a physical233+// LogicalConnection is one client-side logical stream over a physical Pump.
245-// Pump. It implements ports.BrokerLogicalConnection: the embedded typed234+// It implements the raw envelope port; Close, Done and Err manage its lifetime.
246-// ports.ClientConnection carries the session protocol, while Close, Done, and
247-// Err add the stream lifecycle. It is safe for concurrent use.
248 type LogicalConnection struct {235 type LogicalConnection struct {
249- ports.ClientConnection
250-
251 pump *Pump236 pump *Pump
252 ref StreamRef237 ref StreamRef
253 pipe *muxStreamPipe238 pipe *muxStreamPipe
@@ -258,22 +243,16 @@ type LogicalConnection struct {
258 // keeps its exact cause even after the record is evicted.243 // keeps its exact cause even after the record is evicted.
259 cause *terminalState244 cause *terminalState
260 245
261- terminal *terminalState246+ terminal *terminalState
262- // plumbing is the wrapped session connection's handshake seam, resolved
263- // once at construction; nil when the wrapped connection lacks it.
264- plumbing HandshakePlumbing
265- // deadline is the exact absolute handshake deadline of the wrapped session
266- // connection, resolved once at construction.
267- deadline time.Time
268 watchDone chan struct{}247 watchDone chan struct{}
269 closeOnce sync.Once248 closeOnce sync.Once
270 closeErr error249 closeErr error
271 }250 }
272 251
273-var _ ports.BrokerLogicalConnection = (*LogicalConnection)(nil)252+var _ ports.BrokerEnvelopeStream = (*LogicalConnection)(nil)
274 253
275-// newLogicalConnection builds the typed session channel over one confirmed254+// newLogicalConnection builds the framed carriage over one confirmed open
276-// open stream and starts the terminal watcher. It never blocks. cause is the255+// stream and starts the terminal watcher. It never blocks. cause is the
277 // stream's retained terminal authority, captured at open so the connection256 // stream's retained terminal authority, captured at open so the connection
278 // reports its exact outcome independent of the engine's bounded record.257 // reports its exact outcome independent of the engine's bounded record.
279 func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{}, cause *terminalState) *LogicalConnection {258 func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{}, cause *terminalState) *LogicalConnection {
@@ -281,21 +260,14 @@ func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{},
281 pipe := newMuxStreamPipe(pump, ref.Physical, limit, streamDone)260 pipe := newMuxStreamPipe(pump, ref.Physical, limit, streamDone)
282 raw := newMuxTransport(pipe)261 raw := newMuxTransport(pipe)
283 connection := &LogicalConnection{262 connection := &LogicalConnection{
284- ClientConnection: sessionwire.NewClientConnection(raw),263+ pump: pump,
285- pump: pump,264+ ref: ref,
286- ref: ref,265+ pipe: pipe,
287- pipe: pipe,266+ raw: raw,
288- raw: raw,267+ stream: streamDone,
289- stream: streamDone,268+ cause: cause,
290- cause: cause,269+ terminal: newTerminalState(),
291- terminal: newTerminalState(),270+ watchDone: make(chan struct{}),
292- watchDone: make(chan struct{}),
293- }
294- if plumbing, ok := connection.ClientConnection.(HandshakePlumbing); ok {
295- connection.plumbing = plumbing
296- connection.deadline = plumbing.HandshakeDeadline()
297- } else {
298- connection.deadline = time.Now().Add(protocol.HandshakeTimeout)
299 }271 }
300 go connection.watch()272 go connection.watch()
301 return connection273 return connection
@@ -304,25 +276,6 @@ func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{},
304 // Ref returns the complete stream reference recorded at admission.276 // Ref returns the complete stream reference recorded at admission.
305 func (c *LogicalConnection) Ref() StreamRef { return c.ref }277 func (c *LogicalConnection) Ref() StreamRef { return c.ref }
306 278
307-// HandshakeDeadline returns the accepted absolute local deadline of the
308-// session handshake, plumbing the exact 15 s deadline the wrapped session
309-// connection started with. It is resolved once at construction and stable for
310-// the connection's lifetime.
311-func (c *LogicalConnection) HandshakeDeadline() time.Time { return c.deadline }
312-
313-// OnHandshakeComplete registers fn to run exactly once when the session
314-// handshake completes, plumbing the wrapped session connection's completion
315-// hook. Registering after completion runs fn immediately.
316-func (c *LogicalConnection) OnHandshakeComplete(fn func()) {
317- if c.plumbing != nil {
318- c.plumbing.OnHandshakeComplete(fn)
319- return
320- }
321- if fn != nil {
322- fn()
323- }
324-}
325-
326 // Done returns the logical terminal channel independent of reads. It closes279 // Done returns the logical terminal channel independent of reads. It closes
327 // exactly once, before Err becomes stable.280 // exactly once, before Err becomes stable.
328 func (c *LogicalConnection) Done() <-chan struct{} { return c.terminal.Done() }281 func (c *LogicalConnection) Done() <-chan struct{} { return c.terminal.Done() }
@@ -335,39 +288,26 @@ func (c *LogicalConnection) Err() error { return c.terminal.Err() }
335 // FailureKind classifies Err. It is None while open and for an orderly Close.288 // FailureKind classifies Err. It is None while open and for an orderly Close.
336 func (c *LogicalConnection) FailureKind() domain.RemoteFailureKind { return c.terminal.FailureKind() }289 func (c *LogicalConnection) FailureKind() domain.RemoteFailureKind { return c.terminal.FailureKind() }
337 290
338-// SendClient sends one typed client message over the logical stream. It291+func (c *LogicalConnection) SendEnvelope(payload []byte) error {
339-// classifies a terminal transport failure and otherwise returns the session
340-// error verbatim.
341-func (c *LogicalConnection) SendClient(message protocol.ClientMessage) error {
342 if c.settled() {292 if c.settled() {
343 return c.terminalError()293 return c.terminalError()
344 }294 }
345- if err := c.ClientConnection.SendClient(message); err != nil {295+ if len(payload) > wire.AbsoluteEnvelopeLimit {
346- if c.settled() {296+ return streamframe.ErrTooLarge
347- return c.terminalError()
348- }
349- return err
350 }297 }
351- return nil298+ return c.raw.Send(wire.Envelope{Payload: payload})
352 }299 }
353 300
354-// ReceiveServer receives one typed server message. An orderly end of the inner301+func (c *LogicalConnection) RecvEnvelope() ([]byte, error) {
355-// stream returns io.EOF; any other inner-stream error is treated as a302+ envelope, err := c.raw.Recv()
356-// malformed session frame: exactly this stream is reset towards the peer, its303+ if err != nil {
357-// terminal outcome is published, and the error is returned.304+ if errors.Is(err, io.EOF) {
358-func (c *LogicalConnection) ReceiveServer() (protocol.ServerMessage, error) {305+ c.syncFromEngine()
359- if c.settled() {306+ return nil, c.terminalError()
360- return nil, c.terminalError()307+ }
361- }308+ return nil, c.abortMalformed(err)
362- message, err := c.ClientConnection.ReceiveServer()
363- if err == nil {
364- return message, nil
365- }
366- if errors.Is(err, io.EOF) {
367- c.syncFromEngine()
368- return nil, c.terminalError()
369 }309 }
370- return nil, c.abortMalformed(err)310+ return envelope.Payload, nil
371 }311 }
372 312
373 // Close independently closes the logical stream: it discards the inbound313 // Close independently closes the logical stream: it discards the inbound
@@ -660,8 +600,7 @@ func (s *muxStreamPipe) Close() error {
660 }600 }
661 601
662 // muxTransport adapts the ordered byte stream to wire.Transport: streamframe602 // muxTransport adapts the ordered byte stream to wire.Transport: streamframe
663-// owns the 4-byte length framing, and sessionwire owns the typed conversion on603+// owns the 4-byte length framing, and sessionwire owns any typed conversion at the consuming endpoint. Closing it closes the byte stream and joins streamframe's writer.
664-// top. Closing it closes the byte stream and joins streamframe's writer.
665 type muxTransport struct {604 type muxTransport struct {
666 pipe *muxStreamPipe605 pipe *muxStreamPipe
667 framer *streamframe.Framer606 framer *streamframe.Framer
M internal/adapters/daemonmux/logical_conn_test.go +8 -56
@@ -318,13 +318,13 @@ func (a *daemonAcceptor) closePeers() {
318318 }
319319 }
320320
321-func openWithDeadline(t *testing.T, connector *LogicalConnector, stream int) *LogicalConnection {
321+func openWithDeadline(t *testing.T, connector *LogicalConnector, stream int) typedLogical {
322322 t.Helper()
323323 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
324324 defer cancel()
325325 connection, err := connector.Open(ctx, controlRequest(stream))
326326 require.NoError(t, err)
327- return connection
327+ return typedLogical{typedStream: asTyped(connection), LogicalConnection: connection}
328328 }
329329
330330 // watchRegistered reports whether the pump still holds an inbound watch entry
@@ -406,7 +406,7 @@ func TestLogicalTwoConnections(t *testing.T) {
406406 acceptor.start(PhysicalStreamID(i), deadline)
407407 }
408408
409- connections := make([]*LogicalConnection, 0, count)
409+ connections := make([]typedLogical, 0, count)
410410 for i := 1; i <= count; i++ {
411411 connection := openWithDeadline(t, connector, i)
412412 require.NoError(t, connection.SendClient(protocol.Ping{}), "connection %d", i)
@@ -487,11 +487,12 @@ func TestLogicalHundredConnectionsConcurrentAdmission(t *testing.T) {
487487 traffic.Add(1)
488488 go func(index int) {
489489 defer traffic.Done()
490- if err := connections[index].SendClient(protocol.Ping{}); err != nil {
490+ typed := asTyped(connections[index])
491+ if err := typed.SendClient(protocol.Ping{}); err != nil {
491492 trafficErrs[index] = err
492493 return
493494 }
494- message, err := connections[index].ReceiveServer()
495+ message, err := typed.ReceiveServer()
495496 if err != nil {
496497 trafficErrs[index] = err
497498 return
@@ -802,7 +803,7 @@ func TestLogicalPhysicalLossFanout(t *testing.T) {
802803 for i := 1; i <= count; i++ {
803804 acceptor.start(PhysicalStreamID(i), deadline)
804805 }
805- connections := make([]*LogicalConnection, 0, count)
806+ connections := make([]typedLogical, 0, count)
806807 for i := 1; i <= count; i++ {
807808 connection := openWithDeadline(t, connector, i)
808809 require.NoError(t, connection.SendClient(protocol.Ping{}))
@@ -832,55 +833,6 @@ func TestLogicalPhysicalLossFanout(t *testing.T) {
832833 require.Equal(t, domain.RemoteFailureTransport, brokerPump.FailureKind())
833834 }
834835
835-// TestLogicalHandshakeDeadlinePlumbing proves the accepted absolute 15 s
836-// handshake deadline and the completion hook are exposed without restarting a
837-// deadline.
838-func TestLogicalHandshakeDeadlinePlumbing(t *testing.T) {
839- brokerPump, daemonPump := newPairedPumps(t, DefaultMuxCeilings())
840- connector := mustConnector(t, brokerPump)
841- deadline := time.Now().Add(15 * time.Second)
842-
843- acceptor := newDaemonAcceptor(daemonPump, 1)
844- acceptor.start(1, deadline)
845-
846- connection := openWithDeadline(t, connector, 1)
847- require.NoError(t, acceptor.wait(1, deadline))
848-
849- accepted := connection.HandshakeDeadline()
850- require.False(t, accepted.IsZero())
851- require.WithinDuration(t, time.Now().Add(protocol.HandshakeTimeout), accepted, 5*time.Second)
852- require.Equal(t, accepted, connection.HandshakeDeadline(), "deadline is stable")
853-
854- completed := make(chan struct{}, 2)
855- connection.OnHandshakeComplete(func() { completed <- struct{}{} })
856- select {
857- case <-completed:
858- t.Fatal("completion hook fired before the handshake ran")
859- default:
860- }
861-
862- require.NoError(t, connection.SendClient(protocol.Ping{}))
863- message, err := connection.ReceiveServer()
864- require.NoError(t, err)
865- require.Equal(t, protocol.Pong{}, message)
866-
867- select {
868- case <-completed:
869- case <-time.After(5 * time.Second):
870- t.Fatal("completion hook did not fire after the handshake")
871- }
872-
873- // Registering after completion runs immediately.
874- late := make(chan struct{}, 1)
875- connection.OnHandshakeComplete(func() { late <- struct{}{} })
876- select {
877- case <-late:
878- case <-time.After(5 * time.Second):
879- t.Fatal("late completion hook did not run")
880- }
881-}
882-
883-// TestLogicalNoGoroutineLeak proves opening, using, and closing logical
884836 // connections leaves no goroutine behind.
885837 func TestLogicalNoGoroutineLeak(t *testing.T) {
886838 before := runtime.NumGoroutine()
@@ -894,7 +846,7 @@ func TestLogicalNoGoroutineLeak(t *testing.T) {
894846 for i := 1; i <= count; i++ {
895847 acceptor.start(PhysicalStreamID(i), deadline)
896848 }
897- connections := make([]*LogicalConnection, 0, count)
849+ connections := make([]typedLogical, 0, count)
898850 for i := 1; i <= count; i++ {
899851 connection := openWithDeadline(t, connector, i)
900852 require.NoError(t, connection.SendClient(protocol.Ping{}))
@@ -318,13 +318,13 @@ func (a *daemonAcceptor) closePeers() {
318 }318 }
319 }319 }
320 320
321-func openWithDeadline(t *testing.T, connector *LogicalConnector, stream int) *LogicalConnection {321+func openWithDeadline(t *testing.T, connector *LogicalConnector, stream int) typedLogical {
322 t.Helper()322 t.Helper()
323 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)323 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
324 defer cancel()324 defer cancel()
325 connection, err := connector.Open(ctx, controlRequest(stream))325 connection, err := connector.Open(ctx, controlRequest(stream))
326 require.NoError(t, err)326 require.NoError(t, err)
327- return connection327+ return typedLogical{typedStream: asTyped(connection), LogicalConnection: connection}
328 }328 }
329 329
330 // watchRegistered reports whether the pump still holds an inbound watch entry330 // watchRegistered reports whether the pump still holds an inbound watch entry
@@ -406,7 +406,7 @@ func TestLogicalTwoConnections(t *testing.T) {
406 acceptor.start(PhysicalStreamID(i), deadline)406 acceptor.start(PhysicalStreamID(i), deadline)
407 }407 }
408 408
409- connections := make([]*LogicalConnection, 0, count)409+ connections := make([]typedLogical, 0, count)
410 for i := 1; i <= count; i++ {410 for i := 1; i <= count; i++ {
411 connection := openWithDeadline(t, connector, i)411 connection := openWithDeadline(t, connector, i)
412 require.NoError(t, connection.SendClient(protocol.Ping{}), "connection %d", i)412 require.NoError(t, connection.SendClient(protocol.Ping{}), "connection %d", i)
@@ -487,11 +487,12 @@ func TestLogicalHundredConnectionsConcurrentAdmission(t *testing.T) {
487 traffic.Add(1)487 traffic.Add(1)
488 go func(index int) {488 go func(index int) {
489 defer traffic.Done()489 defer traffic.Done()
490- if err := connections[index].SendClient(protocol.Ping{}); err != nil {490+ typed := asTyped(connections[index])
491+ if err := typed.SendClient(protocol.Ping{}); err != nil {
491 trafficErrs[index] = err492 trafficErrs[index] = err
492 return493 return
493 }494 }
494- message, err := connections[index].ReceiveServer()495+ message, err := typed.ReceiveServer()
495 if err != nil {496 if err != nil {
496 trafficErrs[index] = err497 trafficErrs[index] = err
497 return498 return
@@ -802,7 +803,7 @@ func TestLogicalPhysicalLossFanout(t *testing.T) {
802 for i := 1; i <= count; i++ {803 for i := 1; i <= count; i++ {
803 acceptor.start(PhysicalStreamID(i), deadline)804 acceptor.start(PhysicalStreamID(i), deadline)
804 }805 }
805- connections := make([]*LogicalConnection, 0, count)806+ connections := make([]typedLogical, 0, count)
806 for i := 1; i <= count; i++ {807 for i := 1; i <= count; i++ {
807 connection := openWithDeadline(t, connector, i)808 connection := openWithDeadline(t, connector, i)
808 require.NoError(t, connection.SendClient(protocol.Ping{}))809 require.NoError(t, connection.SendClient(protocol.Ping{}))
@@ -832,55 +833,6 @@ func TestLogicalPhysicalLossFanout(t *testing.T) {
832 require.Equal(t, domain.RemoteFailureTransport, brokerPump.FailureKind())833 require.Equal(t, domain.RemoteFailureTransport, brokerPump.FailureKind())
833 }834 }
834 835
835-// TestLogicalHandshakeDeadlinePlumbing proves the accepted absolute 15 s
836-// handshake deadline and the completion hook are exposed without restarting a
837-// deadline.
838-func TestLogicalHandshakeDeadlinePlumbing(t *testing.T) {
839- brokerPump, daemonPump := newPairedPumps(t, DefaultMuxCeilings())
840- connector := mustConnector(t, brokerPump)
841- deadline := time.Now().Add(15 * time.Second)
842-
843- acceptor := newDaemonAcceptor(daemonPump, 1)
844- acceptor.start(1, deadline)
845-
846- connection := openWithDeadline(t, connector, 1)
847- require.NoError(t, acceptor.wait(1, deadline))
848-
849- accepted := connection.HandshakeDeadline()
850- require.False(t, accepted.IsZero())
851- require.WithinDuration(t, time.Now().Add(protocol.HandshakeTimeout), accepted, 5*time.Second)
852- require.Equal(t, accepted, connection.HandshakeDeadline(), "deadline is stable")
853-
854- completed := make(chan struct{}, 2)
855- connection.OnHandshakeComplete(func() { completed <- struct{}{} })
856- select {
857- case <-completed:
858- t.Fatal("completion hook fired before the handshake ran")
859- default:
860- }
861-
862- require.NoError(t, connection.SendClient(protocol.Ping{}))
863- message, err := connection.ReceiveServer()
864- require.NoError(t, err)
865- require.Equal(t, protocol.Pong{}, message)
866-
867- select {
868- case <-completed:
869- case <-time.After(5 * time.Second):
870- t.Fatal("completion hook did not fire after the handshake")
871- }
872-
873- // Registering after completion runs immediately.
874- late := make(chan struct{}, 1)
875- connection.OnHandshakeComplete(func() { late <- struct{}{} })
876- select {
877- case <-late:
878- case <-time.After(5 * time.Second):
879- t.Fatal("late completion hook did not run")
880- }
881-}
882-
883-// TestLogicalNoGoroutineLeak proves opening, using, and closing logical
884 // connections leaves no goroutine behind.836 // connections leaves no goroutine behind.
885 func TestLogicalNoGoroutineLeak(t *testing.T) {837 func TestLogicalNoGoroutineLeak(t *testing.T) {
886 before := runtime.NumGoroutine()838 before := runtime.NumGoroutine()
@@ -894,7 +846,7 @@ func TestLogicalNoGoroutineLeak(t *testing.T) {
894 for i := 1; i <= count; i++ {846 for i := 1; i <= count; i++ {
895 acceptor.start(PhysicalStreamID(i), deadline)847 acceptor.start(PhysicalStreamID(i), deadline)
896 }848 }
897- connections := make([]*LogicalConnection, 0, count)849+ connections := make([]typedLogical, 0, count)
898 for i := 1; i <= count; i++ {850 for i := 1; i <= count; i++ {
899 connection := openWithDeadline(t, connector, i)851 connection := openWithDeadline(t, connector, i)
900 require.NoError(t, connection.SendClient(protocol.Ping{}))852 require.NoError(t, connection.SendClient(protocol.Ping{}))
M internal/adapters/daemonmux/pool_integration_test.go +4 -4
@@ -75,12 +75,12 @@ func TestPoolSharesCompatibleAliasesOverOnePhysical(t *testing.T) {
7575 client, err := pool.RegisterClient()
7676 require.NoError(t, err)
7777
78- first, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
78+ first, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
7979 require.NoError(t, err)
8080 require.NotNil(t, first)
8181 t.Cleanup(func() { _ = first.Close() })
8282
83- second, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasB@host:22", 2, policy))
83+ second, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasB@host:22", 2, policy))
8484 require.NoError(t, err)
8585 require.NotNil(t, second)
8686 t.Cleanup(func() { _ = second.Close() })
@@ -114,7 +114,7 @@ func TestPoolRefusesConflictingPolicyWithoutDial(t *testing.T) {
114114
115115 client, err := pool.RegisterClient()
116116 require.NoError(t, err)
117- _, err = pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, alternatePolicy(binding.Policy())))
117+ _, err = openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, alternatePolicy(binding.Policy())))
118118 require.Error(t, err)
119119 var typed ports.BrokerError
120120 require.ErrorAs(t, err, &typed)
@@ -142,7 +142,7 @@ func TestPoolSurfacesPhysicalLossPerStream(t *testing.T) {
142142
143143 client, err := pool.RegisterClient()
144144 require.NoError(t, err)
145- stream, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
145+ stream, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
146146 require.NoError(t, err)
147147 server.awaitAccepted(t, 1)
148148
@@ -75,12 +75,12 @@ func TestPoolSharesCompatibleAliasesOverOnePhysical(t *testing.T) {
75 client, err := pool.RegisterClient()75 client, err := pool.RegisterClient()
76 require.NoError(t, err)76 require.NoError(t, err)
77 77
78- first, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))78+ first, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
79 require.NoError(t, err)79 require.NoError(t, err)
80 require.NotNil(t, first)80 require.NotNil(t, first)
81 t.Cleanup(func() { _ = first.Close() })81 t.Cleanup(func() { _ = first.Close() })
82 82
83- second, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasB@host:22", 2, policy))83+ second, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasB@host:22", 2, policy))
84 require.NoError(t, err)84 require.NoError(t, err)
85 require.NotNil(t, second)85 require.NotNil(t, second)
86 t.Cleanup(func() { _ = second.Close() })86 t.Cleanup(func() { _ = second.Close() })
@@ -114,7 +114,7 @@ func TestPoolRefusesConflictingPolicyWithoutDial(t *testing.T) {
114 114
115 client, err := pool.RegisterClient()115 client, err := pool.RegisterClient()
116 require.NoError(t, err)116 require.NoError(t, err)
117- _, err = pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, alternatePolicy(binding.Policy())))117+ _, err = openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, alternatePolicy(binding.Policy())))
118 require.Error(t, err)118 require.Error(t, err)
119 var typed ports.BrokerError119 var typed ports.BrokerError
120 require.ErrorAs(t, err, &typed)120 require.ErrorAs(t, err, &typed)
@@ -142,7 +142,7 @@ func TestPoolSurfacesPhysicalLossPerStream(t *testing.T) {
142 142
143 client, err := pool.RegisterClient()143 client, err := pool.RegisterClient()
144 require.NoError(t, err)144 require.NoError(t, err)
145- stream, err := pool.OpenStream(context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))145+ stream, err := openTyped(pool, context.Background(), p3cRemoteRequest(client, "aliasA@host:22", 1, policy))
146 require.NoError(t, err)146 require.NoError(t, err)
147 server.awaitAccepted(t, 1)147 server.awaitAccepted(t, 1)
148 148
M internal/adapters/daemonmux/quic_carriage_test.go +7 -7
@@ -195,10 +195,10 @@ func TestQUICCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
195195 require.NoError(t, daemon.awaitAdoption(t))
196196
197197 // Two independent typed admissions over the one physical carriage.
198- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))
198+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
199199 require.NoError(t, err)
200200 t.Cleanup(func() { _ = first.Close() })
201- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))
201+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
202202 require.NoError(t, err)
203203 t.Cleanup(func() { _ = second.Close() })
204204 daemon.awaitQUICAccepted(t, 2)
@@ -219,7 +219,7 @@ func TestQUICCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
219219 wg.Add(1)
220220 go func(i int) {
221221 defer wg.Done()
222- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))
222+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
223223 }(i)
224224 }
225225 wg.Wait()
@@ -375,11 +375,11 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
375375
376376 lost := make([]ports.BrokerLogicalConnection, 3)
377377 for i := range lost {
378- lost[i], err = physicalA.OpenStream(context.Background(), muxOpenRequest(uint64(i+1), policy))
378+ lost[i], err = openTyped(physicalA, context.Background(), muxOpenRequest(uint64(i+1), policy))
379379 require.NoError(t, err, "stream %d", i+1)
380380 t.Cleanup(func() { _ = lost[i].Close() })
381381 }
382- survivor, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, policy))
382+ survivor, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, policy))
383383 require.NoError(t, err)
384384 t.Cleanup(func() { _ = survivor.Close() })
385385 daemon.awaitQUICAccepted(t, 4)
@@ -429,7 +429,7 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
429429 "the lost QUIC physical child was never reaped")
430430
431431 // The lost physical refuses a fresh stream.
432- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(9, policy))
432+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(9, policy))
433433 require.Error(t, err)
434434 }
435435
@@ -455,7 +455,7 @@ func TestQUICCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) {
455455
456456 cancel()
457457 require.False(t, channelClosed(physical.Done()))
458- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
458+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
459459 require.NoError(t, err)
460460 t.Cleanup(func() { _ = logical.Close() })
461461 daemon.awaitQUICAccepted(t, 1)
@@ -195,10 +195,10 @@ func TestQUICCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
195 require.NoError(t, daemon.awaitAdoption(t))195 require.NoError(t, daemon.awaitAdoption(t))
196 196
197 // Two independent typed admissions over the one physical carriage.197 // Two independent typed admissions over the one physical carriage.
198- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))198+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
199 require.NoError(t, err)199 require.NoError(t, err)
200 t.Cleanup(func() { _ = first.Close() })200 t.Cleanup(func() { _ = first.Close() })
201- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))201+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
202 require.NoError(t, err)202 require.NoError(t, err)
203 t.Cleanup(func() { _ = second.Close() })203 t.Cleanup(func() { _ = second.Close() })
204 daemon.awaitQUICAccepted(t, 2)204 daemon.awaitQUICAccepted(t, 2)
@@ -219,7 +219,7 @@ func TestQUICCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
219 wg.Add(1)219 wg.Add(1)
220 go func(i int) {220 go func(i int) {
221 defer wg.Done()221 defer wg.Done()
222- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))222+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
223 }(i)223 }(i)
224 }224 }
225 wg.Wait()225 wg.Wait()
@@ -375,11 +375,11 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
375 375
376 lost := make([]ports.BrokerLogicalConnection, 3)376 lost := make([]ports.BrokerLogicalConnection, 3)
377 for i := range lost {377 for i := range lost {
378- lost[i], err = physicalA.OpenStream(context.Background(), muxOpenRequest(uint64(i+1), policy))378+ lost[i], err = openTyped(physicalA, context.Background(), muxOpenRequest(uint64(i+1), policy))
379 require.NoError(t, err, "stream %d", i+1)379 require.NoError(t, err, "stream %d", i+1)
380 t.Cleanup(func() { _ = lost[i].Close() })380 t.Cleanup(func() { _ = lost[i].Close() })
381 }381 }
382- survivor, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, policy))382+ survivor, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, policy))
383 require.NoError(t, err)383 require.NoError(t, err)
384 t.Cleanup(func() { _ = survivor.Close() })384 t.Cleanup(func() { _ = survivor.Close() })
385 daemon.awaitQUICAccepted(t, 4)385 daemon.awaitQUICAccepted(t, 4)
@@ -429,7 +429,7 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
429 "the lost QUIC physical child was never reaped")429 "the lost QUIC physical child was never reaped")
430 430
431 // The lost physical refuses a fresh stream.431 // The lost physical refuses a fresh stream.
432- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(9, policy))432+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(9, policy))
433 require.Error(t, err)433 require.Error(t, err)
434 }434 }
435 435
@@ -455,7 +455,7 @@ func TestQUICCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) {
455 455
456 cancel()456 cancel()
457 require.False(t, channelClosed(physical.Done()))457 require.False(t, channelClosed(physical.Done()))
458- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))458+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
459 require.NoError(t, err)459 require.NoError(t, err)
460 t.Cleanup(func() { _ = logical.Close() })460 t.Cleanup(func() { _ = logical.Close() })
461 daemon.awaitQUICAccepted(t, 1)461 daemon.awaitQUICAccepted(t, 1)
A internal/adapters/daemonmux/raw_envelope_test.go +41 -0
@@ -0,0 +1,41 @@
1+package daemonmux
2+
3+import (
4+ "bytes"
5+ "context"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/protocol/wire"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+// TestLogicalRawEnvelopeRelay proves a logical connection carries opaque
14+// envelopes byte-for-byte in both directions without decoding them.
15+func TestLogicalRawEnvelopeRelay(t *testing.T) {
16+ broker, daemon := newPairedPumps(t, DefaultMuxCeilings())
17+ acceptor := newDaemonAcceptor(daemon, 0)
18+ acceptor.startBare(1, time.Now().Add(5*time.Second))
19+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
20+ defer cancel()
21+ conn, err := mustConnector(t, broker).Open(ctx, controlRequest(1))
22+ require.NoError(t, err)
23+ defer conn.Close()
24+ require.NoError(t, acceptor.wait(1, time.Now().Add(5*time.Second)))
25+
26+ payload := []byte{0xff, 0, 0x80, 7}
27+ require.NoError(t, conn.SendEnvelope(payload))
28+ status, ok := daemon.Engine().Status(conn.Ref().Physical)
29+ require.True(t, ok)
30+ peer := newMuxTransport(newMuxStreamPipe(daemon, conn.Ref().Physical, daemon.Ceilings().StreamChunkLimit, status.Done))
31+ defer peer.Close()
32+ got, err := peer.Recv()
33+ require.NoError(t, err)
34+ require.Equal(t, payload, got.Payload)
35+
36+ large := bytes.Repeat([]byte{0xfe}, 2048)
37+ require.NoError(t, peer.Send(wire.Envelope{Payload: large}))
38+ gotPayload, err := conn.RecvEnvelope()
39+ require.NoError(t, err)
40+ require.Equal(t, large, gotPayload)
41+}
@@ -0,0 +1,41 @@
1+package daemonmux
2+
3+import (
4+ "bytes"
5+ "context"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/protocol/wire"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+// TestLogicalRawEnvelopeRelay proves a logical connection carries opaque
14+// envelopes byte-for-byte in both directions without decoding them.
15+func TestLogicalRawEnvelopeRelay(t *testing.T) {
16+ broker, daemon := newPairedPumps(t, DefaultMuxCeilings())
17+ acceptor := newDaemonAcceptor(daemon, 0)
18+ acceptor.startBare(1, time.Now().Add(5*time.Second))
19+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
20+ defer cancel()
21+ conn, err := mustConnector(t, broker).Open(ctx, controlRequest(1))
22+ require.NoError(t, err)
23+ defer conn.Close()
24+ require.NoError(t, acceptor.wait(1, time.Now().Add(5*time.Second)))
25+
26+ payload := []byte{0xff, 0, 0x80, 7}
27+ require.NoError(t, conn.SendEnvelope(payload))
28+ status, ok := daemon.Engine().Status(conn.Ref().Physical)
29+ require.True(t, ok)
30+ peer := newMuxTransport(newMuxStreamPipe(daemon, conn.Ref().Physical, daemon.Ceilings().StreamChunkLimit, status.Done))
31+ defer peer.Close()
32+ got, err := peer.Recv()
33+ require.NoError(t, err)
34+ require.Equal(t, payload, got.Payload)
35+
36+ large := bytes.Repeat([]byte{0xfe}, 2048)
37+ require.NoError(t, peer.Send(wire.Envelope{Payload: large}))
38+ gotPayload, err := conn.RecvEnvelope()
39+ require.NoError(t, err)
40+ require.Equal(t, large, gotPayload)
41+}
M internal/adapters/daemonmux/ssh_carriage_test.go +7 -7
@@ -253,10 +253,10 @@ func TestSSHCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
253253 pid := sshMuxReadPID(t, pidPath)
254254
255255 // Two independent typed admissions over the one physical carriage.
256- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))
256+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
257257 require.NoError(t, err)
258258 t.Cleanup(func() { _ = first.Close() })
259- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))
259+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
260260 require.NoError(t, err)
261261 t.Cleanup(func() { _ = second.Close() })
262262 for _, stream := range []ports.BrokerLogicalConnection{first, second} {
@@ -276,7 +276,7 @@ func TestSSHCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
276276 wg.Add(1)
277277 go func(i int) {
278278 defer wg.Done()
279- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))
279+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
280280 }(i)
281281 }
282282 wg.Wait()
@@ -332,12 +332,12 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
332332
333333 lost := make([]ports.BrokerLogicalConnection, 3)
334334 for i := range lost {
335- connection, err := physicalA.OpenStream(context.Background(), muxOpenRequest(uint64(i+1), policy))
335+ connection, err := openTyped(physicalA, context.Background(), muxOpenRequest(uint64(i+1), policy))
336336 require.NoError(t, err, "stream %d", i+1)
337337 lost[i] = connection
338338 t.Cleanup(func() { _ = connection.Close() })
339339 }
340- survivor, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, policy))
340+ survivor, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, policy))
341341 require.NoError(t, err)
342342 t.Cleanup(func() { _ = survivor.Close() })
343343
@@ -383,7 +383,7 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
383383 require.Equal(t, protocol.Pong{}, message)
384384
385385 // The lost physical refuses a fresh stream.
386- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(9, policy))
386+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(9, policy))
387387 require.Error(t, err)
388388
389389 // The failed subprocess is reaped, not left a zombie.
@@ -409,7 +409,7 @@ func TestSSHCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) {
409409 cancel()
410410
411411 require.False(t, channelClosed(physical.Done()))
412- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
412+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
413413 require.NoError(t, err)
414414 t.Cleanup(func() { _ = logical.Close() })
415415 require.NoError(t, logical.SendClient(protocol.Ping{}))
@@ -253,10 +253,10 @@ func TestSSHCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
253 pid := sshMuxReadPID(t, pidPath)253 pid := sshMuxReadPID(t, pidPath)
254 254
255 // Two independent typed admissions over the one physical carriage.255 // Two independent typed admissions over the one physical carriage.
256- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))256+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
257 require.NoError(t, err)257 require.NoError(t, err)
258 t.Cleanup(func() { _ = first.Close() })258 t.Cleanup(func() { _ = first.Close() })
259- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))259+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
260 require.NoError(t, err)260 require.NoError(t, err)
261 t.Cleanup(func() { _ = second.Close() })261 t.Cleanup(func() { _ = second.Close() })
262 for _, stream := range []ports.BrokerLogicalConnection{first, second} {262 for _, stream := range []ports.BrokerLogicalConnection{first, second} {
@@ -276,7 +276,7 @@ func TestSSHCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
276 wg.Add(1)276 wg.Add(1)
277 go func(i int) {277 go func(i int) {
278 defer wg.Done()278 defer wg.Done()
279- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))279+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
280 }(i)280 }(i)
281 }281 }
282 wg.Wait()282 wg.Wait()
@@ -332,12 +332,12 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
332 332
333 lost := make([]ports.BrokerLogicalConnection, 3)333 lost := make([]ports.BrokerLogicalConnection, 3)
334 for i := range lost {334 for i := range lost {
335- connection, err := physicalA.OpenStream(context.Background(), muxOpenRequest(uint64(i+1), policy))335+ connection, err := openTyped(physicalA, context.Background(), muxOpenRequest(uint64(i+1), policy))
336 require.NoError(t, err, "stream %d", i+1)336 require.NoError(t, err, "stream %d", i+1)
337 lost[i] = connection337 lost[i] = connection
338 t.Cleanup(func() { _ = connection.Close() })338 t.Cleanup(func() { _ = connection.Close() })
339 }339 }
340- survivor, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, policy))340+ survivor, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, policy))
341 require.NoError(t, err)341 require.NoError(t, err)
342 t.Cleanup(func() { _ = survivor.Close() })342 t.Cleanup(func() { _ = survivor.Close() })
343 343
@@ -383,7 +383,7 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) {
383 require.Equal(t, protocol.Pong{}, message)383 require.Equal(t, protocol.Pong{}, message)
384 384
385 // The lost physical refuses a fresh stream.385 // The lost physical refuses a fresh stream.
386- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(9, policy))386+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(9, policy))
387 require.Error(t, err)387 require.Error(t, err)
388 388
389 // The failed subprocess is reaped, not left a zombie.389 // The failed subprocess is reaped, not left a zombie.
@@ -409,7 +409,7 @@ func TestSSHCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) {
409 cancel()409 cancel()
410 410
411 require.False(t, channelClosed(physical.Done()))411 require.False(t, channelClosed(physical.Done()))
412- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))412+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
413 require.NoError(t, err)413 require.NoError(t, err)
414 t.Cleanup(func() { _ = logical.Close() })414 t.Cleanup(func() { _ = logical.Close() })
415 require.NoError(t, logical.SendClient(protocol.Ping{}))415 require.NoError(t, logical.SendClient(protocol.Ping{}))
M internal/adapters/daemonmux/supervisor_test.go +9 -8
@@ -112,8 +112,9 @@ func TestServerSupervisorEnforcesAcceptedPolicyBeforeAdmission(t *testing.T) {
112112 require.NoError(t, err)
113113 require.NotNil(t, connection)
114114 server.awaitAccepted(t, 1)
115- require.NoError(t, connection.SendClient(protocol.Ping{}))
116- message, err := connection.ReceiveServer()
115+ typed := asTyped(connection)
116+ require.NoError(t, typed.SendClient(protocol.Ping{}))
117+ message, err := typed.ReceiveServer()
117118 require.NoError(t, err)
118119 require.Equal(t, protocol.Pong{}, message)
119120 }
@@ -152,7 +153,7 @@ func TestServerSupervisorDeliversFirstHelloDataFrame(t *testing.T) {
152153 accepted <- connection
153154 }()
154155
155- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
156+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
156157 require.NoError(t, err)
157158 var server ports.ServerConnection
158159 select {
@@ -220,7 +221,7 @@ func TestServerSupervisorStampsAcceptedOriginThroughAggregate(t *testing.T) {
220221 accepted <- connection
221222 }()
222223
223- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
224+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
224225 require.NoError(t, err)
225226 t.Cleanup(func() { _ = logical.Close() })
226227
@@ -292,9 +293,9 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) {
292293 physicalB := connectRaw(t, clientB, endpoint)
293294 require.NoError(t, server.awaitAdoption(t))
294295
295- logicalA, err := physicalA.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
296+ logicalA, err := openTyped(physicalA, context.Background(), muxOpenRequest(1, binding.Policy()))
296297 require.NoError(t, err)
297- logicalB, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))
298+ logicalB, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, binding.Policy()))
298299 require.NoError(t, err)
299300 server.awaitAccepted(t, 2)
300301 require.Equal(t, 2, server.supervisor.Children())
@@ -307,14 +308,14 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) {
307308 "the lost child was never reaped")
308309
309310 // The surviving child still accepts and the aggregate never failed.
310- logicalB2, err := physicalB.OpenStream(context.Background(), muxOpenRequest(2, binding.Policy()))
311+ logicalB2, err := openTyped(physicalB, context.Background(), muxOpenRequest(2, binding.Policy()))
311312 require.NoError(t, err)
312313 require.NotNil(t, logicalB2)
313314 server.awaitAccepted(t, 3)
314315
315316 // The lost physical refuses a fresh stream while its published logical
316317 // stream reports the physical loss.
317- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(2, binding.Policy()))
318+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(2, binding.Policy()))
318319 require.Error(t, err)
319320 require.True(t, channelClosed(logicalA.Done()))
320321 require.False(t, channelClosed(logicalB.Done()))
@@ -112,8 +112,9 @@ func TestServerSupervisorEnforcesAcceptedPolicyBeforeAdmission(t *testing.T) {
112 require.NoError(t, err)112 require.NoError(t, err)
113 require.NotNil(t, connection)113 require.NotNil(t, connection)
114 server.awaitAccepted(t, 1)114 server.awaitAccepted(t, 1)
115- require.NoError(t, connection.SendClient(protocol.Ping{}))115+ typed := asTyped(connection)
116- message, err := connection.ReceiveServer()116+ require.NoError(t, typed.SendClient(protocol.Ping{}))
117+ message, err := typed.ReceiveServer()
117 require.NoError(t, err)118 require.NoError(t, err)
118 require.Equal(t, protocol.Pong{}, message)119 require.Equal(t, protocol.Pong{}, message)
119 }120 }
@@ -152,7 +153,7 @@ func TestServerSupervisorDeliversFirstHelloDataFrame(t *testing.T) {
152 accepted <- connection153 accepted <- connection
153 }()154 }()
154 155
155- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))156+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
156 require.NoError(t, err)157 require.NoError(t, err)
157 var server ports.ServerConnection158 var server ports.ServerConnection
158 select {159 select {
@@ -220,7 +221,7 @@ func TestServerSupervisorStampsAcceptedOriginThroughAggregate(t *testing.T) {
220 accepted <- connection221 accepted <- connection
221 }()222 }()
222 223
223- logical, err := physical.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))224+ logical, err := openTyped(physical, context.Background(), muxOpenRequest(1, binding.Policy()))
224 require.NoError(t, err)225 require.NoError(t, err)
225 t.Cleanup(func() { _ = logical.Close() })226 t.Cleanup(func() { _ = logical.Close() })
226 227
@@ -292,9 +293,9 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) {
292 physicalB := connectRaw(t, clientB, endpoint)293 physicalB := connectRaw(t, clientB, endpoint)
293 require.NoError(t, server.awaitAdoption(t))294 require.NoError(t, server.awaitAdoption(t))
294 295
295- logicalA, err := physicalA.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))296+ logicalA, err := openTyped(physicalA, context.Background(), muxOpenRequest(1, binding.Policy()))
296 require.NoError(t, err)297 require.NoError(t, err)
297- logicalB, err := physicalB.OpenStream(context.Background(), muxOpenRequest(1, binding.Policy()))298+ logicalB, err := openTyped(physicalB, context.Background(), muxOpenRequest(1, binding.Policy()))
298 require.NoError(t, err)299 require.NoError(t, err)
299 server.awaitAccepted(t, 2)300 server.awaitAccepted(t, 2)
300 require.Equal(t, 2, server.supervisor.Children())301 require.Equal(t, 2, server.supervisor.Children())
@@ -307,14 +308,14 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) {
307 "the lost child was never reaped")308 "the lost child was never reaped")
308 309
309 // The surviving child still accepts and the aggregate never failed.310 // The surviving child still accepts and the aggregate never failed.
310- logicalB2, err := physicalB.OpenStream(context.Background(), muxOpenRequest(2, binding.Policy()))311+ logicalB2, err := openTyped(physicalB, context.Background(), muxOpenRequest(2, binding.Policy()))
311 require.NoError(t, err)312 require.NoError(t, err)
312 require.NotNil(t, logicalB2)313 require.NotNil(t, logicalB2)
313 server.awaitAccepted(t, 3)314 server.awaitAccepted(t, 3)
314 315
315 // The lost physical refuses a fresh stream while its published logical316 // The lost physical refuses a fresh stream while its published logical
316 // stream reports the physical loss.317 // stream reports the physical loss.
317- _, err = physicalA.OpenStream(context.Background(), muxOpenRequest(2, binding.Policy()))318+ _, err = openTyped(physicalA, context.Background(), muxOpenRequest(2, binding.Policy()))
318 require.Error(t, err)319 require.Error(t, err)
319 require.True(t, channelClosed(logicalA.Done()))320 require.True(t, channelClosed(logicalA.Done()))
320 require.False(t, channelClosed(logicalB.Done()))321 require.False(t, channelClosed(logicalB.Done()))
A internal/adapters/daemonmux/typed_stream_test.go +53 -0
@@ -0,0 +1,53 @@
1+package daemonmux
2+
3+import (
4+ "context"
5+ "sync"
6+
7+ "github.com/bnema/vev/internal/adapters/sessionwire"
8+ "github.com/bnema/vev/internal/ports"
9+)
10+
11+// typedStream adapts one raw mux stream to the typed session port the way the
12+// client side does, while keeping the raw stream's lifecycle observable.
13+type typedStream struct {
14+ ports.ClientConnection
15+ ports.BrokerEnvelopeStream
16+}
17+
18+var _ ports.BrokerLogicalConnection = typedStream{}
19+
20+func (s typedStream) Close() error { return s.ClientConnection.Close() }
21+
22+// typedViews keeps one session codec per raw stream: the codec owns the
23+// preamble, so a second codec on the same stream would restart it.
24+var typedViews sync.Map // ports.BrokerEnvelopeStream -> typedStream
25+
26+func asTyped(stream ports.BrokerEnvelopeStream) typedStream {
27+ if view, ok := typedViews.Load(stream); ok {
28+ return view.(typedStream)
29+ }
30+ view, _ := typedViews.LoadOrStore(stream, typedStream{ClientConnection: sessionwire.BrokerCodec{}.Client(stream), BrokerEnvelopeStream: stream})
31+ return view.(typedStream)
32+}
33+
34+// typedLogical is a typed view of one concrete logical connection.
35+type typedLogical struct {
36+ typedStream
37+ *LogicalConnection
38+}
39+
40+func (c typedLogical) Close() error { return c.typedStream.Close() }
41+
42+type envelopeOpener interface {
43+ OpenStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error)
44+}
45+
46+// openTyped opens one raw stream and returns its typed view; nil on failure.
47+func openTyped(opener envelopeOpener, ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
48+ stream, err := opener.OpenStream(ctx, request)
49+ if err != nil {
50+ return nil, err
51+ }
52+ return asTyped(stream), nil
53+}
@@ -0,0 +1,53 @@
1+package daemonmux
2+
3+import (
4+ "context"
5+ "sync"
6+
7+ "github.com/bnema/vev/internal/adapters/sessionwire"
8+ "github.com/bnema/vev/internal/ports"
9+)
10+
11+// typedStream adapts one raw mux stream to the typed session port the way the
12+// client side does, while keeping the raw stream's lifecycle observable.
13+type typedStream struct {
14+ ports.ClientConnection
15+ ports.BrokerEnvelopeStream
16+}
17+
18+var _ ports.BrokerLogicalConnection = typedStream{}
19+
20+func (s typedStream) Close() error { return s.ClientConnection.Close() }
21+
22+// typedViews keeps one session codec per raw stream: the codec owns the
23+// preamble, so a second codec on the same stream would restart it.
24+var typedViews sync.Map // ports.BrokerEnvelopeStream -> typedStream
25+
26+func asTyped(stream ports.BrokerEnvelopeStream) typedStream {
27+ if view, ok := typedViews.Load(stream); ok {
28+ return view.(typedStream)
29+ }
30+ view, _ := typedViews.LoadOrStore(stream, typedStream{ClientConnection: sessionwire.BrokerCodec{}.Client(stream), BrokerEnvelopeStream: stream})
31+ return view.(typedStream)
32+}
33+
34+// typedLogical is a typed view of one concrete logical connection.
35+type typedLogical struct {
36+ typedStream
37+ *LogicalConnection
38+}
39+
40+func (c typedLogical) Close() error { return c.typedStream.Close() }
41+
42+type envelopeOpener interface {
43+ OpenStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error)
44+}
45+
46+// openTyped opens one raw stream and returns its typed view; nil on failure.
47+func openTyped(opener envelopeOpener, ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
48+ stream, err := opener.OpenStream(ctx, request)
49+ if err != nil {
50+ return nil, err
51+ }
52+ return asTyped(stream), nil
53+}
M internal/adapters/daemonmux/unix_carriage_test.go +3 -3
@@ -148,10 +148,10 @@ func TestUnixCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
148148 require.NoError(t, server.awaitAdoption(t))
149149
150150 // Two independent typed admissions over the one physical carriage.
151- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))
151+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
152152 require.NoError(t, err)
153153 t.Cleanup(func() { _ = first.Close() })
154- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))
154+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
155155 require.NoError(t, err)
156156 t.Cleanup(func() { _ = second.Close() })
157157 server.awaitAccepted(t, 2)
@@ -172,7 +172,7 @@ func TestUnixCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
172172 wg.Add(1)
173173 go func(i int) {
174174 defer wg.Done()
175- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))
175+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
176176 }(i)
177177 }
178178 wg.Wait()
@@ -148,10 +148,10 @@ func TestUnixCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
148 require.NoError(t, server.awaitAdoption(t))148 require.NoError(t, server.awaitAdoption(t))
149 149
150 // Two independent typed admissions over the one physical carriage.150 // Two independent typed admissions over the one physical carriage.
151- first, err := physical.OpenStream(context.Background(), muxOpenRequest(1, policy))151+ first, err := openTyped(physical, context.Background(), muxOpenRequest(1, policy))
152 require.NoError(t, err)152 require.NoError(t, err)
153 t.Cleanup(func() { _ = first.Close() })153 t.Cleanup(func() { _ = first.Close() })
154- second, err := physical.OpenStream(context.Background(), muxOpenRequest(2, policy))154+ second, err := openTyped(physical, context.Background(), muxOpenRequest(2, policy))
155 require.NoError(t, err)155 require.NoError(t, err)
156 t.Cleanup(func() { _ = second.Close() })156 t.Cleanup(func() { _ = second.Close() })
157 server.awaitAccepted(t, 2)157 server.awaitAccepted(t, 2)
@@ -172,7 +172,7 @@ func TestUnixCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) {
172 wg.Add(1)172 wg.Add(1)
173 go func(i int) {173 go func(i int) {
174 defer wg.Done()174 defer wg.Done()
175- connections[i], openErrs[i] = physical.OpenStream(context.Background(), muxOpenRequest(uint64(3+i), policy))175+ connections[i], openErrs[i] = openTyped(physical, context.Background(), muxOpenRequest(uint64(3+i), policy))
176 }(i)176 }(i)
177 }177 }
178 wg.Wait()178 wg.Wait()
A internal/adapters/sessionwire/broker_codec.go +43 -0
@@ -0,0 +1,43 @@
1+package sessionwire
2+
3+import (
4+ "io"
5+
6+ "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol/wire"
8+)
9+
10+// BrokerCodec translates a raw broker envelope stream into the typed session
11+// protocol for the broker's own observation and preview operations.
12+// Attachment traffic stays opaque and never passes through this codec.
13+type BrokerCodec struct{}
14+
15+var _ ports.SessionCodec = BrokerCodec{}
16+
17+// Client returns the typed client view of stream, or nil for a nil stream.
18+func (BrokerCodec) Client(stream ports.BrokerEnvelopeStream) ports.ClientConnection {
19+ if stream == nil {
20+ return nil
21+ }
22+ return NewClientConnection(brokerEnvelopeTransport{stream: stream})
23+}
24+
25+// brokerEnvelopeTransport carries complete envelopes over a broker stream.
26+type brokerEnvelopeTransport struct{ stream ports.BrokerEnvelopeStream }
27+
28+var _ wire.Transport = brokerEnvelopeTransport{}
29+
30+func (t brokerEnvelopeTransport) Send(e wire.Envelope) error { return t.stream.SendEnvelope(e.Payload) }
31+
32+func (t brokerEnvelopeTransport) Recv() (wire.Envelope, error) {
33+ data, err := t.stream.RecvEnvelope()
34+ if err != nil {
35+ return wire.Envelope{}, err
36+ }
37+ if data == nil {
38+ return wire.Envelope{}, io.EOF
39+ }
40+ return wire.Envelope{Payload: data}, nil
41+}
42+
43+func (t brokerEnvelopeTransport) Close() error { return t.stream.Close() }
@@ -0,0 +1,43 @@
1+package sessionwire
2+
3+import (
4+ "io"
5+
6+ "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol/wire"
8+)
9+
10+// BrokerCodec translates a raw broker envelope stream into the typed session
11+// protocol for the broker's own observation and preview operations.
12+// Attachment traffic stays opaque and never passes through this codec.
13+type BrokerCodec struct{}
14+
15+var _ ports.SessionCodec = BrokerCodec{}
16+
17+// Client returns the typed client view of stream, or nil for a nil stream.
18+func (BrokerCodec) Client(stream ports.BrokerEnvelopeStream) ports.ClientConnection {
19+ if stream == nil {
20+ return nil
21+ }
22+ return NewClientConnection(brokerEnvelopeTransport{stream: stream})
23+}
24+
25+// brokerEnvelopeTransport carries complete envelopes over a broker stream.
26+type brokerEnvelopeTransport struct{ stream ports.BrokerEnvelopeStream }
27+
28+var _ wire.Transport = brokerEnvelopeTransport{}
29+
30+func (t brokerEnvelopeTransport) Send(e wire.Envelope) error { return t.stream.SendEnvelope(e.Payload) }
31+
32+func (t brokerEnvelopeTransport) Recv() (wire.Envelope, error) {
33+ data, err := t.stream.RecvEnvelope()
34+ if err != nil {
35+ return wire.Envelope{}, err
36+ }
37+ if data == nil {
38+ return wire.Envelope{}, io.EOF
39+ }
40+ return wire.Envelope{Payload: data}, nil
41+}
42+
43+func (t brokerEnvelopeTransport) Close() error { return t.stream.Close() }
M internal/app/broker_offline.go +3 -2
@@ -21,6 +21,7 @@ import (
2121 "github.com/bnema/vev/internal/adapters/daemonmux"
2222 "github.com/bnema/vev/internal/adapters/ipc"
2323 "github.com/bnema/vev/internal/adapters/lifecycle"
24+ "github.com/bnema/vev/internal/adapters/sessionwire"
2425 "github.com/bnema/vev/internal/logging"
2526 "github.com/bnema/vev/internal/platform"
2627 "github.com/bnema/vev/internal/ports"
@@ -317,7 +318,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
317318 if err != nil {
318319 return err
319320 }
320- remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector}
321+ remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector, codec: sessionwire.BrokerCodec{}}
321322 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)
322323 if err != nil {
323324 return err
@@ -345,7 +346,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
345346 if err := supervisor.RegisterCloseable("pool", pool); err != nil {
346347 return err
347348 }
348- authority, err := broker.NewAuthority(epoch, registry, pool, supervisor)
349+ authority, err := broker.NewAuthority(epoch, registry, pool, supervisor, sessionwire.BrokerCodec{})
349350 if err != nil {
350351 return err
351352 }
@@ -21,6 +21,7 @@ import (
21 "github.com/bnema/vev/internal/adapters/daemonmux"21 "github.com/bnema/vev/internal/adapters/daemonmux"
22 "github.com/bnema/vev/internal/adapters/ipc"22 "github.com/bnema/vev/internal/adapters/ipc"
23 "github.com/bnema/vev/internal/adapters/lifecycle"23 "github.com/bnema/vev/internal/adapters/lifecycle"
24+ "github.com/bnema/vev/internal/adapters/sessionwire"
24 "github.com/bnema/vev/internal/logging"25 "github.com/bnema/vev/internal/logging"
25 "github.com/bnema/vev/internal/platform"26 "github.com/bnema/vev/internal/platform"
26 "github.com/bnema/vev/internal/ports"27 "github.com/bnema/vev/internal/ports"
@@ -317,7 +318,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
317 if err != nil {318 if err != nil {
318 return err319 return err
319 }320 }
320- remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector}321+ remoteProbe := &brokerRemoteProbe{epoch: epoch, routes: resolver, connector: connector, codec: sessionwire.BrokerCodec{}}
321 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)322 registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig)
322 if err != nil {323 if err != nil {
323 return err324 return err
@@ -345,7 +346,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker
345 if err := supervisor.RegisterCloseable("pool", pool); err != nil {346 if err := supervisor.RegisterCloseable("pool", pool); err != nil {
346 return err347 return err
347 }348 }
348- authority, err := broker.NewAuthority(epoch, registry, pool, supervisor)349+ authority, err := broker.NewAuthority(epoch, registry, pool, supervisor, sessionwire.BrokerCodec{})
349 if err != nil {350 if err != nil {
350 return err351 return err
351 }352 }
M internal/app/broker_offline_local.go +5 -3
@@ -7,6 +7,7 @@ import (
77
88 "github.com/bnema/vev/internal/adapters/brokerconfig"
99 "github.com/bnema/vev/internal/adapters/daemonmux"
10+ "github.com/bnema/vev/internal/adapters/sessionwire"
1011 "github.com/bnema/vev/internal/domain"
1112 "github.com/bnema/vev/internal/ports"
1213 "github.com/bnema/vev/internal/usecase/broker"
@@ -60,6 +61,7 @@ type localRouteProbe struct {
6061 loadIdentity localIdentityLoader
6162 connector ports.BrokerEndpointConnector
6263 shared pooledPhysicals
64+ codec ports.SessionCodec
6365 }
6466
6567 var _ broker.LocalProbe = (*localRouteProbe)(nil)
@@ -87,7 +89,7 @@ func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPoli
8789 if err := policy.Validate(); err != nil {
8890 return nil, fmt.Errorf("vev: broker local probe: %w", err)
8991 }
90- return &localRouteProbe{epoch: epoch, route: route, policy: policy, loadIdentity: loadIdentity, connector: connector}, nil
92+ return &localRouteProbe{epoch: epoch, route: route, policy: policy, loadIdentity: loadIdentity, connector: connector, codec: sessionwire.BrokerCodec{}}, nil
9193 }
9294
9395 // ProbeLocal observes the local daemon by authenticating the daemonmux physical
@@ -127,7 +129,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs
127129 if err != nil {
128130 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
129131 }
130- if observation, handled, err := p.shared.observe(ctx, identity, p.policy, request); handled {
132+ if observation, handled, err := p.shared.observe(ctx, identity, p.policy, request, p.codec); handled {
131133 if err != nil {
132134 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
133135 }
@@ -144,7 +146,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs
144146 if physical.Identity() != endpoint.ExpectedIdentity.Identity || !physical.Policy().Compatible(endpoint.Policy) {
145147 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, errors.New("vev: broker local probe authenticated binding mismatch")
146148 }
147- observation, err := observeDaemonCatalogue(ctx, physical, request)
149+ observation, err := observeDaemonCatalogue(ctx, physical, request, p.codec)
148150 if err != nil {
149151 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
150152 }
@@ -7,6 +7,7 @@ import (
7 7
8 "github.com/bnema/vev/internal/adapters/brokerconfig"8 "github.com/bnema/vev/internal/adapters/brokerconfig"
9 "github.com/bnema/vev/internal/adapters/daemonmux"9 "github.com/bnema/vev/internal/adapters/daemonmux"
10+ "github.com/bnema/vev/internal/adapters/sessionwire"
10 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
12 "github.com/bnema/vev/internal/usecase/broker"13 "github.com/bnema/vev/internal/usecase/broker"
@@ -60,6 +61,7 @@ type localRouteProbe struct {
60 loadIdentity localIdentityLoader61 loadIdentity localIdentityLoader
61 connector ports.BrokerEndpointConnector62 connector ports.BrokerEndpointConnector
62 shared pooledPhysicals63 shared pooledPhysicals
64+ codec ports.SessionCodec
63 }65 }
64 66
65 var _ broker.LocalProbe = (*localRouteProbe)(nil)67 var _ broker.LocalProbe = (*localRouteProbe)(nil)
@@ -87,7 +89,7 @@ func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPoli
87 if err := policy.Validate(); err != nil {89 if err := policy.Validate(); err != nil {
88 return nil, fmt.Errorf("vev: broker local probe: %w", err)90 return nil, fmt.Errorf("vev: broker local probe: %w", err)
89 }91 }
90- return &localRouteProbe{epoch: epoch, route: route, policy: policy, loadIdentity: loadIdentity, connector: connector}, nil92+ return &localRouteProbe{epoch: epoch, route: route, policy: policy, loadIdentity: loadIdentity, connector: connector, codec: sessionwire.BrokerCodec{}}, nil
91 }93 }
92 94
93 // ProbeLocal observes the local daemon by authenticating the daemonmux physical95 // ProbeLocal observes the local daemon by authenticating the daemonmux physical
@@ -127,7 +129,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs
127 if err != nil {129 if err != nil {
128 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err130 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
129 }131 }
130- if observation, handled, err := p.shared.observe(ctx, identity, p.policy, request); handled {132+ if observation, handled, err := p.shared.observe(ctx, identity, p.policy, request, p.codec); handled {
131 if err != nil {133 if err != nil {
132 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err134 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
133 }135 }
@@ -144,7 +146,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs
144 if physical.Identity() != endpoint.ExpectedIdentity.Identity || !physical.Policy().Compatible(endpoint.Policy) {146 if physical.Identity() != endpoint.ExpectedIdentity.Identity || !physical.Policy().Compatible(endpoint.Policy) {
145 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, errors.New("vev: broker local probe authenticated binding mismatch")147 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, errors.New("vev: broker local probe authenticated binding mismatch")
146 }148 }
147- observation, err := observeDaemonCatalogue(ctx, physical, request)149+ observation, err := observeDaemonCatalogue(ctx, physical, request, p.codec)
148 if err != nil {150 if err != nil {
149 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err151 return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err
150 }152 }
M internal/app/broker_offline_local_attach_test.go +12 -2
@@ -18,6 +18,7 @@ import (
1818 "github.com/bnema/vev/internal/adapters/daemonmux"
1919 "github.com/bnema/vev/internal/adapters/ipc"
2020 "github.com/bnema/vev/internal/adapters/pty"
21+ "github.com/bnema/vev/internal/adapters/sessionwire"
2122 "github.com/bnema/vev/internal/domain"
2223 "github.com/bnema/vev/internal/ports"
2324 "github.com/bnema/vev/internal/protocol"
@@ -134,10 +135,19 @@ func (f *realLocalDaemonFixture) openStream(t *testing.T, request ports.BrokerOp
134135 }
135136 stream, err := physical.OpenStream(ctx, request)
136137 require.NoError(t, err)
137- t.Cleanup(func() { _ = stream.Close() })
138- return stream
138+ typed := brokerLogicalView{ClientConnection: sessionwire.BrokerCodec{}.Client(stream), BrokerEnvelopeStream: stream}
139+ t.Cleanup(func() { _ = typed.Close() })
140+ return typed
139141 }
140142
143+// brokerLogicalView is the typed client view of one raw logical stream.
144+type brokerLogicalView struct {
145+ ports.ClientConnection
146+ ports.BrokerEnvelopeStream
147+}
148+
149+func (v brokerLogicalView) Close() error { return v.ClientConnection.Close() }
150+
141151 // awaitWelcome receives server messages until the typed Welcome arrives.
142152 func awaitWelcome(t *testing.T, stream ports.BrokerLogicalConnection) protocol.Welcome {
143153 t.Helper()
@@ -18,6 +18,7 @@ import (
18 "github.com/bnema/vev/internal/adapters/daemonmux"18 "github.com/bnema/vev/internal/adapters/daemonmux"
19 "github.com/bnema/vev/internal/adapters/ipc"19 "github.com/bnema/vev/internal/adapters/ipc"
20 "github.com/bnema/vev/internal/adapters/pty"20 "github.com/bnema/vev/internal/adapters/pty"
21+ "github.com/bnema/vev/internal/adapters/sessionwire"
21 "github.com/bnema/vev/internal/domain"22 "github.com/bnema/vev/internal/domain"
22 "github.com/bnema/vev/internal/ports"23 "github.com/bnema/vev/internal/ports"
23 "github.com/bnema/vev/internal/protocol"24 "github.com/bnema/vev/internal/protocol"
@@ -134,10 +135,19 @@ func (f *realLocalDaemonFixture) openStream(t *testing.T, request ports.BrokerOp
134 }135 }
135 stream, err := physical.OpenStream(ctx, request)136 stream, err := physical.OpenStream(ctx, request)
136 require.NoError(t, err)137 require.NoError(t, err)
137- t.Cleanup(func() { _ = stream.Close() })138+ typed := brokerLogicalView{ClientConnection: sessionwire.BrokerCodec{}.Client(stream), BrokerEnvelopeStream: stream}
138- return stream139+ t.Cleanup(func() { _ = typed.Close() })
140+ return typed
139 }141 }
140 142
143+// brokerLogicalView is the typed client view of one raw logical stream.
144+type brokerLogicalView struct {
145+ ports.ClientConnection
146+ ports.BrokerEnvelopeStream
147+}
148+
149+func (v brokerLogicalView) Close() error { return v.ClientConnection.Close() }
150+
141 // awaitWelcome receives server messages until the typed Welcome arrives.151 // awaitWelcome receives server messages until the typed Welcome arrives.
142 func awaitWelcome(t *testing.T, stream ports.BrokerLogicalConnection) protocol.Welcome {152 func awaitWelcome(t *testing.T, stream ports.BrokerLogicalConnection) protocol.Welcome {
143 t.Helper()153 t.Helper()
M internal/app/broker_offline_test.go +4 -4
@@ -142,9 +142,9 @@ func awaitSandboxCancel(t *testing.T, cancel context.CancelFunc, done <-chan err
142142 // fixed terminal error, so a test can drive the accept drain without a carriage.
143143 type fatalListener struct{ err error }
144144
145-func (l fatalListener) Accept() (ports.BrokerService, error) { return nil, l.err }
146-func (l fatalListener) Close() error { return nil }
147-func (l fatalListener) Addr() string { return "fatal" }
145+func (l fatalListener) Accept() (ports.BrokerCoreService, error) { return nil, l.err }
146+func (l fatalListener) Close() error { return nil }
147+func (l fatalListener) Addr() string { return "fatal" }
148148
149149 // TestDrainBrokerAcceptClassifiesTerminalFailure proves the drain treats a
150150 // listener close as an orderly end with no log and no failure signal, while any
@@ -367,7 +367,7 @@ type wedgedListener struct {
367367 closed atomic.Bool
368368 }
369369
370-func (l *wedgedListener) Accept() (ports.BrokerService, error) { return nil, l.err }
370+func (l *wedgedListener) Accept() (ports.BrokerCoreService, error) { return nil, l.err }
371371 func (l *wedgedListener) Close() error {
372372 l.closed.Store(true)
373373 return nil
@@ -142,9 +142,9 @@ func awaitSandboxCancel(t *testing.T, cancel context.CancelFunc, done <-chan err
142 // fixed terminal error, so a test can drive the accept drain without a carriage.142 // fixed terminal error, so a test can drive the accept drain without a carriage.
143 type fatalListener struct{ err error }143 type fatalListener struct{ err error }
144 144
145-func (l fatalListener) Accept() (ports.BrokerService, error) { return nil, l.err }145+func (l fatalListener) Accept() (ports.BrokerCoreService, error) { return nil, l.err }
146-func (l fatalListener) Close() error { return nil }146+func (l fatalListener) Close() error { return nil }
147-func (l fatalListener) Addr() string { return "fatal" }147+func (l fatalListener) Addr() string { return "fatal" }
148 148
149 // TestDrainBrokerAcceptClassifiesTerminalFailure proves the drain treats a149 // TestDrainBrokerAcceptClassifiesTerminalFailure proves the drain treats a
150 // listener close as an orderly end with no log and no failure signal, while any150 // listener close as an orderly end with no log and no failure signal, while any
@@ -367,7 +367,7 @@ type wedgedListener struct {
367 closed atomic.Bool367 closed atomic.Bool
368 }368 }
369 369
370-func (l *wedgedListener) Accept() (ports.BrokerService, error) { return nil, l.err }370+func (l *wedgedListener) Accept() (ports.BrokerCoreService, error) { return nil, l.err }
371 func (l *wedgedListener) Close() error {371 func (l *wedgedListener) Close() error {
372 l.closed.Store(true)372 l.closed.Store(true)
373 return nil373 return nil
M internal/app/broker_remote_probe.go +11 -10
@@ -36,6 +36,7 @@ type brokerRemoteProbe struct {
3636 connector ports.BrokerEndpointConnector
3737 hosts ports.BrokerHostAuthorityReader
3838 shared pooledPhysicals
39+ codec ports.SessionCodec
3940 }
4041
4142 // sharedPhysicals lends the pooled transport of an attached or warm daemon.
@@ -60,7 +61,7 @@ func (s *pooledPhysicals) adopt(physical ports.BrokerPhysicalConnection, policy
6061 }
6162
6263 // observe reports handled=false when the probe must dial its own transport.
63-func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaemonIdentity, policy ports.BrokerPolicy, request ports.BrokerOpenStreamRequest) (ports.BrokerDaemonObservation, bool, error) {
64+func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaemonIdentity, policy ports.BrokerPolicy, request ports.BrokerOpenStreamRequest, codec ports.SessionCodec) (ports.BrokerDaemonObservation, bool, error) {
6465 pool, _ := s.pool.Load().(sharedPhysicals)
6566 if pool == nil || identity == "" {
6667 return ports.BrokerDaemonObservation{}, false, nil
@@ -69,7 +70,7 @@ func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaem
6970 if !ok {
7071 return ports.BrokerDaemonObservation{}, false, nil
7172 }
72- observation, err := observeDaemonCatalogue(ctx, physical, request)
73+ observation, err := observeDaemonCatalogue(ctx, physical, request, codec)
7374 if err == nil || ctx.Err() != nil {
7475 return observation, true, err
7576 }
@@ -99,7 +100,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
99100 // bootstrapping SSH/QUIC again. A borrowed transport that retires mid-probe
100101 // falls back to a fresh dial.
101102 if endpoint.ExpectedIdentity.Bound {
102- if observation, handled, err := p.shared.observe(ctx, endpoint.ExpectedIdentity.Identity, endpoint.Policy, request); handled {
103+ if observation, handled, err := p.shared.observe(ctx, endpoint.ExpectedIdentity.Identity, endpoint.Policy, request, p.codec); handled {
103104 return observation, err
104105 }
105106 }
@@ -127,7 +128,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
127128 if err != nil || identity != physical.Identity() {
128129 return ports.BrokerDaemonObservation{}, errors.New("vev: remote probe could not commit authenticated binding")
129130 }
130- observation, err := observeDaemonCatalogue(ctx, physical, request)
131+ observation, err := observeDaemonCatalogue(ctx, physical, request, p.codec)
131132 if err == nil && ctx.Err() == nil {
132133 adopted = p.shared.adopt(physical, endpoint.Policy)
133134 }
@@ -147,8 +148,8 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
147148 // catalogue, and a catalogue outside the exact current schema all fail closed:
148149 // the caller receives an error and no observation it could publish, so a probe
149150 // never invents inventory it did not observe.
150-func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalConnection, request ports.BrokerOpenStreamRequest) (ports.BrokerDaemonObservation, error) {
151- if physical == nil || request.Purpose != ports.BrokerStreamObservation {
151+func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalConnection, request ports.BrokerOpenStreamRequest, codec ports.SessionCodec) (ports.BrokerDaemonObservation, error) {
152+ if physical == nil || codec == nil || request.Purpose != ports.BrokerStreamObservation {
152153 return ports.BrokerDaemonObservation{}, errors.New("vev: invalid daemon observation request")
153154 }
154155 logical, err := physical.OpenStream(ctx, request)
@@ -167,7 +168,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo
167168 if err != nil {
168169 return ports.BrokerDaemonObservation{}, err
169170 }
170- reply, err := exchangeObservationCommand(ctx, logical, protocol.CommandRequest{RequestID: commandID, Version: protocol.Version, Slug: "remote-catalog", JSON: true})
171+ reply, err := exchangeObservationCommand(ctx, codec.Client(logical), logical, protocol.CommandRequest{RequestID: commandID, Version: protocol.Version, Slug: "remote-catalog", JSON: true})
171172 if err != nil {
172173 return ports.BrokerDaemonObservation{}, err
173174 }
@@ -201,7 +202,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo
201202 // joined before returning, so an observation always returns promptly once its
202203 // attempt is retired instead of parking the daemon's reader (or a probe
203204 // attempt) on a daemon that answers nothing.
204-func exchangeObservationCommand(ctx context.Context, logical ports.BrokerLogicalConnection, command protocol.CommandRequest) (protocol.ServerMessage, error) {
205+func exchangeObservationCommand(ctx context.Context, typed ports.ClientConnection, logical ports.BrokerEnvelopeStream, command protocol.CommandRequest) (protocol.ServerMessage, error) {
205206 if ctx == nil {
206207 ctx = context.Background()
207208 }
@@ -211,11 +212,11 @@ func exchangeObservationCommand(ctx context.Context, logical ports.BrokerLogical
211212 }
212213 done := make(chan outcome, 1)
213214 go func() {
214- if err := logical.SendClient(command); err != nil {
215+ if err := typed.SendClient(command); err != nil {
215216 done <- outcome{err: err}
216217 return
217218 }
218- message, err := logical.ReceiveServer()
219+ message, err := typed.ReceiveServer()
219220 done <- outcome{message: message, err: err}
220221 }()
221222 select {
@@ -36,6 +36,7 @@ type brokerRemoteProbe struct {
36 connector ports.BrokerEndpointConnector36 connector ports.BrokerEndpointConnector
37 hosts ports.BrokerHostAuthorityReader37 hosts ports.BrokerHostAuthorityReader
38 shared pooledPhysicals38 shared pooledPhysicals
39+ codec ports.SessionCodec
39 }40 }
40 41
41 // sharedPhysicals lends the pooled transport of an attached or warm daemon.42 // sharedPhysicals lends the pooled transport of an attached or warm daemon.
@@ -60,7 +61,7 @@ func (s *pooledPhysicals) adopt(physical ports.BrokerPhysicalConnection, policy
60 }61 }
61 62
62 // observe reports handled=false when the probe must dial its own transport.63 // observe reports handled=false when the probe must dial its own transport.
63-func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaemonIdentity, policy ports.BrokerPolicy, request ports.BrokerOpenStreamRequest) (ports.BrokerDaemonObservation, bool, error) {64+func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaemonIdentity, policy ports.BrokerPolicy, request ports.BrokerOpenStreamRequest, codec ports.SessionCodec) (ports.BrokerDaemonObservation, bool, error) {
64 pool, _ := s.pool.Load().(sharedPhysicals)65 pool, _ := s.pool.Load().(sharedPhysicals)
65 if pool == nil || identity == "" {66 if pool == nil || identity == "" {
66 return ports.BrokerDaemonObservation{}, false, nil67 return ports.BrokerDaemonObservation{}, false, nil
@@ -69,7 +70,7 @@ func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaem
69 if !ok {70 if !ok {
70 return ports.BrokerDaemonObservation{}, false, nil71 return ports.BrokerDaemonObservation{}, false, nil
71 }72 }
72- observation, err := observeDaemonCatalogue(ctx, physical, request)73+ observation, err := observeDaemonCatalogue(ctx, physical, request, codec)
73 if err == nil || ctx.Err() != nil {74 if err == nil || ctx.Err() != nil {
74 return observation, true, err75 return observation, true, err
75 }76 }
@@ -99,7 +100,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
99 // bootstrapping SSH/QUIC again. A borrowed transport that retires mid-probe100 // bootstrapping SSH/QUIC again. A borrowed transport that retires mid-probe
100 // falls back to a fresh dial.101 // falls back to a fresh dial.
101 if endpoint.ExpectedIdentity.Bound {102 if endpoint.ExpectedIdentity.Bound {
102- if observation, handled, err := p.shared.observe(ctx, endpoint.ExpectedIdentity.Identity, endpoint.Policy, request); handled {103+ if observation, handled, err := p.shared.observe(ctx, endpoint.ExpectedIdentity.Identity, endpoint.Policy, request, p.codec); handled {
103 return observation, err104 return observation, err
104 }105 }
105 }106 }
@@ -127,7 +128,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
127 if err != nil || identity != physical.Identity() {128 if err != nil || identity != physical.Identity() {
128 return ports.BrokerDaemonObservation{}, errors.New("vev: remote probe could not commit authenticated binding")129 return ports.BrokerDaemonObservation{}, errors.New("vev: remote probe could not commit authenticated binding")
129 }130 }
130- observation, err := observeDaemonCatalogue(ctx, physical, request)131+ observation, err := observeDaemonCatalogue(ctx, physical, request, p.codec)
131 if err == nil && ctx.Err() == nil {132 if err == nil && ctx.Err() == nil {
132 adopted = p.shared.adopt(physical, endpoint.Policy)133 adopted = p.shared.adopt(physical, endpoint.Policy)
133 }134 }
@@ -147,8 +148,8 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot
147 // catalogue, and a catalogue outside the exact current schema all fail closed:148 // catalogue, and a catalogue outside the exact current schema all fail closed:
148 // the caller receives an error and no observation it could publish, so a probe149 // the caller receives an error and no observation it could publish, so a probe
149 // never invents inventory it did not observe.150 // never invents inventory it did not observe.
150-func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalConnection, request ports.BrokerOpenStreamRequest) (ports.BrokerDaemonObservation, error) {151+func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalConnection, request ports.BrokerOpenStreamRequest, codec ports.SessionCodec) (ports.BrokerDaemonObservation, error) {
151- if physical == nil || request.Purpose != ports.BrokerStreamObservation {152+ if physical == nil || codec == nil || request.Purpose != ports.BrokerStreamObservation {
152 return ports.BrokerDaemonObservation{}, errors.New("vev: invalid daemon observation request")153 return ports.BrokerDaemonObservation{}, errors.New("vev: invalid daemon observation request")
153 }154 }
154 logical, err := physical.OpenStream(ctx, request)155 logical, err := physical.OpenStream(ctx, request)
@@ -167,7 +168,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo
167 if err != nil {168 if err != nil {
168 return ports.BrokerDaemonObservation{}, err169 return ports.BrokerDaemonObservation{}, err
169 }170 }
170- reply, err := exchangeObservationCommand(ctx, logical, protocol.CommandRequest{RequestID: commandID, Version: protocol.Version, Slug: "remote-catalog", JSON: true})171+ reply, err := exchangeObservationCommand(ctx, codec.Client(logical), logical, protocol.CommandRequest{RequestID: commandID, Version: protocol.Version, Slug: "remote-catalog", JSON: true})
171 if err != nil {172 if err != nil {
172 return ports.BrokerDaemonObservation{}, err173 return ports.BrokerDaemonObservation{}, err
173 }174 }
@@ -201,7 +202,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo
201 // joined before returning, so an observation always returns promptly once its202 // joined before returning, so an observation always returns promptly once its
202 // attempt is retired instead of parking the daemon's reader (or a probe203 // attempt is retired instead of parking the daemon's reader (or a probe
203 // attempt) on a daemon that answers nothing.204 // attempt) on a daemon that answers nothing.
204-func exchangeObservationCommand(ctx context.Context, logical ports.BrokerLogicalConnection, command protocol.CommandRequest) (protocol.ServerMessage, error) {205+func exchangeObservationCommand(ctx context.Context, typed ports.ClientConnection, logical ports.BrokerEnvelopeStream, command protocol.CommandRequest) (protocol.ServerMessage, error) {
205 if ctx == nil {206 if ctx == nil {
206 ctx = context.Background()207 ctx = context.Background()
207 }208 }
@@ -211,11 +212,11 @@ func exchangeObservationCommand(ctx context.Context, logical ports.BrokerLogical
211 }212 }
212 done := make(chan outcome, 1)213 done := make(chan outcome, 1)
213 go func() {214 go func() {
214- if err := logical.SendClient(command); err != nil {215+ if err := typed.SendClient(command); err != nil {
215 done <- outcome{err: err}216 done <- outcome{err: err}
216 return217 return
217 }218 }
218- message, err := logical.ReceiveServer()219+ message, err := typed.ReceiveServer()
219 done <- outcome{message: message, err: err}220 done <- outcome{message: message, err: err}
220 }()221 }()
221 select {222 select {
M internal/app/broker_remote_probe_shared_test.go +15 -9
@@ -56,6 +56,7 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
5656 connector.EXPECT().Connect(mock.Anything, mock.Anything).Return(nil, dialErr).Times(tt.wantDials)
5757 }
5858 physical := portsmocks.NewMockBrokerPhysicalConnection(t)
59+ codec := portsmocks.NewMockSessionCodec(t)
5960 done := make(chan struct{})
6061 if tt.retired {
6162 close(done)
@@ -67,13 +68,15 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
6768 physical.EXPECT().Done().Return(done)
6869 }
6970 } else {
70- logical := portsmocks.NewMockBrokerLogicalConnection(t)
71+ logical := portsmocks.NewMockBrokerEnvelopeStream(t)
72+ typed := portsmocks.NewMockClientConnection(t)
73+ codec.EXPECT().Client(logical).Return(typed)
7174 var sent protocol.CommandRequest
72- logical.EXPECT().SendClient(mock.Anything).RunAndReturn(func(message protocol.ClientMessage) error {
75+ typed.EXPECT().SendClient(mock.Anything).RunAndReturn(func(message protocol.ClientMessage) error {
7376 sent = message.(protocol.CommandRequest)
7477 return nil
7578 })
76- logical.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
79+ typed.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
7780 return protocol.CommandResult{RequestID: sent.RequestID, Outcome: protocol.CommandSucceeded, Output: testLocalCatalogue(t)}, nil
7881 })
7982 logical.EXPECT().Close().Return(nil)
@@ -84,7 +87,7 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
8487 }
8588 probe := &brokerRemoteProbe{
8689 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,
87- hosts: &routeTestHosts{record: record, found: true},
90+ hosts: &routeTestHosts{record: record, found: true}, codec: codec,
8891 }
8992 // Use the real pool for borrowing. Stateful pool retirement is
9093 // covered with a fake physical in broker's worker tests.
@@ -145,6 +148,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
145148 // No Connect expectation: any redial fails the mock.
146149 connector := portsmocks.NewMockBrokerEndpointConnector(t)
147150 physical := portsmocks.NewMockBrokerPhysicalConnection(t)
151+ codec := portsmocks.NewMockSessionCodec(t)
148152 healthy := make(chan struct{})
149153 physical.EXPECT().Policy().Return(policy).Maybe()
150154 physical.EXPECT().Identity().Return(identity).Maybe()
@@ -158,17 +162,19 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
158162 // worker only signals, the test goroutine cancels and asserts.
159163 waiting := make(chan struct{}, 1)
160164 if tt.silentOpen {
161- physical.EXPECT().OpenStream(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
165+ physical.EXPECT().OpenStream(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
162166 waiting <- struct{}{}
163167 <-ctx.Done()
164168 return nil, ctx.Err()
165169 })
166170 } else {
167- logical := portsmocks.NewMockBrokerLogicalConnection(t)
171+ logical := portsmocks.NewMockBrokerEnvelopeStream(t)
172+ typed := portsmocks.NewMockClientConnection(t)
173+ codec.EXPECT().Client(logical).Return(typed)
168174 closed := make(chan struct{})
169175 var once sync.Once
170- logical.EXPECT().SendClient(mock.Anything).Return(nil)
171- logical.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
176+ typed.EXPECT().SendClient(mock.Anything).Return(nil)
177+ typed.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
172178 waiting <- struct{}{}
173179 <-closed
174180 return nil, errors.New("stream closed")
@@ -178,7 +184,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
178184 }
179185 probe := &brokerRemoteProbe{
180186 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,
181- hosts: &routeTestHosts{record: record, found: true},
187+ hosts: &routeTestHosts{record: record, found: true}, codec: codec,
182188 }
183189 pool, err := broker.NewPool(7, routes, probe.binder, connector, clock.New(), broker.PoolLimits{Physical: 1, Clients: 1, Streams: 2, StreamsPerClient: 2, Warm: 1})
184190 require.NoError(t, err)
@@ -56,6 +56,7 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
56 connector.EXPECT().Connect(mock.Anything, mock.Anything).Return(nil, dialErr).Times(tt.wantDials)56 connector.EXPECT().Connect(mock.Anything, mock.Anything).Return(nil, dialErr).Times(tt.wantDials)
57 }57 }
58 physical := portsmocks.NewMockBrokerPhysicalConnection(t)58 physical := portsmocks.NewMockBrokerPhysicalConnection(t)
59+ codec := portsmocks.NewMockSessionCodec(t)
59 done := make(chan struct{})60 done := make(chan struct{})
60 if tt.retired {61 if tt.retired {
61 close(done)62 close(done)
@@ -67,13 +68,15 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
67 physical.EXPECT().Done().Return(done)68 physical.EXPECT().Done().Return(done)
68 }69 }
69 } else {70 } else {
70- logical := portsmocks.NewMockBrokerLogicalConnection(t)71+ logical := portsmocks.NewMockBrokerEnvelopeStream(t)
72+ typed := portsmocks.NewMockClientConnection(t)
73+ codec.EXPECT().Client(logical).Return(typed)
71 var sent protocol.CommandRequest74 var sent protocol.CommandRequest
72- logical.EXPECT().SendClient(mock.Anything).RunAndReturn(func(message protocol.ClientMessage) error {75+ typed.EXPECT().SendClient(mock.Anything).RunAndReturn(func(message protocol.ClientMessage) error {
73 sent = message.(protocol.CommandRequest)76 sent = message.(protocol.CommandRequest)
74 return nil77 return nil
75 })78 })
76- logical.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {79+ typed.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
77 return protocol.CommandResult{RequestID: sent.RequestID, Outcome: protocol.CommandSucceeded, Output: testLocalCatalogue(t)}, nil80 return protocol.CommandResult{RequestID: sent.RequestID, Outcome: protocol.CommandSucceeded, Output: testLocalCatalogue(t)}, nil
78 })81 })
79 logical.EXPECT().Close().Return(nil)82 logical.EXPECT().Close().Return(nil)
@@ -84,7 +87,7 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) {
84 }87 }
85 probe := &brokerRemoteProbe{88 probe := &brokerRemoteProbe{
86 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,89 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,
87- hosts: &routeTestHosts{record: record, found: true},90+ hosts: &routeTestHosts{record: record, found: true}, codec: codec,
88 }91 }
89 // Use the real pool for borrowing. Stateful pool retirement is92 // Use the real pool for borrowing. Stateful pool retirement is
90 // covered with a fake physical in broker's worker tests.93 // covered with a fake physical in broker's worker tests.
@@ -145,6 +148,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
145 // No Connect expectation: any redial fails the mock.148 // No Connect expectation: any redial fails the mock.
146 connector := portsmocks.NewMockBrokerEndpointConnector(t)149 connector := portsmocks.NewMockBrokerEndpointConnector(t)
147 physical := portsmocks.NewMockBrokerPhysicalConnection(t)150 physical := portsmocks.NewMockBrokerPhysicalConnection(t)
151+ codec := portsmocks.NewMockSessionCodec(t)
148 healthy := make(chan struct{})152 healthy := make(chan struct{})
149 physical.EXPECT().Policy().Return(policy).Maybe()153 physical.EXPECT().Policy().Return(policy).Maybe()
150 physical.EXPECT().Identity().Return(identity).Maybe()154 physical.EXPECT().Identity().Return(identity).Maybe()
@@ -158,17 +162,19 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
158 // worker only signals, the test goroutine cancels and asserts.162 // worker only signals, the test goroutine cancels and asserts.
159 waiting := make(chan struct{}, 1)163 waiting := make(chan struct{}, 1)
160 if tt.silentOpen {164 if tt.silentOpen {
161- physical.EXPECT().OpenStream(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {165+ physical.EXPECT().OpenStream(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, _ ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
162 waiting <- struct{}{}166 waiting <- struct{}{}
163 <-ctx.Done()167 <-ctx.Done()
164 return nil, ctx.Err()168 return nil, ctx.Err()
165 })169 })
166 } else {170 } else {
167- logical := portsmocks.NewMockBrokerLogicalConnection(t)171+ logical := portsmocks.NewMockBrokerEnvelopeStream(t)
172+ typed := portsmocks.NewMockClientConnection(t)
173+ codec.EXPECT().Client(logical).Return(typed)
168 closed := make(chan struct{})174 closed := make(chan struct{})
169 var once sync.Once175 var once sync.Once
170- logical.EXPECT().SendClient(mock.Anything).Return(nil)176+ typed.EXPECT().SendClient(mock.Anything).Return(nil)
171- logical.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {177+ typed.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) {
172 waiting <- struct{}{}178 waiting <- struct{}{}
173 <-closed179 <-closed
174 return nil, errors.New("stream closed")180 return nil, errors.New("stream closed")
@@ -178,7 +184,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) {
178 }184 }
179 probe := &brokerRemoteProbe{185 probe := &brokerRemoteProbe{
180 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,186 epoch: 7, routes: routes, binder: portsmocks.NewMockBrokerIdentityBinder(t), connector: connector,
181- hosts: &routeTestHosts{record: record, found: true},187+ hosts: &routeTestHosts{record: record, found: true}, codec: codec,
182 }188 }
183 pool, err := broker.NewPool(7, routes, probe.binder, connector, clock.New(), broker.PoolLimits{Physical: 1, Clients: 1, Streams: 2, StreamsPerClient: 2, Warm: 1})189 pool, err := broker.NewPool(7, routes, probe.binder, connector, clock.New(), broker.PoolLimits{Physical: 1, Clients: 1, Streams: 2, StreamsPerClient: 2, Warm: 1})
184 require.NoError(t, err)190 require.NoError(t, err)
M internal/ports/broker.go +30 -5
@@ -823,7 +823,7 @@ type BrokerPreviewSubscription interface {
823823 // local and remote daemons through it, including the local daemon.
824824 // Snapshot access performs no I/O; request methods are non-blocking
825825 // coalesced hints except where the context explicitly bounds them.
826-type BrokerService interface {
826+type BrokerSession interface {
827827 // ConnectionID returns the ID assigned when this client connection was
828828 // accepted. Clients carry it on stream operations to fence stale requests.
829829 ConnectionID() BrokerConnectionID
@@ -844,7 +844,6 @@ type BrokerService interface {
844844 Snapshot() BrokerSnapshot
845845 Subscribe() (BrokerSubscription, error)
846846 SubscribePreview(request BrokerPreviewRequest) (BrokerPreviewSubscription, error)
847- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
848847 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
849848 // AddHost durably adds or pins endpoint under policy and returns its authority.
850849 AddHost(ctx context.Context, endpoint string, policy BrokerPolicy) (domain.RemoteRegistration, error)
@@ -856,6 +855,32 @@ type BrokerService interface {
856855 Close() error
857856 }
858857
858+// BrokerService is the typed client-side broker connection.
859+type BrokerService interface {
860+ BrokerSession
861+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
862+}
863+
864+// BrokerEnvelopeStream carries opaque complete session envelopes across the core.
865+type BrokerEnvelopeStream interface {
866+ SendEnvelope([]byte) error
867+ RecvEnvelope() ([]byte, error)
868+ Done() <-chan struct{}
869+ Err() error
870+ Close() error
871+}
872+
873+// BrokerCoreService is one admitted connection on the broker side.
874+type BrokerCoreService interface {
875+ BrokerSession
876+ OpenEnvelopeStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error)
877+}
878+
879+// SessionCodec adapts a raw core stream for broker-owned typed operations.
880+type SessionCodec interface {
881+ Client(BrokerEnvelopeStream) ClientConnection
882+}
883+
859884 // BrokerSnapshotStore persists the durable broker snapshot independently
860885 // from session state. The broker is its sole writer; session persistence
861886 // stays daemon-owned and is never moved here. Load is called once while the
@@ -910,7 +935,7 @@ type BrokerPhysicalConnection interface {
910935 Identity() BrokerDaemonIdentity
911936 Incarnation() BrokerDaemonIncarnation
912937 Policy() BrokerPolicy
913- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
938+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error)
914939 Close() error
915940 }
916941
@@ -961,7 +986,7 @@ type BrokerConnector interface {
961986 // parked in admission immediately. The admitted service gets its own
962987 // connection-lived context and must not derive from this one.
963988 type BrokerAuthority interface {
964- AdmitClient(ctx context.Context) (BrokerService, error)
989+ AdmitClient(ctx context.Context) (BrokerCoreService, error)
965990 }
966991
967992 // BrokerListener accepts client connections to the per-user broker
@@ -972,7 +997,7 @@ type BrokerAuthority interface {
972997 // Implementations own the owner-only directory/socket, peer credential
973998 // checks, bounded clients and queues, and cancellation-safe accept loops.
974999 type BrokerListener interface {
975- Accept() (BrokerService, error)
1000+ Accept() (BrokerCoreService, error)
9761001 Close() error
9771002 Addr() string
9781003 }
@@ -823,7 +823,7 @@ type BrokerPreviewSubscription interface {
823 // local and remote daemons through it, including the local daemon.823 // local and remote daemons through it, including the local daemon.
824 // Snapshot access performs no I/O; request methods are non-blocking824 // Snapshot access performs no I/O; request methods are non-blocking
825 // coalesced hints except where the context explicitly bounds them.825 // coalesced hints except where the context explicitly bounds them.
826-type BrokerService interface {826+type BrokerSession interface {
827 // ConnectionID returns the ID assigned when this client connection was827 // ConnectionID returns the ID assigned when this client connection was
828 // accepted. Clients carry it on stream operations to fence stale requests.828 // accepted. Clients carry it on stream operations to fence stale requests.
829 ConnectionID() BrokerConnectionID829 ConnectionID() BrokerConnectionID
@@ -844,7 +844,6 @@ type BrokerService interface {
844 Snapshot() BrokerSnapshot844 Snapshot() BrokerSnapshot
845 Subscribe() (BrokerSubscription, error)845 Subscribe() (BrokerSubscription, error)
846 SubscribePreview(request BrokerPreviewRequest) (BrokerPreviewSubscription, error)846 SubscribePreview(request BrokerPreviewRequest) (BrokerPreviewSubscription, error)
847- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
848 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error847 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
849 // AddHost durably adds or pins endpoint under policy and returns its authority.848 // AddHost durably adds or pins endpoint under policy and returns its authority.
850 AddHost(ctx context.Context, endpoint string, policy BrokerPolicy) (domain.RemoteRegistration, error)849 AddHost(ctx context.Context, endpoint string, policy BrokerPolicy) (domain.RemoteRegistration, error)
@@ -856,6 +855,32 @@ type BrokerService interface {
856 Close() error855 Close() error
857 }856 }
858 857
858+// BrokerService is the typed client-side broker connection.
859+type BrokerService interface {
860+ BrokerSession
861+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
862+}
863+
864+// BrokerEnvelopeStream carries opaque complete session envelopes across the core.
865+type BrokerEnvelopeStream interface {
866+ SendEnvelope([]byte) error
867+ RecvEnvelope() ([]byte, error)
868+ Done() <-chan struct{}
869+ Err() error
870+ Close() error
871+}
872+
873+// BrokerCoreService is one admitted connection on the broker side.
874+type BrokerCoreService interface {
875+ BrokerSession
876+ OpenEnvelopeStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error)
877+}
878+
879+// SessionCodec adapts a raw core stream for broker-owned typed operations.
880+type SessionCodec interface {
881+ Client(BrokerEnvelopeStream) ClientConnection
882+}
883+
859 // BrokerSnapshotStore persists the durable broker snapshot independently884 // BrokerSnapshotStore persists the durable broker snapshot independently
860 // from session state. The broker is its sole writer; session persistence885 // from session state. The broker is its sole writer; session persistence
861 // stays daemon-owned and is never moved here. Load is called once while the886 // stays daemon-owned and is never moved here. Load is called once while the
@@ -910,7 +935,7 @@ type BrokerPhysicalConnection interface {
910 Identity() BrokerDaemonIdentity935 Identity() BrokerDaemonIdentity
911 Incarnation() BrokerDaemonIncarnation936 Incarnation() BrokerDaemonIncarnation
912 Policy() BrokerPolicy937 Policy() BrokerPolicy
913- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)938+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error)
914 Close() error939 Close() error
915 }940 }
916 941
@@ -961,7 +986,7 @@ type BrokerConnector interface {
961 // parked in admission immediately. The admitted service gets its own986 // parked in admission immediately. The admitted service gets its own
962 // connection-lived context and must not derive from this one.987 // connection-lived context and must not derive from this one.
963 type BrokerAuthority interface {988 type BrokerAuthority interface {
964- AdmitClient(ctx context.Context) (BrokerService, error)989+ AdmitClient(ctx context.Context) (BrokerCoreService, error)
965 }990 }
966 991
967 // BrokerListener accepts client connections to the per-user broker992 // BrokerListener accepts client connections to the per-user broker
@@ -972,7 +997,7 @@ type BrokerAuthority interface {
972 // Implementations own the owner-only directory/socket, peer credential997 // Implementations own the owner-only directory/socket, peer credential
973 // checks, bounded clients and queues, and cancellation-safe accept loops.998 // checks, bounded clients and queues, and cancellation-safe accept loops.
974 type BrokerListener interface {999 type BrokerListener interface {
975- Accept() (BrokerService, error)1000+ Accept() (BrokerCoreService, error)
976 Close() error1001 Close() error
977 Addr() string1002 Addr() string
978 }1003 }
M internal/ports/mocks/broker_mock.go +2036 -165 large diff (2201 lines changed): collapsed
M internal/usecase/broker/pool.go +4 -4
@@ -160,7 +160,7 @@ func (p *Pool) CloseStream(id ports.BrokerConnectionID, stream ports.BrokerStrea
160160 return nil
161161 }
162162
163-func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
163+func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
164164 if req.Epoch != p.epoch {
165165 return nil, ports.BrokerError{Code: ports.BrokerErrorStaleEpoch}
166166 }
@@ -324,7 +324,7 @@ func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest
324324 _ = raw.Close()
325325 return nil, terminal
326326 }
327- stream := &pooledStream{BrokerLogicalConnection: raw, done: make(chan struct{}), release: release}
327+ stream := &pooledStream{BrokerEnvelopeStream: raw, done: make(chan struct{}), release: release}
328328 success = true
329329 go func() {
330330 var terminal error
@@ -610,7 +610,7 @@ func (p *Pool) Close() error {
610610 }
611611
612612 type pooledStream struct {
613- ports.BrokerLogicalConnection
613+ ports.BrokerEnvelopeStream
614614 once sync.Once
615615 done chan struct{}
616616 err error
@@ -629,7 +629,7 @@ func (s *pooledStream) Err() error {
629629 }
630630 func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr }
631631 func (s *pooledStream) finish(err error) {
632- s.once.Do(func() { s.err = err; s.closeErr = s.BrokerLogicalConnection.Close(); s.release(); close(s.done) })
632+ s.once.Do(func() { s.err = err; s.closeErr = s.BrokerEnvelopeStream.Close(); s.release(); close(s.done) })
633633 }
634634
635635 // One goroutine owns this timer. Drain a buffered expiry before every reset,
@@ -160,7 +160,7 @@ func (p *Pool) CloseStream(id ports.BrokerConnectionID, stream ports.BrokerStrea
160 return nil160 return nil
161 }161 }
162 162
163-func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {163+func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
164 if req.Epoch != p.epoch {164 if req.Epoch != p.epoch {
165 return nil, ports.BrokerError{Code: ports.BrokerErrorStaleEpoch}165 return nil, ports.BrokerError{Code: ports.BrokerErrorStaleEpoch}
166 }166 }
@@ -324,7 +324,7 @@ func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest
324 _ = raw.Close()324 _ = raw.Close()
325 return nil, terminal325 return nil, terminal
326 }326 }
327- stream := &pooledStream{BrokerLogicalConnection: raw, done: make(chan struct{}), release: release}327+ stream := &pooledStream{BrokerEnvelopeStream: raw, done: make(chan struct{}), release: release}
328 success = true328 success = true
329 go func() {329 go func() {
330 var terminal error330 var terminal error
@@ -610,7 +610,7 @@ func (p *Pool) Close() error {
610 }610 }
611 611
612 type pooledStream struct {612 type pooledStream struct {
613- ports.BrokerLogicalConnection613+ ports.BrokerEnvelopeStream
614 once sync.Once614 once sync.Once
615 done chan struct{}615 done chan struct{}
616 err error616 err error
@@ -629,7 +629,7 @@ func (s *pooledStream) Err() error {
629 }629 }
630 func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr }630 func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr }
631 func (s *pooledStream) finish(err error) {631 func (s *pooledStream) finish(err error) {
632- s.once.Do(func() { s.err = err; s.closeErr = s.BrokerLogicalConnection.Close(); s.release(); close(s.done) })632+ s.once.Do(func() { s.err = err; s.closeErr = s.BrokerEnvelopeStream.Close(); s.release(); close(s.done) })
633 }633 }
634 634
635 // One goroutine owns this timer. Drain a buffered expiry before every reset,635 // One goroutine owns this timer. Drain a buffered expiry before every reset,
M internal/usecase/broker/pool_review_test.go +4 -4
@@ -28,7 +28,7 @@ func TestPoolAdapterFailures(t *testing.T) {
2828 if stage == "connector" {
2929 return nil, tc.err
3030 }
31- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
31+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
3232 return nil, tc.err
3333 }}, nil
3434 })
@@ -52,7 +52,7 @@ func TestPoolRejectedPaths(t *testing.T) {
5252 for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} {
5353 t.Run(name, func(t *testing.T) {
5454 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
55- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
55+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
5656 return nil, nil
5757 }}, nil
5858 })
@@ -125,7 +125,7 @@ func TestPoolPostOpenPhysicalLoss(t *testing.T) {
125125 raw := newFakeLogical()
126126 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
127127 f := &fakePhysical{endpoint: e, done: make(chan struct{})}
128- f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
128+ f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
129129 _ = f.Close()
130130 return raw, nil
131131 }
@@ -209,7 +209,7 @@ func (l *closeErrorLogical) Close() error { _ = l.fakeLogical.Close(); return l.
209209 func TestPooledStreamPreservesCloseError(t *testing.T) {
210210 cause := errors.New("close failed")
211211 calls := 0
212- s := &pooledStream{BrokerLogicalConnection: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}
212+ s := &pooledStream{BrokerEnvelopeStream: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}
213213 require.ErrorIs(t, s.Close(), cause)
214214 require.ErrorIs(t, s.Close(), cause)
215215 require.Equal(t, 1, calls)
@@ -28,7 +28,7 @@ func TestPoolAdapterFailures(t *testing.T) {
28 if stage == "connector" {28 if stage == "connector" {
29 return nil, tc.err29 return nil, tc.err
30 }30 }
31- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {31+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
32 return nil, tc.err32 return nil, tc.err
33 }}, nil33 }}, nil
34 })34 })
@@ -52,7 +52,7 @@ func TestPoolRejectedPaths(t *testing.T) {
52 for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} {52 for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} {
53 t.Run(name, func(t *testing.T) {53 t.Run(name, func(t *testing.T) {
54 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {54 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
55- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {55+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
56 return nil, nil56 return nil, nil
57 }}, nil57 }}, nil
58 })58 })
@@ -125,7 +125,7 @@ func TestPoolPostOpenPhysicalLoss(t *testing.T) {
125 raw := newFakeLogical()125 raw := newFakeLogical()
126 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {126 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
127 f := &fakePhysical{endpoint: e, done: make(chan struct{})}127 f := &fakePhysical{endpoint: e, done: make(chan struct{})}
128- f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {128+ f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
129 _ = f.Close()129 _ = f.Close()
130 return raw, nil130 return raw, nil
131 }131 }
@@ -209,7 +209,7 @@ func (l *closeErrorLogical) Close() error { _ = l.fakeLogical.Close(); return l.
209 func TestPooledStreamPreservesCloseError(t *testing.T) {209 func TestPooledStreamPreservesCloseError(t *testing.T) {
210 cause := errors.New("close failed")210 cause := errors.New("close failed")
211 calls := 0211 calls := 0
212- s := &pooledStream{BrokerLogicalConnection: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}212+ s := &pooledStream{BrokerEnvelopeStream: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}
213 require.ErrorIs(t, s.Close(), cause)213 require.ErrorIs(t, s.Close(), cause)
214 require.ErrorIs(t, s.Close(), cause)214 require.ErrorIs(t, s.Close(), cause)
215 require.Equal(t, 1, calls)215 require.Equal(t, 1, calls)
M internal/usecase/broker/pool_test.go +13 -13
@@ -36,16 +36,16 @@ func (f poolConnector) Connect(c context.Context, r ports.BrokerDialTarget) (por
3636 }
3737
3838 type fakeLogical struct {
39- ports.ClientConnection
4039 done chan struct{}
4140 once sync.Once
4241 }
4342
44-func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }
45-func (f *fakeLogical) Done() <-chan struct{} { return f.done }
46-func (f *fakeLogical) Err() error { return nil }
47-func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
48-func (f *fakeLogical) SendClient(protocol.ClientMessage) error { <-f.done; return errors.New("closed") }
43+func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }
44+func (f *fakeLogical) Done() <-chan struct{} { return f.done }
45+func (f *fakeLogical) Err() error { return nil }
46+func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
47+func (f *fakeLogical) SendEnvelope([]byte) error { <-f.done; return errors.New("closed") }
48+func (f *fakeLogical) RecvEnvelope() ([]byte, error) { <-f.done; return nil, errors.New("closed") }
4949
5050 type fakePhysical struct {
5151 endpoint ports.BrokerDialTarget
@@ -54,7 +54,7 @@ type fakePhysical struct {
5454 identity ports.BrokerDaemonIdentity
5555 done chan struct{}
5656 once sync.Once
57- open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)
57+ open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error)
5858 }
5959
6060 func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity {
@@ -71,7 +71,7 @@ func (f *fakePhysical) FailureKind() domain.RemoteFailureKind { return domain.Re
7171 func (f *fakePhysical) Done() <-chan struct{} { return f.done }
7272 func (f *fakePhysical) Err() error { return errors.New("physical loss") }
7373 func (f *fakePhysical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
74-func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
74+func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
7575 if f.open != nil {
7676 return f.open(c, r)
7777 }
@@ -140,7 +140,7 @@ func TestPoolOpenFailureWithTypedNilConnection(t *testing.T) {
140140 return &fakePhysical{
141141 endpoint: e,
142142 done: make(chan struct{}),
143- open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
143+ open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
144144 var absent *fakeLogical
145145 return absent, errors.New("open failed")
146146 },
@@ -164,7 +164,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
164164 physical = &fakePhysical{endpoint: e, done: make(chan struct{})}
165165 return physical, nil
166166 })
167- streams := make([]ports.BrokerLogicalConnection, 100)
167+ streams := make([]ports.BrokerEnvelopeStream, 100)
168168 var wg sync.WaitGroup
169169 for i := range streams {
170170 id, err := p.RegisterClient()
@@ -183,7 +183,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
183183 wg.Wait()
184184 require.EqualValues(t, 1, calls.Load())
185185 blocked := make(chan struct{})
186- go func() { _ = streams[0].SendClient(nil); close(blocked) }()
186+ go func() { _ = streams[0].SendEnvelope(nil); close(blocked) }()
187187 require.NoError(t, streams[0].Close())
188188 await(t, blocked)
189189 select {
@@ -336,7 +336,7 @@ func TestPoolCoalescedCancellation(t *testing.T) {
336336 result := make(chan error, 1)
337337 go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }()
338338 await(t, entered)
339- result2 := make(chan ports.BrokerLogicalConnection, 1)
339+ result2 := make(chan ports.BrokerEnvelopeStream, 1)
340340 go func() {
341341 s, err := p.OpenStream(context.Background(), poolRequest(id2, 1))
342342 if err != nil {
@@ -475,7 +475,7 @@ func TestPoolAliasesAndPolicyPartition(t *testing.T) {
475475 func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) {
476476 entered := make(chan struct{})
477477 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
478- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
478+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
479479 if r.Stream == 1 {
480480 close(entered)
481481 <-ctx.Done()
@@ -36,16 +36,16 @@ func (f poolConnector) Connect(c context.Context, r ports.BrokerDialTarget) (por
36 }36 }
37 37
38 type fakeLogical struct {38 type fakeLogical struct {
39- ports.ClientConnection
40 done chan struct{}39 done chan struct{}
41 once sync.Once40 once sync.Once
42 }41 }
43 42
44-func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }43+func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }
45-func (f *fakeLogical) Done() <-chan struct{} { return f.done }44+func (f *fakeLogical) Done() <-chan struct{} { return f.done }
46-func (f *fakeLogical) Err() error { return nil }45+func (f *fakeLogical) Err() error { return nil }
47-func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }46+func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
48-func (f *fakeLogical) SendClient(protocol.ClientMessage) error { <-f.done; return errors.New("closed") }47+func (f *fakeLogical) SendEnvelope([]byte) error { <-f.done; return errors.New("closed") }
48+func (f *fakeLogical) RecvEnvelope() ([]byte, error) { <-f.done; return nil, errors.New("closed") }
49 49
50 type fakePhysical struct {50 type fakePhysical struct {
51 endpoint ports.BrokerDialTarget51 endpoint ports.BrokerDialTarget
@@ -54,7 +54,7 @@ type fakePhysical struct {
54 identity ports.BrokerDaemonIdentity54 identity ports.BrokerDaemonIdentity
55 done chan struct{}55 done chan struct{}
56 once sync.Once56 once sync.Once
57- open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)57+ open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error)
58 }58 }
59 59
60 func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity {60 func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity {
@@ -71,7 +71,7 @@ func (f *fakePhysical) FailureKind() domain.RemoteFailureKind { return domain.Re
71 func (f *fakePhysical) Done() <-chan struct{} { return f.done }71 func (f *fakePhysical) Done() <-chan struct{} { return f.done }
72 func (f *fakePhysical) Err() error { return errors.New("physical loss") }72 func (f *fakePhysical) Err() error { return errors.New("physical loss") }
73 func (f *fakePhysical) Close() error { f.once.Do(func() { close(f.done) }); return nil }73 func (f *fakePhysical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
74-func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {74+func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
75 if f.open != nil {75 if f.open != nil {
76 return f.open(c, r)76 return f.open(c, r)
77 }77 }
@@ -140,7 +140,7 @@ func TestPoolOpenFailureWithTypedNilConnection(t *testing.T) {
140 return &fakePhysical{140 return &fakePhysical{
141 endpoint: e,141 endpoint: e,
142 done: make(chan struct{}),142 done: make(chan struct{}),
143- open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {143+ open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
144 var absent *fakeLogical144 var absent *fakeLogical
145 return absent, errors.New("open failed")145 return absent, errors.New("open failed")
146 },146 },
@@ -164,7 +164,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
164 physical = &fakePhysical{endpoint: e, done: make(chan struct{})}164 physical = &fakePhysical{endpoint: e, done: make(chan struct{})}
165 return physical, nil165 return physical, nil
166 })166 })
167- streams := make([]ports.BrokerLogicalConnection, 100)167+ streams := make([]ports.BrokerEnvelopeStream, 100)
168 var wg sync.WaitGroup168 var wg sync.WaitGroup
169 for i := range streams {169 for i := range streams {
170 id, err := p.RegisterClient()170 id, err := p.RegisterClient()
@@ -183,7 +183,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
183 wg.Wait()183 wg.Wait()
184 require.EqualValues(t, 1, calls.Load())184 require.EqualValues(t, 1, calls.Load())
185 blocked := make(chan struct{})185 blocked := make(chan struct{})
186- go func() { _ = streams[0].SendClient(nil); close(blocked) }()186+ go func() { _ = streams[0].SendEnvelope(nil); close(blocked) }()
187 require.NoError(t, streams[0].Close())187 require.NoError(t, streams[0].Close())
188 await(t, blocked)188 await(t, blocked)
189 select {189 select {
@@ -336,7 +336,7 @@ func TestPoolCoalescedCancellation(t *testing.T) {
336 result := make(chan error, 1)336 result := make(chan error, 1)
337 go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }()337 go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }()
338 await(t, entered)338 await(t, entered)
339- result2 := make(chan ports.BrokerLogicalConnection, 1)339+ result2 := make(chan ports.BrokerEnvelopeStream, 1)
340 go func() {340 go func() {
341 s, err := p.OpenStream(context.Background(), poolRequest(id2, 1))341 s, err := p.OpenStream(context.Background(), poolRequest(id2, 1))
342 if err != nil {342 if err != nil {
@@ -475,7 +475,7 @@ func TestPoolAliasesAndPolicyPartition(t *testing.T) {
475 func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) {475 func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) {
476 entered := make(chan struct{})476 entered := make(chan struct{})
477 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {477 p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
478- return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {478+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
479 if r.Stream == 1 {479 if r.Stream == 1 {
480 close(entered)480 close(entered)
481 <-ctx.Done()481 <-ctx.Done()
M internal/usecase/broker/service.go +14 -11
@@ -12,7 +12,7 @@ import (
1212 // Offline broker composition.
1313 //
1414 // Authority admits one accepted client connection to the broker core and
15-// returns the connection-scoped ports.BrokerService for exactly that
15+// returns the connection-scoped ports.BrokerCoreService for exactly that
1616 // connection. It is the structural implementation of ports.BrokerAuthority,
1717 // the admission seam the broker IPC listener (internal/adapters/brokeripc)
1818 // consumes and which this use case must not import: admission obtains one
@@ -40,6 +40,7 @@ type Authority struct {
4040 pool *Pool
4141 supervisor *Supervisor
4242 clock ports.Clock
43+ codec ports.SessionCodec
4344 }
4445
4546 var _ ports.BrokerAuthority = (*Authority)(nil)
@@ -53,7 +54,7 @@ var _ ports.BrokerAuthority = (*Authority)(nil)
5354 // neither starts nor settles Run: serving connections without a live Run leaks
5455 // the writer and never flushes the newest staged publication, and a registry
5556 // that was never run leaves its durable writer undrained.
56-func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, supervisor *Supervisor, clocks ...ports.Clock) (*Authority, error) {
57+func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, supervisor *Supervisor, codec ports.SessionCodec, clocks ...ports.Clock) (*Authority, error) {
5758 if epoch == 0 || nilDependency(registry) || nilDependency(pool) || nilDependency(supervisor) {
5859 return nil, errors.New("broker: invalid authority dependencies")
5960 }
@@ -64,10 +65,10 @@ func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, super
6465 if len(clocks) > 0 {
6566 clk = clocks[0]
6667 }
67- if nilDependency(clk) {
68+ if nilDependency(clk) || nilDependency(codec) {
6869 return nil, errors.New("broker: invalid authority clock")
6970 }
70- return &Authority{epoch: epoch, registry: registry, pool: pool, supervisor: supervisor, clock: clk}, nil
71+ return &Authority{epoch: epoch, registry: registry, pool: pool, supervisor: supervisor, clock: clk, codec: codec}, nil
7172 }
7273
7374 // AdmitClient admits exactly one accepted client connection. The supervisor
@@ -76,7 +77,7 @@ func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, super
7677 // commits both or neither. ctx bounds admission only and is never retained: the
7778 // returned service derives its own connection-lived context from the supervisor
7879 // root, so a caller's setup deadline cannot disturb an admitted connection.
79-func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {
80+func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
8081 if err := ctx.Err(); err != nil {
8182 return nil, err
8283 }
@@ -103,12 +104,12 @@ func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerService, error
103104 }
104105 return newService(serviceConfig{
105106 epoch: a.epoch, id: id, previewID: previewID, registry: a.registry,
106- pool: a.pool, supervisor: a.supervisor, lease: lease, clock: a.clock,
107+ pool: a.pool, supervisor: a.supervisor, lease: lease, clock: a.clock, codec: a.codec,
107108 }), nil
108109 }
109110
110111 // Service is one admitted client connection: the core's service for exactly one
111-// pool connection identity and broker epoch. It implements ports.BrokerService.
112+// pool connection identity and broker epoch. It implements ports.BrokerCoreService.
112113 type Service struct {
113114 epoch ports.BrokerEpoch
114115 id ports.BrokerConnectionID
@@ -118,6 +119,7 @@ type Service struct {
118119 supervisor *Supervisor
119120 lease *Lease
120121 clock ports.Clock
122+ codec ports.SessionCodec
121123
122124 // ctx owns everything this connection started: in-flight setup opens and
123125 // live streams. It is derived from the supervisor root, never from the
@@ -154,7 +156,7 @@ type Service struct {
154156 termErr error
155157 }
156158
157-var _ ports.BrokerService = (*Service)(nil)
159+var _ ports.BrokerCoreService = (*Service)(nil)
158160
159161 type serviceConfig struct {
160162 epoch ports.BrokerEpoch
@@ -164,11 +166,12 @@ type serviceConfig struct {
164166 supervisor *Supervisor
165167 lease *Lease
166168 clock ports.Clock
169+ codec ports.SessionCodec
167170 }
168171
169172 func (c serviceConfig) validate() error {
170173 if c.epoch == 0 || c.id == (ports.BrokerConnectionID{}) || c.previewID == (ports.BrokerConnectionID{}) || c.id == c.previewID ||
171- nilDependency(c.registry) || nilDependency(c.pool) || nilDependency(c.supervisor) || nilDependency(c.lease) || nilDependency(c.clock) {
174+ nilDependency(c.registry) || nilDependency(c.pool) || nilDependency(c.supervisor) || nilDependency(c.lease) || nilDependency(c.clock) || nilDependency(c.codec) {
172175 return errors.New("broker: invalid service dependencies")
173176 }
174177 return nil
@@ -180,7 +183,7 @@ func newService(c serviceConfig) *Service {
180183 }
181184 ctx, cancel := context.WithCancel(c.supervisor.RootContext())
182185 s := &Service{
183- epoch: c.epoch, id: c.id, previewID: c.previewID, registry: c.registry, pool: c.pool, supervisor: c.supervisor, lease: c.lease, clock: c.clock,
186+ epoch: c.epoch, id: c.id, previewID: c.previewID, registry: c.registry, pool: c.pool, supervisor: c.supervisor, lease: c.lease, clock: c.clock, codec: c.codec,
184187 ctx: ctx, cancel: cancel, subs: make(map[*serviceSubscription]struct{}),
185188 done: make(chan struct{}),
186189 }
@@ -284,7 +287,7 @@ func (s *Service) subscribe(demand bool) (ports.BrokerSubscription, error) {
284287 // the pool sees it, and one operation lease pins the broker only while setup is
285288 // in flight: a stream that opens successfully outlives its setup lease, and
286289 // Close or broker shutdown still aborts anything that has not finished.
287-func (s *Service) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
290+func (s *Service) OpenEnvelopeStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
288291 if err := s.scope(&request); err != nil {
289292 return nil, err
290293 }
@@ -12,7 +12,7 @@ import (
12 // Offline broker composition.12 // Offline broker composition.
13 //13 //
14 // Authority admits one accepted client connection to the broker core and14 // Authority admits one accepted client connection to the broker core and
15-// returns the connection-scoped ports.BrokerService for exactly that15+// returns the connection-scoped ports.BrokerCoreService for exactly that
16 // connection. It is the structural implementation of ports.BrokerAuthority,16 // connection. It is the structural implementation of ports.BrokerAuthority,
17 // the admission seam the broker IPC listener (internal/adapters/brokeripc)17 // the admission seam the broker IPC listener (internal/adapters/brokeripc)
18 // consumes and which this use case must not import: admission obtains one18 // consumes and which this use case must not import: admission obtains one
@@ -40,6 +40,7 @@ type Authority struct {
40 pool *Pool40 pool *Pool
41 supervisor *Supervisor41 supervisor *Supervisor
42 clock ports.Clock42 clock ports.Clock
43+ codec ports.SessionCodec
43 }44 }
44 45
45 var _ ports.BrokerAuthority = (*Authority)(nil)46 var _ ports.BrokerAuthority = (*Authority)(nil)
@@ -53,7 +54,7 @@ var _ ports.BrokerAuthority = (*Authority)(nil)
53 // neither starts nor settles Run: serving connections without a live Run leaks54 // neither starts nor settles Run: serving connections without a live Run leaks
54 // the writer and never flushes the newest staged publication, and a registry55 // the writer and never flushes the newest staged publication, and a registry
55 // that was never run leaves its durable writer undrained.56 // that was never run leaves its durable writer undrained.
56-func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, supervisor *Supervisor, clocks ...ports.Clock) (*Authority, error) {57+func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, supervisor *Supervisor, codec ports.SessionCodec, clocks ...ports.Clock) (*Authority, error) {
57 if epoch == 0 || nilDependency(registry) || nilDependency(pool) || nilDependency(supervisor) {58 if epoch == 0 || nilDependency(registry) || nilDependency(pool) || nilDependency(supervisor) {
58 return nil, errors.New("broker: invalid authority dependencies")59 return nil, errors.New("broker: invalid authority dependencies")
59 }60 }
@@ -64,10 +65,10 @@ func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, super
64 if len(clocks) > 0 {65 if len(clocks) > 0 {
65 clk = clocks[0]66 clk = clocks[0]
66 }67 }
67- if nilDependency(clk) {68+ if nilDependency(clk) || nilDependency(codec) {
68 return nil, errors.New("broker: invalid authority clock")69 return nil, errors.New("broker: invalid authority clock")
69 }70 }
70- return &Authority{epoch: epoch, registry: registry, pool: pool, supervisor: supervisor, clock: clk}, nil71+ return &Authority{epoch: epoch, registry: registry, pool: pool, supervisor: supervisor, clock: clk, codec: codec}, nil
71 }72 }
72 73
73 // AdmitClient admits exactly one accepted client connection. The supervisor74 // AdmitClient admits exactly one accepted client connection. The supervisor
@@ -76,7 +77,7 @@ func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, super
76 // commits both or neither. ctx bounds admission only and is never retained: the77 // commits both or neither. ctx bounds admission only and is never retained: the
77 // returned service derives its own connection-lived context from the supervisor78 // returned service derives its own connection-lived context from the supervisor
78 // root, so a caller's setup deadline cannot disturb an admitted connection.79 // root, so a caller's setup deadline cannot disturb an admitted connection.
79-func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {80+func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
80 if err := ctx.Err(); err != nil {81 if err := ctx.Err(); err != nil {
81 return nil, err82 return nil, err
82 }83 }
@@ -103,12 +104,12 @@ func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerService, error
103 }104 }
104 return newService(serviceConfig{105 return newService(serviceConfig{
105 epoch: a.epoch, id: id, previewID: previewID, registry: a.registry,106 epoch: a.epoch, id: id, previewID: previewID, registry: a.registry,
106- pool: a.pool, supervisor: a.supervisor, lease: lease, clock: a.clock,107+ pool: a.pool, supervisor: a.supervisor, lease: lease, clock: a.clock, codec: a.codec,
107 }), nil108 }), nil
108 }109 }
109 110
110 // Service is one admitted client connection: the core's service for exactly one111 // Service is one admitted client connection: the core's service for exactly one
111-// pool connection identity and broker epoch. It implements ports.BrokerService.112+// pool connection identity and broker epoch. It implements ports.BrokerCoreService.
112 type Service struct {113 type Service struct {
113 epoch ports.BrokerEpoch114 epoch ports.BrokerEpoch
114 id ports.BrokerConnectionID115 id ports.BrokerConnectionID
@@ -118,6 +119,7 @@ type Service struct {
118 supervisor *Supervisor119 supervisor *Supervisor
119 lease *Lease120 lease *Lease
120 clock ports.Clock121 clock ports.Clock
122+ codec ports.SessionCodec
121 123
122 // ctx owns everything this connection started: in-flight setup opens and124 // ctx owns everything this connection started: in-flight setup opens and
123 // live streams. It is derived from the supervisor root, never from the125 // live streams. It is derived from the supervisor root, never from the
@@ -154,7 +156,7 @@ type Service struct {
154 termErr error156 termErr error
155 }157 }
156 158
157-var _ ports.BrokerService = (*Service)(nil)159+var _ ports.BrokerCoreService = (*Service)(nil)
158 160
159 type serviceConfig struct {161 type serviceConfig struct {
160 epoch ports.BrokerEpoch162 epoch ports.BrokerEpoch
@@ -164,11 +166,12 @@ type serviceConfig struct {
164 supervisor *Supervisor166 supervisor *Supervisor
165 lease *Lease167 lease *Lease
166 clock ports.Clock168 clock ports.Clock
169+ codec ports.SessionCodec
167 }170 }
168 171
169 func (c serviceConfig) validate() error {172 func (c serviceConfig) validate() error {
170 if c.epoch == 0 || c.id == (ports.BrokerConnectionID{}) || c.previewID == (ports.BrokerConnectionID{}) || c.id == c.previewID ||173 if c.epoch == 0 || c.id == (ports.BrokerConnectionID{}) || c.previewID == (ports.BrokerConnectionID{}) || c.id == c.previewID ||
171- nilDependency(c.registry) || nilDependency(c.pool) || nilDependency(c.supervisor) || nilDependency(c.lease) || nilDependency(c.clock) {174+ nilDependency(c.registry) || nilDependency(c.pool) || nilDependency(c.supervisor) || nilDependency(c.lease) || nilDependency(c.clock) || nilDependency(c.codec) {
172 return errors.New("broker: invalid service dependencies")175 return errors.New("broker: invalid service dependencies")
173 }176 }
174 return nil177 return nil
@@ -180,7 +183,7 @@ func newService(c serviceConfig) *Service {
180 }183 }
181 ctx, cancel := context.WithCancel(c.supervisor.RootContext())184 ctx, cancel := context.WithCancel(c.supervisor.RootContext())
182 s := &Service{185 s := &Service{
183- epoch: c.epoch, id: c.id, previewID: c.previewID, registry: c.registry, pool: c.pool, supervisor: c.supervisor, lease: c.lease, clock: c.clock,186+ epoch: c.epoch, id: c.id, previewID: c.previewID, registry: c.registry, pool: c.pool, supervisor: c.supervisor, lease: c.lease, clock: c.clock, codec: c.codec,
184 ctx: ctx, cancel: cancel, subs: make(map[*serviceSubscription]struct{}),187 ctx: ctx, cancel: cancel, subs: make(map[*serviceSubscription]struct{}),
185 done: make(chan struct{}),188 done: make(chan struct{}),
186 }189 }
@@ -284,7 +287,7 @@ func (s *Service) subscribe(demand bool) (ports.BrokerSubscription, error) {
284 // the pool sees it, and one operation lease pins the broker only while setup is287 // the pool sees it, and one operation lease pins the broker only while setup is
285 // in flight: a stream that opens successfully outlives its setup lease, and288 // in flight: a stream that opens successfully outlives its setup lease, and
286 // Close or broker shutdown still aborts anything that has not finished.289 // Close or broker shutdown still aborts anything that has not finished.
287-func (s *Service) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {290+func (s *Service) OpenEnvelopeStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
288 if err := s.scope(&request); err != nil {291 if err := s.scope(&request); err != nil {
289 return nil, err292 return nil, err
290 }293 }
M internal/usecase/broker/service_preview.go +4 -3
@@ -199,7 +199,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
199199 interval = previewLocalInterval
200200 }
201201 route, err := p.service.previewStreamRequest(p.request.Route)
202- var stream ports.BrokerLogicalConnection
202+ var stream ports.BrokerEnvelopeStream
203203 if err == nil {
204204 stream, err = p.service.pool.OpenStream(p.ctx, route)
205205 }
@@ -209,7 +209,8 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
209209 defer func() { _ = stream.Close() }()
210210 stop := context.AfterFunc(p.ctx, func() { _ = stream.Close() })
211211 defer stop()
212- if err := stream.SendClient(protocol.RemotePreviewWatch{Request: p.request.Preview, MinInterval: interval}); err != nil {
212+ typed := p.service.codec.Client(stream)
213+ if err := typed.SendClient(protocol.RemotePreviewWatch{Request: p.request.Preview, MinInterval: interval}); err != nil {
213214 return p.fail(err), false
214215 }
215216
@@ -232,7 +233,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
232233 defer func() { markFirst(); <-deadlineDone }()
233234
234235 for {
235- message, err := stream.ReceiveServer()
236+ message, err := typed.ReceiveServer()
236237 if err != nil {
237238 return p.fail(err), delivered
238239 }
@@ -199,7 +199,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
199 interval = previewLocalInterval199 interval = previewLocalInterval
200 }200 }
201 route, err := p.service.previewStreamRequest(p.request.Route)201 route, err := p.service.previewStreamRequest(p.request.Route)
202- var stream ports.BrokerLogicalConnection202+ var stream ports.BrokerEnvelopeStream
203 if err == nil {203 if err == nil {
204 stream, err = p.service.pool.OpenStream(p.ctx, route)204 stream, err = p.service.pool.OpenStream(p.ctx, route)
205 }205 }
@@ -209,7 +209,8 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
209 defer func() { _ = stream.Close() }()209 defer func() { _ = stream.Close() }()
210 stop := context.AfterFunc(p.ctx, func() { _ = stream.Close() })210 stop := context.AfterFunc(p.ctx, func() { _ = stream.Close() })
211 defer stop()211 defer stop()
212- if err := stream.SendClient(protocol.RemotePreviewWatch{Request: p.request.Preview, MinInterval: interval}); err != nil {212+ typed := p.service.codec.Client(stream)
213+ if err := typed.SendClient(protocol.RemotePreviewWatch{Request: p.request.Preview, MinInterval: interval}); err != nil {
213 return p.fail(err), false214 return p.fail(err), false
214 }215 }
215 216
@@ -232,7 +233,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) {
232 defer func() { markFirst(); <-deadlineDone }()233 defer func() { markFirst(); <-deadlineDone }()
233 234
234 for {235 for {
235- message, err := stream.ReceiveServer()236+ message, err := typed.ReceiveServer()
236 if err != nil {237 if err != nil {
237 return p.fail(err), delivered238 return p.fail(err), delivered
238 }239 }
M internal/usecase/broker/service_preview_test.go +7 -4
@@ -58,6 +58,8 @@ func (c *previewWatchConn) Capabilities() protocol.ConnectionCapabilities {
5858 }
5959 func (c *previewWatchConn) LinkState() ports.LinkState { return ports.LinkStateConnected }
6060 func (c *previewWatchConn) LinkEvents() <-chan ports.LinkEvent { return nil }
61+func (c *previewWatchConn) SendEnvelope([]byte) error { return nil }
62+func (c *previewWatchConn) RecvEnvelope() ([]byte, error) { <-c.done; return nil, errors.New("closed") }
6163 func (c *previewWatchConn) Done() <-chan struct{} { return c.done }
6264 func (c *previewWatchConn) Err() error { return nil }
6365 func (c *previewWatchConn) Close() error {
@@ -80,7 +82,7 @@ type previewTestConnector struct {
8082
8183 func (c *previewTestConnector) Connect(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
8284 if c.scripted == nil {
83- c.scripted = &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
85+ c.scripted = &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
8486 conn := newPreviewWatchConn(r.Stream)
8587 c.opened <- conn
8688 return conn, nil
@@ -96,7 +98,7 @@ func previewTarget(lifecycle byte, tab string) domain.RemoteSessionTarget {
9698 }
9799 }
98100
99-func previewRequestFor(service ports.BrokerService, generation ports.BrokerPreviewGeneration, target domain.RemoteSessionTarget) ports.BrokerPreviewRequest {
101+func previewRequestFor(service ports.BrokerCoreService, generation ports.BrokerPreviewGeneration, target domain.RemoteSessionTarget) ports.BrokerPreviewRequest {
100102 return ports.BrokerPreviewRequest{
101103 Epoch: 1, Connection: service.ConnectionID(), Generation: generation,
102104 Route: ports.BrokerPreviewRoute{Local: true, Policy: poolPolicy()},
@@ -110,7 +112,7 @@ func previewAnswer(lifecycle byte, tab string, width, height int) protocol.Remot
110112
111113 type previewFixture struct {
112114 t *testing.T
113- service ports.BrokerService
115+ service ports.BrokerCoreService
114116 sub ports.BrokerPreviewSubscription
115117 connector *previewTestConnector
116118 clock *manualClock
@@ -125,6 +127,7 @@ func newPreviewFixture(t *testing.T, target domain.RemoteSessionTarget) *preview
125127 registry, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})
126128 require.NoError(t, err)
127129 authority, _, _, _ := composeTestAuthority(t, 1, registry, connector.Connect, clock)
130+ authority.codec = testSessionCodec{}
128131 service, err := authority.AdmitClient(context.Background())
129132 require.NoError(t, err)
130133 t.Cleanup(func() { require.NoError(t, service.Close()) })
@@ -197,7 +200,7 @@ func TestServicePreviewStreamIDsDoNotConsumeClientIDs(t *testing.T) {
197200 id, err := service.NextStreamID()
198201 require.NoError(t, err)
199202 require.Equal(t, want, id)
200- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), uint64(id)))
203+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), uint64(id)))
201204 require.NoError(t, err)
202205 t.Cleanup(func() { require.NoError(t, stream.Close()) })
203206 }
@@ -58,6 +58,8 @@ func (c *previewWatchConn) Capabilities() protocol.ConnectionCapabilities {
58 }58 }
59 func (c *previewWatchConn) LinkState() ports.LinkState { return ports.LinkStateConnected }59 func (c *previewWatchConn) LinkState() ports.LinkState { return ports.LinkStateConnected }
60 func (c *previewWatchConn) LinkEvents() <-chan ports.LinkEvent { return nil }60 func (c *previewWatchConn) LinkEvents() <-chan ports.LinkEvent { return nil }
61+func (c *previewWatchConn) SendEnvelope([]byte) error { return nil }
62+func (c *previewWatchConn) RecvEnvelope() ([]byte, error) { <-c.done; return nil, errors.New("closed") }
61 func (c *previewWatchConn) Done() <-chan struct{} { return c.done }63 func (c *previewWatchConn) Done() <-chan struct{} { return c.done }
62 func (c *previewWatchConn) Err() error { return nil }64 func (c *previewWatchConn) Err() error { return nil }
63 func (c *previewWatchConn) Close() error {65 func (c *previewWatchConn) Close() error {
@@ -80,7 +82,7 @@ type previewTestConnector struct {
80 82
81 func (c *previewTestConnector) Connect(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {83 func (c *previewTestConnector) Connect(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) {
82 if c.scripted == nil {84 if c.scripted == nil {
83- c.scripted = &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {85+ c.scripted = &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
84 conn := newPreviewWatchConn(r.Stream)86 conn := newPreviewWatchConn(r.Stream)
85 c.opened <- conn87 c.opened <- conn
86 return conn, nil88 return conn, nil
@@ -96,7 +98,7 @@ func previewTarget(lifecycle byte, tab string) domain.RemoteSessionTarget {
96 }98 }
97 }99 }
98 100
99-func previewRequestFor(service ports.BrokerService, generation ports.BrokerPreviewGeneration, target domain.RemoteSessionTarget) ports.BrokerPreviewRequest {101+func previewRequestFor(service ports.BrokerCoreService, generation ports.BrokerPreviewGeneration, target domain.RemoteSessionTarget) ports.BrokerPreviewRequest {
100 return ports.BrokerPreviewRequest{102 return ports.BrokerPreviewRequest{
101 Epoch: 1, Connection: service.ConnectionID(), Generation: generation,103 Epoch: 1, Connection: service.ConnectionID(), Generation: generation,
102 Route: ports.BrokerPreviewRoute{Local: true, Policy: poolPolicy()},104 Route: ports.BrokerPreviewRoute{Local: true, Policy: poolPolicy()},
@@ -110,7 +112,7 @@ func previewAnswer(lifecycle byte, tab string, width, height int) protocol.Remot
110 112
111 type previewFixture struct {113 type previewFixture struct {
112 t *testing.T114 t *testing.T
113- service ports.BrokerService115+ service ports.BrokerCoreService
114 sub ports.BrokerPreviewSubscription116 sub ports.BrokerPreviewSubscription
115 connector *previewTestConnector117 connector *previewTestConnector
116 clock *manualClock118 clock *manualClock
@@ -125,6 +127,7 @@ func newPreviewFixture(t *testing.T, target domain.RemoteSessionTarget) *preview
125 registry, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})127 registry, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})
126 require.NoError(t, err)128 require.NoError(t, err)
127 authority, _, _, _ := composeTestAuthority(t, 1, registry, connector.Connect, clock)129 authority, _, _, _ := composeTestAuthority(t, 1, registry, connector.Connect, clock)
130+ authority.codec = testSessionCodec{}
128 service, err := authority.AdmitClient(context.Background())131 service, err := authority.AdmitClient(context.Background())
129 require.NoError(t, err)132 require.NoError(t, err)
130 t.Cleanup(func() { require.NoError(t, service.Close()) })133 t.Cleanup(func() { require.NoError(t, service.Close()) })
@@ -197,7 +200,7 @@ func TestServicePreviewStreamIDsDoNotConsumeClientIDs(t *testing.T) {
197 id, err := service.NextStreamID()200 id, err := service.NextStreamID()
198 require.NoError(t, err)201 require.NoError(t, err)
199 require.Equal(t, want, id)202 require.Equal(t, want, id)
200- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), uint64(id)))203+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), uint64(id)))
201 require.NoError(t, err)204 require.NoError(t, err)
202 t.Cleanup(func() { require.NoError(t, stream.Close()) })205 t.Cleanup(func() { require.NoError(t, stream.Close()) })
203 }206 }
M internal/usecase/broker/service_test.go +33 -22
@@ -43,7 +43,7 @@ func composeTestAuthority(t testing.TB, epoch ports.BrokerEpoch, registry *Regis
4343 require.NoError(t, err)
4444 t.Cleanup(func() { require.NoError(t, pool.Close()) })
4545 supervisor, idleClock := newTestSupervisor(t, time.Hour)
46- authority, err := NewAuthority(epoch, registry, pool, supervisor)
46+ authority, err := NewAuthority(epoch, registry, pool, supervisor, testSessionCodec{})
4747 require.NoError(t, err)
4848 // The caller owns Registry.Run (see NewAuthority): start the single run and
4949 // cancel it at cleanup, so the registry's durable writer is drained and its
@@ -195,7 +195,7 @@ func TestAuthorityAdmissionContextNotRetained(t *testing.T) {
195195 require.NoError(t, registry.setHosts(hostRecords(registration(t, "user@host:22", 1))))
196196 require.Len(t, service.Snapshot().Daemons, 1)
197197
198- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
198+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
199199 require.NoError(t, err)
200200 require.NoError(t, stream.Close())
201201 }
@@ -210,7 +210,7 @@ func TestAuthorityValidation(t *testing.T) {
210210 require.NoError(t, err)
211211 t.Cleanup(mutableRemote.settle)
212212 require.NoError(t, mutableRemote.setHosts(hostRecords(registration(t, "user@host:22", 1))))
213- _, err = NewAuthority(1, mutableRemote, pool, supervisor)
213+ _, err = NewAuthority(1, mutableRemote, pool, supervisor, testSessionCodec{})
214214 require.NoError(t, err, "a mutable remote registry must be served")
215215
216216 // A local-only observer is served: its entry is configured authority rather
@@ -223,7 +223,7 @@ func TestAuthorityValidation(t *testing.T) {
223223 }})
224224 require.NoError(t, err)
225225 t.Cleanup(localOnly.settle)
226- _, err = NewAuthority(1, localOnly, pool, supervisor)
226+ _, err = NewAuthority(1, localOnly, pool, supervisor, testSessionCodec{})
227227 require.NoError(t, err, "a local-only observer must be served")
228228
229229 // Construction still rejects remote observation without a probe: a registry
@@ -235,17 +235,17 @@ func TestAuthorityValidation(t *testing.T) {
235235 disabled, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})
236236 require.NoError(t, err)
237237 t.Cleanup(disabled.settle)
238- _, err = NewAuthority(2, disabled, pool, supervisor)
238+ _, err = NewAuthority(2, disabled, pool, supervisor, testSessionCodec{})
239239 require.Error(t, err, "an epoch mismatch must be refused")
240240
241- _, err = NewAuthority(1, nil, pool, supervisor)
241+ _, err = NewAuthority(1, nil, pool, supervisor, testSessionCodec{})
242242 require.Error(t, err, "a missing registry must be refused")
243- _, err = NewAuthority(1, disabled, nil, supervisor)
243+ _, err = NewAuthority(1, disabled, nil, supervisor, testSessionCodec{})
244244 require.Error(t, err, "a missing pool must be refused")
245- _, err = NewAuthority(1, disabled, pool, nil)
245+ _, err = NewAuthority(1, disabled, pool, nil, testSessionCodec{})
246246 require.Error(t, err, "a missing supervisor must be refused")
247247
248- _, err = NewAuthority(1, disabled, pool, supervisor)
248+ _, err = NewAuthority(1, disabled, pool, supervisor, testSessionCodec{})
249249 require.NoError(t, err)
250250 }
251251
@@ -289,7 +289,7 @@ func TestServiceOpenStreamScopeFencing(t *testing.T) {
289289 require.NoError(t, err)
290290 t.Cleanup(func() { require.NoError(t, service.Close()) })
291291
292- stream, err := service.OpenStream(context.Background(), tc.build(service.ConnectionID()))
292+ stream, err := service.OpenEnvelopeStream(context.Background(), tc.build(service.ConnectionID()))
293293 switch {
294294 case tc.wantStale:
295295 require.ErrorIs(t, err, ports.BrokerAdmissionStale)
@@ -344,7 +344,7 @@ func TestServiceOpenStreamRequestsLocalReprobeOnOpenAndClose(t *testing.T) {
344344 waitLocal(t, registry, func(o ports.BrokerDaemonObservation) bool { return !o.Checking })
345345
346346 // Opening a local control stream must itself mark a re-probe pending.
347- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
347+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
348348 require.NoError(t, err)
349349 opened := receiveLocalCall(t, local)
350350 opened.result <- reachable()
@@ -421,11 +421,11 @@ func TestServiceNextStreamIDAllocation(t *testing.T) {
421421 require.NoError(t, err)
422422 request := poolRequest(service.ConnectionID(), 0)
423423 request.Stream = stream
424- opened, err := service.OpenStream(context.Background(), request)
424+ opened, err := service.OpenEnvelopeStream(context.Background(), request)
425425 require.NoError(t, err)
426426 require.NoError(t, opened.Close())
427427 // Replaying the exact identity is refused as stale.
428- _, err = service.OpenStream(context.Background(), request)
428+ _, err = service.OpenEnvelopeStream(context.Background(), request)
429429 require.ErrorIs(t, err, ports.BrokerAdmissionStale)
430430 })
431431 }
@@ -448,7 +448,7 @@ func TestServiceOpenStreamAdmissionValidatesCallerInput(t *testing.T) {
448448 t.Run(tc.name, func(t *testing.T) {
449449 request := poolRequest(service.ConnectionID(), 1)
450450 tc.mutate(&request)
451- _, err := service.OpenStream(context.Background(), request)
451+ _, err := service.OpenEnvelopeStream(context.Background(), request)
452452 require.ErrorIs(t, err, ports.BrokerAdmissionInvalid)
453453 })
454454 }
@@ -461,7 +461,7 @@ func TestServiceCloseStreamScopeFencing(t *testing.T) {
461461 t.Cleanup(func() { require.NoError(t, service.Close()) })
462462 id := service.ConnectionID()
463463
464- stream, err := service.OpenStream(context.Background(), poolRequest(id, 1))
464+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(id, 1))
465465 require.NoError(t, err)
466466
467467 // A wrong-scope close must not retire this connection's stream.
@@ -610,12 +610,12 @@ func TestServiceOperationLeaseLifetime(t *testing.T) {
610610 t.Cleanup(func() { require.NoError(t, service.Close()) })
611611
612612 type opened struct {
613- stream ports.BrokerLogicalConnection
613+ stream ports.BrokerEnvelopeStream
614614 err error
615615 }
616616 result := make(chan opened, 1)
617617 go func() {
618- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
618+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
619619 result <- opened{stream: stream, err: err}
620620 }()
621621 <-entered
@@ -656,7 +656,7 @@ func TestServiceCloseOpenRace(t *testing.T) {
656656
657657 result := make(chan error, 1)
658658 go func() {
659- _, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
659+ _, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
660660 result <- err
661661 }()
662662 <-entered
@@ -683,7 +683,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) {
683683 require.NoError(t, err)
684684 _, err = service.Subscribe()
685685 require.NoError(t, err)
686- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
686+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
687687 require.NoError(t, err)
688688
689689 const closers = 8
@@ -708,7 +708,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) {
708708 require.Zero(t, clients)
709709 await(t, stream.Done())
710710
711- _, err = service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 2))
711+ _, err = service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 2))
712712 requireBrokerClosed(t, err)
713713 }
714714
@@ -747,7 +747,7 @@ func TestServiceShutdownTerminatesOwnedWork(t *testing.T) {
747747 service, err := authority.AdmitClient(context.Background())
748748 require.NoError(t, err)
749749
750- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))
750+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
751751 require.NoError(t, err)
752752
753753 // Broker shutdown cancels the connection root: owned streams end even before
@@ -1156,7 +1156,7 @@ func TestRegistryObservationDisabledDrainsWriter(t *testing.T) {
11561156 }
11571157
11581158 func TestServiceConfigValidation(t *testing.T) {
1159- valid := serviceConfig{epoch: 1, id: ports.BrokerConnectionID{1}, previewID: ports.BrokerConnectionID{2}, registry: &Registry{}, pool: &Pool{}, supervisor: &Supervisor{}, lease: &Lease{}, clock: newManualClock(time.Unix(0, 0))}
1159+ valid := serviceConfig{epoch: 1, id: ports.BrokerConnectionID{1}, previewID: ports.BrokerConnectionID{2}, registry: &Registry{}, pool: &Pool{}, supervisor: &Supervisor{}, lease: &Lease{}, clock: newManualClock(time.Unix(0, 0)), codec: testSessionCodec{}}
11601160 tests := []struct {
11611161 name string
11621162 mutate func(*serviceConfig)
@@ -1170,6 +1170,7 @@ func TestServiceConfigValidation(t *testing.T) {
11701170 {"nil supervisor", func(c *serviceConfig) { c.supervisor = nil }},
11711171 {"nil lease", func(c *serviceConfig) { c.lease = nil }},
11721172 {"nil clock", func(c *serviceConfig) { c.clock = nil }},
1173+ {"nil codec", func(c *serviceConfig) { c.codec = nil }},
11731174 }
11741175 require.NoError(t, valid.validate())
11751176 for _, tt := range tests {
@@ -1180,3 +1181,13 @@ func TestServiceConfigValidation(t *testing.T) {
11801181 })
11811182 }
11821183 }
1184+
1185+// testSessionCodec is used only for scripted preview streams; raw traffic is never decoded.
1186+type testSessionCodec struct{}
1187+
1188+func (testSessionCodec) Client(stream ports.BrokerEnvelopeStream) ports.ClientConnection {
1189+ if pooled, ok := stream.(*pooledStream); ok {
1190+ stream = pooled.BrokerEnvelopeStream
1191+ }
1192+ return stream.(*previewWatchConn)
1193+}
@@ -43,7 +43,7 @@ func composeTestAuthority(t testing.TB, epoch ports.BrokerEpoch, registry *Regis
43 require.NoError(t, err)43 require.NoError(t, err)
44 t.Cleanup(func() { require.NoError(t, pool.Close()) })44 t.Cleanup(func() { require.NoError(t, pool.Close()) })
45 supervisor, idleClock := newTestSupervisor(t, time.Hour)45 supervisor, idleClock := newTestSupervisor(t, time.Hour)
46- authority, err := NewAuthority(epoch, registry, pool, supervisor)46+ authority, err := NewAuthority(epoch, registry, pool, supervisor, testSessionCodec{})
47 require.NoError(t, err)47 require.NoError(t, err)
48 // The caller owns Registry.Run (see NewAuthority): start the single run and48 // The caller owns Registry.Run (see NewAuthority): start the single run and
49 // cancel it at cleanup, so the registry's durable writer is drained and its49 // cancel it at cleanup, so the registry's durable writer is drained and its
@@ -195,7 +195,7 @@ func TestAuthorityAdmissionContextNotRetained(t *testing.T) {
195 require.NoError(t, registry.setHosts(hostRecords(registration(t, "user@host:22", 1))))195 require.NoError(t, registry.setHosts(hostRecords(registration(t, "user@host:22", 1))))
196 require.Len(t, service.Snapshot().Daemons, 1)196 require.Len(t, service.Snapshot().Daemons, 1)
197 197
198- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))198+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
199 require.NoError(t, err)199 require.NoError(t, err)
200 require.NoError(t, stream.Close())200 require.NoError(t, stream.Close())
201 }201 }
@@ -210,7 +210,7 @@ func TestAuthorityValidation(t *testing.T) {
210 require.NoError(t, err)210 require.NoError(t, err)
211 t.Cleanup(mutableRemote.settle)211 t.Cleanup(mutableRemote.settle)
212 require.NoError(t, mutableRemote.setHosts(hostRecords(registration(t, "user@host:22", 1))))212 require.NoError(t, mutableRemote.setHosts(hostRecords(registration(t, "user@host:22", 1))))
213- _, err = NewAuthority(1, mutableRemote, pool, supervisor)213+ _, err = NewAuthority(1, mutableRemote, pool, supervisor, testSessionCodec{})
214 require.NoError(t, err, "a mutable remote registry must be served")214 require.NoError(t, err, "a mutable remote registry must be served")
215 215
216 // A local-only observer is served: its entry is configured authority rather216 // A local-only observer is served: its entry is configured authority rather
@@ -223,7 +223,7 @@ func TestAuthorityValidation(t *testing.T) {
223 }})223 }})
224 require.NoError(t, err)224 require.NoError(t, err)
225 t.Cleanup(localOnly.settle)225 t.Cleanup(localOnly.settle)
226- _, err = NewAuthority(1, localOnly, pool, supervisor)226+ _, err = NewAuthority(1, localOnly, pool, supervisor, testSessionCodec{})
227 require.NoError(t, err, "a local-only observer must be served")227 require.NoError(t, err, "a local-only observer must be served")
228 228
229 // Construction still rejects remote observation without a probe: a registry229 // Construction still rejects remote observation without a probe: a registry
@@ -235,17 +235,17 @@ func TestAuthorityValidation(t *testing.T) {
235 disabled, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})235 disabled, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true})
236 require.NoError(t, err)236 require.NoError(t, err)
237 t.Cleanup(disabled.settle)237 t.Cleanup(disabled.settle)
238- _, err = NewAuthority(2, disabled, pool, supervisor)238+ _, err = NewAuthority(2, disabled, pool, supervisor, testSessionCodec{})
239 require.Error(t, err, "an epoch mismatch must be refused")239 require.Error(t, err, "an epoch mismatch must be refused")
240 240
241- _, err = NewAuthority(1, nil, pool, supervisor)241+ _, err = NewAuthority(1, nil, pool, supervisor, testSessionCodec{})
242 require.Error(t, err, "a missing registry must be refused")242 require.Error(t, err, "a missing registry must be refused")
243- _, err = NewAuthority(1, disabled, nil, supervisor)243+ _, err = NewAuthority(1, disabled, nil, supervisor, testSessionCodec{})
244 require.Error(t, err, "a missing pool must be refused")244 require.Error(t, err, "a missing pool must be refused")
245- _, err = NewAuthority(1, disabled, pool, nil)245+ _, err = NewAuthority(1, disabled, pool, nil, testSessionCodec{})
246 require.Error(t, err, "a missing supervisor must be refused")246 require.Error(t, err, "a missing supervisor must be refused")
247 247
248- _, err = NewAuthority(1, disabled, pool, supervisor)248+ _, err = NewAuthority(1, disabled, pool, supervisor, testSessionCodec{})
249 require.NoError(t, err)249 require.NoError(t, err)
250 }250 }
251 251
@@ -289,7 +289,7 @@ func TestServiceOpenStreamScopeFencing(t *testing.T) {
289 require.NoError(t, err)289 require.NoError(t, err)
290 t.Cleanup(func() { require.NoError(t, service.Close()) })290 t.Cleanup(func() { require.NoError(t, service.Close()) })
291 291
292- stream, err := service.OpenStream(context.Background(), tc.build(service.ConnectionID()))292+ stream, err := service.OpenEnvelopeStream(context.Background(), tc.build(service.ConnectionID()))
293 switch {293 switch {
294 case tc.wantStale:294 case tc.wantStale:
295 require.ErrorIs(t, err, ports.BrokerAdmissionStale)295 require.ErrorIs(t, err, ports.BrokerAdmissionStale)
@@ -344,7 +344,7 @@ func TestServiceOpenStreamRequestsLocalReprobeOnOpenAndClose(t *testing.T) {
344 waitLocal(t, registry, func(o ports.BrokerDaemonObservation) bool { return !o.Checking })344 waitLocal(t, registry, func(o ports.BrokerDaemonObservation) bool { return !o.Checking })
345 345
346 // Opening a local control stream must itself mark a re-probe pending.346 // Opening a local control stream must itself mark a re-probe pending.
347- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))347+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
348 require.NoError(t, err)348 require.NoError(t, err)
349 opened := receiveLocalCall(t, local)349 opened := receiveLocalCall(t, local)
350 opened.result <- reachable()350 opened.result <- reachable()
@@ -421,11 +421,11 @@ func TestServiceNextStreamIDAllocation(t *testing.T) {
421 require.NoError(t, err)421 require.NoError(t, err)
422 request := poolRequest(service.ConnectionID(), 0)422 request := poolRequest(service.ConnectionID(), 0)
423 request.Stream = stream423 request.Stream = stream
424- opened, err := service.OpenStream(context.Background(), request)424+ opened, err := service.OpenEnvelopeStream(context.Background(), request)
425 require.NoError(t, err)425 require.NoError(t, err)
426 require.NoError(t, opened.Close())426 require.NoError(t, opened.Close())
427 // Replaying the exact identity is refused as stale.427 // Replaying the exact identity is refused as stale.
428- _, err = service.OpenStream(context.Background(), request)428+ _, err = service.OpenEnvelopeStream(context.Background(), request)
429 require.ErrorIs(t, err, ports.BrokerAdmissionStale)429 require.ErrorIs(t, err, ports.BrokerAdmissionStale)
430 })430 })
431 }431 }
@@ -448,7 +448,7 @@ func TestServiceOpenStreamAdmissionValidatesCallerInput(t *testing.T) {
448 t.Run(tc.name, func(t *testing.T) {448 t.Run(tc.name, func(t *testing.T) {
449 request := poolRequest(service.ConnectionID(), 1)449 request := poolRequest(service.ConnectionID(), 1)
450 tc.mutate(&request)450 tc.mutate(&request)
451- _, err := service.OpenStream(context.Background(), request)451+ _, err := service.OpenEnvelopeStream(context.Background(), request)
452 require.ErrorIs(t, err, ports.BrokerAdmissionInvalid)452 require.ErrorIs(t, err, ports.BrokerAdmissionInvalid)
453 })453 })
454 }454 }
@@ -461,7 +461,7 @@ func TestServiceCloseStreamScopeFencing(t *testing.T) {
461 t.Cleanup(func() { require.NoError(t, service.Close()) })461 t.Cleanup(func() { require.NoError(t, service.Close()) })
462 id := service.ConnectionID()462 id := service.ConnectionID()
463 463
464- stream, err := service.OpenStream(context.Background(), poolRequest(id, 1))464+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(id, 1))
465 require.NoError(t, err)465 require.NoError(t, err)
466 466
467 // A wrong-scope close must not retire this connection's stream.467 // A wrong-scope close must not retire this connection's stream.
@@ -610,12 +610,12 @@ func TestServiceOperationLeaseLifetime(t *testing.T) {
610 t.Cleanup(func() { require.NoError(t, service.Close()) })610 t.Cleanup(func() { require.NoError(t, service.Close()) })
611 611
612 type opened struct {612 type opened struct {
613- stream ports.BrokerLogicalConnection613+ stream ports.BrokerEnvelopeStream
614 err error614 err error
615 }615 }
616 result := make(chan opened, 1)616 result := make(chan opened, 1)
617 go func() {617 go func() {
618- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))618+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
619 result <- opened{stream: stream, err: err}619 result <- opened{stream: stream, err: err}
620 }()620 }()
621 <-entered621 <-entered
@@ -656,7 +656,7 @@ func TestServiceCloseOpenRace(t *testing.T) {
656 656
657 result := make(chan error, 1)657 result := make(chan error, 1)
658 go func() {658 go func() {
659- _, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))659+ _, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
660 result <- err660 result <- err
661 }()661 }()
662 <-entered662 <-entered
@@ -683,7 +683,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) {
683 require.NoError(t, err)683 require.NoError(t, err)
684 _, err = service.Subscribe()684 _, err = service.Subscribe()
685 require.NoError(t, err)685 require.NoError(t, err)
686- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))686+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
687 require.NoError(t, err)687 require.NoError(t, err)
688 688
689 const closers = 8689 const closers = 8
@@ -708,7 +708,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) {
708 require.Zero(t, clients)708 require.Zero(t, clients)
709 await(t, stream.Done())709 await(t, stream.Done())
710 710
711- _, err = service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 2))711+ _, err = service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 2))
712 requireBrokerClosed(t, err)712 requireBrokerClosed(t, err)
713 }713 }
714 714
@@ -747,7 +747,7 @@ func TestServiceShutdownTerminatesOwnedWork(t *testing.T) {
747 service, err := authority.AdmitClient(context.Background())747 service, err := authority.AdmitClient(context.Background())
748 require.NoError(t, err)748 require.NoError(t, err)
749 749
750- stream, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1))750+ stream, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1))
751 require.NoError(t, err)751 require.NoError(t, err)
752 752
753 // Broker shutdown cancels the connection root: owned streams end even before753 // Broker shutdown cancels the connection root: owned streams end even before
@@ -1156,7 +1156,7 @@ func TestRegistryObservationDisabledDrainsWriter(t *testing.T) {
1156 }1156 }
1157 1157
1158 func TestServiceConfigValidation(t *testing.T) {1158 func TestServiceConfigValidation(t *testing.T) {
1159- valid := serviceConfig{epoch: 1, id: ports.BrokerConnectionID{1}, previewID: ports.BrokerConnectionID{2}, registry: &Registry{}, pool: &Pool{}, supervisor: &Supervisor{}, lease: &Lease{}, clock: newManualClock(time.Unix(0, 0))}1159+ valid := serviceConfig{epoch: 1, id: ports.BrokerConnectionID{1}, previewID: ports.BrokerConnectionID{2}, registry: &Registry{}, pool: &Pool{}, supervisor: &Supervisor{}, lease: &Lease{}, clock: newManualClock(time.Unix(0, 0)), codec: testSessionCodec{}}
1160 tests := []struct {1160 tests := []struct {
1161 name string1161 name string
1162 mutate func(*serviceConfig)1162 mutate func(*serviceConfig)
@@ -1170,6 +1170,7 @@ func TestServiceConfigValidation(t *testing.T) {
1170 {"nil supervisor", func(c *serviceConfig) { c.supervisor = nil }},1170 {"nil supervisor", func(c *serviceConfig) { c.supervisor = nil }},
1171 {"nil lease", func(c *serviceConfig) { c.lease = nil }},1171 {"nil lease", func(c *serviceConfig) { c.lease = nil }},
1172 {"nil clock", func(c *serviceConfig) { c.clock = nil }},1172 {"nil clock", func(c *serviceConfig) { c.clock = nil }},
1173+ {"nil codec", func(c *serviceConfig) { c.codec = nil }},
1173 }1174 }
1174 require.NoError(t, valid.validate())1175 require.NoError(t, valid.validate())
1175 for _, tt := range tests {1176 for _, tt := range tests {
@@ -1180,3 +1181,13 @@ func TestServiceConfigValidation(t *testing.T) {
1180 })1181 })
1181 }1182 }
1182 }1183 }
1184+
1185+// testSessionCodec is used only for scripted preview streams; raw traffic is never decoded.
1186+type testSessionCodec struct{}
1187+
1188+func (testSessionCodec) Client(stream ports.BrokerEnvelopeStream) ports.ClientConnection {
1189+ if pooled, ok := stream.(*pooledStream); ok {
1190+ stream = pooled.BrokerEnvelopeStream
1191+ }
1192+ return stream.(*previewWatchConn)
1193+}
M internal/usecase/client/supervisor_integration_test.go +2 -2
@@ -65,7 +65,7 @@ func (c *integrationCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.Br
6565 return nil, errors.New("integration core does not support preview")
6666 }
6767
68-func (c *integrationCore) OpenStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
68+func (c *integrationCore) OpenEnvelopeStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
6969 return nil, errors.New("integration core does not support streams")
7070 }
7171
@@ -110,7 +110,7 @@ type integrationAuthority struct {
110110
111111 var _ ports.BrokerAuthority = (*integrationAuthority)(nil)
112112
113-func (a *integrationAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {
113+func (a *integrationAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
114114 if err := ctx.Err(); err != nil {
115115 return nil, err
116116 }
@@ -65,7 +65,7 @@ func (c *integrationCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.Br
65 return nil, errors.New("integration core does not support preview")65 return nil, errors.New("integration core does not support preview")
66 }66 }
67 67
68-func (c *integrationCore) OpenStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {68+func (c *integrationCore) OpenEnvelopeStream(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerEnvelopeStream, error) {
69 return nil, errors.New("integration core does not support streams")69 return nil, errors.New("integration core does not support streams")
70 }70 }
71 71
@@ -110,7 +110,7 @@ type integrationAuthority struct {
110 110
111 var _ ports.BrokerAuthority = (*integrationAuthority)(nil)111 var _ ports.BrokerAuthority = (*integrationAuthority)(nil)
112 112
113-func (a *integrationAuthority) AdmitClient(ctx context.Context) (ports.BrokerService, error) {113+func (a *integrationAuthority) AdmitClient(ctx context.Context) (ports.BrokerCoreService, error) {
114 if err := ctx.Err(); err != nil {114 if err := ctx.Err(); err != nil {
115 return nil, err115 return nil, err
116 }116 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close