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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
36 files · +2540 -505
M internal/adapters/brokeripc/helpers_test.go +39 -17
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package brokeripc | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 5 | 6 | "encoding/binary" | |
| 6 | 7 | "errors" | |
| @@ -15,6 +16,7 @@ import ( | |||
| 15 | 16 | "github.com/stretchr/testify/require" | |
| 16 | 17 | ||
| 17 | 18 | "github.com/bnema/vev/internal/adapters/brokerwire" | |
| 19 | + | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 18 | 20 | "github.com/bnema/vev/internal/domain" | |
| 19 | 21 | "github.com/bnema/vev/internal/ports" | |
| 20 | 22 | "github.com/bnema/vev/internal/protocol" | |
| @@ -72,7 +74,7 @@ func attachmentRequest(stream ports.BrokerStreamID, admission ports.BrokerStream | |||
| 72 | 74 | ||
| 73 | 75 | // nextStreamID reads one fresh identity from the service's only allocator, | |
| 74 | 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 | 78 | t.Helper() | |
| 77 | 79 | stream, err := service.NextStreamID() | |
| 78 | 80 | require.NoError(t, err) | |
| @@ -87,14 +89,14 @@ const membershipProbeEndpoint = "probe@host:22" | |||
| 87 | 89 | // membership round trip after a stream-local or subscriber-local event: the | |
| 88 | 90 | // probe adds one host through the same delegated path a real caller uses and | |
| 89 | 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 | 93 | t.Helper() | |
| 92 | 94 | require.NoError(t, connectionDelegates(context.Background(), service)) | |
| 93 | 95 | } | |
| 94 | 96 | ||
| 95 | 97 | // connectionDelegates is the error-returning form of requireConnectionDelegates, | |
| 96 | 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 | 100 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |
| 99 | 101 | defer cancel() | |
| 100 | 102 | registration, err := service.AddHost(ctx, membershipProbeEndpoint, testPolicy()) | |
| @@ -284,22 +286,38 @@ func (s *fakeSubscription) notify() { | |||
| 284 | 286 | type fakeLogicalConn struct { | |
| 285 | 287 | fromClient chan protocol.ClientMessage | |
| 286 | 288 | 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 | |
| 292 | 297 | } | |
| 293 | 298 | ||
| 294 | 299 | func newFakeLogicalConn() *fakeLogicalConn { | |
| 295 | 300 | return &fakeLogicalConn{ | |
| 296 | 301 | fromClient: make(chan protocol.ClientMessage, 64), | |
| 297 | 302 | toClient: make(chan protocol.ServerMessage, 64), | |
| 303 | + | preamble: make(chan []byte, 1), | |
| 298 | 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 | 321 | select { | |
| 304 | 322 | case c.fromClient <- message: | |
| 305 | 323 | 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 | 330 | // A message already queued when Close ran must still be observed first: a | |
| 313 | 331 | // real carriage never reports the connection done before a message the peer | |
| 314 | 332 | // sent ahead of an orderly close has been delivered. Draining non-blocking | |
| 315 | 333 | // before the select keeps that ordering deterministic instead of racing a | |
| 316 | 334 | // buffered send against an already-closed done channel. | |
| 317 | 335 | select { | |
| 336 | + | case response := <-c.preamble: | |
| 337 | + | return response, nil | |
| 318 | 338 | case message := <-c.toClient: | |
| 319 | - | return message, nil | |
| 339 | + | return sessionwire.EncodeServerMessage(message) | |
| 320 | 340 | default: | |
| 321 | 341 | } | |
| 322 | 342 | select { | |
| 343 | + | case response := <-c.preamble: | |
| 344 | + | return response, nil | |
| 323 | 345 | case message := <-c.toClient: | |
| 324 | - | return message, nil | |
| 346 | + | return sessionwire.EncodeServerMessage(message) | |
| 325 | 347 | case <-c.done: | |
| 326 | 348 | if err := c.Err(); err != nil { | |
| 327 | 349 | return nil, err | |
| @@ -487,7 +509,7 @@ func (c *fakeCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.BrokerPre | |||
| 487 | 509 | 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 | 513 | c.mu.Lock() | |
| 492 | 514 | defer c.mu.Unlock() | |
| 493 | 515 | if c.openErr != nil { | |
| @@ -721,7 +743,7 @@ func newFakeAuthority(epoch ports.BrokerEpoch, snapshot ports.BrokerSnapshot) *f | |||
| 721 | 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 | 747 | if err := ctx.Err(); err != nil { | |
| 726 | 748 | return nil, err | |
| 727 | 749 | } | |
| @@ -770,10 +792,10 @@ func startEndpoint(t *testing.T, cfg Config) *endpoint { | |||
| 770 | 792 | } | |
| 771 | 793 | ||
| 772 | 794 | // accept returns the next admitted session. | |
| 773 | - | func (e *endpoint) accept() ports.BrokerService { | |
| 795 | + | func (e *endpoint) accept() ports.BrokerCoreService { | |
| 774 | 796 | e.t.Helper() | |
| 775 | 797 | type result struct { | |
| 776 | - | service ports.BrokerService | |
| 798 | + | service ports.BrokerCoreService | |
| 777 | 799 | err error | |
| 778 | 800 | } | |
| 779 | 801 | out := make(chan result, 1) | |
| @@ -807,7 +829,7 @@ func (e *endpoint) dialWith(cfg Config) ports.BrokerService { | |||
| 807 | 829 | } | |
| 808 | 830 | ||
| 809 | 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 | 833 | e.t.Helper() | |
| 812 | 834 | client := e.dial() | |
| 813 | 835 | session := e.accept() | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package brokeripc | 1 | 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: the | 89 | // 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 and | 90 | // 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.ClientMessage | 287 | fromClient chan protocol.ClientMessage | ||
| 286 | toClient chan protocol.ServerMessage | 288 | toClient chan protocol.ServerMessage | ||
| 287 | - | done chan struct{} | 289 | + | // preamble carries the canned session preamble acceptance: the raw relay |
| 288 | - | once sync.Once | 290 | + | // forwards the client's preamble request like any other envelope. |
| 289 | - | mu sync.Mutex | 291 | + | preamble chan []byte |
| 290 | - | err error | 292 | + | done chan struct{} |
| 291 | - | closed bool | 293 | + | 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 nil | 323 | 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: a | 330 | // 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 peer | 331 | // real carriage never reports the connection done before a message the peer | ||
| 314 | // sent ahead of an orderly close has been delivered. Draining non-blocking | 332 | // sent ahead of an orderly close has been delivered. Draining non-blocking | ||
| 315 | // before the select keeps that ordering deterministic instead of racing a | 333 | // 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, nil | 339 | + | 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, nil | 346 | + | 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, err | 349 | return nil, err | ||
| @@ -487,7 +509,7 @@ func (c *fakeCore) SubscribePreview(ports.BrokerPreviewRequest) (ports.BrokerPre | |||||
| 487 | return newFakePreviewSubscription(), nil | 509 | 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, err | 748 | 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.BrokerService | 798 | + | service ports.BrokerCoreService |
| 777 | err error | 799 | 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 { | |||
| 27 | 27 | 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 | 31 | a.once.Do(func() { close(a.entered) }) | |
| 32 | 32 | <-a.gate | |
| 33 | 33 | return a.inner.AdmitClient(ctx) | |
| @@ -41,7 +41,7 @@ type ctxAuthority struct { | |||
| 41 | 41 | 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 | 45 | a.once.Do(func() { close(a.entered) }) | |
| 46 | 46 | <-ctx.Done() | |
| 47 | 47 | return nil, ctx.Err() | |
| @@ -429,7 +429,7 @@ func TestStalledHandshakeDoesNotBlockOtherClients(t *testing.T) { | |||
| 429 | 429 | func TestConcurrentDialAndClose(t *testing.T) { | |
| 430 | 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 | 433 | go func() { | |
| 434 | 434 | defer close(sessions) | |
| 435 | 435 | for i := 0; i < cap(sessions); i++ { | |
| @@ -27,7 +27,7 @@ type gatedAuthority struct { | |||||
| 27 | once sync.Once | 27 | 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.gate | 32 | <-a.gate | ||
| 33 | return a.inner.AdmitClient(ctx) | 33 | return a.inner.AdmitClient(ctx) | ||
| @@ -41,7 +41,7 @@ type ctxAuthority struct { | |||||
| 41 | once sync.Once | 41 | 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 | |||
| 57 | 57 | // serverPreviewGeneration returns the server session's current preview | |
| 58 | 58 | // generation: it observes the same replacement bookkeeping startPreview | |
| 59 | 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 | 61 | server, ok := session.(*serverSession) | |
| 62 | 62 | if !ok { | |
| 63 | 63 | return 0 | |
| @@ -68,7 +68,7 @@ func serverPreviewGeneration(session ports.BrokerService) ports.BrokerPreviewGen | |||
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | 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 | 72 | server, ok := session.(*serverSession) | |
| 73 | 73 | if !ok { | |
| 74 | 74 | return nil | |
| @@ -57,7 +57,7 @@ func awaitPreviewChanged(t *testing.T, sub ports.BrokerPreviewSubscription) port | |||||
| 57 | // serverPreviewGeneration returns the server session's current preview | 57 | // serverPreviewGeneration returns the server session's current preview | ||
| 58 | // generation: it observes the same replacement bookkeeping startPreview | 58 | // 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 0 | 63 | 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 nil | 74 | 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 { | |||
| 281 | 281 | // credential, or an unsupported build) or a client that fails the preamble or | |
| 282 | 282 | // admission is isolated: that peer is dropped and the listener keeps serving. | |
| 283 | 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 | 285 | if l == nil { | |
| 286 | 286 | return nil, ErrListenerClosed | |
| 287 | 287 | } | |
| @@ -466,7 +466,7 @@ type serverSession struct { | |||
| 466 | 466 | epoch ports.BrokerEpoch | |
| 467 | 467 | scope brokerwire.Scope | |
| 468 | 468 | conn *brokerwire.Connection | |
| 469 | - | core ports.BrokerService | |
| 469 | + | core ports.BrokerCoreService | |
| 470 | 470 | transport wire.BoundedTransport | |
| 471 | 471 | ceilings brokerwire.Ceilings | |
| 472 | 472 | cfg Config | |
| @@ -513,7 +513,7 @@ type serverSession struct { | |||
| 513 | 513 | done chan struct{} | |
| 514 | 514 | } | |
| 515 | 515 | ||
| 516 | - | var _ ports.BrokerService = (*serverSession)(nil) | |
| 516 | + | var _ ports.BrokerCoreService = (*serverSession)(nil) | |
| 517 | 517 | ||
| 518 | 518 | // registrationDeadline returns the absolute instant by which an accepted client | |
| 519 | 519 | // must send its Register: the accept-time handshake deadline that already | |
| @@ -535,7 +535,7 @@ func registrationDeadline(ctx context.Context) time.Time { | |||
| 535 | 535 | // registerDeadline is the absolute instant by which the client's Register must | |
| 536 | 536 | // arrive (the accept-time handshake deadline); the zero time disables the bound | |
| 537 | 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 | 539 | if epoch == 0 || transport == nil || core == nil { | |
| 540 | 540 | return nil, ErrConfig | |
| 541 | 541 | } | |
| @@ -983,14 +983,14 @@ func (s *serverSession) SubscribePreview(request ports.BrokerPreviewRequest) (po | |||
| 983 | 983 | // allocated (NextStreamID) and is transmitted unchanged: this listener never | |
| 984 | 984 | // re-allocates a wire identity, whether the request arrives over the wire or | |
| 985 | 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 | 987 | if err := s.checkRequestScope(request.Epoch, request.Connection); err != nil { | |
| 988 | 988 | return nil, err | |
| 989 | 989 | } | |
| 990 | 990 | scoped := request | |
| 991 | 991 | scoped.Epoch = s.epoch | |
| 992 | 992 | scoped.Connection = s.scope.Connection | |
| 993 | - | return s.core.OpenStream(ctx, scoped) | |
| 993 | + | return s.core.OpenEnvelopeStream(ctx, scoped) | |
| 994 | 994 | } | |
| 995 | 995 | ||
| 996 | 996 | // 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 or | 281 | // 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, ErrListenerClosed | 286 | return nil, ErrListenerClosed | ||
| 287 | } | 287 | } | ||
| @@ -466,7 +466,7 @@ type serverSession struct { | |||||
| 466 | epoch ports.BrokerEpoch | 466 | epoch ports.BrokerEpoch | ||
| 467 | scope brokerwire.Scope | 467 | scope brokerwire.Scope | ||
| 468 | conn *brokerwire.Connection | 468 | conn *brokerwire.Connection | ||
| 469 | - | core ports.BrokerService | 469 | + | core ports.BrokerCoreService |
| 470 | transport wire.BoundedTransport | 470 | transport wire.BoundedTransport | ||
| 471 | ceilings brokerwire.Ceilings | 471 | ceilings brokerwire.Ceilings | ||
| 472 | cfg Config | 472 | 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 client | 518 | // registrationDeadline returns the absolute instant by which an accepted client | ||
| 519 | // must send its Register: the accept-time handshake deadline that already | 519 | // 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 must | 535 | // registerDeadline is the absolute instant by which the client's Register must | ||
| 536 | // arrive (the accept-time handshake deadline); the zero time disables the bound | 536 | // 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, ErrConfig | 540 | 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 never | 983 | // allocated (NextStreamID) and is transmitted unchanged: this listener never | ||
| 984 | // re-allocates a wire identity, whether the request arrives over the wire or | 984 | // 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, err | 988 | return nil, err | ||
| 989 | } | 989 | } | ||
| 990 | scoped := request | 990 | scoped := request | ||
| 991 | scoped.Epoch = s.epoch | 991 | scoped.Epoch = s.epoch | ||
| 992 | scoped.Connection = s.scope.Connection | 992 | 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 is | 996 | // 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) { | |||
| 110 | 110 | ||
| 111 | 111 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 112 | 112 | defer cancel() | |
| 113 | - | opened, err := session.OpenStream(ctx, openRequest(1)) | |
| 113 | + | opened, err := session.OpenEnvelopeStream(ctx, openRequest(1)) | |
| 114 | 114 | require.NoError(t, err) | |
| 115 | 115 | require.NotNil(t, opened) | |
| 116 | 116 | ||
| @@ -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 ( | |||
| 9 | 9 | "github.com/bnema/vev/internal/adapters/brokerwire" | |
| 10 | 10 | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 11 | 11 | "github.com/bnema/vev/internal/ports" | |
| 12 | + | "github.com/bnema/vev/internal/protocol/wire" | |
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | // Logical stream bridging. | |
| 15 | 16 | // | |
| 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. | |
| 22 | 20 | // | |
| 23 | 21 | // Identity and cancellation stay per stream: a stream the core loses, a stream | |
| 24 | 22 | // whose local consumer stalled, and a stream the peer closed each settle alone, | |
| @@ -29,10 +27,9 @@ import ( | |||
| 29 | 27 | type serverStream struct { | |
| 30 | 28 | session *serverSession | |
| 31 | 29 | id ports.BrokerStreamID | |
| 32 | - | core ports.BrokerLogicalConnection | |
| 30 | + | core ports.BrokerEnvelopeStream | |
| 33 | 31 | pipe *streamPipe | |
| 34 | 32 | carriage *carriage | |
| 35 | - | wire ports.ServerConnection | |
| 36 | 33 | ||
| 37 | 34 | mu sync.Mutex | |
| 38 | 35 | closed bool | |
| @@ -40,17 +37,13 @@ type serverStream struct { | |||
| 40 | 37 | } | |
| 41 | 38 | ||
| 42 | 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 | 41 | if s == nil || core == nil || id == 0 { | |
| 45 | 42 | return nil, ErrConfig | |
| 46 | 43 | } | |
| 47 | 44 | st := &serverStream{session: s, id: id, core: core, done: make(chan struct{})} | |
| 48 | 45 | st.pipe = newStreamPipe(s.cfg.StreamInboundChunks, s.cfg.StreamInboundBytes, st.sendChunk) | |
| 49 | 46 | st.carriage = newCarriage(st.pipe) | |
| 50 | - | st.wire = sessionwire.NewServerConnection(st.carriage) | |
| 51 | - | if st.wire == nil { | |
| 52 | - | return nil, ErrConfig | |
| 53 | - | } | |
| 54 | 47 | return st, nil | |
| 55 | 48 | } | |
| 56 | 49 | ||
| @@ -85,34 +78,36 @@ func (st *serverStream) start() { | |||
| 85 | 78 | go st.watchCore() | |
| 86 | 79 | } | |
| 87 | 80 | ||
| 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. | |
| 90 | 82 | func (st *serverStream) relayClient() { | |
| 91 | 83 | defer st.session.wg.Done() | |
| 92 | 84 | for { | |
| 93 | - | message, err := st.wire.ReceiveClient() | |
| 85 | + | envelope, err := st.carriage.RecvBounded(wire.AbsoluteEnvelopeLimit) | |
| 94 | 86 | if err != nil { | |
| 95 | 87 | st.fail(cancelOrCause(st.session.ctx, streamFailure(err))) | |
| 96 | 88 | return | |
| 97 | 89 | } | |
| 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)) | |
| 100 | 92 | return | |
| 101 | 93 | } | |
| 102 | 94 | } | |
| 103 | 95 | } | |
| 104 | 96 | ||
| 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. | |
| 107 | 98 | func (st *serverStream) relayServer() { | |
| 108 | 99 | defer st.session.wg.Done() | |
| 109 | 100 | for { | |
| 110 | - | message, err := st.core.ReceiveServer() | |
| 101 | + | payload, err := st.core.RecvEnvelope() | |
| 111 | 102 | if err != nil { | |
| 112 | 103 | st.fail(coreFailure(st.core, err)) | |
| 113 | 104 | 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 | 111 | st.fail(streamFailure(err)) | |
| 117 | 112 | return | |
| 118 | 113 | } | |
| @@ -121,10 +116,10 @@ func (st *serverStream) relayServer() { | |||
| 121 | 116 | ||
| 122 | 117 | // watchCore publishes the core connection's terminal outcome even when no read | |
| 123 | 118 | // 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 | 120 | // call observes the same terminal outcome right after forwarding any reply | |
| 126 | 121 | // 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 | |
| 128 | 123 | // close the carriage while a received reply is still being written out to the | |
| 129 | 124 | // client, discarding it. | |
| 130 | 125 | func (st *serverStream) watchCore() { | |
| @@ -147,8 +142,7 @@ func (st *serverStream) fail(err error) { | |||
| 147 | 142 | } | |
| 148 | 143 | ||
| 149 | 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 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. | |
| 152 | 146 | func (st *serverStream) close(cause error) bool { | |
| 153 | 147 | if st == nil { | |
| 154 | 148 | return false | |
| @@ -161,7 +155,7 @@ func (st *serverStream) close(cause error) bool { | |||
| 161 | 155 | st.closed = true | |
| 162 | 156 | st.mu.Unlock() | |
| 163 | 157 | st.pipe.closeWith(cause) | |
| 164 | - | _ = st.wire.Close() | |
| 158 | + | _ = st.carriage.Close() | |
| 165 | 159 | _ = st.core.Close() | |
| 166 | 160 | close(st.done) | |
| 167 | 161 | return true | |
| @@ -178,7 +172,7 @@ func streamFailure(err error) error { | |||
| 178 | 172 | ||
| 179 | 173 | // coreFailure classifies one core connection outcome. The core's typed terminal | |
| 180 | 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 | 176 | if err == nil && core != nil { | |
| 183 | 177 | err = core.Err() | |
| 184 | 178 | } | |
| @@ -221,7 +215,7 @@ func (s *serverSession) startStream(m brokerwire.OpenStream) { | |||
| 221 | 215 | s.wg.Add(1) | |
| 222 | 216 | go func() { | |
| 223 | 217 | defer s.wg.Done() | |
| 224 | - | core, err := s.core.OpenStream(s.ctx, request) | |
| 218 | + | core, err := s.core.OpenEnvelopeStream(s.ctx, request) | |
| 225 | 219 | if err != nil { | |
| 226 | 220 | _, _ = s.conn.CloseStream(m.Stream) | |
| 227 | 221 | 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 a | 17 | + | // One brokerwire logical stream carries the session protocol between client |
| 17 | - | // client and the daemon behind the broker. The broker terminates both hops: the | 18 | + | // and daemon. The broker relays complete opaque envelope payloads through |
| 18 | - | // client's session connection is built by sessionwire over the stream's private | 19 | + | // 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 stream | 21 | // 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 *serverSession | 28 | session *serverSession | ||
| 31 | id ports.BrokerStreamID | 29 | id ports.BrokerStreamID | ||
| 32 | - | core ports.BrokerLogicalConnection | 30 | + | core ports.BrokerEnvelopeStream |
| 33 | pipe *streamPipe | 31 | pipe *streamPipe | ||
| 34 | carriage *carriage | 32 | carriage *carriage | ||
| 35 | - | wire ports.ServerConnection | |||
| 36 | 33 | ||||
| 37 | mu sync.Mutex | 34 | mu sync.Mutex | ||
| 38 | closed bool | 35 | 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, ErrConfig | 42 | 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, nil | 47 | 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 session | 81 | + | // 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 | return | 88 | 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 | return | 92 | return | ||
| 101 | } | 93 | } | ||
| 102 | } | 94 | } | ||
| 103 | } | 95 | } | ||
| 104 | 96 | ||||
| 105 | - | // relayServer carries daemon-to-client messages from the admitted core | 97 | + | // 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 | return | 104 | 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 | return | 112 | 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 read | 117 | // 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 orderly | 118 | // 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 reply | 120 | // call observes the same terminal outcome right after forwarding any reply | ||
| 126 | // that was already in flight, and settles the stream from there. Settling here | 121 | // 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 |
| 128 | // close the carriage while a received reply is still being written out to the | 123 | // 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 is | 145 | + | // 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 false | 148 | return false | ||
| @@ -161,7 +155,7 @@ func (st *serverStream) close(cause error) bool { | |||||
| 161 | st.closed = true | 155 | 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 true | 161 | return true | ||
| @@ -178,7 +172,7 @@ func streamFailure(err error) error { | |||||
| 178 | 172 | ||||
| 179 | // coreFailure classifies one core connection outcome. The core's typed terminal | 173 | // 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 { | |||
| 305 | 305 | // the LogicalConnector. A policy that differs in any field is refused as a | |
| 306 | 306 | // conflicting policy without touching the carriage, so a physical connection | |
| 307 | 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 | 309 | if c == nil || c.logical == nil { | |
| 310 | 310 | return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ErrLogicalConfig} | |
| 311 | 311 | } | |
| @@ -305,7 +305,7 @@ func (c *PhysicalConnection) Policy() ports.BrokerPolicy { | |||||
| 305 | // the LogicalConnector. A policy that differs in any field is refused as a | 305 | // the LogicalConnector. A policy that differs in any field is refused as a | ||
| 306 | // conflicting policy without touching the carriage, so a physical connection | 306 | // 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) { | |||
| 292 | 292 | require.Equal(t, domain.RemoteFailureNone, physical.FailureKind()) | |
| 293 | 293 | ||
| 294 | 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 | 296 | require.Error(t, err) | |
| 297 | 297 | var typed ports.BrokerError | |
| 298 | 298 | require.ErrorAs(t, err, &typed) | |
| @@ -300,7 +300,7 @@ func TestEndpointConnectorExposesAuthorityAndPolicyGuard(t *testing.T) { | |||
| 300 | 300 | require.Zero(t, server.acceptedCount(), "a policy-mismatched open is never admitted") | |
| 301 | 301 | ||
| 302 | 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 | 304 | require.NoError(t, err) | |
| 305 | 305 | server.awaitAccepted(t, 1) | |
| 306 | 306 | require.NoError(t, logical.SendClient(protocol.Ping{})) | |
| @@ -334,7 +334,7 @@ func TestEndpointConnectorDetachesSetupContext(t *testing.T) { | |||
| 334 | 334 | cancel() | |
| 335 | 335 | // The setup context ended; the pooled connection must still work. | |
| 336 | 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 | 338 | require.NoError(t, err) | |
| 339 | 339 | server.awaitAccepted(t, 1) | |
| 340 | 340 | require.NoError(t, logical.SendClient(protocol.Ping{})) | |
| @@ -360,7 +360,7 @@ func TestPhysicalDoneBeforeLogicalLoss(t *testing.T) { | |||
| 360 | 360 | t.Cleanup(func() { _ = physical.Close() }) | |
| 361 | 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 | 364 | require.NoError(t, err) | |
| 365 | 365 | server.awaitAccepted(t, 1) | |
| 366 | 366 | 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.BrokerError | 297 | 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 { | |||
| 285 | 285 | } | |
| 286 | 286 | ||
| 287 | 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 | 289 | t.Helper() | |
| 290 | + | conn := asTyped(stream) | |
| 290 | 291 | require.NoError(t, conn.SendClient(protocol.Ping{})) | |
| 291 | 292 | message, err := conn.ReceiveServer() | |
| 292 | 293 | 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. | |
| 2 | 2 | // | |
| 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. | |
| 10 | 6 | // | |
| 11 | 7 | // The carriage between the two is a private, ordered, bounded byte stream | |
| 12 | 8 | // (muxStreamPipe). Its read side pulls already-queued chunks from the pump | |
| @@ -24,10 +20,6 @@ | |||
| 24 | 20 | // resets exactly that stream (one mux Reset) and leaves every sibling and the | |
| 25 | 21 | // physical connection untouched. Done, Err, and a physical-loss | |
| 26 | 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 | 23 | package daemonmux | |
| 32 | 24 | ||
| 33 | 25 | import ( | |
| @@ -35,13 +27,10 @@ import ( | |||
| 35 | 27 | "errors" | |
| 36 | 28 | "io" | |
| 37 | 29 | "sync" | |
| 38 | - | "time" | |
| 39 | 30 | ||
| 40 | - | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 41 | 31 | "github.com/bnema/vev/internal/adapters/streamframe" | |
| 42 | 32 | "github.com/bnema/vev/internal/domain" | |
| 43 | 33 | "github.com/bnema/vev/internal/ports" | |
| 44 | - | "github.com/bnema/vev/internal/protocol" | |
| 45 | 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 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. | |
| 248 | 235 | type LogicalConnection struct { | |
| 249 | - | ports.ClientConnection | |
| 250 | - | ||
| 251 | 236 | pump *Pump | |
| 252 | 237 | ref StreamRef | |
| 253 | 238 | pipe *muxStreamPipe | |
| @@ -258,22 +243,16 @@ type LogicalConnection struct { | |||
| 258 | 243 | // keeps its exact cause even after the record is evicted. | |
| 259 | 244 | cause *terminalState | |
| 260 | 245 | ||
| 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 | |
| 268 | 247 | watchDone chan struct{} | |
| 269 | 248 | closeOnce sync.Once | |
| 270 | 249 | 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 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 | |
| 277 | 256 | // stream's retained terminal authority, captured at open so the connection | |
| 278 | 257 | // reports its exact outcome independent of the engine's bounded record. | |
| 279 | 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 | 260 | pipe := newMuxStreamPipe(pump, ref.Physical, limit, streamDone) | |
| 282 | 261 | raw := newMuxTransport(pipe) | |
| 283 | 262 | 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{}), | |
| 299 | 271 | } | |
| 300 | 272 | go connection.watch() | |
| 301 | 273 | return connection | |
| @@ -304,25 +276,6 @@ func newLogicalConnection(pump *Pump, ref StreamRef, streamDone <-chan struct{}, | |||
| 304 | 276 | // Ref returns the complete stream reference recorded at admission. | |
| 305 | 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 | 279 | // Done returns the logical terminal channel independent of reads. It closes | |
| 327 | 280 | // exactly once, before Err becomes stable. | |
| 328 | 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 | 288 | // FailureKind classifies Err. It is None while open and for an orderly Close. | |
| 336 | 289 | func (c *LogicalConnection) FailureKind() domain.RemoteFailureKind { return c.terminal.FailureKind() } | |
| 337 | 290 | ||
| 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 { | |
| 342 | 292 | if c.settled() { | |
| 343 | 293 | return c.terminalError() | |
| 344 | 294 | } | |
| 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 | |
| 350 | 297 | } | |
| 351 | - | return nil | |
| 298 | + | return c.raw.Send(wire.Envelope{Payload: payload}) | |
| 352 | 299 | } | |
| 353 | 300 | ||
| 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) | |
| 369 | 309 | } | |
| 370 | - | return nil, c.abortMalformed(err) | |
| 310 | + | return envelope.Payload, nil | |
| 371 | 311 | } | |
| 372 | 312 | ||
| 373 | 313 | // Close independently closes the logical stream: it discards the inbound | |
| @@ -660,8 +600,7 @@ func (s *muxStreamPipe) Close() error { | |||
| 660 | 600 | } | |
| 661 | 601 | ||
| 662 | 602 | // 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. | |
| 665 | 604 | type muxTransport struct { | |
| 666 | 605 | pipe *muxStreamPipe | |
| 667 | 606 | 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 stream | 3 | + | // A LogicalConnection carries complete raw session envelopes across the broker, |
| 4 | - | // multiplexed over one physical daemonmux Pump. It implements | 4 | + | // without decoding them. Typed preview and observation are adapted by their |
| 5 | - | // ports.BrokerLogicalConnection by embedding the typed ports.ClientConnection | 5 | + | // 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 stream | 7 | // The carriage between the two is a private, ordered, bounded byte stream | ||
| 12 | // (muxStreamPipe). Its read side pulls already-queued chunks from the pump | 8 | // (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 the | 20 | // resets exactly that stream (one mux Reset) and leaves every sibling and the | ||
| 25 | // physical connection untouched. Done, Err, and a physical-loss | 21 | // 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 daemonmux | 23 | 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 physical | 233 | + | // LogicalConnection is one client-side logical stream over a physical Pump. |
| 245 | - | // Pump. It implements ports.BrokerLogicalConnection: the embedded typed | 234 | + | // 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 *Pump | 236 | pump *Pump | ||
| 252 | ref StreamRef | 237 | ref StreamRef | ||
| 253 | pipe *muxStreamPipe | 238 | 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 *terminalState | 244 | cause *terminalState | ||
| 260 | 245 | ||||
| 261 | - | terminal *terminalState | 246 | + | 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.Once | 248 | closeOnce sync.Once | ||
| 270 | closeErr error | 249 | 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 confirmed | 254 | + | // newLogicalConnection builds the framed carriage over one confirmed open |
| 276 | - | // open stream and starts the terminal watcher. It never blocks. cause is the | 255 | + | // stream and starts the terminal watcher. It never blocks. cause is the |
| 277 | // stream's retained terminal authority, captured at open so the connection | 256 | // 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 connection | 273 | 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 closes | 279 | // 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. It | 291 | + | 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 nil | 298 | + | return c.raw.Send(wire.Envelope{Payload: payload}) |
| 352 | } | 299 | } | ||
| 353 | 300 | ||||
| 354 | - | // ReceiveServer receives one typed server message. An orderly end of the inner | 301 | + | func (c *LogicalConnection) RecvEnvelope() ([]byte, error) { |
| 355 | - | // stream returns io.EOF; any other inner-stream error is treated as a | 302 | + | envelope, err := c.raw.Recv() |
| 356 | - | // malformed session frame: exactly this stream is reset towards the peer, its | 303 | + | 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 inbound | 313 | // 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: streamframe | 602 | // muxTransport adapts the ordered byte stream to wire.Transport: streamframe | ||
| 663 | - | // owns the 4-byte length framing, and sessionwire owns the typed conversion on | 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. |
| 664 | - | // top. Closing it closes the byte stream and joins streamframe's writer. | |||
| 665 | type muxTransport struct { | 604 | type muxTransport struct { | ||
| 666 | pipe *muxStreamPipe | 605 | pipe *muxStreamPipe | ||
| 667 | framer *streamframe.Framer | 606 | framer *streamframe.Framer | ||
M internal/adapters/daemonmux/logical_conn_test.go +8 -56
| @@ -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 | 322 | t.Helper() | |
| 323 | 323 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) | |
| 324 | 324 | defer cancel() | |
| 325 | 325 | connection, err := connector.Open(ctx, controlRequest(stream)) | |
| 326 | 326 | require.NoError(t, err) | |
| 327 | - | return connection | |
| 327 | + | return typedLogical{typedStream: asTyped(connection), LogicalConnection: connection} | |
| 328 | 328 | } | |
| 329 | 329 | ||
| 330 | 330 | // watchRegistered reports whether the pump still holds an inbound watch entry | |
| @@ -406,7 +406,7 @@ func TestLogicalTwoConnections(t *testing.T) { | |||
| 406 | 406 | acceptor.start(PhysicalStreamID(i), deadline) | |
| 407 | 407 | } | |
| 408 | 408 | ||
| 409 | - | connections := make([]*LogicalConnection, 0, count) | |
| 409 | + | connections := make([]typedLogical, 0, count) | |
| 410 | 410 | for i := 1; i <= count; i++ { | |
| 411 | 411 | connection := openWithDeadline(t, connector, i) | |
| 412 | 412 | require.NoError(t, connection.SendClient(protocol.Ping{}), "connection %d", i) | |
| @@ -487,11 +487,12 @@ func TestLogicalHundredConnectionsConcurrentAdmission(t *testing.T) { | |||
| 487 | 487 | traffic.Add(1) | |
| 488 | 488 | go func(index int) { | |
| 489 | 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 | 492 | trafficErrs[index] = err | |
| 492 | 493 | return | |
| 493 | 494 | } | |
| 494 | - | message, err := connections[index].ReceiveServer() | |
| 495 | + | message, err := typed.ReceiveServer() | |
| 495 | 496 | if err != nil { | |
| 496 | 497 | trafficErrs[index] = err | |
| 497 | 498 | return | |
| @@ -802,7 +803,7 @@ func TestLogicalPhysicalLossFanout(t *testing.T) { | |||
| 802 | 803 | for i := 1; i <= count; i++ { | |
| 803 | 804 | acceptor.start(PhysicalStreamID(i), deadline) | |
| 804 | 805 | } | |
| 805 | - | connections := make([]*LogicalConnection, 0, count) | |
| 806 | + | connections := make([]typedLogical, 0, count) | |
| 806 | 807 | for i := 1; i <= count; i++ { | |
| 807 | 808 | connection := openWithDeadline(t, connector, i) | |
| 808 | 809 | require.NoError(t, connection.SendClient(protocol.Ping{})) | |
| @@ -832,55 +833,6 @@ func TestLogicalPhysicalLossFanout(t *testing.T) { | |||
| 832 | 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 | 836 | // connections leaves no goroutine behind. | |
| 885 | 837 | func TestLogicalNoGoroutineLeak(t *testing.T) { | |
| 886 | 838 | before := runtime.NumGoroutine() | |
| @@ -894,7 +846,7 @@ func TestLogicalNoGoroutineLeak(t *testing.T) { | |||
| 894 | 846 | for i := 1; i <= count; i++ { | |
| 895 | 847 | acceptor.start(PhysicalStreamID(i), deadline) | |
| 896 | 848 | } | |
| 897 | - | connections := make([]*LogicalConnection, 0, count) | |
| 849 | + | connections := make([]typedLogical, 0, count) | |
| 898 | 850 | for i := 1; i <= count; i++ { | |
| 899 | 851 | connection := openWithDeadline(t, connector, i) | |
| 900 | 852 | 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 connection | 327 | + | return typedLogical{typedStream: asTyped(connection), LogicalConnection: connection} |
| 328 | } | 328 | } | ||
| 329 | 329 | ||||
| 330 | // watchRegistered reports whether the pump still holds an inbound watch entry | 330 | // 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] = err | 492 | trafficErrs[index] = err | ||
| 492 | return | 493 | 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] = err | 497 | trafficErrs[index] = err | ||
| 497 | return | 498 | 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) { | |||
| 75 | 75 | client, err := pool.RegisterClient() | |
| 76 | 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 | 79 | require.NoError(t, err) | |
| 80 | 80 | require.NotNil(t, first) | |
| 81 | 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 | 84 | require.NoError(t, err) | |
| 85 | 85 | require.NotNil(t, second) | |
| 86 | 86 | t.Cleanup(func() { _ = second.Close() }) | |
| @@ -114,7 +114,7 @@ func TestPoolRefusesConflictingPolicyWithoutDial(t *testing.T) { | |||
| 114 | 114 | ||
| 115 | 115 | client, err := pool.RegisterClient() | |
| 116 | 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 | 118 | require.Error(t, err) | |
| 119 | 119 | var typed ports.BrokerError | |
| 120 | 120 | require.ErrorAs(t, err, &typed) | |
| @@ -142,7 +142,7 @@ func TestPoolSurfacesPhysicalLossPerStream(t *testing.T) { | |||
| 142 | 142 | ||
| 143 | 143 | client, err := pool.RegisterClient() | |
| 144 | 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 | 146 | require.NoError(t, err) | |
| 147 | 147 | server.awaitAccepted(t, 1) | |
| 148 | 148 | ||
| @@ -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.BrokerError | 119 | 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) { | |||
| 195 | 195 | require.NoError(t, daemon.awaitAdoption(t)) | |
| 196 | 196 | ||
| 197 | 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 | 199 | require.NoError(t, err) | |
| 200 | 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 | 202 | require.NoError(t, err) | |
| 203 | 203 | t.Cleanup(func() { _ = second.Close() }) | |
| 204 | 204 | daemon.awaitQUICAccepted(t, 2) | |
| @@ -219,7 +219,7 @@ func TestQUICCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) { | |||
| 219 | 219 | wg.Add(1) | |
| 220 | 220 | go func(i int) { | |
| 221 | 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 | 223 | }(i) | |
| 224 | 224 | } | |
| 225 | 225 | wg.Wait() | |
| @@ -375,11 +375,11 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) { | |||
| 375 | 375 | ||
| 376 | 376 | lost := make([]ports.BrokerLogicalConnection, 3) | |
| 377 | 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 | 379 | require.NoError(t, err, "stream %d", i+1) | |
| 380 | 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 | 383 | require.NoError(t, err) | |
| 384 | 384 | t.Cleanup(func() { _ = survivor.Close() }) | |
| 385 | 385 | daemon.awaitQUICAccepted(t, 4) | |
| @@ -429,7 +429,7 @@ func TestQUICCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) { | |||
| 429 | 429 | "the lost QUIC physical child was never reaped") | |
| 430 | 430 | ||
| 431 | 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 | 433 | require.Error(t, err) | |
| 434 | 434 | } | |
| 435 | 435 | ||
| @@ -455,7 +455,7 @@ func TestQUICCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) { | |||
| 455 | 455 | ||
| 456 | 456 | cancel() | |
| 457 | 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 | 459 | require.NoError(t, err) | |
| 460 | 460 | t.Cleanup(func() { _ = logical.Close() }) | |
| 461 | 461 | 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) { | |||
| 253 | 253 | pid := sshMuxReadPID(t, pidPath) | |
| 254 | 254 | ||
| 255 | 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 | 257 | require.NoError(t, err) | |
| 258 | 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 | 260 | require.NoError(t, err) | |
| 261 | 261 | t.Cleanup(func() { _ = second.Close() }) | |
| 262 | 262 | for _, stream := range []ports.BrokerLogicalConnection{first, second} { | |
| @@ -276,7 +276,7 @@ func TestSSHCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) { | |||
| 276 | 276 | wg.Add(1) | |
| 277 | 277 | go func(i int) { | |
| 278 | 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 | 280 | }(i) | |
| 281 | 281 | } | |
| 282 | 282 | wg.Wait() | |
| @@ -332,12 +332,12 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) { | |||
| 332 | 332 | ||
| 333 | 333 | lost := make([]ports.BrokerLogicalConnection, 3) | |
| 334 | 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 | 336 | require.NoError(t, err, "stream %d", i+1) | |
| 337 | 337 | lost[i] = connection | |
| 338 | 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 | 341 | require.NoError(t, err) | |
| 342 | 342 | t.Cleanup(func() { _ = survivor.Close() }) | |
| 343 | 343 | ||
| @@ -383,7 +383,7 @@ func TestSSHCarriagePhysicalFailureFansOutAndIsIsolated(t *testing.T) { | |||
| 383 | 383 | require.Equal(t, protocol.Pong{}, message) | |
| 384 | 384 | ||
| 385 | 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 | 387 | require.Error(t, err) | |
| 388 | 388 | ||
| 389 | 389 | // The failed subprocess is reaped, not left a zombie. | |
| @@ -409,7 +409,7 @@ func TestSSHCarriageSetupContextDetachedFromPhysicalLifetime(t *testing.T) { | |||
| 409 | 409 | cancel() | |
| 410 | 410 | ||
| 411 | 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 | 413 | require.NoError(t, err) | |
| 414 | 414 | t.Cleanup(func() { _ = logical.Close() }) | |
| 415 | 415 | 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] = connection | 337 | 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) { | |||
| 112 | 112 | require.NoError(t, err) | |
| 113 | 113 | require.NotNil(t, connection) | |
| 114 | 114 | 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() | |
| 117 | 118 | require.NoError(t, err) | |
| 118 | 119 | require.Equal(t, protocol.Pong{}, message) | |
| 119 | 120 | } | |
| @@ -152,7 +153,7 @@ func TestServerSupervisorDeliversFirstHelloDataFrame(t *testing.T) { | |||
| 152 | 153 | 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 | 157 | require.NoError(t, err) | |
| 157 | 158 | var server ports.ServerConnection | |
| 158 | 159 | select { | |
| @@ -220,7 +221,7 @@ func TestServerSupervisorStampsAcceptedOriginThroughAggregate(t *testing.T) { | |||
| 220 | 221 | 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 | 225 | require.NoError(t, err) | |
| 225 | 226 | t.Cleanup(func() { _ = logical.Close() }) | |
| 226 | 227 | ||
| @@ -292,9 +293,9 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) { | |||
| 292 | 293 | physicalB := connectRaw(t, clientB, endpoint) | |
| 293 | 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 | 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 | 299 | require.NoError(t, err) | |
| 299 | 300 | server.awaitAccepted(t, 2) | |
| 300 | 301 | require.Equal(t, 2, server.supervisor.Children()) | |
| @@ -307,14 +308,14 @@ func TestServerSupervisorServerLossIsolated(t *testing.T) { | |||
| 307 | 308 | "the lost child was never reaped") | |
| 308 | 309 | ||
| 309 | 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 | 312 | require.NoError(t, err) | |
| 312 | 313 | require.NotNil(t, logicalB2) | |
| 313 | 314 | server.awaitAccepted(t, 3) | |
| 314 | 315 | ||
| 315 | 316 | // The lost physical refuses a fresh stream while its published logical | |
| 316 | 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 | 319 | require.Error(t, err) | |
| 319 | 320 | require.True(t, channelClosed(logicalA.Done())) | |
| 320 | 321 | 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 <- connection | 153 | 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.ServerConnection | 158 | var server ports.ServerConnection | ||
| 158 | select { | 159 | select { | ||
| @@ -220,7 +221,7 @@ func TestServerSupervisorStampsAcceptedOriginThroughAggregate(t *testing.T) { | |||||
| 220 | accepted <- connection | 221 | 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 logical | 316 | // 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) { | |||
| 148 | 148 | require.NoError(t, server.awaitAdoption(t)) | |
| 149 | 149 | ||
| 150 | 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 | 152 | require.NoError(t, err) | |
| 153 | 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 | 155 | require.NoError(t, err) | |
| 156 | 156 | t.Cleanup(func() { _ = second.Close() }) | |
| 157 | 157 | server.awaitAccepted(t, 2) | |
| @@ -172,7 +172,7 @@ func TestUnixCarriageSupervisorTwoThenHundredTypedAdmissions(t *testing.T) { | |||
| 172 | 172 | wg.Add(1) | |
| 173 | 173 | go func(i int) { | |
| 174 | 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 | 176 | }(i) | |
| 177 | 177 | } | |
| 178 | 178 | 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 ( | |||
| 21 | 21 | "github.com/bnema/vev/internal/adapters/daemonmux" | |
| 22 | 22 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 23 | 23 | "github.com/bnema/vev/internal/adapters/lifecycle" | |
| 24 | + | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 24 | 25 | "github.com/bnema/vev/internal/logging" | |
| 25 | 26 | "github.com/bnema/vev/internal/platform" | |
| 26 | 27 | "github.com/bnema/vev/internal/ports" | |
| @@ -317,7 +318,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker | |||
| 317 | 318 | if err != nil { | |
| 318 | 319 | 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 | 322 | registry, err := broker.NewRegistryWithConfig(epoch, store, remoteProbe, clk, log, registryConfig) | |
| 322 | 323 | if err != nil { | |
| 323 | 324 | return err | |
| @@ -345,7 +346,7 @@ func runBrokerServe(ctx context.Context, options brokerServeOptions, deps broker | |||
| 345 | 346 | if err := supervisor.RegisterCloseable("pool", pool); err != nil { | |
| 346 | 347 | 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 | 350 | if err != nil { | |
| 350 | 351 | return err | |
| 351 | 352 | } | |
| @@ -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 err | 319 | 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 err | 324 | 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 err | 347 | 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 err | 351 | return err | ||
| 351 | } | 352 | } | ||
M internal/app/broker_offline_local.go +5 -3
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/vev/internal/adapters/brokerconfig" | |
| 9 | 9 | "github.com/bnema/vev/internal/adapters/daemonmux" | |
| 10 | + | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 10 | 11 | "github.com/bnema/vev/internal/domain" | |
| 11 | 12 | "github.com/bnema/vev/internal/ports" | |
| 12 | 13 | "github.com/bnema/vev/internal/usecase/broker" | |
| @@ -60,6 +61,7 @@ type localRouteProbe struct { | |||
| 60 | 61 | loadIdentity localIdentityLoader | |
| 61 | 62 | connector ports.BrokerEndpointConnector | |
| 62 | 63 | shared pooledPhysicals | |
| 64 | + | codec ports.SessionCodec | |
| 63 | 65 | } | |
| 64 | 66 | ||
| 65 | 67 | var _ broker.LocalProbe = (*localRouteProbe)(nil) | |
| @@ -87,7 +89,7 @@ func newDynamicLocalRouteProbe(route brokerconfig.Route, policy ports.BrokerPoli | |||
| 87 | 89 | if err := policy.Validate(); err != nil { | |
| 88 | 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}, nil | |
| 92 | + | return &localRouteProbe{epoch: epoch, route: route, policy: policy, loadIdentity: loadIdentity, connector: connector, codec: sessionwire.BrokerCodec{}}, nil | |
| 91 | 93 | } | |
| 92 | 94 | ||
| 93 | 95 | // ProbeLocal observes the local daemon by authenticating the daemonmux physical | |
| @@ -127,7 +129,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs | |||
| 127 | 129 | if err != nil { | |
| 128 | 130 | 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 | 133 | if err != nil { | |
| 132 | 134 | return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err | |
| 133 | 135 | } | |
| @@ -144,7 +146,7 @@ func (p *localRouteProbe) ProbeLocal(ctx context.Context) (ports.BrokerDaemonObs | |||
| 144 | 146 | if physical.Identity() != endpoint.ExpectedIdentity.Identity || !physical.Policy().Compatible(endpoint.Policy) { | |
| 145 | 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 | 150 | if err != nil { | |
| 149 | 151 | return ports.BrokerDaemonObservation{Availability: domain.RemoteAvailabilityUnreachable}, err | |
| 150 | 152 | } | |
| @@ -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 localIdentityLoader | 61 | loadIdentity localIdentityLoader | ||
| 61 | connector ports.BrokerEndpointConnector | 62 | connector ports.BrokerEndpointConnector | ||
| 62 | shared pooledPhysicals | 63 | 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}, nil | 92 | + | 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 physical | 95 | // 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}, err | 130 | 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}, err | 134 | 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}, err | 151 | 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 ( | |||
| 18 | 18 | "github.com/bnema/vev/internal/adapters/daemonmux" | |
| 19 | 19 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 20 | 20 | "github.com/bnema/vev/internal/adapters/pty" | |
| 21 | + | "github.com/bnema/vev/internal/adapters/sessionwire" | |
| 21 | 22 | "github.com/bnema/vev/internal/domain" | |
| 22 | 23 | "github.com/bnema/vev/internal/ports" | |
| 23 | 24 | "github.com/bnema/vev/internal/protocol" | |
| @@ -134,10 +135,19 @@ func (f *realLocalDaemonFixture) openStream(t *testing.T, request ports.BrokerOp | |||
| 134 | 135 | } | |
| 135 | 136 | stream, err := physical.OpenStream(ctx, request) | |
| 136 | 137 | 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 | |
| 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 | 151 | // awaitWelcome receives server messages until the typed Welcome arrives. | |
| 142 | 152 | func awaitWelcome(t *testing.T, stream ports.BrokerLogicalConnection) protocol.Welcome { | |
| 143 | 153 | 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 stream | 139 | + | 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 | |||
| 142 | 142 | // fixed terminal error, so a test can drive the accept drain without a carriage. | |
| 143 | 143 | type fatalListener struct{ err error } | |
| 144 | 144 | ||
| 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" } | |
| 148 | 148 | ||
| 149 | 149 | // TestDrainBrokerAcceptClassifiesTerminalFailure proves the drain treats a | |
| 150 | 150 | // listener close as an orderly end with no log and no failure signal, while any | |
| @@ -367,7 +367,7 @@ type wedgedListener struct { | |||
| 367 | 367 | 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 | 371 | func (l *wedgedListener) Close() error { | |
| 372 | 372 | l.closed.Store(true) | |
| 373 | 373 | 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 a | 149 | // TestDrainBrokerAcceptClassifiesTerminalFailure proves the drain treats a | ||
| 150 | // listener close as an orderly end with no log and no failure signal, while any | 150 | // 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.Bool | 367 | 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 nil | 373 | return nil | ||
M internal/app/broker_remote_probe.go +11 -10
| @@ -36,6 +36,7 @@ type brokerRemoteProbe struct { | |||
| 36 | 36 | connector ports.BrokerEndpointConnector | |
| 37 | 37 | hosts ports.BrokerHostAuthorityReader | |
| 38 | 38 | shared pooledPhysicals | |
| 39 | + | codec ports.SessionCodec | |
| 39 | 40 | } | |
| 40 | 41 | ||
| 41 | 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 | 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 | 65 | pool, _ := s.pool.Load().(sharedPhysicals) | |
| 65 | 66 | if pool == nil || identity == "" { | |
| 66 | 67 | return ports.BrokerDaemonObservation{}, false, nil | |
| @@ -69,7 +70,7 @@ func (s *pooledPhysicals) observe(ctx context.Context, identity ports.BrokerDaem | |||
| 69 | 70 | if !ok { | |
| 70 | 71 | return ports.BrokerDaemonObservation{}, false, nil | |
| 71 | 72 | } | |
| 72 | - | observation, err := observeDaemonCatalogue(ctx, physical, request) | |
| 73 | + | observation, err := observeDaemonCatalogue(ctx, physical, request, codec) | |
| 73 | 74 | if err == nil || ctx.Err() != nil { | |
| 74 | 75 | return observation, true, err | |
| 75 | 76 | } | |
| @@ -99,7 +100,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot | |||
| 99 | 100 | // bootstrapping SSH/QUIC again. A borrowed transport that retires mid-probe | |
| 100 | 101 | // falls back to a fresh dial. | |
| 101 | 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 | 104 | return observation, err | |
| 104 | 105 | } | |
| 105 | 106 | } | |
| @@ -127,7 +128,7 @@ func (p *brokerRemoteProbe) Probe(ctx context.Context, registration domain.Remot | |||
| 127 | 128 | if err != nil || identity != physical.Identity() { | |
| 128 | 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 | 132 | if err == nil && ctx.Err() == nil { | |
| 132 | 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 | 148 | // catalogue, and a catalogue outside the exact current schema all fail closed: | |
| 148 | 149 | // the caller receives an error and no observation it could publish, so a probe | |
| 149 | 150 | // 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 { | |
| 152 | 153 | return ports.BrokerDaemonObservation{}, errors.New("vev: invalid daemon observation request") | |
| 153 | 154 | } | |
| 154 | 155 | logical, err := physical.OpenStream(ctx, request) | |
| @@ -167,7 +168,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo | |||
| 167 | 168 | if err != nil { | |
| 168 | 169 | 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 | 172 | if err != nil { | |
| 172 | 173 | return ports.BrokerDaemonObservation{}, err | |
| 173 | 174 | } | |
| @@ -201,7 +202,7 @@ func observeDaemonCatalogue(ctx context.Context, physical ports.BrokerPhysicalCo | |||
| 201 | 202 | // joined before returning, so an observation always returns promptly once its | |
| 202 | 203 | // attempt is retired instead of parking the daemon's reader (or a probe | |
| 203 | 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 | 206 | if ctx == nil { | |
| 206 | 207 | ctx = context.Background() | |
| 207 | 208 | } | |
| @@ -211,11 +212,11 @@ func exchangeObservationCommand(ctx context.Context, logical ports.BrokerLogical | |||
| 211 | 212 | } | |
| 212 | 213 | done := make(chan outcome, 1) | |
| 213 | 214 | go func() { | |
| 214 | - | if err := logical.SendClient(command); err != nil { | |
| 215 | + | if err := typed.SendClient(command); err != nil { | |
| 215 | 216 | done <- outcome{err: err} | |
| 216 | 217 | return | |
| 217 | 218 | } | |
| 218 | - | message, err := logical.ReceiveServer() | |
| 219 | + | message, err := typed.ReceiveServer() | |
| 219 | 220 | done <- outcome{message: message, err: err} | |
| 220 | 221 | }() | |
| 221 | 222 | select { | |
| @@ -36,6 +36,7 @@ type brokerRemoteProbe struct { | |||||
| 36 | connector ports.BrokerEndpointConnector | 36 | connector ports.BrokerEndpointConnector | ||
| 37 | hosts ports.BrokerHostAuthorityReader | 37 | hosts ports.BrokerHostAuthorityReader | ||
| 38 | shared pooledPhysicals | 38 | 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, nil | 67 | 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, nil | 71 | 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, err | 75 | 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-probe | 100 | // 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, err | 104 | 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 probe | 149 | // 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{}, err | 169 | 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{}, err | 173 | 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 its | 202 | // joined before returning, so an observation always returns promptly once its | ||
| 202 | // attempt is retired instead of parking the daemon's reader (or a probe | 203 | // 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 | return | 217 | 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) { | |||
| 56 | 56 | connector.EXPECT().Connect(mock.Anything, mock.Anything).Return(nil, dialErr).Times(tt.wantDials) | |
| 57 | 57 | } | |
| 58 | 58 | physical := portsmocks.NewMockBrokerPhysicalConnection(t) | |
| 59 | + | codec := portsmocks.NewMockSessionCodec(t) | |
| 59 | 60 | done := make(chan struct{}) | |
| 60 | 61 | if tt.retired { | |
| 61 | 62 | close(done) | |
| @@ -67,13 +68,15 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) { | |||
| 67 | 68 | physical.EXPECT().Done().Return(done) | |
| 68 | 69 | } | |
| 69 | 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 | 74 | 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 | 76 | sent = message.(protocol.CommandRequest) | |
| 74 | 77 | return nil | |
| 75 | 78 | }) | |
| 76 | - | logical.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) { | |
| 79 | + | typed.EXPECT().ReceiveServer().RunAndReturn(func() (protocol.ServerMessage, error) { | |
| 77 | 80 | return protocol.CommandResult{RequestID: sent.RequestID, Outcome: protocol.CommandSucceeded, Output: testLocalCatalogue(t)}, nil | |
| 78 | 81 | }) | |
| 79 | 82 | logical.EXPECT().Close().Return(nil) | |
| @@ -84,7 +87,7 @@ func TestBrokerRemoteProbeBorrowsWarmTransport(t *testing.T) { | |||
| 84 | 87 | } | |
| 85 | 88 | probe := &brokerRemoteProbe{ | |
| 86 | 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 | 92 | // Use the real pool for borrowing. Stateful pool retirement is | |
| 90 | 93 | // covered with a fake physical in broker's worker tests. | |
| @@ -145,6 +148,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) { | |||
| 145 | 148 | // No Connect expectation: any redial fails the mock. | |
| 146 | 149 | connector := portsmocks.NewMockBrokerEndpointConnector(t) | |
| 147 | 150 | physical := portsmocks.NewMockBrokerPhysicalConnection(t) | |
| 151 | + | codec := portsmocks.NewMockSessionCodec(t) | |
| 148 | 152 | healthy := make(chan struct{}) | |
| 149 | 153 | physical.EXPECT().Policy().Return(policy).Maybe() | |
| 150 | 154 | physical.EXPECT().Identity().Return(identity).Maybe() | |
| @@ -158,17 +162,19 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) { | |||
| 158 | 162 | // worker only signals, the test goroutine cancels and asserts. | |
| 159 | 163 | waiting := make(chan struct{}, 1) | |
| 160 | 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 | 166 | waiting <- struct{}{} | |
| 163 | 167 | <-ctx.Done() | |
| 164 | 168 | return nil, ctx.Err() | |
| 165 | 169 | }) | |
| 166 | 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 | 174 | closed := make(chan struct{}) | |
| 169 | 175 | 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) { | |
| 172 | 178 | waiting <- struct{}{} | |
| 173 | 179 | <-closed | |
| 174 | 180 | return nil, errors.New("stream closed") | |
| @@ -178,7 +184,7 @@ func TestBrokerRemoteProbeSilentBorrowedTransportNeverRedials(t *testing.T) { | |||
| 178 | 184 | } | |
| 179 | 185 | probe := &brokerRemoteProbe{ | |
| 180 | 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 | 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 | 190 | 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.CommandRequest | 74 | 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 nil | 77 | 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)}, nil | 80 | 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 is | 92 | // 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.Once | 175 | 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 | <-closed | 179 | <-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 { | |||
| 823 | 823 | // local and remote daemons through it, including the local daemon. | |
| 824 | 824 | // Snapshot access performs no I/O; request methods are non-blocking | |
| 825 | 825 | // coalesced hints except where the context explicitly bounds them. | |
| 826 | - | type BrokerService interface { | |
| 826 | + | type BrokerSession interface { | |
| 827 | 827 | // ConnectionID returns the ID assigned when this client connection was | |
| 828 | 828 | // accepted. Clients carry it on stream operations to fence stale requests. | |
| 829 | 829 | ConnectionID() BrokerConnectionID | |
| @@ -844,7 +844,6 @@ type BrokerService interface { | |||
| 844 | 844 | Snapshot() BrokerSnapshot | |
| 845 | 845 | Subscribe() (BrokerSubscription, error) | |
| 846 | 846 | SubscribePreview(request BrokerPreviewRequest) (BrokerPreviewSubscription, error) | |
| 847 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) | |
| 848 | 847 | CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error | |
| 849 | 848 | // AddHost durably adds or pins endpoint under policy and returns its authority. | |
| 850 | 849 | AddHost(ctx context.Context, endpoint string, policy BrokerPolicy) (domain.RemoteRegistration, error) | |
| @@ -856,6 +855,32 @@ type BrokerService interface { | |||
| 856 | 855 | 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 | 884 | // BrokerSnapshotStore persists the durable broker snapshot independently | |
| 860 | 885 | // from session state. The broker is its sole writer; session persistence | |
| 861 | 886 | // stays daemon-owned and is never moved here. Load is called once while the | |
| @@ -910,7 +935,7 @@ type BrokerPhysicalConnection interface { | |||
| 910 | 935 | Identity() BrokerDaemonIdentity | |
| 911 | 936 | Incarnation() BrokerDaemonIncarnation | |
| 912 | 937 | Policy() BrokerPolicy | |
| 913 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) | |
| 938 | + | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error) | |
| 914 | 939 | Close() error | |
| 915 | 940 | } | |
| 916 | 941 | ||
| @@ -961,7 +986,7 @@ type BrokerConnector interface { | |||
| 961 | 986 | // parked in admission immediately. The admitted service gets its own | |
| 962 | 987 | // connection-lived context and must not derive from this one. | |
| 963 | 988 | type BrokerAuthority interface { | |
| 964 | - | AdmitClient(ctx context.Context) (BrokerService, error) | |
| 989 | + | AdmitClient(ctx context.Context) (BrokerCoreService, error) | |
| 965 | 990 | } | |
| 966 | 991 | ||
| 967 | 992 | // BrokerListener accepts client connections to the per-user broker | |
| @@ -972,7 +997,7 @@ type BrokerAuthority interface { | |||
| 972 | 997 | // Implementations own the owner-only directory/socket, peer credential | |
| 973 | 998 | // checks, bounded clients and queues, and cancellation-safe accept loops. | |
| 974 | 999 | type BrokerListener interface { | |
| 975 | - | Accept() (BrokerService, error) | |
| 1000 | + | Accept() (BrokerCoreService, error) | |
| 976 | 1001 | Close() error | |
| 977 | 1002 | Addr() string | |
| 978 | 1003 | } | |
| @@ -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-blocking | 824 | // 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 was | 827 | // 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() BrokerConnectionID | 829 | ConnectionID() BrokerConnectionID | ||
| @@ -844,7 +844,6 @@ type BrokerService interface { | |||||
| 844 | Snapshot() BrokerSnapshot | 844 | 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) error | 847 | 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() error | 855 | 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 independently | 884 | // BrokerSnapshotStore persists the durable broker snapshot independently | ||
| 860 | // from session state. The broker is its sole writer; session persistence | 885 | // 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 the | 886 | // stays daemon-owned and is never moved here. Load is called once while the | ||
| @@ -910,7 +935,7 @@ type BrokerPhysicalConnection interface { | |||||
| 910 | Identity() BrokerDaemonIdentity | 935 | Identity() BrokerDaemonIdentity | ||
| 911 | Incarnation() BrokerDaemonIncarnation | 936 | Incarnation() BrokerDaemonIncarnation | ||
| 912 | Policy() BrokerPolicy | 937 | Policy() BrokerPolicy | ||
| 913 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) | 938 | + | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerEnvelopeStream, error) |
| 914 | Close() error | 939 | Close() error | ||
| 915 | } | 940 | } | ||
| 916 | 941 | ||||
| @@ -961,7 +986,7 @@ type BrokerConnector interface { | |||||
| 961 | // parked in admission immediately. The admitted service gets its own | 986 | // 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 broker | 992 | // 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 credential | 997 | // 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() error | 1001 | Close() error | ||
| 977 | Addr() string | 1002 | 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 | |||
| 160 | 160 | 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 | 164 | if req.Epoch != p.epoch { | |
| 165 | 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 | 324 | _ = raw.Close() | |
| 325 | 325 | 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 | 328 | success = true | |
| 329 | 329 | go func() { | |
| 330 | 330 | var terminal error | |
| @@ -610,7 +610,7 @@ func (p *Pool) Close() error { | |||
| 610 | 610 | } | |
| 611 | 611 | ||
| 612 | 612 | type pooledStream struct { | |
| 613 | - | ports.BrokerLogicalConnection | |
| 613 | + | ports.BrokerEnvelopeStream | |
| 614 | 614 | once sync.Once | |
| 615 | 615 | done chan struct{} | |
| 616 | 616 | err error | |
| @@ -629,7 +629,7 @@ func (s *pooledStream) Err() error { | |||
| 629 | 629 | } | |
| 630 | 630 | func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr } | |
| 631 | 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 | 635 | // 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 nil | 160 | 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, terminal | 325 | 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 = true | 328 | success = true | ||
| 329 | go func() { | 329 | go func() { | ||
| 330 | var terminal error | 330 | 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.BrokerLogicalConnection | 613 | + | ports.BrokerEnvelopeStream |
| 614 | once sync.Once | 614 | once sync.Once | ||
| 615 | done chan struct{} | 615 | done chan struct{} | ||
| 616 | err error | 616 | 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) { | |||
| 28 | 28 | if stage == "connector" { | |
| 29 | 29 | 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 | 32 | return nil, tc.err | |
| 33 | 33 | }}, nil | |
| 34 | 34 | }) | |
| @@ -52,7 +52,7 @@ func TestPoolRejectedPaths(t *testing.T) { | |||
| 52 | 52 | for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} { | |
| 53 | 53 | t.Run(name, func(t *testing.T) { | |
| 54 | 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 | 56 | return nil, nil | |
| 57 | 57 | }}, nil | |
| 58 | 58 | }) | |
| @@ -125,7 +125,7 @@ func TestPoolPostOpenPhysicalLoss(t *testing.T) { | |||
| 125 | 125 | raw := newFakeLogical() | |
| 126 | 126 | p, _ := setupPool(t, func(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) { | |
| 127 | 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 | 129 | _ = f.Close() | |
| 130 | 130 | return raw, nil | |
| 131 | 131 | } | |
| @@ -209,7 +209,7 @@ func (l *closeErrorLogical) Close() error { _ = l.fakeLogical.Close(); return l. | |||
| 209 | 209 | func TestPooledStreamPreservesCloseError(t *testing.T) { | |
| 210 | 210 | cause := errors.New("close failed") | |
| 211 | 211 | 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 | 213 | require.ErrorIs(t, s.Close(), cause) | |
| 214 | 214 | require.ErrorIs(t, s.Close(), cause) | |
| 215 | 215 | 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.err | 29 | 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.err | 32 | return nil, tc.err | ||
| 33 | }}, nil | 33 | }}, 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, nil | 56 | return nil, nil | ||
| 57 | }}, nil | 57 | }}, 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, nil | 130 | 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 := 0 | 211 | 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 | |||
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | type fakeLogical struct { | |
| 39 | - | ports.ClientConnection | |
| 40 | 39 | done chan struct{} | |
| 41 | 40 | once sync.Once | |
| 42 | 41 | } | |
| 43 | 42 | ||
| 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") } | |
| 49 | 49 | ||
| 50 | 50 | type fakePhysical struct { | |
| 51 | 51 | endpoint ports.BrokerDialTarget | |
| @@ -54,7 +54,7 @@ type fakePhysical struct { | |||
| 54 | 54 | identity ports.BrokerDaemonIdentity | |
| 55 | 55 | done chan struct{} | |
| 56 | 56 | 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 | 60 | func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity { | |
| @@ -71,7 +71,7 @@ func (f *fakePhysical) FailureKind() domain.RemoteFailureKind { return domain.Re | |||
| 71 | 71 | func (f *fakePhysical) Done() <-chan struct{} { return f.done } | |
| 72 | 72 | func (f *fakePhysical) Err() error { return errors.New("physical loss") } | |
| 73 | 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 | 75 | if f.open != nil { | |
| 76 | 76 | return f.open(c, r) | |
| 77 | 77 | } | |
| @@ -140,7 +140,7 @@ func TestPoolOpenFailureWithTypedNilConnection(t *testing.T) { | |||
| 140 | 140 | return &fakePhysical{ | |
| 141 | 141 | endpoint: e, | |
| 142 | 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 | 144 | var absent *fakeLogical | |
| 145 | 145 | return absent, errors.New("open failed") | |
| 146 | 146 | }, | |
| @@ -164,7 +164,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) { | |||
| 164 | 164 | physical = &fakePhysical{endpoint: e, done: make(chan struct{})} | |
| 165 | 165 | return physical, nil | |
| 166 | 166 | }) | |
| 167 | - | streams := make([]ports.BrokerLogicalConnection, 100) | |
| 167 | + | streams := make([]ports.BrokerEnvelopeStream, 100) | |
| 168 | 168 | var wg sync.WaitGroup | |
| 169 | 169 | for i := range streams { | |
| 170 | 170 | id, err := p.RegisterClient() | |
| @@ -183,7 +183,7 @@ func TestPoolHundredStreamsLossAndIsolation(t *testing.T) { | |||
| 183 | 183 | wg.Wait() | |
| 184 | 184 | require.EqualValues(t, 1, calls.Load()) | |
| 185 | 185 | blocked := make(chan struct{}) | |
| 186 | - | go func() { _ = streams[0].SendClient(nil); close(blocked) }() | |
| 186 | + | go func() { _ = streams[0].SendEnvelope(nil); close(blocked) }() | |
| 187 | 187 | require.NoError(t, streams[0].Close()) | |
| 188 | 188 | await(t, blocked) | |
| 189 | 189 | select { | |
| @@ -336,7 +336,7 @@ func TestPoolCoalescedCancellation(t *testing.T) { | |||
| 336 | 336 | result := make(chan error, 1) | |
| 337 | 337 | go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }() | |
| 338 | 338 | await(t, entered) | |
| 339 | - | result2 := make(chan ports.BrokerLogicalConnection, 1) | |
| 339 | + | result2 := make(chan ports.BrokerEnvelopeStream, 1) | |
| 340 | 340 | go func() { | |
| 341 | 341 | s, err := p.OpenStream(context.Background(), poolRequest(id2, 1)) | |
| 342 | 342 | if err != nil { | |
| @@ -475,7 +475,7 @@ func TestPoolAliasesAndPolicyPartition(t *testing.T) { | |||
| 475 | 475 | func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) { | |
| 476 | 476 | entered := make(chan struct{}) | |
| 477 | 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 | 479 | if r.Stream == 1 { | |
| 480 | 480 | close(entered) | |
| 481 | 481 | <-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.Once | 40 | 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.BrokerDialTarget | 51 | endpoint ports.BrokerDialTarget | ||
| @@ -54,7 +54,7 @@ type fakePhysical struct { | |||||
| 54 | identity ports.BrokerDaemonIdentity | 54 | identity ports.BrokerDaemonIdentity | ||
| 55 | done chan struct{} | 55 | done chan struct{} | ||
| 56 | once sync.Once | 56 | 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 *fakeLogical | 144 | 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, nil | 165 | return physical, nil | ||
| 166 | }) | 166 | }) | ||
| 167 | - | streams := make([]ports.BrokerLogicalConnection, 100) | 167 | + | streams := make([]ports.BrokerEnvelopeStream, 100) |
| 168 | var wg sync.WaitGroup | 168 | 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 ( | |||
| 12 | 12 | // Offline broker composition. | |
| 13 | 13 | // | |
| 14 | 14 | // 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 | |
| 16 | 16 | // connection. It is the structural implementation of ports.BrokerAuthority, | |
| 17 | 17 | // the admission seam the broker IPC listener (internal/adapters/brokeripc) | |
| 18 | 18 | // consumes and which this use case must not import: admission obtains one | |
| @@ -40,6 +40,7 @@ type Authority struct { | |||
| 40 | 40 | pool *Pool | |
| 41 | 41 | supervisor *Supervisor | |
| 42 | 42 | clock ports.Clock | |
| 43 | + | codec ports.SessionCodec | |
| 43 | 44 | } | |
| 44 | 45 | ||
| 45 | 46 | var _ ports.BrokerAuthority = (*Authority)(nil) | |
| @@ -53,7 +54,7 @@ var _ ports.BrokerAuthority = (*Authority)(nil) | |||
| 53 | 54 | // neither starts nor settles Run: serving connections without a live Run leaks | |
| 54 | 55 | // the writer and never flushes the newest staged publication, and a registry | |
| 55 | 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 | 58 | if epoch == 0 || nilDependency(registry) || nilDependency(pool) || nilDependency(supervisor) { | |
| 58 | 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 | 65 | if len(clocks) > 0 { | |
| 65 | 66 | clk = clocks[0] | |
| 66 | 67 | } | |
| 67 | - | if nilDependency(clk) { | |
| 68 | + | if nilDependency(clk) || nilDependency(codec) { | |
| 68 | 69 | return nil, errors.New("broker: invalid authority clock") | |
| 69 | 70 | } | |
| 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 | |
| 71 | 72 | } | |
| 72 | 73 | ||
| 73 | 74 | // AdmitClient admits exactly one accepted client connection. The supervisor | |
| @@ -76,7 +77,7 @@ func NewAuthority(epoch ports.BrokerEpoch, registry *Registry, pool *Pool, super | |||
| 76 | 77 | // commits both or neither. ctx bounds admission only and is never retained: the | |
| 77 | 78 | // returned service derives its own connection-lived context from the supervisor | |
| 78 | 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 | 81 | if err := ctx.Err(); err != nil { | |
| 81 | 82 | return nil, err | |
| 82 | 83 | } | |
| @@ -103,12 +104,12 @@ func (a *Authority) AdmitClient(ctx context.Context) (ports.BrokerService, error | |||
| 103 | 104 | } | |
| 104 | 105 | return newService(serviceConfig{ | |
| 105 | 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 | 108 | }), nil | |
| 108 | 109 | } | |
| 109 | 110 | ||
| 110 | 111 | // 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 | 113 | type Service struct { | |
| 113 | 114 | epoch ports.BrokerEpoch | |
| 114 | 115 | id ports.BrokerConnectionID | |
| @@ -118,6 +119,7 @@ type Service struct { | |||
| 118 | 119 | supervisor *Supervisor | |
| 119 | 120 | lease *Lease | |
| 120 | 121 | clock ports.Clock | |
| 122 | + | codec ports.SessionCodec | |
| 121 | 123 | ||
| 122 | 124 | // ctx owns everything this connection started: in-flight setup opens and | |
| 123 | 125 | // live streams. It is derived from the supervisor root, never from the | |
| @@ -154,7 +156,7 @@ type Service struct { | |||
| 154 | 156 | termErr error | |
| 155 | 157 | } | |
| 156 | 158 | ||
| 157 | - | var _ ports.BrokerService = (*Service)(nil) | |
| 159 | + | var _ ports.BrokerCoreService = (*Service)(nil) | |
| 158 | 160 | ||
| 159 | 161 | type serviceConfig struct { | |
| 160 | 162 | epoch ports.BrokerEpoch | |
| @@ -164,11 +166,12 @@ type serviceConfig struct { | |||
| 164 | 166 | supervisor *Supervisor | |
| 165 | 167 | lease *Lease | |
| 166 | 168 | clock ports.Clock | |
| 169 | + | codec ports.SessionCodec | |
| 167 | 170 | } | |
| 168 | 171 | ||
| 169 | 172 | func (c serviceConfig) validate() error { | |
| 170 | 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 | 175 | return errors.New("broker: invalid service dependencies") | |
| 173 | 176 | } | |
| 174 | 177 | return nil | |
| @@ -180,7 +183,7 @@ func newService(c serviceConfig) *Service { | |||
| 180 | 183 | } | |
| 181 | 184 | ctx, cancel := context.WithCancel(c.supervisor.RootContext()) | |
| 182 | 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 | 187 | ctx: ctx, cancel: cancel, subs: make(map[*serviceSubscription]struct{}), | |
| 185 | 188 | done: make(chan struct{}), | |
| 186 | 189 | } | |
| @@ -284,7 +287,7 @@ func (s *Service) subscribe(demand bool) (ports.BrokerSubscription, error) { | |||
| 284 | 287 | // the pool sees it, and one operation lease pins the broker only while setup is | |
| 285 | 288 | // in flight: a stream that opens successfully outlives its setup lease, and | |
| 286 | 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 | 291 | if err := s.scope(&request); err != nil { | |
| 289 | 292 | return nil, err | |
| 290 | 293 | } | |
| @@ -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 and | 14 | // 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 |
| 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 one | 18 | // consumes and which this use case must not import: admission obtains one | ||
| @@ -40,6 +40,7 @@ type Authority struct { | |||||
| 40 | pool *Pool | 40 | pool *Pool | ||
| 41 | supervisor *Supervisor | 41 | supervisor *Supervisor | ||
| 42 | clock ports.Clock | 42 | 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 leaks | 54 | // neither starts nor settles Run: serving connections without a live Run leaks | ||
| 54 | // the writer and never flushes the newest staged publication, and a registry | 55 | // 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}, nil | 71 | + | 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 supervisor | 74 | // 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: the | 77 | // commits both or neither. ctx bounds admission only and is never retained: the | ||
| 77 | // returned service derives its own connection-lived context from the supervisor | 78 | // 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, err | 82 | 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 | }), nil | 108 | }), nil | ||
| 108 | } | 109 | } | ||
| 109 | 110 | ||||
| 110 | // Service is one admitted client connection: the core's service for exactly one | 111 | // 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.BrokerEpoch | 114 | epoch ports.BrokerEpoch | ||
| 114 | id ports.BrokerConnectionID | 115 | id ports.BrokerConnectionID | ||
| @@ -118,6 +119,7 @@ type Service struct { | |||||
| 118 | supervisor *Supervisor | 119 | supervisor *Supervisor | ||
| 119 | lease *Lease | 120 | lease *Lease | ||
| 120 | clock ports.Clock | 121 | clock ports.Clock | ||
| 122 | + | codec ports.SessionCodec | |||
| 121 | 123 | ||||
| 122 | // ctx owns everything this connection started: in-flight setup opens and | 124 | // ctx owns everything this connection started: in-flight setup opens and | ||
| 123 | // live streams. It is derived from the supervisor root, never from the | 125 | // live streams. It is derived from the supervisor root, never from the | ||
| @@ -154,7 +156,7 @@ type Service struct { | |||||
| 154 | termErr error | 156 | 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.BrokerEpoch | 162 | epoch ports.BrokerEpoch | ||
| @@ -164,11 +166,12 @@ type serviceConfig struct { | |||||
| 164 | supervisor *Supervisor | 166 | supervisor *Supervisor | ||
| 165 | lease *Lease | 167 | lease *Lease | ||
| 166 | clock ports.Clock | 168 | 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 nil | 177 | 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 is | 287 | // 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, and | 288 | // 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, err | 292 | 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) { | |||
| 199 | 199 | interval = previewLocalInterval | |
| 200 | 200 | } | |
| 201 | 201 | route, err := p.service.previewStreamRequest(p.request.Route) | |
| 202 | - | var stream ports.BrokerLogicalConnection | |
| 202 | + | var stream ports.BrokerEnvelopeStream | |
| 203 | 203 | if err == nil { | |
| 204 | 204 | stream, err = p.service.pool.OpenStream(p.ctx, route) | |
| 205 | 205 | } | |
| @@ -209,7 +209,8 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) { | |||
| 209 | 209 | defer func() { _ = stream.Close() }() | |
| 210 | 210 | stop := context.AfterFunc(p.ctx, func() { _ = stream.Close() }) | |
| 211 | 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 | 214 | return p.fail(err), false | |
| 214 | 215 | } | |
| 215 | 216 | ||
| @@ -232,7 +233,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) { | |||
| 232 | 233 | defer func() { markFirst(); <-deadlineDone }() | |
| 233 | 234 | ||
| 234 | 235 | for { | |
| 235 | - | message, err := stream.ReceiveServer() | |
| 236 | + | message, err := typed.ReceiveServer() | |
| 236 | 237 | if err != nil { | |
| 237 | 238 | return p.fail(err), delivered | |
| 238 | 239 | } | |
| @@ -199,7 +199,7 @@ func (p *servicePreviewSubscription) watch() (reopen, delivered bool) { | |||||
| 199 | interval = previewLocalInterval | 199 | 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.BrokerLogicalConnection | 202 | + | 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), false | 214 | 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), delivered | 238 | 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 { | |||
| 58 | 58 | } | |
| 59 | 59 | func (c *previewWatchConn) LinkState() ports.LinkState { return ports.LinkStateConnected } | |
| 60 | 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 | 63 | func (c *previewWatchConn) Done() <-chan struct{} { return c.done } | |
| 62 | 64 | func (c *previewWatchConn) Err() error { return nil } | |
| 63 | 65 | func (c *previewWatchConn) Close() error { | |
| @@ -80,7 +82,7 @@ type previewTestConnector struct { | |||
| 80 | 82 | ||
| 81 | 83 | func (c *previewTestConnector) Connect(_ context.Context, e ports.BrokerDialTarget) (ports.BrokerPhysicalConnection, error) { | |
| 82 | 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 | 86 | conn := newPreviewWatchConn(r.Stream) | |
| 85 | 87 | c.opened <- conn | |
| 86 | 88 | 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 | 102 | return ports.BrokerPreviewRequest{ | |
| 101 | 103 | Epoch: 1, Connection: service.ConnectionID(), Generation: generation, | |
| 102 | 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 | 113 | type previewFixture struct { | |
| 112 | 114 | t *testing.T | |
| 113 | - | service ports.BrokerService | |
| 115 | + | service ports.BrokerCoreService | |
| 114 | 116 | sub ports.BrokerPreviewSubscription | |
| 115 | 117 | connector *previewTestConnector | |
| 116 | 118 | clock *manualClock | |
| @@ -125,6 +127,7 @@ func newPreviewFixture(t *testing.T, target domain.RemoteSessionTarget) *preview | |||
| 125 | 127 | registry, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true}) | |
| 126 | 128 | require.NoError(t, err) | |
| 127 | 129 | authority, _, _, _ := composeTestAuthority(t, 1, registry, connector.Connect, clock) | |
| 130 | + | authority.codec = testSessionCodec{} | |
| 128 | 131 | service, err := authority.AdmitClient(context.Background()) | |
| 129 | 132 | require.NoError(t, err) | |
| 130 | 133 | t.Cleanup(func() { require.NoError(t, service.Close()) }) | |
| @@ -197,7 +200,7 @@ func TestServicePreviewStreamIDsDoNotConsumeClientIDs(t *testing.T) { | |||
| 197 | 200 | id, err := service.NextStreamID() | |
| 198 | 201 | require.NoError(t, err) | |
| 199 | 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 | 204 | require.NoError(t, err) | |
| 202 | 205 | t.Cleanup(func() { require.NoError(t, stream.Close()) }) | |
| 203 | 206 | } | |
| @@ -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 <- conn | 87 | c.opened <- conn | ||
| 86 | return conn, nil | 88 | 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.T | 114 | t *testing.T | ||
| 113 | - | service ports.BrokerService | 115 | + | service ports.BrokerCoreService |
| 114 | sub ports.BrokerPreviewSubscription | 116 | sub ports.BrokerPreviewSubscription | ||
| 115 | connector *previewTestConnector | 117 | connector *previewTestConnector | ||
| 116 | clock *manualClock | 118 | 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 | |||
| 43 | 43 | require.NoError(t, err) | |
| 44 | 44 | t.Cleanup(func() { require.NoError(t, pool.Close()) }) | |
| 45 | 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 | 47 | require.NoError(t, err) | |
| 48 | 48 | // The caller owns Registry.Run (see NewAuthority): start the single run and | |
| 49 | 49 | // cancel it at cleanup, so the registry's durable writer is drained and its | |
| @@ -195,7 +195,7 @@ func TestAuthorityAdmissionContextNotRetained(t *testing.T) { | |||
| 195 | 195 | require.NoError(t, registry.setHosts(hostRecords(registration(t, "user@host:22", 1)))) | |
| 196 | 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 | 199 | require.NoError(t, err) | |
| 200 | 200 | require.NoError(t, stream.Close()) | |
| 201 | 201 | } | |
| @@ -210,7 +210,7 @@ func TestAuthorityValidation(t *testing.T) { | |||
| 210 | 210 | require.NoError(t, err) | |
| 211 | 211 | t.Cleanup(mutableRemote.settle) | |
| 212 | 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 | 214 | require.NoError(t, err, "a mutable remote registry must be served") | |
| 215 | 215 | ||
| 216 | 216 | // A local-only observer is served: its entry is configured authority rather | |
| @@ -223,7 +223,7 @@ func TestAuthorityValidation(t *testing.T) { | |||
| 223 | 223 | }}) | |
| 224 | 224 | require.NoError(t, err) | |
| 225 | 225 | t.Cleanup(localOnly.settle) | |
| 226 | - | _, err = NewAuthority(1, localOnly, pool, supervisor) | |
| 226 | + | _, err = NewAuthority(1, localOnly, pool, supervisor, testSessionCodec{}) | |
| 227 | 227 | require.NoError(t, err, "a local-only observer must be served") | |
| 228 | 228 | ||
| 229 | 229 | // Construction still rejects remote observation without a probe: a registry | |
| @@ -235,17 +235,17 @@ func TestAuthorityValidation(t *testing.T) { | |||
| 235 | 235 | disabled, err := NewRegistryWithConfig(1, newTestStore(), nil, clock, nil, RegistryConfig{ObservationDisabled: true}) | |
| 236 | 236 | require.NoError(t, err) | |
| 237 | 237 | t.Cleanup(disabled.settle) | |
| 238 | - | _, err = NewAuthority(2, disabled, pool, supervisor) | |
| 238 | + | _, err = NewAuthority(2, disabled, pool, supervisor, testSessionCodec{}) | |
| 239 | 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 | 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 | 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 | 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 | 249 | require.NoError(t, err) | |
| 250 | 250 | } | |
| 251 | 251 | ||
| @@ -289,7 +289,7 @@ func TestServiceOpenStreamScopeFencing(t *testing.T) { | |||
| 289 | 289 | require.NoError(t, err) | |
| 290 | 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 | 293 | switch { | |
| 294 | 294 | case tc.wantStale: | |
| 295 | 295 | require.ErrorIs(t, err, ports.BrokerAdmissionStale) | |
| @@ -344,7 +344,7 @@ func TestServiceOpenStreamRequestsLocalReprobeOnOpenAndClose(t *testing.T) { | |||
| 344 | 344 | waitLocal(t, registry, func(o ports.BrokerDaemonObservation) bool { return !o.Checking }) | |
| 345 | 345 | ||
| 346 | 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 | 348 | require.NoError(t, err) | |
| 349 | 349 | opened := receiveLocalCall(t, local) | |
| 350 | 350 | opened.result <- reachable() | |
| @@ -421,11 +421,11 @@ func TestServiceNextStreamIDAllocation(t *testing.T) { | |||
| 421 | 421 | require.NoError(t, err) | |
| 422 | 422 | request := poolRequest(service.ConnectionID(), 0) | |
| 423 | 423 | request.Stream = stream | |
| 424 | - | opened, err := service.OpenStream(context.Background(), request) | |
| 424 | + | opened, err := service.OpenEnvelopeStream(context.Background(), request) | |
| 425 | 425 | require.NoError(t, err) | |
| 426 | 426 | require.NoError(t, opened.Close()) | |
| 427 | 427 | // Replaying the exact identity is refused as stale. | |
| 428 | - | _, err = service.OpenStream(context.Background(), request) | |
| 428 | + | _, err = service.OpenEnvelopeStream(context.Background(), request) | |
| 429 | 429 | require.ErrorIs(t, err, ports.BrokerAdmissionStale) | |
| 430 | 430 | }) | |
| 431 | 431 | } | |
| @@ -448,7 +448,7 @@ func TestServiceOpenStreamAdmissionValidatesCallerInput(t *testing.T) { | |||
| 448 | 448 | t.Run(tc.name, func(t *testing.T) { | |
| 449 | 449 | request := poolRequest(service.ConnectionID(), 1) | |
| 450 | 450 | tc.mutate(&request) | |
| 451 | - | _, err := service.OpenStream(context.Background(), request) | |
| 451 | + | _, err := service.OpenEnvelopeStream(context.Background(), request) | |
| 452 | 452 | require.ErrorIs(t, err, ports.BrokerAdmissionInvalid) | |
| 453 | 453 | }) | |
| 454 | 454 | } | |
| @@ -461,7 +461,7 @@ func TestServiceCloseStreamScopeFencing(t *testing.T) { | |||
| 461 | 461 | t.Cleanup(func() { require.NoError(t, service.Close()) }) | |
| 462 | 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 | 465 | require.NoError(t, err) | |
| 466 | 466 | ||
| 467 | 467 | // A wrong-scope close must not retire this connection's stream. | |
| @@ -610,12 +610,12 @@ func TestServiceOperationLeaseLifetime(t *testing.T) { | |||
| 610 | 610 | t.Cleanup(func() { require.NoError(t, service.Close()) }) | |
| 611 | 611 | ||
| 612 | 612 | type opened struct { | |
| 613 | - | stream ports.BrokerLogicalConnection | |
| 613 | + | stream ports.BrokerEnvelopeStream | |
| 614 | 614 | err error | |
| 615 | 615 | } | |
| 616 | 616 | result := make(chan opened, 1) | |
| 617 | 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 | 619 | result <- opened{stream: stream, err: err} | |
| 620 | 620 | }() | |
| 621 | 621 | <-entered | |
| @@ -656,7 +656,7 @@ func TestServiceCloseOpenRace(t *testing.T) { | |||
| 656 | 656 | ||
| 657 | 657 | result := make(chan error, 1) | |
| 658 | 658 | go func() { | |
| 659 | - | _, err := service.OpenStream(context.Background(), poolRequest(service.ConnectionID(), 1)) | |
| 659 | + | _, err := service.OpenEnvelopeStream(context.Background(), poolRequest(service.ConnectionID(), 1)) | |
| 660 | 660 | result <- err | |
| 661 | 661 | }() | |
| 662 | 662 | <-entered | |
| @@ -683,7 +683,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) { | |||
| 683 | 683 | require.NoError(t, err) | |
| 684 | 684 | _, err = service.Subscribe() | |
| 685 | 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 | 687 | require.NoError(t, err) | |
| 688 | 688 | ||
| 689 | 689 | const closers = 8 | |
| @@ -708,7 +708,7 @@ func TestServiceCloseIsIdempotentAndConcurrent(t *testing.T) { | |||
| 708 | 708 | require.Zero(t, clients) | |
| 709 | 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 | 712 | requireBrokerClosed(t, err) | |
| 713 | 713 | } | |
| 714 | 714 | ||
| @@ -747,7 +747,7 @@ func TestServiceShutdownTerminatesOwnedWork(t *testing.T) { | |||
| 747 | 747 | service, err := authority.AdmitClient(context.Background()) | |
| 748 | 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 | 751 | require.NoError(t, err) | |
| 752 | 752 | ||
| 753 | 753 | // 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 | 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 | 1160 | tests := []struct { | |
| 1161 | 1161 | name string | |
| 1162 | 1162 | mutate func(*serviceConfig) | |
| @@ -1170,6 +1170,7 @@ func TestServiceConfigValidation(t *testing.T) { | |||
| 1170 | 1170 | {"nil supervisor", func(c *serviceConfig) { c.supervisor = nil }}, | |
| 1171 | 1171 | {"nil lease", func(c *serviceConfig) { c.lease = nil }}, | |
| 1172 | 1172 | {"nil clock", func(c *serviceConfig) { c.clock = nil }}, | |
| 1173 | + | {"nil codec", func(c *serviceConfig) { c.codec = nil }}, | |
| 1173 | 1174 | } | |
| 1174 | 1175 | require.NoError(t, valid.validate()) | |
| 1175 | 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 | + | } | |
| @@ -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 and | 48 | // 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 its | 49 | // 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 rather | 216 | // 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 registry | 229 | // 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 = stream | 423 | 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.BrokerLogicalConnection | 613 | + | stream ports.BrokerEnvelopeStream |
| 614 | err error | 614 | 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 | <-entered | 621 | <-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 <- err | 660 | result <- err | ||
| 661 | }() | 661 | }() | ||
| 662 | <-entered | 662 | <-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 = 8 | 689 | 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 before | 753 | // 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 string | 1161 | 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 | |||
| 65 | 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 | 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 | 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 | 114 | if err := ctx.Err(); err != nil { | |
| 115 | 115 | return nil, err | |
| 116 | 116 | } | |
| @@ -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, err | 115 | return nil, err | ||
| 116 | } | 116 | } | ||