detached a3b975e

feat(broker): add bounded connection pooling

commit
a3b975e6903dc579d16950d15b3dae9abbffb01c
author
brice · · 2026-09-16 10:20 UTC
parent
ad3de98
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +2086 -59
M docs/architecture.md +48 -0
@@ -102,3 +102,51 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
102102 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.
103103 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
104104 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `51`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
105+
106+## Broker pool core (Plan 001 P2.2, not activated)
107+
108+`internal/usecase/broker.Pool` owns bounded physical keys and logical stream
109+reservations, but no session data or traffic queues. Local and registered remote
110+requests use the same operation. A broker-owned `BrokerEndpointResolver` checks
111+current registration authority and exact requested policy on every request and
112+returns an authenticated service binding. Display aliases and adapter addresses
113+are not keys. Identity plus complete trust, launch, isolation, transport,
114+protocol, catalogue, and environment policy select one physical connection;
115+authenticated identity and policy are checked again after connection.
116+
117+Clients receive pool-issued epoch-scoped connection IDs and supply strictly
118+increasing stream IDs. Failed admitted opens consume their IDs. Active clients,
119+physical keys, and pending/live streams have explicit caps; retired IDs need no
120+unbounded tombstone collection. Coalesced connection attempts are independent
121+of any single waiter and are canceled when their final reservation leaves.
122+Physical retirement keeps its key occupied until Close finishes. Fake-clock idle
123+eviction and shutdown close transports outside the bookkeeping lock.
124+
125+Logical and physical ports expose terminal signals independent of message reads.
126+A stream terminal failure releases only its reservation; physical failure fans
127+out separately scoped typed loss errors. The pool owns neither framing nor
128+fairness: future transport adapters must implement independently bounded queues
129+and prompt cancellation/Close. Shutdown joins owned operations and relies on
130+that explicit port contract. No production composition or real multiplexing is
131+introduced here, and existing production connectivity ownership is unchanged.
132+
133+Physical `Done` is authoritative: adapters publish stable `Err` and
134+`FailureKind`, then close physical `Done` before terminating affected logical
135+streams. Loss carries the exact epoch/connection/stream and a nonzero failure
136+kind (invalid/absent adapter kinds defensively become transport failure), with
137+its diagnostic cause retained locally through `errors.Is/As`. Resolver,
138+connector, and open errors map deterministically: deadline before cancellation,
139+then a validated broker code, otherwise unavailable. Invalid resolved bindings
140+and nil successful opens are incompatible adapter results. Adapter diagnostics
141+are never copied to display text. Resolved addresses are bounded opaque tokens,
142+not display labels or pooling identities.
143+
144+At terminal observation the precedence is pool shutdown, request/client
145+cancellation, physical loss, entry retirement, then logical completion. Thus
146+shutdown reports cancelled rather than a synthetic transport loss caused by
147+local Close; a physical loss already published to a stream is not rewritten by
148+later shutdown. Explicit logical Close remains idempotent and preserves the
149+underlying Close error. Timer ownership is single-goroutine; Stop followed by a
150+nonblocking drain on false precedes Reset, supporting buffered timer adapters.
151+Policy identities have independent field-labelled validation and a named bound.
152+These are P2.2 port contracts, not a wire migration or production activation.
@@ -102,3 +102,51 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
102 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.102 Never edit generated `*.pb.go`; never add manual IDs or dispatch tables.
103 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.103 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
104 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `51`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.104 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `51`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
105+
106+## Broker pool core (Plan 001 P2.2, not activated)
107+
108+`internal/usecase/broker.Pool` owns bounded physical keys and logical stream
109+reservations, but no session data or traffic queues. Local and registered remote
110+requests use the same operation. A broker-owned `BrokerEndpointResolver` checks
111+current registration authority and exact requested policy on every request and
112+returns an authenticated service binding. Display aliases and adapter addresses
113+are not keys. Identity plus complete trust, launch, isolation, transport,
114+protocol, catalogue, and environment policy select one physical connection;
115+authenticated identity and policy are checked again after connection.
116+
117+Clients receive pool-issued epoch-scoped connection IDs and supply strictly
118+increasing stream IDs. Failed admitted opens consume their IDs. Active clients,
119+physical keys, and pending/live streams have explicit caps; retired IDs need no
120+unbounded tombstone collection. Coalesced connection attempts are independent
121+of any single waiter and are canceled when their final reservation leaves.
122+Physical retirement keeps its key occupied until Close finishes. Fake-clock idle
123+eviction and shutdown close transports outside the bookkeeping lock.
124+
125+Logical and physical ports expose terminal signals independent of message reads.
126+A stream terminal failure releases only its reservation; physical failure fans
127+out separately scoped typed loss errors. The pool owns neither framing nor
128+fairness: future transport adapters must implement independently bounded queues
129+and prompt cancellation/Close. Shutdown joins owned operations and relies on
130+that explicit port contract. No production composition or real multiplexing is
131+introduced here, and existing production connectivity ownership is unchanged.
132+
133+Physical `Done` is authoritative: adapters publish stable `Err` and
134+`FailureKind`, then close physical `Done` before terminating affected logical
135+streams. Loss carries the exact epoch/connection/stream and a nonzero failure
136+kind (invalid/absent adapter kinds defensively become transport failure), with
137+its diagnostic cause retained locally through `errors.Is/As`. Resolver,
138+connector, and open errors map deterministically: deadline before cancellation,
139+then a validated broker code, otherwise unavailable. Invalid resolved bindings
140+and nil successful opens are incompatible adapter results. Adapter diagnostics
141+are never copied to display text. Resolved addresses are bounded opaque tokens,
142+not display labels or pooling identities.
143+
144+At terminal observation the precedence is pool shutdown, request/client
145+cancellation, physical loss, entry retirement, then logical completion. Thus
146+shutdown reports cancelled rather than a synthetic transport loss caused by
147+local Close; a physical loss already published to a stream is not rewritten by
148+later shutdown. Explicit logical Close remains idempotent and preserves the
149+underlying Close error. Timer ownership is single-goroutine; Stop followed by a
150+nonblocking drain on false precedes Reset, supporting buffered timer adapters.
151+Policy identities have independent field-labelled validation and a named bound.
152+These are P2.2 port contracts, not a wire migration or production activation.
M internal/ports/broker.go +64 -17
@@ -65,6 +65,8 @@ const (
6565 BrokerMaxTransportBytes = 64
6666 // BrokerMaxIdentityBytes bounds the authenticated daemon identity token.
6767 BrokerMaxIdentityBytes = 256
68+ // BrokerMaxPolicyTokenBytes bounds each authoritative policy identity.
69+ BrokerMaxPolicyTokenBytes = 256
6870 )
6971
7072 // BrokerEpoch identifies one broker process incarnation. Every broker
@@ -147,9 +149,18 @@ type BrokerPolicy struct {
147149 CatalogSchemaVersion uint16
148150 EnvironmentPolicy protocol.EnvironmentPolicy
149151 Transport string
152+ // Opaque, authoritative policy identities, not display labels or secrets.
153+ Trust string
154+ Launch string
155+ Isolation string
150156 }
151157
152158 func (p BrokerPolicy) Validate() error {
159+ for _, token := range []struct{ value, label string }{{p.Trust, "policy trust"}, {p.Launch, "policy launch"}, {p.Isolation, "policy isolation"}} {
160+ if err := validateBrokerToken(token.value, BrokerMaxPolicyTokenBytes, token.label); err != nil {
161+ return err
162+ }
163+ }
153164 if p.ProtocolVersion == 0 {
154165 return errors.New("ports: broker policy has no protocol version")
155166 }
@@ -168,10 +179,7 @@ func (p BrokerPolicy) Validate() error {
168179 // Compatible reports whether two validated policies may share a pooled
169180 // physical transport. It requires exact equality across every field.
170181 func (p BrokerPolicy) Compatible(other BrokerPolicy) bool {
171- return p.ProtocolVersion == other.ProtocolVersion &&
172- p.CatalogSchemaVersion == other.CatalogSchemaVersion &&
173- p.EnvironmentPolicy == other.EnvironmentPolicy &&
174- p.Transport == other.Transport
182+ return p == other
175183 }
176184
177185 // BrokerHostTombstone fences a removed host registration so a stale probe,
@@ -322,6 +330,9 @@ func BrokerCompletionIsStale(currentRegistration domain.RemoteRegistration, curr
322330 // session environment; it is never inherited from the broker process
323331 // environment.
324332 type BrokerOpenStreamRequest struct {
333+ Epoch BrokerEpoch
334+ Purpose BrokerStreamPurpose
335+ Local bool
325336 Connection BrokerConnectionID
326337 Stream BrokerStreamID
327338 Endpoint string
@@ -332,23 +343,38 @@ type BrokerOpenStreamRequest struct {
332343 }
333344
334345 func (r BrokerOpenStreamRequest) Validate() error {
346+ if r.Epoch == 0 {
347+ return errors.New("ports: missing broker epoch")
348+ }
335349 if err := r.Connection.Validate(); err != nil {
336350 return err
337351 }
338352 if err := r.Stream.Validate(); err != nil {
339353 return err
340354 }
341- if err := domain.ValidateRemoteHostTarget(r.Endpoint); err != nil {
342- return fmt.Errorf("ports: broker open stream: %w", err)
343- }
344- if err := r.Registration.Validate(); err != nil {
345- return fmt.Errorf("ports: broker open stream: %w", err)
346- }
347- if r.Endpoint != r.Registration.Endpoint {
348- return errors.New("ports: broker open stream endpoint does not match registration")
355+ if r.Local {
356+ if r.Endpoint != "" || r.Registration != (domain.RemoteRegistration{}) {
357+ return errors.New("ports: local endpoint carries remote registration")
358+ }
359+ } else {
360+ if err := r.Registration.Validate(); err != nil {
361+ return err
362+ }
363+ if r.Endpoint != r.Registration.Endpoint {
364+ return errors.New("ports: endpoint registration mismatch")
365+ }
349366 }
350- if err := r.Target.Validate(); err != nil {
351- return fmt.Errorf("ports: broker open stream: %w", err)
367+ switch r.Purpose {
368+ case BrokerStreamAttachment:
369+ if err := r.Target.Validate(); err != nil {
370+ return err
371+ }
372+ case BrokerStreamControl, BrokerStreamObservation:
373+ if r.Target != (protocol.ExactSessionTarget{}) || len(r.Env) != 0 {
374+ return errors.New("ports: control/observation carries attachment state")
375+ }
376+ default:
377+ return errors.New("ports: invalid stream purpose")
352378 }
353379 if uint64(len(r.Env)) > BrokerMaxEnvEntries {
354380 return errors.New("ports: broker open stream has too many environment entries")
@@ -492,6 +518,8 @@ func (c BrokerErrorCode) Validate() error {
492518 type BrokerError struct {
493519 Code BrokerErrorCode
494520 Text string
521+ // Cause is local-only diagnostic authority, never serialized as display text.
522+ Cause error
495523 }
496524
497525 func (e BrokerError) Error() string {
@@ -501,6 +529,8 @@ func (e BrokerError) Error() string {
501529 return "vev: broker " + e.Code.String() + ": " + e.Text
502530 }
503531
532+func (e BrokerError) Unwrap() error { return e.Cause }
533+
504534 func (e BrokerError) Validate() error {
505535 if err := e.Code.Validate(); err != nil {
506536 return err
@@ -522,9 +552,19 @@ type BrokerStreamLost struct {
522552 Stream BrokerStreamID
523553 Epoch BrokerEpoch
524554 Cause domain.RemoteFailureKind
555+ Err error
556+}
557+
558+// Error and Unwrap retain exact stream scope while allowing generic lifecycle handling.
559+func (e BrokerStreamLost) Error() string { return "vev: broker attachment_lost" }
560+func (e BrokerStreamLost) Unwrap() error {
561+ return BrokerError{Code: BrokerErrorAttachmentLost, Cause: e.Err}
525562 }
526563
527564 func (e BrokerStreamLost) Validate() error {
565+ if e.Cause < domain.RemoteFailureTransport || e.Cause > domain.RemoteFailureInvalidResponse {
566+ return errors.New("ports: broker stream loss has invalid cause")
567+ }
528568 if err := e.Connection.Validate(); err != nil {
529569 return err
530570 }
@@ -555,7 +595,7 @@ type BrokerService interface {
555595 ConnectionID() BrokerConnectionID
556596 Snapshot() BrokerSnapshot
557597 Subscribe() (BrokerSubscription, error)
558- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
598+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
559599 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
560600 AddHost(ctx context.Context, target string) error
561601 RemoveHost(ctx context.Context, target string) (bool, error)
@@ -602,10 +642,17 @@ type BrokerHostProbe interface {
602642 // state, identity, geometry, input, output, cancellation, or application
603643 // errors between streams.
604644 type BrokerPhysicalConnection interface {
645+ // Done is the physical terminal authority, independent of reads. On
646+ // physical failure, publish Err and FailureKind and close Done BEFORE
647+ // terminating affected logical streams. Both results are stable after Done.
648+ // FailureKind is nonzero for failure; None is allowed only for local Close.
649+ Done() <-chan struct{}
650+ Err() error
651+ FailureKind() domain.RemoteFailureKind
605652 Identity() BrokerDaemonIdentity
606653 Incarnation() BrokerDaemonIncarnation
607654 Policy() BrokerPolicy
608- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
655+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
609656 Close() error
610657 }
611658
@@ -613,7 +660,7 @@ type BrokerPhysicalConnection interface {
613660 // one endpoint under an exact compatible policy. Conflicting policy is
614661 // rejected; the first launching client never becomes policy authority.
615662 type BrokerEndpointConnector interface {
616- Connect(ctx context.Context, endpoint string, policy BrokerPolicy) (BrokerPhysicalConnection, error)
663+ Connect(ctx context.Context, endpoint BrokerResolvedEndpoint) (BrokerPhysicalConnection, error)
617664 }
618665
619666 // BrokerListener accepts client connections to the per-user broker
@@ -65,6 +65,8 @@ const (
65 BrokerMaxTransportBytes = 6465 BrokerMaxTransportBytes = 64
66 // BrokerMaxIdentityBytes bounds the authenticated daemon identity token.66 // BrokerMaxIdentityBytes bounds the authenticated daemon identity token.
67 BrokerMaxIdentityBytes = 25667 BrokerMaxIdentityBytes = 256
68+ // BrokerMaxPolicyTokenBytes bounds each authoritative policy identity.
69+ BrokerMaxPolicyTokenBytes = 256
68 )70 )
69 71
70 // BrokerEpoch identifies one broker process incarnation. Every broker72 // BrokerEpoch identifies one broker process incarnation. Every broker
@@ -147,9 +149,18 @@ type BrokerPolicy struct {
147 CatalogSchemaVersion uint16149 CatalogSchemaVersion uint16
148 EnvironmentPolicy protocol.EnvironmentPolicy150 EnvironmentPolicy protocol.EnvironmentPolicy
149 Transport string151 Transport string
152+ // Opaque, authoritative policy identities, not display labels or secrets.
153+ Trust string
154+ Launch string
155+ Isolation string
150 }156 }
151 157
152 func (p BrokerPolicy) Validate() error {158 func (p BrokerPolicy) Validate() error {
159+ for _, token := range []struct{ value, label string }{{p.Trust, "policy trust"}, {p.Launch, "policy launch"}, {p.Isolation, "policy isolation"}} {
160+ if err := validateBrokerToken(token.value, BrokerMaxPolicyTokenBytes, token.label); err != nil {
161+ return err
162+ }
163+ }
153 if p.ProtocolVersion == 0 {164 if p.ProtocolVersion == 0 {
154 return errors.New("ports: broker policy has no protocol version")165 return errors.New("ports: broker policy has no protocol version")
155 }166 }
@@ -168,10 +179,7 @@ func (p BrokerPolicy) Validate() error {
168 // Compatible reports whether two validated policies may share a pooled179 // Compatible reports whether two validated policies may share a pooled
169 // physical transport. It requires exact equality across every field.180 // physical transport. It requires exact equality across every field.
170 func (p BrokerPolicy) Compatible(other BrokerPolicy) bool {181 func (p BrokerPolicy) Compatible(other BrokerPolicy) bool {
171- return p.ProtocolVersion == other.ProtocolVersion &&182+ return p == other
172- p.CatalogSchemaVersion == other.CatalogSchemaVersion &&
173- p.EnvironmentPolicy == other.EnvironmentPolicy &&
174- p.Transport == other.Transport
175 }183 }
176 184
177 // BrokerHostTombstone fences a removed host registration so a stale probe,185 // BrokerHostTombstone fences a removed host registration so a stale probe,
@@ -322,6 +330,9 @@ func BrokerCompletionIsStale(currentRegistration domain.RemoteRegistration, curr
322 // session environment; it is never inherited from the broker process330 // session environment; it is never inherited from the broker process
323 // environment.331 // environment.
324 type BrokerOpenStreamRequest struct {332 type BrokerOpenStreamRequest struct {
333+ Epoch BrokerEpoch
334+ Purpose BrokerStreamPurpose
335+ Local bool
325 Connection BrokerConnectionID336 Connection BrokerConnectionID
326 Stream BrokerStreamID337 Stream BrokerStreamID
327 Endpoint string338 Endpoint string
@@ -332,23 +343,38 @@ type BrokerOpenStreamRequest struct {
332 }343 }
333 344
334 func (r BrokerOpenStreamRequest) Validate() error {345 func (r BrokerOpenStreamRequest) Validate() error {
346+ if r.Epoch == 0 {
347+ return errors.New("ports: missing broker epoch")
348+ }
335 if err := r.Connection.Validate(); err != nil {349 if err := r.Connection.Validate(); err != nil {
336 return err350 return err
337 }351 }
338 if err := r.Stream.Validate(); err != nil {352 if err := r.Stream.Validate(); err != nil {
339 return err353 return err
340 }354 }
341- if err := domain.ValidateRemoteHostTarget(r.Endpoint); err != nil {355+ if r.Local {
342- return fmt.Errorf("ports: broker open stream: %w", err)356+ if r.Endpoint != "" || r.Registration != (domain.RemoteRegistration{}) {
343- }357+ return errors.New("ports: local endpoint carries remote registration")
344- if err := r.Registration.Validate(); err != nil {358+ }
345- return fmt.Errorf("ports: broker open stream: %w", err)359+ } else {
346- }360+ if err := r.Registration.Validate(); err != nil {
347- if r.Endpoint != r.Registration.Endpoint {361+ return err
348- return errors.New("ports: broker open stream endpoint does not match registration")362+ }
363+ if r.Endpoint != r.Registration.Endpoint {
364+ return errors.New("ports: endpoint registration mismatch")
365+ }
349 }366 }
350- if err := r.Target.Validate(); err != nil {367+ switch r.Purpose {
351- return fmt.Errorf("ports: broker open stream: %w", err)368+ case BrokerStreamAttachment:
369+ if err := r.Target.Validate(); err != nil {
370+ return err
371+ }
372+ case BrokerStreamControl, BrokerStreamObservation:
373+ if r.Target != (protocol.ExactSessionTarget{}) || len(r.Env) != 0 {
374+ return errors.New("ports: control/observation carries attachment state")
375+ }
376+ default:
377+ return errors.New("ports: invalid stream purpose")
352 }378 }
353 if uint64(len(r.Env)) > BrokerMaxEnvEntries {379 if uint64(len(r.Env)) > BrokerMaxEnvEntries {
354 return errors.New("ports: broker open stream has too many environment entries")380 return errors.New("ports: broker open stream has too many environment entries")
@@ -492,6 +518,8 @@ func (c BrokerErrorCode) Validate() error {
492 type BrokerError struct {518 type BrokerError struct {
493 Code BrokerErrorCode519 Code BrokerErrorCode
494 Text string520 Text string
521+ // Cause is local-only diagnostic authority, never serialized as display text.
522+ Cause error
495 }523 }
496 524
497 func (e BrokerError) Error() string {525 func (e BrokerError) Error() string {
@@ -501,6 +529,8 @@ func (e BrokerError) Error() string {
501 return "vev: broker " + e.Code.String() + ": " + e.Text529 return "vev: broker " + e.Code.String() + ": " + e.Text
502 }530 }
503 531
532+func (e BrokerError) Unwrap() error { return e.Cause }
533+
504 func (e BrokerError) Validate() error {534 func (e BrokerError) Validate() error {
505 if err := e.Code.Validate(); err != nil {535 if err := e.Code.Validate(); err != nil {
506 return err536 return err
@@ -522,9 +552,19 @@ type BrokerStreamLost struct {
522 Stream BrokerStreamID552 Stream BrokerStreamID
523 Epoch BrokerEpoch553 Epoch BrokerEpoch
524 Cause domain.RemoteFailureKind554 Cause domain.RemoteFailureKind
555+ Err error
556+}
557+
558+// Error and Unwrap retain exact stream scope while allowing generic lifecycle handling.
559+func (e BrokerStreamLost) Error() string { return "vev: broker attachment_lost" }
560+func (e BrokerStreamLost) Unwrap() error {
561+ return BrokerError{Code: BrokerErrorAttachmentLost, Cause: e.Err}
525 }562 }
526 563
527 func (e BrokerStreamLost) Validate() error {564 func (e BrokerStreamLost) Validate() error {
565+ if e.Cause < domain.RemoteFailureTransport || e.Cause > domain.RemoteFailureInvalidResponse {
566+ return errors.New("ports: broker stream loss has invalid cause")
567+ }
528 if err := e.Connection.Validate(); err != nil {568 if err := e.Connection.Validate(); err != nil {
529 return err569 return err
530 }570 }
@@ -555,7 +595,7 @@ type BrokerService interface {
555 ConnectionID() BrokerConnectionID595 ConnectionID() BrokerConnectionID
556 Snapshot() BrokerSnapshot596 Snapshot() BrokerSnapshot
557 Subscribe() (BrokerSubscription, error)597 Subscribe() (BrokerSubscription, error)
558- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)598+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
559 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error599 CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
560 AddHost(ctx context.Context, target string) error600 AddHost(ctx context.Context, target string) error
561 RemoveHost(ctx context.Context, target string) (bool, error)601 RemoveHost(ctx context.Context, target string) (bool, error)
@@ -602,10 +642,17 @@ type BrokerHostProbe interface {
602 // state, identity, geometry, input, output, cancellation, or application642 // state, identity, geometry, input, output, cancellation, or application
603 // errors between streams.643 // errors between streams.
604 type BrokerPhysicalConnection interface {644 type BrokerPhysicalConnection interface {
645+ // Done is the physical terminal authority, independent of reads. On
646+ // physical failure, publish Err and FailureKind and close Done BEFORE
647+ // terminating affected logical streams. Both results are stable after Done.
648+ // FailureKind is nonzero for failure; None is allowed only for local Close.
649+ Done() <-chan struct{}
650+ Err() error
651+ FailureKind() domain.RemoteFailureKind
605 Identity() BrokerDaemonIdentity652 Identity() BrokerDaemonIdentity
606 Incarnation() BrokerDaemonIncarnation653 Incarnation() BrokerDaemonIncarnation
607 Policy() BrokerPolicy654 Policy() BrokerPolicy
608- OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)655+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error)
609 Close() error656 Close() error
610 }657 }
611 658
@@ -613,7 +660,7 @@ type BrokerPhysicalConnection interface {
613 // one endpoint under an exact compatible policy. Conflicting policy is660 // one endpoint under an exact compatible policy. Conflicting policy is
614 // rejected; the first launching client never becomes policy authority.661 // rejected; the first launching client never becomes policy authority.
615 type BrokerEndpointConnector interface {662 type BrokerEndpointConnector interface {
616- Connect(ctx context.Context, endpoint string, policy BrokerPolicy) (BrokerPhysicalConnection, error)663+ Connect(ctx context.Context, endpoint BrokerResolvedEndpoint) (BrokerPhysicalConnection, error)
617 }664 }
618 665
619 // BrokerListener accepts client connections to the per-user broker666 // BrokerListener accepts client connections to the per-user broker
A internal/ports/broker_pool.go +73 -0
@@ -0,0 +1,73 @@
1+package ports
2+
3+import "context"
4+
5+// BrokerMaxAddressBytes bounds the opaque adapter route.
6+const BrokerMaxAddressBytes = 4096
7+
8+// BrokerStreamPurpose keeps observation and control independent from attachments.
9+type BrokerStreamPurpose uint8
10+
11+const (
12+ BrokerStreamAttachment BrokerStreamPurpose = iota + 1
13+ BrokerStreamControl
14+ BrokerStreamObservation
15+)
16+
17+// BrokerResolvedEndpoint is an authenticated service binding, never a hostname
18+// guess. Resolver owns registration fencing and machine-wide policy authority.
19+// Address is an opaque adapter route, not a pooling identity. Resolution must
20+// not start a daemon for observation. Connect must verify Identity and Policy;
21+// the pool verifies them again before publication. Incarnation is learned on
22+// connection and is deliberately not a stable pooling key.
23+type BrokerResolvedEndpoint struct {
24+ Identity BrokerDaemonIdentity
25+ Policy BrokerPolicy
26+ Address string
27+}
28+
29+func (e BrokerResolvedEndpoint) Validate() error {
30+ if err := e.Identity.Validate(); err != nil {
31+ return err
32+ }
33+ if err := e.Policy.Validate(); err != nil {
34+ return err
35+ }
36+ return validateBrokerToken(e.Address, BrokerMaxAddressBytes, "resolved address")
37+}
38+
39+// Resolve must validate current registration and reject conflicting requested
40+// policy. It must honor cancellation and return bounded, authenticated values.
41+// No resolver cache or alias normalization in the pool can bypass this check.
42+// Resolver, connector and OpenStream adapters should return BrokerError for
43+// semantic rejection and context errors for cancellation/deadline. Unknown
44+// errors become Unavailable; causes remain accessible through errors.Is/As.
45+// Never place secrets or unbounded adapter diagnostics in BrokerError.Text.
46+type BrokerEndpointResolver interface {
47+ Resolve(context.Context, BrokerOpenStreamRequest) (BrokerResolvedEndpoint, error)
48+}
49+
50+// BrokerLogicalConnection supplies terminal notification independent of reads.
51+// Done closes exactly once; Err is stable afterwards (nil for orderly close).
52+// Close is concurrent-safe, prompt, and unblocks all I/O. Physical Close and
53+// connector/resolver/open cancellation must likewise complete promptly. The
54+// pool does not buffer traffic: adapters must isolate bounded per-stream queues.
55+type BrokerLogicalConnection interface {
56+ ClientConnection
57+ Done() <-chan struct{}
58+ Err() error
59+}
60+
61+// BrokerAdmissionError distinguishes resource/identity rejection from transport
62+// failure. Rejected or failed reservations still consume the monotonic stream
63+// ID; callers must use a fresh ID. A connection ID is never reused in an epoch.
64+type BrokerAdmissionError string
65+
66+func (e BrokerAdmissionError) Error() string { return "vev: broker admission: " + string(e) }
67+
68+const (
69+ BrokerAdmissionLimit BrokerAdmissionError = "limit"
70+ BrokerAdmissionClosed BrokerAdmissionError = "closed"
71+ BrokerAdmissionStale BrokerAdmissionError = "stale connection or stream"
72+ BrokerAdmissionInvalid BrokerAdmissionError = "invalid request"
73+)
@@ -0,0 +1,73 @@
1+package ports
2+
3+import "context"
4+
5+// BrokerMaxAddressBytes bounds the opaque adapter route.
6+const BrokerMaxAddressBytes = 4096
7+
8+// BrokerStreamPurpose keeps observation and control independent from attachments.
9+type BrokerStreamPurpose uint8
10+
11+const (
12+ BrokerStreamAttachment BrokerStreamPurpose = iota + 1
13+ BrokerStreamControl
14+ BrokerStreamObservation
15+)
16+
17+// BrokerResolvedEndpoint is an authenticated service binding, never a hostname
18+// guess. Resolver owns registration fencing and machine-wide policy authority.
19+// Address is an opaque adapter route, not a pooling identity. Resolution must
20+// not start a daemon for observation. Connect must verify Identity and Policy;
21+// the pool verifies them again before publication. Incarnation is learned on
22+// connection and is deliberately not a stable pooling key.
23+type BrokerResolvedEndpoint struct {
24+ Identity BrokerDaemonIdentity
25+ Policy BrokerPolicy
26+ Address string
27+}
28+
29+func (e BrokerResolvedEndpoint) Validate() error {
30+ if err := e.Identity.Validate(); err != nil {
31+ return err
32+ }
33+ if err := e.Policy.Validate(); err != nil {
34+ return err
35+ }
36+ return validateBrokerToken(e.Address, BrokerMaxAddressBytes, "resolved address")
37+}
38+
39+// Resolve must validate current registration and reject conflicting requested
40+// policy. It must honor cancellation and return bounded, authenticated values.
41+// No resolver cache or alias normalization in the pool can bypass this check.
42+// Resolver, connector and OpenStream adapters should return BrokerError for
43+// semantic rejection and context errors for cancellation/deadline. Unknown
44+// errors become Unavailable; causes remain accessible through errors.Is/As.
45+// Never place secrets or unbounded adapter diagnostics in BrokerError.Text.
46+type BrokerEndpointResolver interface {
47+ Resolve(context.Context, BrokerOpenStreamRequest) (BrokerResolvedEndpoint, error)
48+}
49+
50+// BrokerLogicalConnection supplies terminal notification independent of reads.
51+// Done closes exactly once; Err is stable afterwards (nil for orderly close).
52+// Close is concurrent-safe, prompt, and unblocks all I/O. Physical Close and
53+// connector/resolver/open cancellation must likewise complete promptly. The
54+// pool does not buffer traffic: adapters must isolate bounded per-stream queues.
55+type BrokerLogicalConnection interface {
56+ ClientConnection
57+ Done() <-chan struct{}
58+ Err() error
59+}
60+
61+// BrokerAdmissionError distinguishes resource/identity rejection from transport
62+// failure. Rejected or failed reservations still consume the monotonic stream
63+// ID; callers must use a fresh ID. A connection ID is never reused in an epoch.
64+type BrokerAdmissionError string
65+
66+func (e BrokerAdmissionError) Error() string { return "vev: broker admission: " + string(e) }
67+
68+const (
69+ BrokerAdmissionLimit BrokerAdmissionError = "limit"
70+ BrokerAdmissionClosed BrokerAdmissionError = "closed"
71+ BrokerAdmissionStale BrokerAdmissionError = "stale connection or stream"
72+ BrokerAdmissionInvalid BrokerAdmissionError = "invalid request"
73+)
A internal/ports/broker_pool_test.go +100 -0
@@ -0,0 +1,100 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestBrokerPolicyTokens(t *testing.T) {
13+ for _, field := range []string{"trust", "launch", "isolation"} {
14+ for _, value := range []string{"", "bad token", "\xff", "bad\u202e", strings.Repeat("x", BrokerMaxPolicyTokenBytes+1), strings.Repeat("x", BrokerMaxPolicyTokenBytes)} {
15+ t.Run(field+"/"+value, func(t *testing.T) {
16+ p := testBrokerPolicy()
17+ switch field {
18+ case "trust":
19+ p.Trust = value
20+ case "launch":
21+ p.Launch = value
22+ case "isolation":
23+ p.Isolation = value
24+ }
25+ err := p.Validate()
26+ if len(value) == BrokerMaxPolicyTokenBytes {
27+ require.NoError(t, err)
28+ } else {
29+ require.ErrorContains(t, err, "policy "+field)
30+ }
31+ })
32+ }
33+ }
34+}
35+
36+func TestBrokerResolvedEndpointValidation(t *testing.T) {
37+ for _, tc := range []struct {
38+ name, address string
39+ valid bool
40+ }{
41+ {"valid", "route", true}, {"bound", strings.Repeat("x", BrokerMaxAddressBytes), true},
42+ {"empty", "", false}, {"over", strings.Repeat("x", BrokerMaxAddressBytes+1), false},
43+ {"utf8", "\xff", false}, {"control", "route\n", false},
44+ } {
45+ t.Run(tc.name, func(t *testing.T) {
46+ e := BrokerResolvedEndpoint{Identity: "daemon", Policy: testBrokerPolicy(), Address: tc.address}
47+ require.Equal(t, tc.valid, e.Validate() == nil)
48+ })
49+ }
50+ e := BrokerResolvedEndpoint{Identity: "daemon", Policy: testBrokerPolicy(), Address: "route"}
51+ e.Identity = ""
52+ require.Error(t, e.Validate())
53+ e.Identity = "daemon"
54+ e.Policy.Trust = ""
55+ require.Error(t, e.Validate())
56+}
57+
58+func TestBrokerStreamLostErrorChain(t *testing.T) {
59+ cause := errors.New("transport diagnostic")
60+ loss := BrokerStreamLost{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Cause: domain.RemoteFailureTrust, Err: cause}
61+ require.NoError(t, loss.Validate())
62+ require.Equal(t, "vev: broker attachment_lost", loss.Error())
63+ require.ErrorIs(t, loss, cause)
64+ var typed BrokerError
65+ require.ErrorAs(t, loss, &typed)
66+ require.Equal(t, BrokerErrorAttachmentLost, typed.Code)
67+ loss.Cause = domain.RemoteFailureNone
68+ require.Error(t, loss.Validate())
69+ loss.Cause = 99
70+ require.Error(t, loss.Validate())
71+}
72+
73+func TestBrokerRequestLocalRemoteFencing(t *testing.T) {
74+ for _, tc := range []struct {
75+ name string
76+ mutate func(*BrokerOpenStreamRequest)
77+ valid bool
78+ }{
79+ {"local", func(*BrokerOpenStreamRequest) {}, true},
80+ {"local endpoint", func(r *BrokerOpenStreamRequest) { r.Endpoint = "user@host" }, false},
81+ {"local registration", func(r *BrokerOpenStreamRequest) { r.Registration = testBrokerRegistration("user@host", 1, 1) }, false},
82+ {"remote missing", func(r *BrokerOpenStreamRequest) { r.Local = false }, false},
83+ {"remote valid", func(r *BrokerOpenStreamRequest) {
84+ r.Local = false
85+ r.Endpoint = "user@host"
86+ r.Registration = testBrokerRegistration(r.Endpoint, 1, 1)
87+ }, true},
88+ {"remote mismatch", func(r *BrokerOpenStreamRequest) {
89+ r.Local = false
90+ r.Endpoint = "user@other"
91+ r.Registration = testBrokerRegistration("user@host", 1, 1)
92+ }, false},
93+ } {
94+ t.Run(tc.name, func(t *testing.T) {
95+ r := BrokerOpenStreamRequest{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Local: true, Purpose: BrokerStreamControl, Policy: testBrokerPolicy()}
96+ tc.mutate(&r)
97+ require.Equal(t, tc.valid, r.Validate() == nil)
98+ })
99+ }
100+}
@@ -0,0 +1,100 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestBrokerPolicyTokens(t *testing.T) {
13+ for _, field := range []string{"trust", "launch", "isolation"} {
14+ for _, value := range []string{"", "bad token", "\xff", "bad\u202e", strings.Repeat("x", BrokerMaxPolicyTokenBytes+1), strings.Repeat("x", BrokerMaxPolicyTokenBytes)} {
15+ t.Run(field+"/"+value, func(t *testing.T) {
16+ p := testBrokerPolicy()
17+ switch field {
18+ case "trust":
19+ p.Trust = value
20+ case "launch":
21+ p.Launch = value
22+ case "isolation":
23+ p.Isolation = value
24+ }
25+ err := p.Validate()
26+ if len(value) == BrokerMaxPolicyTokenBytes {
27+ require.NoError(t, err)
28+ } else {
29+ require.ErrorContains(t, err, "policy "+field)
30+ }
31+ })
32+ }
33+ }
34+}
35+
36+func TestBrokerResolvedEndpointValidation(t *testing.T) {
37+ for _, tc := range []struct {
38+ name, address string
39+ valid bool
40+ }{
41+ {"valid", "route", true}, {"bound", strings.Repeat("x", BrokerMaxAddressBytes), true},
42+ {"empty", "", false}, {"over", strings.Repeat("x", BrokerMaxAddressBytes+1), false},
43+ {"utf8", "\xff", false}, {"control", "route\n", false},
44+ } {
45+ t.Run(tc.name, func(t *testing.T) {
46+ e := BrokerResolvedEndpoint{Identity: "daemon", Policy: testBrokerPolicy(), Address: tc.address}
47+ require.Equal(t, tc.valid, e.Validate() == nil)
48+ })
49+ }
50+ e := BrokerResolvedEndpoint{Identity: "daemon", Policy: testBrokerPolicy(), Address: "route"}
51+ e.Identity = ""
52+ require.Error(t, e.Validate())
53+ e.Identity = "daemon"
54+ e.Policy.Trust = ""
55+ require.Error(t, e.Validate())
56+}
57+
58+func TestBrokerStreamLostErrorChain(t *testing.T) {
59+ cause := errors.New("transport diagnostic")
60+ loss := BrokerStreamLost{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Cause: domain.RemoteFailureTrust, Err: cause}
61+ require.NoError(t, loss.Validate())
62+ require.Equal(t, "vev: broker attachment_lost", loss.Error())
63+ require.ErrorIs(t, loss, cause)
64+ var typed BrokerError
65+ require.ErrorAs(t, loss, &typed)
66+ require.Equal(t, BrokerErrorAttachmentLost, typed.Code)
67+ loss.Cause = domain.RemoteFailureNone
68+ require.Error(t, loss.Validate())
69+ loss.Cause = 99
70+ require.Error(t, loss.Validate())
71+}
72+
73+func TestBrokerRequestLocalRemoteFencing(t *testing.T) {
74+ for _, tc := range []struct {
75+ name string
76+ mutate func(*BrokerOpenStreamRequest)
77+ valid bool
78+ }{
79+ {"local", func(*BrokerOpenStreamRequest) {}, true},
80+ {"local endpoint", func(r *BrokerOpenStreamRequest) { r.Endpoint = "user@host" }, false},
81+ {"local registration", func(r *BrokerOpenStreamRequest) { r.Registration = testBrokerRegistration("user@host", 1, 1) }, false},
82+ {"remote missing", func(r *BrokerOpenStreamRequest) { r.Local = false }, false},
83+ {"remote valid", func(r *BrokerOpenStreamRequest) {
84+ r.Local = false
85+ r.Endpoint = "user@host"
86+ r.Registration = testBrokerRegistration(r.Endpoint, 1, 1)
87+ }, true},
88+ {"remote mismatch", func(r *BrokerOpenStreamRequest) {
89+ r.Local = false
90+ r.Endpoint = "user@other"
91+ r.Registration = testBrokerRegistration("user@host", 1, 1)
92+ }, false},
93+ } {
94+ t.Run(tc.name, func(t *testing.T) {
95+ r := BrokerOpenStreamRequest{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Local: true, Purpose: BrokerStreamControl, Policy: testBrokerPolicy()}
96+ tc.mutate(&r)
97+ require.Equal(t, tc.valid, r.Validate() == nil)
98+ })
99+ }
100+}
M internal/ports/broker_test.go +42 -5
@@ -32,6 +32,7 @@ func testBrokerPolicy() BrokerPolicy {
3232 CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion,
3333 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
3434 Transport: "quic",
35+ Trust: "trust", Launch: "launch", Isolation: "user",
3536 }
3637 }
3738
@@ -266,6 +267,9 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
266267 "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }),
267268 "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }),
268269 "transport": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "stdio" }),
270+ "trust": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Trust = "other" }),
271+ "launch": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Launch = "other" }),
272+ "isolation": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Isolation = "other" }),
269273 }
270274 for name, conflict := range conflicts {
271275 if base.Compatible(conflict) {
@@ -273,11 +277,10 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
273277 }
274278 }
275279 invalid := []BrokerPolicy{
276- {},
277- {ProtocolVersion: protocol.Version},
278- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion},
279- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicy(9), Transport: "quic"},
280- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned},
280+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.ProtocolVersion = 0 }),
281+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion = 0 }),
282+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicy(9) }),
283+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "" }),
281284 }
282285 for i, policy := range invalid {
283286 if err := policy.Validate(); err == nil {
@@ -289,6 +292,7 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
289292 func TestBrokerOpenStreamRequestValidate(t *testing.T) {
290293 valid := func() BrokerOpenStreamRequest {
291294 return BrokerOpenStreamRequest{
295+ Epoch: 1, Purpose: BrokerStreamAttachment,
292296 Connection: BrokerConnectionID{1},
293297 Stream: 2,
294298 Endpoint: "user@arch",
@@ -466,6 +470,8 @@ func TestBrokerInterfacesArePorts(t *testing.T) {
466470 // adapters can implement them without importing use cases.
467471 for name, iface := range map[string]reflect.Type{
468472 "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(),
473+ "BrokerEndpointResolver": reflect.TypeOf((*BrokerEndpointResolver)(nil)).Elem(),
474+ "BrokerLogicalConnection": reflect.TypeOf((*BrokerLogicalConnection)(nil)).Elem(),
469475 "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(),
470476 "BrokerHostProbe": reflect.TypeOf((*BrokerHostProbe)(nil)).Elem(),
471477 "BrokerPhysicalConnection": reflect.TypeOf((*BrokerPhysicalConnection)(nil)).Elem(),
@@ -478,3 +484,34 @@ func TestBrokerInterfacesArePorts(t *testing.T) {
478484 }
479485 }
480486 }
487+
488+func TestBrokerStreamPurposeShapes(t *testing.T) {
489+ for _, tc := range []struct {
490+ name string
491+ purpose BrokerStreamPurpose
492+ target bool
493+ env bool
494+ valid bool
495+ }{
496+ {"attachment", BrokerStreamAttachment, true, true, true},
497+ {"attachment missing target", BrokerStreamAttachment, false, false, false},
498+ {"control", BrokerStreamControl, false, false, true},
499+ {"observation", BrokerStreamObservation, false, false, true},
500+ {"control target", BrokerStreamControl, true, false, false},
501+ {"observation env", BrokerStreamObservation, false, true, false},
502+ {"unknown", 0, false, false, false},
503+ } {
504+ t.Run(tc.name, func(t *testing.T) {
505+ r := BrokerOpenStreamRequest{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Local: true, Purpose: tc.purpose, Policy: testBrokerPolicy()}
506+ if tc.target {
507+ r.Target = testBrokerTarget()
508+ }
509+ if tc.env {
510+ r.Env = []string{"TERM=xterm"}
511+ }
512+ if (r.Validate() == nil) != tc.valid {
513+ t.Fatalf("Validate()=%v", r.Validate())
514+ }
515+ })
516+ }
517+}
@@ -32,6 +32,7 @@ func testBrokerPolicy() BrokerPolicy {
32 CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion,32 CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion,
33 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,33 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
34 Transport: "quic",34 Transport: "quic",
35+ Trust: "trust", Launch: "launch", Isolation: "user",
35 }36 }
36 }37 }
37 38
@@ -266,6 +267,9 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
266 "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }),267 "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }),
267 "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }),268 "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }),
268 "transport": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "stdio" }),269 "transport": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "stdio" }),
270+ "trust": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Trust = "other" }),
271+ "launch": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Launch = "other" }),
272+ "isolation": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Isolation = "other" }),
269 }273 }
270 for name, conflict := range conflicts {274 for name, conflict := range conflicts {
271 if base.Compatible(conflict) {275 if base.Compatible(conflict) {
@@ -273,11 +277,10 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
273 }277 }
274 }278 }
275 invalid := []BrokerPolicy{279 invalid := []BrokerPolicy{
276- {},280+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.ProtocolVersion = 0 }),
277- {ProtocolVersion: protocol.Version},281+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion = 0 }),
278- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion},282+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicy(9) }),
279- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicy(9), Transport: "quic"},283+ withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "" }),
280- {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned},
281 }284 }
282 for i, policy := range invalid {285 for i, policy := range invalid {
283 if err := policy.Validate(); err == nil {286 if err := policy.Validate(); err == nil {
@@ -289,6 +292,7 @@ func TestBrokerPolicyCompatibility(t *testing.T) {
289 func TestBrokerOpenStreamRequestValidate(t *testing.T) {292 func TestBrokerOpenStreamRequestValidate(t *testing.T) {
290 valid := func() BrokerOpenStreamRequest {293 valid := func() BrokerOpenStreamRequest {
291 return BrokerOpenStreamRequest{294 return BrokerOpenStreamRequest{
295+ Epoch: 1, Purpose: BrokerStreamAttachment,
292 Connection: BrokerConnectionID{1},296 Connection: BrokerConnectionID{1},
293 Stream: 2,297 Stream: 2,
294 Endpoint: "user@arch",298 Endpoint: "user@arch",
@@ -466,6 +470,8 @@ func TestBrokerInterfacesArePorts(t *testing.T) {
466 // adapters can implement them without importing use cases.470 // adapters can implement them without importing use cases.
467 for name, iface := range map[string]reflect.Type{471 for name, iface := range map[string]reflect.Type{
468 "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(),472 "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(),
473+ "BrokerEndpointResolver": reflect.TypeOf((*BrokerEndpointResolver)(nil)).Elem(),
474+ "BrokerLogicalConnection": reflect.TypeOf((*BrokerLogicalConnection)(nil)).Elem(),
469 "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(),475 "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(),
470 "BrokerHostProbe": reflect.TypeOf((*BrokerHostProbe)(nil)).Elem(),476 "BrokerHostProbe": reflect.TypeOf((*BrokerHostProbe)(nil)).Elem(),
471 "BrokerPhysicalConnection": reflect.TypeOf((*BrokerPhysicalConnection)(nil)).Elem(),477 "BrokerPhysicalConnection": reflect.TypeOf((*BrokerPhysicalConnection)(nil)).Elem(),
@@ -478,3 +484,34 @@ func TestBrokerInterfacesArePorts(t *testing.T) {
478 }484 }
479 }485 }
480 }486 }
487+
488+func TestBrokerStreamPurposeShapes(t *testing.T) {
489+ for _, tc := range []struct {
490+ name string
491+ purpose BrokerStreamPurpose
492+ target bool
493+ env bool
494+ valid bool
495+ }{
496+ {"attachment", BrokerStreamAttachment, true, true, true},
497+ {"attachment missing target", BrokerStreamAttachment, false, false, false},
498+ {"control", BrokerStreamControl, false, false, true},
499+ {"observation", BrokerStreamObservation, false, false, true},
500+ {"control target", BrokerStreamControl, true, false, false},
501+ {"observation env", BrokerStreamObservation, false, true, false},
502+ {"unknown", 0, false, false, false},
503+ } {
504+ t.Run(tc.name, func(t *testing.T) {
505+ r := BrokerOpenStreamRequest{Epoch: 1, Connection: BrokerConnectionID{1}, Stream: 1, Local: true, Purpose: tc.purpose, Policy: testBrokerPolicy()}
506+ if tc.target {
507+ r.Target = testBrokerTarget()
508+ }
509+ if tc.env {
510+ r.Env = []string{"TERM=xterm"}
511+ }
512+ if (r.Validate() == nil) != tc.valid {
513+ t.Fatalf("Validate()=%v", r.Validate())
514+ }
515+ })
516+ }
517+}
M internal/ports/mocks/broker_mock.go +165 -37
@@ -350,23 +350,23 @@ func (_c *MockBrokerService_ConnectionID_Call) RunAndReturn(run func() ports.Bro
350350 }
351351
352352 // OpenStream provides a mock function for the type MockBrokerService
353-func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
353+func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
354354 ret := _mock.Called(ctx, request)
355355
356356 if len(ret) == 0 {
357357 panic("no return value specified for OpenStream")
358358 }
359359
360- var r0 ports.ClientConnection
360+ var r0 ports.BrokerLogicalConnection
361361 var r1 error
362- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
362+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)); ok {
363363 return returnFunc(ctx, request)
364364 }
365- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
365+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerLogicalConnection); ok {
366366 r0 = returnFunc(ctx, request)
367367 } else {
368368 if ret.Get(0) != nil {
369- r0 = ret.Get(0).(ports.ClientConnection)
369+ r0 = ret.Get(0).(ports.BrokerLogicalConnection)
370370 }
371371 }
372372 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
@@ -407,12 +407,12 @@ func (_c *MockBrokerService_OpenStream_Call) Run(run func(ctx context.Context, r
407407 return _c
408408 }
409409
410-func (_c *MockBrokerService_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerService_OpenStream_Call {
411- _c.Call.Return(clientConnection, err)
410+func (_c *MockBrokerService_OpenStream_Call) Return(brokerLogicalConnection ports.BrokerLogicalConnection, err error) *MockBrokerService_OpenStream_Call {
411+ _c.Call.Return(brokerLogicalConnection, err)
412412 return _c
413413 }
414414
415-func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerService_OpenStream_Call {
415+func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)) *MockBrokerService_OpenStream_Call {
416416 _c.Call.Return(run)
417417 return _c
418418 }
@@ -917,6 +917,140 @@ func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error
917917 return _c
918918 }
919919
920+// Done provides a mock function for the type MockBrokerPhysicalConnection
921+func (_mock *MockBrokerPhysicalConnection) Done() <-chan struct{} {
922+ ret := _mock.Called()
923+
924+ if len(ret) == 0 {
925+ panic("no return value specified for Done")
926+ }
927+
928+ var r0 <-chan struct{}
929+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
930+ r0 = returnFunc()
931+ } else {
932+ if ret.Get(0) != nil {
933+ r0 = ret.Get(0).(<-chan struct{})
934+ }
935+ }
936+ return r0
937+}
938+
939+// MockBrokerPhysicalConnection_Done_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Done'
940+type MockBrokerPhysicalConnection_Done_Call struct {
941+ *mock.Call
942+}
943+
944+// Done is a helper method to define mock.On call
945+func (_e *MockBrokerPhysicalConnection_Expecter) Done() *MockBrokerPhysicalConnection_Done_Call {
946+ return &MockBrokerPhysicalConnection_Done_Call{Call: _e.mock.On("Done")}
947+}
948+
949+func (_c *MockBrokerPhysicalConnection_Done_Call) Run(run func()) *MockBrokerPhysicalConnection_Done_Call {
950+ _c.Call.Run(func(args mock.Arguments) {
951+ run()
952+ })
953+ return _c
954+}
955+
956+func (_c *MockBrokerPhysicalConnection_Done_Call) Return(valCh <-chan struct{}) *MockBrokerPhysicalConnection_Done_Call {
957+ _c.Call.Return(valCh)
958+ return _c
959+}
960+
961+func (_c *MockBrokerPhysicalConnection_Done_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerPhysicalConnection_Done_Call {
962+ _c.Call.Return(run)
963+ return _c
964+}
965+
966+// Err provides a mock function for the type MockBrokerPhysicalConnection
967+func (_mock *MockBrokerPhysicalConnection) Err() error {
968+ ret := _mock.Called()
969+
970+ if len(ret) == 0 {
971+ panic("no return value specified for Err")
972+ }
973+
974+ var r0 error
975+ if returnFunc, ok := ret.Get(0).(func() error); ok {
976+ r0 = returnFunc()
977+ } else {
978+ r0 = ret.Error(0)
979+ }
980+ return r0
981+}
982+
983+// MockBrokerPhysicalConnection_Err_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Err'
984+type MockBrokerPhysicalConnection_Err_Call struct {
985+ *mock.Call
986+}
987+
988+// Err is a helper method to define mock.On call
989+func (_e *MockBrokerPhysicalConnection_Expecter) Err() *MockBrokerPhysicalConnection_Err_Call {
990+ return &MockBrokerPhysicalConnection_Err_Call{Call: _e.mock.On("Err")}
991+}
992+
993+func (_c *MockBrokerPhysicalConnection_Err_Call) Run(run func()) *MockBrokerPhysicalConnection_Err_Call {
994+ _c.Call.Run(func(args mock.Arguments) {
995+ run()
996+ })
997+ return _c
998+}
999+
1000+func (_c *MockBrokerPhysicalConnection_Err_Call) Return(err error) *MockBrokerPhysicalConnection_Err_Call {
1001+ _c.Call.Return(err)
1002+ return _c
1003+}
1004+
1005+func (_c *MockBrokerPhysicalConnection_Err_Call) RunAndReturn(run func() error) *MockBrokerPhysicalConnection_Err_Call {
1006+ _c.Call.Return(run)
1007+ return _c
1008+}
1009+
1010+// FailureKind provides a mock function for the type MockBrokerPhysicalConnection
1011+func (_mock *MockBrokerPhysicalConnection) FailureKind() domain.RemoteFailureKind {
1012+ ret := _mock.Called()
1013+
1014+ if len(ret) == 0 {
1015+ panic("no return value specified for FailureKind")
1016+ }
1017+
1018+ var r0 domain.RemoteFailureKind
1019+ if returnFunc, ok := ret.Get(0).(func() domain.RemoteFailureKind); ok {
1020+ r0 = returnFunc()
1021+ } else {
1022+ r0 = ret.Get(0).(domain.RemoteFailureKind)
1023+ }
1024+ return r0
1025+}
1026+
1027+// MockBrokerPhysicalConnection_FailureKind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailureKind'
1028+type MockBrokerPhysicalConnection_FailureKind_Call struct {
1029+ *mock.Call
1030+}
1031+
1032+// FailureKind is a helper method to define mock.On call
1033+func (_e *MockBrokerPhysicalConnection_Expecter) FailureKind() *MockBrokerPhysicalConnection_FailureKind_Call {
1034+ return &MockBrokerPhysicalConnection_FailureKind_Call{Call: _e.mock.On("FailureKind")}
1035+}
1036+
1037+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) Run(run func()) *MockBrokerPhysicalConnection_FailureKind_Call {
1038+ _c.Call.Run(func(args mock.Arguments) {
1039+ run()
1040+ })
1041+ return _c
1042+}
1043+
1044+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) Return(remoteFailureKind domain.RemoteFailureKind) *MockBrokerPhysicalConnection_FailureKind_Call {
1045+ _c.Call.Return(remoteFailureKind)
1046+ return _c
1047+}
1048+
1049+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) RunAndReturn(run func() domain.RemoteFailureKind) *MockBrokerPhysicalConnection_FailureKind_Call {
1050+ _c.Call.Return(run)
1051+ return _c
1052+}
1053+
9201054 // Identity provides a mock function for the type MockBrokerPhysicalConnection
9211055 func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity {
9221056 ret := _mock.Called()
@@ -1008,23 +1142,23 @@ func (_c *MockBrokerPhysicalConnection_Incarnation_Call) RunAndReturn(run func()
10081142 }
10091143
10101144 // OpenStream provides a mock function for the type MockBrokerPhysicalConnection
1011-func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
1145+func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
10121146 ret := _mock.Called(ctx, request)
10131147
10141148 if len(ret) == 0 {
10151149 panic("no return value specified for OpenStream")
10161150 }
10171151
1018- var r0 ports.ClientConnection
1152+ var r0 ports.BrokerLogicalConnection
10191153 var r1 error
1020- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
1154+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)); ok {
10211155 return returnFunc(ctx, request)
10221156 }
1023- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
1157+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerLogicalConnection); ok {
10241158 r0 = returnFunc(ctx, request)
10251159 } else {
10261160 if ret.Get(0) != nil {
1027- r0 = ret.Get(0).(ports.ClientConnection)
1161+ r0 = ret.Get(0).(ports.BrokerLogicalConnection)
10281162 }
10291163 }
10301164 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
@@ -1065,12 +1199,12 @@ func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Run(run func(ctx context
10651199 return _c
10661200 }
10671201
1068-func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {
1069- _c.Call.Return(clientConnection, err)
1202+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(brokerLogicalConnection ports.BrokerLogicalConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {
1203+ _c.Call.Return(brokerLogicalConnection, err)
10701204 return _c
10711205 }
10721206
1073-func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {
1207+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {
10741208 _c.Call.Return(run)
10751209 return _c
10761210 }
@@ -1147,8 +1281,8 @@ func (_m *MockBrokerEndpointConnector) EXPECT() *MockBrokerEndpointConnector_Exp
11471281 }
11481282
11491283 // Connect provides a mock function for the type MockBrokerEndpointConnector
1150-func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error) {
1151- ret := _mock.Called(ctx, endpoint, policy)
1284+func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
1285+ ret := _mock.Called(ctx, endpoint)
11521286
11531287 if len(ret) == 0 {
11541288 panic("no return value specified for Connect")
@@ -1156,18 +1290,18 @@ func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint
11561290
11571291 var r0 ports.BrokerPhysicalConnection
11581292 var r1 error
1159- if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)); ok {
1160- return returnFunc(ctx, endpoint, policy)
1293+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)); ok {
1294+ return returnFunc(ctx, endpoint)
11611295 }
1162- if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) ports.BrokerPhysicalConnection); ok {
1163- r0 = returnFunc(ctx, endpoint, policy)
1296+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerResolvedEndpoint) ports.BrokerPhysicalConnection); ok {
1297+ r0 = returnFunc(ctx, endpoint)
11641298 } else {
11651299 if ret.Get(0) != nil {
11661300 r0 = ret.Get(0).(ports.BrokerPhysicalConnection)
11671301 }
11681302 }
1169- if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.BrokerPolicy) error); ok {
1170- r1 = returnFunc(ctx, endpoint, policy)
1303+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerResolvedEndpoint) error); ok {
1304+ r1 = returnFunc(ctx, endpoint)
11711305 } else {
11721306 r1 = ret.Error(1)
11731307 }
@@ -1181,30 +1315,24 @@ type MockBrokerEndpointConnector_Connect_Call struct {
11811315
11821316 // Connect is a helper method to define mock.On call
11831317 // - ctx context.Context
1184-// - endpoint string
1185-// - policy ports.BrokerPolicy
1186-func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any, policy any) *MockBrokerEndpointConnector_Connect_Call {
1187- return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint, policy)}
1318+// - endpoint ports.BrokerResolvedEndpoint
1319+func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any) *MockBrokerEndpointConnector_Connect_Call {
1320+ return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint)}
11881321 }
11891322
1190-func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy)) *MockBrokerEndpointConnector_Connect_Call {
1323+func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint ports.BrokerResolvedEndpoint)) *MockBrokerEndpointConnector_Connect_Call {
11911324 _c.Call.Run(func(args mock.Arguments) {
11921325 var arg0 context.Context
11931326 if args[0] != nil {
11941327 arg0 = args[0].(context.Context)
11951328 }
1196- var arg1 string
1329+ var arg1 ports.BrokerResolvedEndpoint
11971330 if args[1] != nil {
1198- arg1 = args[1].(string)
1199- }
1200- var arg2 ports.BrokerPolicy
1201- if args[2] != nil {
1202- arg2 = args[2].(ports.BrokerPolicy)
1331+ arg1 = args[1].(ports.BrokerResolvedEndpoint)
12031332 }
12041333 run(
12051334 arg0,
12061335 arg1,
1207- arg2,
12081336 )
12091337 })
12101338 return _c
@@ -1215,7 +1343,7 @@ func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnect
12151343 return _c
12161344 }
12171345
1218-func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {
1346+func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {
12191347 _c.Call.Return(run)
12201348 return _c
12211349 }
@@ -350,23 +350,23 @@ func (_c *MockBrokerService_ConnectionID_Call) RunAndReturn(run func() ports.Bro
350 }350 }
351 351
352 // OpenStream provides a mock function for the type MockBrokerService352 // OpenStream provides a mock function for the type MockBrokerService
353-func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {353+func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
354 ret := _mock.Called(ctx, request)354 ret := _mock.Called(ctx, request)
355 355
356 if len(ret) == 0 {356 if len(ret) == 0 {
357 panic("no return value specified for OpenStream")357 panic("no return value specified for OpenStream")
358 }358 }
359 359
360- var r0 ports.ClientConnection360+ var r0 ports.BrokerLogicalConnection
361 var r1 error361 var r1 error
362- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {362+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)); ok {
363 return returnFunc(ctx, request)363 return returnFunc(ctx, request)
364 }364 }
365- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {365+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerLogicalConnection); ok {
366 r0 = returnFunc(ctx, request)366 r0 = returnFunc(ctx, request)
367 } else {367 } else {
368 if ret.Get(0) != nil {368 if ret.Get(0) != nil {
369- r0 = ret.Get(0).(ports.ClientConnection)369+ r0 = ret.Get(0).(ports.BrokerLogicalConnection)
370 }370 }
371 }371 }
372 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {372 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
@@ -407,12 +407,12 @@ func (_c *MockBrokerService_OpenStream_Call) Run(run func(ctx context.Context, r
407 return _c407 return _c
408 }408 }
409 409
410-func (_c *MockBrokerService_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerService_OpenStream_Call {410+func (_c *MockBrokerService_OpenStream_Call) Return(brokerLogicalConnection ports.BrokerLogicalConnection, err error) *MockBrokerService_OpenStream_Call {
411- _c.Call.Return(clientConnection, err)411+ _c.Call.Return(brokerLogicalConnection, err)
412 return _c412 return _c
413 }413 }
414 414
415-func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerService_OpenStream_Call {415+func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)) *MockBrokerService_OpenStream_Call {
416 _c.Call.Return(run)416 _c.Call.Return(run)
417 return _c417 return _c
418 }418 }
@@ -917,6 +917,140 @@ func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error
917 return _c917 return _c
918 }918 }
919 919
920+// Done provides a mock function for the type MockBrokerPhysicalConnection
921+func (_mock *MockBrokerPhysicalConnection) Done() <-chan struct{} {
922+ ret := _mock.Called()
923+
924+ if len(ret) == 0 {
925+ panic("no return value specified for Done")
926+ }
927+
928+ var r0 <-chan struct{}
929+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
930+ r0 = returnFunc()
931+ } else {
932+ if ret.Get(0) != nil {
933+ r0 = ret.Get(0).(<-chan struct{})
934+ }
935+ }
936+ return r0
937+}
938+
939+// MockBrokerPhysicalConnection_Done_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Done'
940+type MockBrokerPhysicalConnection_Done_Call struct {
941+ *mock.Call
942+}
943+
944+// Done is a helper method to define mock.On call
945+func (_e *MockBrokerPhysicalConnection_Expecter) Done() *MockBrokerPhysicalConnection_Done_Call {
946+ return &MockBrokerPhysicalConnection_Done_Call{Call: _e.mock.On("Done")}
947+}
948+
949+func (_c *MockBrokerPhysicalConnection_Done_Call) Run(run func()) *MockBrokerPhysicalConnection_Done_Call {
950+ _c.Call.Run(func(args mock.Arguments) {
951+ run()
952+ })
953+ return _c
954+}
955+
956+func (_c *MockBrokerPhysicalConnection_Done_Call) Return(valCh <-chan struct{}) *MockBrokerPhysicalConnection_Done_Call {
957+ _c.Call.Return(valCh)
958+ return _c
959+}
960+
961+func (_c *MockBrokerPhysicalConnection_Done_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerPhysicalConnection_Done_Call {
962+ _c.Call.Return(run)
963+ return _c
964+}
965+
966+// Err provides a mock function for the type MockBrokerPhysicalConnection
967+func (_mock *MockBrokerPhysicalConnection) Err() error {
968+ ret := _mock.Called()
969+
970+ if len(ret) == 0 {
971+ panic("no return value specified for Err")
972+ }
973+
974+ var r0 error
975+ if returnFunc, ok := ret.Get(0).(func() error); ok {
976+ r0 = returnFunc()
977+ } else {
978+ r0 = ret.Error(0)
979+ }
980+ return r0
981+}
982+
983+// MockBrokerPhysicalConnection_Err_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Err'
984+type MockBrokerPhysicalConnection_Err_Call struct {
985+ *mock.Call
986+}
987+
988+// Err is a helper method to define mock.On call
989+func (_e *MockBrokerPhysicalConnection_Expecter) Err() *MockBrokerPhysicalConnection_Err_Call {
990+ return &MockBrokerPhysicalConnection_Err_Call{Call: _e.mock.On("Err")}
991+}
992+
993+func (_c *MockBrokerPhysicalConnection_Err_Call) Run(run func()) *MockBrokerPhysicalConnection_Err_Call {
994+ _c.Call.Run(func(args mock.Arguments) {
995+ run()
996+ })
997+ return _c
998+}
999+
1000+func (_c *MockBrokerPhysicalConnection_Err_Call) Return(err error) *MockBrokerPhysicalConnection_Err_Call {
1001+ _c.Call.Return(err)
1002+ return _c
1003+}
1004+
1005+func (_c *MockBrokerPhysicalConnection_Err_Call) RunAndReturn(run func() error) *MockBrokerPhysicalConnection_Err_Call {
1006+ _c.Call.Return(run)
1007+ return _c
1008+}
1009+
1010+// FailureKind provides a mock function for the type MockBrokerPhysicalConnection
1011+func (_mock *MockBrokerPhysicalConnection) FailureKind() domain.RemoteFailureKind {
1012+ ret := _mock.Called()
1013+
1014+ if len(ret) == 0 {
1015+ panic("no return value specified for FailureKind")
1016+ }
1017+
1018+ var r0 domain.RemoteFailureKind
1019+ if returnFunc, ok := ret.Get(0).(func() domain.RemoteFailureKind); ok {
1020+ r0 = returnFunc()
1021+ } else {
1022+ r0 = ret.Get(0).(domain.RemoteFailureKind)
1023+ }
1024+ return r0
1025+}
1026+
1027+// MockBrokerPhysicalConnection_FailureKind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailureKind'
1028+type MockBrokerPhysicalConnection_FailureKind_Call struct {
1029+ *mock.Call
1030+}
1031+
1032+// FailureKind is a helper method to define mock.On call
1033+func (_e *MockBrokerPhysicalConnection_Expecter) FailureKind() *MockBrokerPhysicalConnection_FailureKind_Call {
1034+ return &MockBrokerPhysicalConnection_FailureKind_Call{Call: _e.mock.On("FailureKind")}
1035+}
1036+
1037+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) Run(run func()) *MockBrokerPhysicalConnection_FailureKind_Call {
1038+ _c.Call.Run(func(args mock.Arguments) {
1039+ run()
1040+ })
1041+ return _c
1042+}
1043+
1044+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) Return(remoteFailureKind domain.RemoteFailureKind) *MockBrokerPhysicalConnection_FailureKind_Call {
1045+ _c.Call.Return(remoteFailureKind)
1046+ return _c
1047+}
1048+
1049+func (_c *MockBrokerPhysicalConnection_FailureKind_Call) RunAndReturn(run func() domain.RemoteFailureKind) *MockBrokerPhysicalConnection_FailureKind_Call {
1050+ _c.Call.Return(run)
1051+ return _c
1052+}
1053+
920 // Identity provides a mock function for the type MockBrokerPhysicalConnection1054 // Identity provides a mock function for the type MockBrokerPhysicalConnection
921 func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity {1055 func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity {
922 ret := _mock.Called()1056 ret := _mock.Called()
@@ -1008,23 +1142,23 @@ func (_c *MockBrokerPhysicalConnection_Incarnation_Call) RunAndReturn(run func()
1008 }1142 }
1009 1143
1010 // OpenStream provides a mock function for the type MockBrokerPhysicalConnection1144 // OpenStream provides a mock function for the type MockBrokerPhysicalConnection
1011-func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {1145+func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
1012 ret := _mock.Called(ctx, request)1146 ret := _mock.Called(ctx, request)
1013 1147
1014 if len(ret) == 0 {1148 if len(ret) == 0 {
1015 panic("no return value specified for OpenStream")1149 panic("no return value specified for OpenStream")
1016 }1150 }
1017 1151
1018- var r0 ports.ClientConnection1152+ var r0 ports.BrokerLogicalConnection
1019 var r1 error1153 var r1 error
1020- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {1154+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)); ok {
1021 return returnFunc(ctx, request)1155 return returnFunc(ctx, request)
1022 }1156 }
1023- if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {1157+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerLogicalConnection); ok {
1024 r0 = returnFunc(ctx, request)1158 r0 = returnFunc(ctx, request)
1025 } else {1159 } else {
1026 if ret.Get(0) != nil {1160 if ret.Get(0) != nil {
1027- r0 = ret.Get(0).(ports.ClientConnection)1161+ r0 = ret.Get(0).(ports.BrokerLogicalConnection)
1028 }1162 }
1029 }1163 }
1030 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {1164 if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
@@ -1065,12 +1199,12 @@ func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Run(run func(ctx context
1065 return _c1199 return _c
1066 }1200 }
1067 1201
1068-func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {1202+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(brokerLogicalConnection ports.BrokerLogicalConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {
1069- _c.Call.Return(clientConnection, err)1203+ _c.Call.Return(brokerLogicalConnection, err)
1070 return _c1204 return _c
1071 }1205 }
1072 1206
1073-func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {1207+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {
1074 _c.Call.Return(run)1208 _c.Call.Return(run)
1075 return _c1209 return _c
1076 }1210 }
@@ -1147,8 +1281,8 @@ func (_m *MockBrokerEndpointConnector) EXPECT() *MockBrokerEndpointConnector_Exp
1147 }1281 }
1148 1282
1149 // Connect provides a mock function for the type MockBrokerEndpointConnector1283 // Connect provides a mock function for the type MockBrokerEndpointConnector
1150-func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error) {1284+func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
1151- ret := _mock.Called(ctx, endpoint, policy)1285+ ret := _mock.Called(ctx, endpoint)
1152 1286
1153 if len(ret) == 0 {1287 if len(ret) == 0 {
1154 panic("no return value specified for Connect")1288 panic("no return value specified for Connect")
@@ -1156,18 +1290,18 @@ func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint
1156 1290
1157 var r0 ports.BrokerPhysicalConnection1291 var r0 ports.BrokerPhysicalConnection
1158 var r1 error1292 var r1 error
1159- if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)); ok {1293+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)); ok {
1160- return returnFunc(ctx, endpoint, policy)1294+ return returnFunc(ctx, endpoint)
1161 }1295 }
1162- if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) ports.BrokerPhysicalConnection); ok {1296+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerResolvedEndpoint) ports.BrokerPhysicalConnection); ok {
1163- r0 = returnFunc(ctx, endpoint, policy)1297+ r0 = returnFunc(ctx, endpoint)
1164 } else {1298 } else {
1165 if ret.Get(0) != nil {1299 if ret.Get(0) != nil {
1166 r0 = ret.Get(0).(ports.BrokerPhysicalConnection)1300 r0 = ret.Get(0).(ports.BrokerPhysicalConnection)
1167 }1301 }
1168 }1302 }
1169- if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.BrokerPolicy) error); ok {1303+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerResolvedEndpoint) error); ok {
1170- r1 = returnFunc(ctx, endpoint, policy)1304+ r1 = returnFunc(ctx, endpoint)
1171 } else {1305 } else {
1172 r1 = ret.Error(1)1306 r1 = ret.Error(1)
1173 }1307 }
@@ -1181,30 +1315,24 @@ type MockBrokerEndpointConnector_Connect_Call struct {
1181 1315
1182 // Connect is a helper method to define mock.On call1316 // Connect is a helper method to define mock.On call
1183 // - ctx context.Context1317 // - ctx context.Context
1184-// - endpoint string1318+// - endpoint ports.BrokerResolvedEndpoint
1185-// - policy ports.BrokerPolicy1319+func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any) *MockBrokerEndpointConnector_Connect_Call {
1186-func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any, policy any) *MockBrokerEndpointConnector_Connect_Call {1320+ return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint)}
1187- return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint, policy)}
1188 }1321 }
1189 1322
1190-func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy)) *MockBrokerEndpointConnector_Connect_Call {1323+func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint ports.BrokerResolvedEndpoint)) *MockBrokerEndpointConnector_Connect_Call {
1191 _c.Call.Run(func(args mock.Arguments) {1324 _c.Call.Run(func(args mock.Arguments) {
1192 var arg0 context.Context1325 var arg0 context.Context
1193 if args[0] != nil {1326 if args[0] != nil {
1194 arg0 = args[0].(context.Context)1327 arg0 = args[0].(context.Context)
1195 }1328 }
1196- var arg1 string1329+ var arg1 ports.BrokerResolvedEndpoint
1197 if args[1] != nil {1330 if args[1] != nil {
1198- arg1 = args[1].(string)1331+ arg1 = args[1].(ports.BrokerResolvedEndpoint)
1199- }
1200- var arg2 ports.BrokerPolicy
1201- if args[2] != nil {
1202- arg2 = args[2].(ports.BrokerPolicy)
1203 }1332 }
1204 run(1333 run(
1205 arg0,1334 arg0,
1206 arg1,1335 arg1,
1207- arg2,
1208 )1336 )
1209 })1337 })
1210 return _c1338 return _c
@@ -1215,7 +1343,7 @@ func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnect
1215 return _c1343 return _c
1216 }1344 }
1217 1345
1218-func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {1346+func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {
1219 _c.Call.Return(run)1347 _c.Call.Return(run)
1220 return _c1348 return _c
1221 }1349 }
A internal/ports/mocks/broker_pool_mock.go +507 -0
@@ -0,0 +1,507 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewMockBrokerEndpointResolver creates a new instance of MockBrokerEndpointResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewMockBrokerEndpointResolver(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *MockBrokerEndpointResolver {
21+ mock := &MockBrokerEndpointResolver{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() { mock.AssertExpectations(t) })
25+
26+ return mock
27+}
28+
29+// MockBrokerEndpointResolver is an autogenerated mock type for the BrokerEndpointResolver type
30+type MockBrokerEndpointResolver struct {
31+ mock.Mock
32+}
33+
34+type MockBrokerEndpointResolver_Expecter struct {
35+ mock *mock.Mock
36+}
37+
38+func (_m *MockBrokerEndpointResolver) EXPECT() *MockBrokerEndpointResolver_Expecter {
39+ return &MockBrokerEndpointResolver_Expecter{mock: &_m.Mock}
40+}
41+
42+// Resolve provides a mock function for the type MockBrokerEndpointResolver
43+func (_mock *MockBrokerEndpointResolver) Resolve(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
44+ ret := _mock.Called(context1, brokerOpenStreamRequest)
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for Resolve")
48+ }
49+
50+ var r0 ports.BrokerResolvedEndpoint
51+ var r1 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)); ok {
53+ return returnFunc(context1, brokerOpenStreamRequest)
54+ }
55+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerResolvedEndpoint); ok {
56+ r0 = returnFunc(context1, brokerOpenStreamRequest)
57+ } else {
58+ r0 = ret.Get(0).(ports.BrokerResolvedEndpoint)
59+ }
60+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
61+ r1 = returnFunc(context1, brokerOpenStreamRequest)
62+ } else {
63+ r1 = ret.Error(1)
64+ }
65+ return r0, r1
66+}
67+
68+// MockBrokerEndpointResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
69+type MockBrokerEndpointResolver_Resolve_Call struct {
70+ *mock.Call
71+}
72+
73+// Resolve is a helper method to define mock.On call
74+// - context1 context.Context
75+// - brokerOpenStreamRequest ports.BrokerOpenStreamRequest
76+func (_e *MockBrokerEndpointResolver_Expecter) Resolve(context1 any, brokerOpenStreamRequest any) *MockBrokerEndpointResolver_Resolve_Call {
77+ return &MockBrokerEndpointResolver_Resolve_Call{Call: _e.mock.On("Resolve", context1, brokerOpenStreamRequest)}
78+}
79+
80+func (_c *MockBrokerEndpointResolver_Resolve_Call) Run(run func(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest)) *MockBrokerEndpointResolver_Resolve_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ var arg1 ports.BrokerOpenStreamRequest
87+ if args[1] != nil {
88+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
89+ }
90+ run(
91+ arg0,
92+ arg1,
93+ )
94+ })
95+ return _c
96+}
97+
98+func (_c *MockBrokerEndpointResolver_Resolve_Call) Return(brokerResolvedEndpoint ports.BrokerResolvedEndpoint, err error) *MockBrokerEndpointResolver_Resolve_Call {
99+ _c.Call.Return(brokerResolvedEndpoint, err)
100+ return _c
101+}
102+
103+func (_c *MockBrokerEndpointResolver_Resolve_Call) RunAndReturn(run func(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)) *MockBrokerEndpointResolver_Resolve_Call {
104+ _c.Call.Return(run)
105+ return _c
106+}
107+
108+// NewMockBrokerLogicalConnection creates a new instance of MockBrokerLogicalConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
109+// The first argument is typically a *testing.T value.
110+func NewMockBrokerLogicalConnection(t interface {
111+ mock.TestingT
112+ Cleanup(func())
113+}) *MockBrokerLogicalConnection {
114+ mock := &MockBrokerLogicalConnection{}
115+ mock.Mock.Test(t)
116+
117+ t.Cleanup(func() { mock.AssertExpectations(t) })
118+
119+ return mock
120+}
121+
122+// MockBrokerLogicalConnection is an autogenerated mock type for the BrokerLogicalConnection type
123+type MockBrokerLogicalConnection struct {
124+ mock.Mock
125+}
126+
127+type MockBrokerLogicalConnection_Expecter struct {
128+ mock *mock.Mock
129+}
130+
131+func (_m *MockBrokerLogicalConnection) EXPECT() *MockBrokerLogicalConnection_Expecter {
132+ return &MockBrokerLogicalConnection_Expecter{mock: &_m.Mock}
133+}
134+
135+// Capabilities provides a mock function for the type MockBrokerLogicalConnection
136+func (_mock *MockBrokerLogicalConnection) Capabilities() protocol.ConnectionCapabilities {
137+ ret := _mock.Called()
138+
139+ if len(ret) == 0 {
140+ panic("no return value specified for Capabilities")
141+ }
142+
143+ var r0 protocol.ConnectionCapabilities
144+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
145+ r0 = returnFunc()
146+ } else {
147+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
148+ }
149+ return r0
150+}
151+
152+// MockBrokerLogicalConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
153+type MockBrokerLogicalConnection_Capabilities_Call struct {
154+ *mock.Call
155+}
156+
157+// Capabilities is a helper method to define mock.On call
158+func (_e *MockBrokerLogicalConnection_Expecter) Capabilities() *MockBrokerLogicalConnection_Capabilities_Call {
159+ return &MockBrokerLogicalConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
160+}
161+
162+func (_c *MockBrokerLogicalConnection_Capabilities_Call) Run(run func()) *MockBrokerLogicalConnection_Capabilities_Call {
163+ _c.Call.Run(func(args mock.Arguments) {
164+ run()
165+ })
166+ return _c
167+}
168+
169+func (_c *MockBrokerLogicalConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockBrokerLogicalConnection_Capabilities_Call {
170+ _c.Call.Return(connectionCapabilities)
171+ return _c
172+}
173+
174+func (_c *MockBrokerLogicalConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockBrokerLogicalConnection_Capabilities_Call {
175+ _c.Call.Return(run)
176+ return _c
177+}
178+
179+// Close provides a mock function for the type MockBrokerLogicalConnection
180+func (_mock *MockBrokerLogicalConnection) Close() error {
181+ ret := _mock.Called()
182+
183+ if len(ret) == 0 {
184+ panic("no return value specified for Close")
185+ }
186+
187+ var r0 error
188+ if returnFunc, ok := ret.Get(0).(func() error); ok {
189+ r0 = returnFunc()
190+ } else {
191+ r0 = ret.Error(0)
192+ }
193+ return r0
194+}
195+
196+// MockBrokerLogicalConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
197+type MockBrokerLogicalConnection_Close_Call struct {
198+ *mock.Call
199+}
200+
201+// Close is a helper method to define mock.On call
202+func (_e *MockBrokerLogicalConnection_Expecter) Close() *MockBrokerLogicalConnection_Close_Call {
203+ return &MockBrokerLogicalConnection_Close_Call{Call: _e.mock.On("Close")}
204+}
205+
206+func (_c *MockBrokerLogicalConnection_Close_Call) Run(run func()) *MockBrokerLogicalConnection_Close_Call {
207+ _c.Call.Run(func(args mock.Arguments) {
208+ run()
209+ })
210+ return _c
211+}
212+
213+func (_c *MockBrokerLogicalConnection_Close_Call) Return(err error) *MockBrokerLogicalConnection_Close_Call {
214+ _c.Call.Return(err)
215+ return _c
216+}
217+
218+func (_c *MockBrokerLogicalConnection_Close_Call) RunAndReturn(run func() error) *MockBrokerLogicalConnection_Close_Call {
219+ _c.Call.Return(run)
220+ return _c
221+}
222+
223+// Done provides a mock function for the type MockBrokerLogicalConnection
224+func (_mock *MockBrokerLogicalConnection) Done() <-chan struct{} {
225+ ret := _mock.Called()
226+
227+ if len(ret) == 0 {
228+ panic("no return value specified for Done")
229+ }
230+
231+ var r0 <-chan struct{}
232+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
233+ r0 = returnFunc()
234+ } else {
235+ if ret.Get(0) != nil {
236+ r0 = ret.Get(0).(<-chan struct{})
237+ }
238+ }
239+ return r0
240+}
241+
242+// MockBrokerLogicalConnection_Done_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Done'
243+type MockBrokerLogicalConnection_Done_Call struct {
244+ *mock.Call
245+}
246+
247+// Done is a helper method to define mock.On call
248+func (_e *MockBrokerLogicalConnection_Expecter) Done() *MockBrokerLogicalConnection_Done_Call {
249+ return &MockBrokerLogicalConnection_Done_Call{Call: _e.mock.On("Done")}
250+}
251+
252+func (_c *MockBrokerLogicalConnection_Done_Call) Run(run func()) *MockBrokerLogicalConnection_Done_Call {
253+ _c.Call.Run(func(args mock.Arguments) {
254+ run()
255+ })
256+ return _c
257+}
258+
259+func (_c *MockBrokerLogicalConnection_Done_Call) Return(valCh <-chan struct{}) *MockBrokerLogicalConnection_Done_Call {
260+ _c.Call.Return(valCh)
261+ return _c
262+}
263+
264+func (_c *MockBrokerLogicalConnection_Done_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerLogicalConnection_Done_Call {
265+ _c.Call.Return(run)
266+ return _c
267+}
268+
269+// Err provides a mock function for the type MockBrokerLogicalConnection
270+func (_mock *MockBrokerLogicalConnection) Err() error {
271+ ret := _mock.Called()
272+
273+ if len(ret) == 0 {
274+ panic("no return value specified for Err")
275+ }
276+
277+ var r0 error
278+ if returnFunc, ok := ret.Get(0).(func() error); ok {
279+ r0 = returnFunc()
280+ } else {
281+ r0 = ret.Error(0)
282+ }
283+ return r0
284+}
285+
286+// MockBrokerLogicalConnection_Err_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Err'
287+type MockBrokerLogicalConnection_Err_Call struct {
288+ *mock.Call
289+}
290+
291+// Err is a helper method to define mock.On call
292+func (_e *MockBrokerLogicalConnection_Expecter) Err() *MockBrokerLogicalConnection_Err_Call {
293+ return &MockBrokerLogicalConnection_Err_Call{Call: _e.mock.On("Err")}
294+}
295+
296+func (_c *MockBrokerLogicalConnection_Err_Call) Run(run func()) *MockBrokerLogicalConnection_Err_Call {
297+ _c.Call.Run(func(args mock.Arguments) {
298+ run()
299+ })
300+ return _c
301+}
302+
303+func (_c *MockBrokerLogicalConnection_Err_Call) Return(err error) *MockBrokerLogicalConnection_Err_Call {
304+ _c.Call.Return(err)
305+ return _c
306+}
307+
308+func (_c *MockBrokerLogicalConnection_Err_Call) RunAndReturn(run func() error) *MockBrokerLogicalConnection_Err_Call {
309+ _c.Call.Return(run)
310+ return _c
311+}
312+
313+// LinkEvents provides a mock function for the type MockBrokerLogicalConnection
314+func (_mock *MockBrokerLogicalConnection) LinkEvents() <-chan ports.LinkEvent {
315+ ret := _mock.Called()
316+
317+ if len(ret) == 0 {
318+ panic("no return value specified for LinkEvents")
319+ }
320+
321+ var r0 <-chan ports.LinkEvent
322+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
323+ r0 = returnFunc()
324+ } else {
325+ if ret.Get(0) != nil {
326+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
327+ }
328+ }
329+ return r0
330+}
331+
332+// MockBrokerLogicalConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
333+type MockBrokerLogicalConnection_LinkEvents_Call struct {
334+ *mock.Call
335+}
336+
337+// LinkEvents is a helper method to define mock.On call
338+func (_e *MockBrokerLogicalConnection_Expecter) LinkEvents() *MockBrokerLogicalConnection_LinkEvents_Call {
339+ return &MockBrokerLogicalConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
340+}
341+
342+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) Run(run func()) *MockBrokerLogicalConnection_LinkEvents_Call {
343+ _c.Call.Run(func(args mock.Arguments) {
344+ run()
345+ })
346+ return _c
347+}
348+
349+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockBrokerLogicalConnection_LinkEvents_Call {
350+ _c.Call.Return(linkEventCh)
351+ return _c
352+}
353+
354+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockBrokerLogicalConnection_LinkEvents_Call {
355+ _c.Call.Return(run)
356+ return _c
357+}
358+
359+// LinkState provides a mock function for the type MockBrokerLogicalConnection
360+func (_mock *MockBrokerLogicalConnection) LinkState() ports.LinkState {
361+ ret := _mock.Called()
362+
363+ if len(ret) == 0 {
364+ panic("no return value specified for LinkState")
365+ }
366+
367+ var r0 ports.LinkState
368+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
369+ r0 = returnFunc()
370+ } else {
371+ r0 = ret.Get(0).(ports.LinkState)
372+ }
373+ return r0
374+}
375+
376+// MockBrokerLogicalConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
377+type MockBrokerLogicalConnection_LinkState_Call struct {
378+ *mock.Call
379+}
380+
381+// LinkState is a helper method to define mock.On call
382+func (_e *MockBrokerLogicalConnection_Expecter) LinkState() *MockBrokerLogicalConnection_LinkState_Call {
383+ return &MockBrokerLogicalConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
384+}
385+
386+func (_c *MockBrokerLogicalConnection_LinkState_Call) Run(run func()) *MockBrokerLogicalConnection_LinkState_Call {
387+ _c.Call.Run(func(args mock.Arguments) {
388+ run()
389+ })
390+ return _c
391+}
392+
393+func (_c *MockBrokerLogicalConnection_LinkState_Call) Return(linkState ports.LinkState) *MockBrokerLogicalConnection_LinkState_Call {
394+ _c.Call.Return(linkState)
395+ return _c
396+}
397+
398+func (_c *MockBrokerLogicalConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockBrokerLogicalConnection_LinkState_Call {
399+ _c.Call.Return(run)
400+ return _c
401+}
402+
403+// ReceiveServer provides a mock function for the type MockBrokerLogicalConnection
404+func (_mock *MockBrokerLogicalConnection) ReceiveServer() (protocol.ServerMessage, error) {
405+ ret := _mock.Called()
406+
407+ if len(ret) == 0 {
408+ panic("no return value specified for ReceiveServer")
409+ }
410+
411+ var r0 protocol.ServerMessage
412+ var r1 error
413+ if returnFunc, ok := ret.Get(0).(func() (protocol.ServerMessage, error)); ok {
414+ return returnFunc()
415+ }
416+ if returnFunc, ok := ret.Get(0).(func() protocol.ServerMessage); ok {
417+ r0 = returnFunc()
418+ } else {
419+ if ret.Get(0) != nil {
420+ r0 = ret.Get(0).(protocol.ServerMessage)
421+ }
422+ }
423+ if returnFunc, ok := ret.Get(1).(func() error); ok {
424+ r1 = returnFunc()
425+ } else {
426+ r1 = ret.Error(1)
427+ }
428+ return r0, r1
429+}
430+
431+// MockBrokerLogicalConnection_ReceiveServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveServer'
432+type MockBrokerLogicalConnection_ReceiveServer_Call struct {
433+ *mock.Call
434+}
435+
436+// ReceiveServer is a helper method to define mock.On call
437+func (_e *MockBrokerLogicalConnection_Expecter) ReceiveServer() *MockBrokerLogicalConnection_ReceiveServer_Call {
438+ return &MockBrokerLogicalConnection_ReceiveServer_Call{Call: _e.mock.On("ReceiveServer")}
439+}
440+
441+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) Run(run func()) *MockBrokerLogicalConnection_ReceiveServer_Call {
442+ _c.Call.Run(func(args mock.Arguments) {
443+ run()
444+ })
445+ return _c
446+}
447+
448+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) Return(serverMessage protocol.ServerMessage, err error) *MockBrokerLogicalConnection_ReceiveServer_Call {
449+ _c.Call.Return(serverMessage, err)
450+ return _c
451+}
452+
453+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) RunAndReturn(run func() (protocol.ServerMessage, error)) *MockBrokerLogicalConnection_ReceiveServer_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
457+
458+// SendClient provides a mock function for the type MockBrokerLogicalConnection
459+func (_mock *MockBrokerLogicalConnection) SendClient(clientMessage protocol.ClientMessage) error {
460+ ret := _mock.Called(clientMessage)
461+
462+ if len(ret) == 0 {
463+ panic("no return value specified for SendClient")
464+ }
465+
466+ var r0 error
467+ if returnFunc, ok := ret.Get(0).(func(protocol.ClientMessage) error); ok {
468+ r0 = returnFunc(clientMessage)
469+ } else {
470+ r0 = ret.Error(0)
471+ }
472+ return r0
473+}
474+
475+// MockBrokerLogicalConnection_SendClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendClient'
476+type MockBrokerLogicalConnection_SendClient_Call struct {
477+ *mock.Call
478+}
479+
480+// SendClient is a helper method to define mock.On call
481+// - clientMessage protocol.ClientMessage
482+func (_e *MockBrokerLogicalConnection_Expecter) SendClient(clientMessage any) *MockBrokerLogicalConnection_SendClient_Call {
483+ return &MockBrokerLogicalConnection_SendClient_Call{Call: _e.mock.On("SendClient", clientMessage)}
484+}
485+
486+func (_c *MockBrokerLogicalConnection_SendClient_Call) Run(run func(clientMessage protocol.ClientMessage)) *MockBrokerLogicalConnection_SendClient_Call {
487+ _c.Call.Run(func(args mock.Arguments) {
488+ var arg0 protocol.ClientMessage
489+ if args[0] != nil {
490+ arg0 = args[0].(protocol.ClientMessage)
491+ }
492+ run(
493+ arg0,
494+ )
495+ })
496+ return _c
497+}
498+
499+func (_c *MockBrokerLogicalConnection_SendClient_Call) Return(err error) *MockBrokerLogicalConnection_SendClient_Call {
500+ _c.Call.Return(err)
501+ return _c
502+}
503+
504+func (_c *MockBrokerLogicalConnection_SendClient_Call) RunAndReturn(run func(clientMessage protocol.ClientMessage) error) *MockBrokerLogicalConnection_SendClient_Call {
505+ _c.Call.Return(run)
506+ return _c
507+}
@@ -0,0 +1,507 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewMockBrokerEndpointResolver creates a new instance of MockBrokerEndpointResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewMockBrokerEndpointResolver(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *MockBrokerEndpointResolver {
21+ mock := &MockBrokerEndpointResolver{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() { mock.AssertExpectations(t) })
25+
26+ return mock
27+}
28+
29+// MockBrokerEndpointResolver is an autogenerated mock type for the BrokerEndpointResolver type
30+type MockBrokerEndpointResolver struct {
31+ mock.Mock
32+}
33+
34+type MockBrokerEndpointResolver_Expecter struct {
35+ mock *mock.Mock
36+}
37+
38+func (_m *MockBrokerEndpointResolver) EXPECT() *MockBrokerEndpointResolver_Expecter {
39+ return &MockBrokerEndpointResolver_Expecter{mock: &_m.Mock}
40+}
41+
42+// Resolve provides a mock function for the type MockBrokerEndpointResolver
43+func (_mock *MockBrokerEndpointResolver) Resolve(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
44+ ret := _mock.Called(context1, brokerOpenStreamRequest)
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for Resolve")
48+ }
49+
50+ var r0 ports.BrokerResolvedEndpoint
51+ var r1 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)); ok {
53+ return returnFunc(context1, brokerOpenStreamRequest)
54+ }
55+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.BrokerResolvedEndpoint); ok {
56+ r0 = returnFunc(context1, brokerOpenStreamRequest)
57+ } else {
58+ r0 = ret.Get(0).(ports.BrokerResolvedEndpoint)
59+ }
60+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
61+ r1 = returnFunc(context1, brokerOpenStreamRequest)
62+ } else {
63+ r1 = ret.Error(1)
64+ }
65+ return r0, r1
66+}
67+
68+// MockBrokerEndpointResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
69+type MockBrokerEndpointResolver_Resolve_Call struct {
70+ *mock.Call
71+}
72+
73+// Resolve is a helper method to define mock.On call
74+// - context1 context.Context
75+// - brokerOpenStreamRequest ports.BrokerOpenStreamRequest
76+func (_e *MockBrokerEndpointResolver_Expecter) Resolve(context1 any, brokerOpenStreamRequest any) *MockBrokerEndpointResolver_Resolve_Call {
77+ return &MockBrokerEndpointResolver_Resolve_Call{Call: _e.mock.On("Resolve", context1, brokerOpenStreamRequest)}
78+}
79+
80+func (_c *MockBrokerEndpointResolver_Resolve_Call) Run(run func(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest)) *MockBrokerEndpointResolver_Resolve_Call {
81+ _c.Call.Run(func(args mock.Arguments) {
82+ var arg0 context.Context
83+ if args[0] != nil {
84+ arg0 = args[0].(context.Context)
85+ }
86+ var arg1 ports.BrokerOpenStreamRequest
87+ if args[1] != nil {
88+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
89+ }
90+ run(
91+ arg0,
92+ arg1,
93+ )
94+ })
95+ return _c
96+}
97+
98+func (_c *MockBrokerEndpointResolver_Resolve_Call) Return(brokerResolvedEndpoint ports.BrokerResolvedEndpoint, err error) *MockBrokerEndpointResolver_Resolve_Call {
99+ _c.Call.Return(brokerResolvedEndpoint, err)
100+ return _c
101+}
102+
103+func (_c *MockBrokerEndpointResolver_Resolve_Call) RunAndReturn(run func(context1 context.Context, brokerOpenStreamRequest ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)) *MockBrokerEndpointResolver_Resolve_Call {
104+ _c.Call.Return(run)
105+ return _c
106+}
107+
108+// NewMockBrokerLogicalConnection creates a new instance of MockBrokerLogicalConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
109+// The first argument is typically a *testing.T value.
110+func NewMockBrokerLogicalConnection(t interface {
111+ mock.TestingT
112+ Cleanup(func())
113+}) *MockBrokerLogicalConnection {
114+ mock := &MockBrokerLogicalConnection{}
115+ mock.Mock.Test(t)
116+
117+ t.Cleanup(func() { mock.AssertExpectations(t) })
118+
119+ return mock
120+}
121+
122+// MockBrokerLogicalConnection is an autogenerated mock type for the BrokerLogicalConnection type
123+type MockBrokerLogicalConnection struct {
124+ mock.Mock
125+}
126+
127+type MockBrokerLogicalConnection_Expecter struct {
128+ mock *mock.Mock
129+}
130+
131+func (_m *MockBrokerLogicalConnection) EXPECT() *MockBrokerLogicalConnection_Expecter {
132+ return &MockBrokerLogicalConnection_Expecter{mock: &_m.Mock}
133+}
134+
135+// Capabilities provides a mock function for the type MockBrokerLogicalConnection
136+func (_mock *MockBrokerLogicalConnection) Capabilities() protocol.ConnectionCapabilities {
137+ ret := _mock.Called()
138+
139+ if len(ret) == 0 {
140+ panic("no return value specified for Capabilities")
141+ }
142+
143+ var r0 protocol.ConnectionCapabilities
144+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
145+ r0 = returnFunc()
146+ } else {
147+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
148+ }
149+ return r0
150+}
151+
152+// MockBrokerLogicalConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
153+type MockBrokerLogicalConnection_Capabilities_Call struct {
154+ *mock.Call
155+}
156+
157+// Capabilities is a helper method to define mock.On call
158+func (_e *MockBrokerLogicalConnection_Expecter) Capabilities() *MockBrokerLogicalConnection_Capabilities_Call {
159+ return &MockBrokerLogicalConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
160+}
161+
162+func (_c *MockBrokerLogicalConnection_Capabilities_Call) Run(run func()) *MockBrokerLogicalConnection_Capabilities_Call {
163+ _c.Call.Run(func(args mock.Arguments) {
164+ run()
165+ })
166+ return _c
167+}
168+
169+func (_c *MockBrokerLogicalConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockBrokerLogicalConnection_Capabilities_Call {
170+ _c.Call.Return(connectionCapabilities)
171+ return _c
172+}
173+
174+func (_c *MockBrokerLogicalConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockBrokerLogicalConnection_Capabilities_Call {
175+ _c.Call.Return(run)
176+ return _c
177+}
178+
179+// Close provides a mock function for the type MockBrokerLogicalConnection
180+func (_mock *MockBrokerLogicalConnection) Close() error {
181+ ret := _mock.Called()
182+
183+ if len(ret) == 0 {
184+ panic("no return value specified for Close")
185+ }
186+
187+ var r0 error
188+ if returnFunc, ok := ret.Get(0).(func() error); ok {
189+ r0 = returnFunc()
190+ } else {
191+ r0 = ret.Error(0)
192+ }
193+ return r0
194+}
195+
196+// MockBrokerLogicalConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
197+type MockBrokerLogicalConnection_Close_Call struct {
198+ *mock.Call
199+}
200+
201+// Close is a helper method to define mock.On call
202+func (_e *MockBrokerLogicalConnection_Expecter) Close() *MockBrokerLogicalConnection_Close_Call {
203+ return &MockBrokerLogicalConnection_Close_Call{Call: _e.mock.On("Close")}
204+}
205+
206+func (_c *MockBrokerLogicalConnection_Close_Call) Run(run func()) *MockBrokerLogicalConnection_Close_Call {
207+ _c.Call.Run(func(args mock.Arguments) {
208+ run()
209+ })
210+ return _c
211+}
212+
213+func (_c *MockBrokerLogicalConnection_Close_Call) Return(err error) *MockBrokerLogicalConnection_Close_Call {
214+ _c.Call.Return(err)
215+ return _c
216+}
217+
218+func (_c *MockBrokerLogicalConnection_Close_Call) RunAndReturn(run func() error) *MockBrokerLogicalConnection_Close_Call {
219+ _c.Call.Return(run)
220+ return _c
221+}
222+
223+// Done provides a mock function for the type MockBrokerLogicalConnection
224+func (_mock *MockBrokerLogicalConnection) Done() <-chan struct{} {
225+ ret := _mock.Called()
226+
227+ if len(ret) == 0 {
228+ panic("no return value specified for Done")
229+ }
230+
231+ var r0 <-chan struct{}
232+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
233+ r0 = returnFunc()
234+ } else {
235+ if ret.Get(0) != nil {
236+ r0 = ret.Get(0).(<-chan struct{})
237+ }
238+ }
239+ return r0
240+}
241+
242+// MockBrokerLogicalConnection_Done_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Done'
243+type MockBrokerLogicalConnection_Done_Call struct {
244+ *mock.Call
245+}
246+
247+// Done is a helper method to define mock.On call
248+func (_e *MockBrokerLogicalConnection_Expecter) Done() *MockBrokerLogicalConnection_Done_Call {
249+ return &MockBrokerLogicalConnection_Done_Call{Call: _e.mock.On("Done")}
250+}
251+
252+func (_c *MockBrokerLogicalConnection_Done_Call) Run(run func()) *MockBrokerLogicalConnection_Done_Call {
253+ _c.Call.Run(func(args mock.Arguments) {
254+ run()
255+ })
256+ return _c
257+}
258+
259+func (_c *MockBrokerLogicalConnection_Done_Call) Return(valCh <-chan struct{}) *MockBrokerLogicalConnection_Done_Call {
260+ _c.Call.Return(valCh)
261+ return _c
262+}
263+
264+func (_c *MockBrokerLogicalConnection_Done_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerLogicalConnection_Done_Call {
265+ _c.Call.Return(run)
266+ return _c
267+}
268+
269+// Err provides a mock function for the type MockBrokerLogicalConnection
270+func (_mock *MockBrokerLogicalConnection) Err() error {
271+ ret := _mock.Called()
272+
273+ if len(ret) == 0 {
274+ panic("no return value specified for Err")
275+ }
276+
277+ var r0 error
278+ if returnFunc, ok := ret.Get(0).(func() error); ok {
279+ r0 = returnFunc()
280+ } else {
281+ r0 = ret.Error(0)
282+ }
283+ return r0
284+}
285+
286+// MockBrokerLogicalConnection_Err_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Err'
287+type MockBrokerLogicalConnection_Err_Call struct {
288+ *mock.Call
289+}
290+
291+// Err is a helper method to define mock.On call
292+func (_e *MockBrokerLogicalConnection_Expecter) Err() *MockBrokerLogicalConnection_Err_Call {
293+ return &MockBrokerLogicalConnection_Err_Call{Call: _e.mock.On("Err")}
294+}
295+
296+func (_c *MockBrokerLogicalConnection_Err_Call) Run(run func()) *MockBrokerLogicalConnection_Err_Call {
297+ _c.Call.Run(func(args mock.Arguments) {
298+ run()
299+ })
300+ return _c
301+}
302+
303+func (_c *MockBrokerLogicalConnection_Err_Call) Return(err error) *MockBrokerLogicalConnection_Err_Call {
304+ _c.Call.Return(err)
305+ return _c
306+}
307+
308+func (_c *MockBrokerLogicalConnection_Err_Call) RunAndReturn(run func() error) *MockBrokerLogicalConnection_Err_Call {
309+ _c.Call.Return(run)
310+ return _c
311+}
312+
313+// LinkEvents provides a mock function for the type MockBrokerLogicalConnection
314+func (_mock *MockBrokerLogicalConnection) LinkEvents() <-chan ports.LinkEvent {
315+ ret := _mock.Called()
316+
317+ if len(ret) == 0 {
318+ panic("no return value specified for LinkEvents")
319+ }
320+
321+ var r0 <-chan ports.LinkEvent
322+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
323+ r0 = returnFunc()
324+ } else {
325+ if ret.Get(0) != nil {
326+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
327+ }
328+ }
329+ return r0
330+}
331+
332+// MockBrokerLogicalConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
333+type MockBrokerLogicalConnection_LinkEvents_Call struct {
334+ *mock.Call
335+}
336+
337+// LinkEvents is a helper method to define mock.On call
338+func (_e *MockBrokerLogicalConnection_Expecter) LinkEvents() *MockBrokerLogicalConnection_LinkEvents_Call {
339+ return &MockBrokerLogicalConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
340+}
341+
342+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) Run(run func()) *MockBrokerLogicalConnection_LinkEvents_Call {
343+ _c.Call.Run(func(args mock.Arguments) {
344+ run()
345+ })
346+ return _c
347+}
348+
349+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockBrokerLogicalConnection_LinkEvents_Call {
350+ _c.Call.Return(linkEventCh)
351+ return _c
352+}
353+
354+func (_c *MockBrokerLogicalConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockBrokerLogicalConnection_LinkEvents_Call {
355+ _c.Call.Return(run)
356+ return _c
357+}
358+
359+// LinkState provides a mock function for the type MockBrokerLogicalConnection
360+func (_mock *MockBrokerLogicalConnection) LinkState() ports.LinkState {
361+ ret := _mock.Called()
362+
363+ if len(ret) == 0 {
364+ panic("no return value specified for LinkState")
365+ }
366+
367+ var r0 ports.LinkState
368+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
369+ r0 = returnFunc()
370+ } else {
371+ r0 = ret.Get(0).(ports.LinkState)
372+ }
373+ return r0
374+}
375+
376+// MockBrokerLogicalConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
377+type MockBrokerLogicalConnection_LinkState_Call struct {
378+ *mock.Call
379+}
380+
381+// LinkState is a helper method to define mock.On call
382+func (_e *MockBrokerLogicalConnection_Expecter) LinkState() *MockBrokerLogicalConnection_LinkState_Call {
383+ return &MockBrokerLogicalConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
384+}
385+
386+func (_c *MockBrokerLogicalConnection_LinkState_Call) Run(run func()) *MockBrokerLogicalConnection_LinkState_Call {
387+ _c.Call.Run(func(args mock.Arguments) {
388+ run()
389+ })
390+ return _c
391+}
392+
393+func (_c *MockBrokerLogicalConnection_LinkState_Call) Return(linkState ports.LinkState) *MockBrokerLogicalConnection_LinkState_Call {
394+ _c.Call.Return(linkState)
395+ return _c
396+}
397+
398+func (_c *MockBrokerLogicalConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockBrokerLogicalConnection_LinkState_Call {
399+ _c.Call.Return(run)
400+ return _c
401+}
402+
403+// ReceiveServer provides a mock function for the type MockBrokerLogicalConnection
404+func (_mock *MockBrokerLogicalConnection) ReceiveServer() (protocol.ServerMessage, error) {
405+ ret := _mock.Called()
406+
407+ if len(ret) == 0 {
408+ panic("no return value specified for ReceiveServer")
409+ }
410+
411+ var r0 protocol.ServerMessage
412+ var r1 error
413+ if returnFunc, ok := ret.Get(0).(func() (protocol.ServerMessage, error)); ok {
414+ return returnFunc()
415+ }
416+ if returnFunc, ok := ret.Get(0).(func() protocol.ServerMessage); ok {
417+ r0 = returnFunc()
418+ } else {
419+ if ret.Get(0) != nil {
420+ r0 = ret.Get(0).(protocol.ServerMessage)
421+ }
422+ }
423+ if returnFunc, ok := ret.Get(1).(func() error); ok {
424+ r1 = returnFunc()
425+ } else {
426+ r1 = ret.Error(1)
427+ }
428+ return r0, r1
429+}
430+
431+// MockBrokerLogicalConnection_ReceiveServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveServer'
432+type MockBrokerLogicalConnection_ReceiveServer_Call struct {
433+ *mock.Call
434+}
435+
436+// ReceiveServer is a helper method to define mock.On call
437+func (_e *MockBrokerLogicalConnection_Expecter) ReceiveServer() *MockBrokerLogicalConnection_ReceiveServer_Call {
438+ return &MockBrokerLogicalConnection_ReceiveServer_Call{Call: _e.mock.On("ReceiveServer")}
439+}
440+
441+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) Run(run func()) *MockBrokerLogicalConnection_ReceiveServer_Call {
442+ _c.Call.Run(func(args mock.Arguments) {
443+ run()
444+ })
445+ return _c
446+}
447+
448+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) Return(serverMessage protocol.ServerMessage, err error) *MockBrokerLogicalConnection_ReceiveServer_Call {
449+ _c.Call.Return(serverMessage, err)
450+ return _c
451+}
452+
453+func (_c *MockBrokerLogicalConnection_ReceiveServer_Call) RunAndReturn(run func() (protocol.ServerMessage, error)) *MockBrokerLogicalConnection_ReceiveServer_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
457+
458+// SendClient provides a mock function for the type MockBrokerLogicalConnection
459+func (_mock *MockBrokerLogicalConnection) SendClient(clientMessage protocol.ClientMessage) error {
460+ ret := _mock.Called(clientMessage)
461+
462+ if len(ret) == 0 {
463+ panic("no return value specified for SendClient")
464+ }
465+
466+ var r0 error
467+ if returnFunc, ok := ret.Get(0).(func(protocol.ClientMessage) error); ok {
468+ r0 = returnFunc(clientMessage)
469+ } else {
470+ r0 = ret.Error(0)
471+ }
472+ return r0
473+}
474+
475+// MockBrokerLogicalConnection_SendClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendClient'
476+type MockBrokerLogicalConnection_SendClient_Call struct {
477+ *mock.Call
478+}
479+
480+// SendClient is a helper method to define mock.On call
481+// - clientMessage protocol.ClientMessage
482+func (_e *MockBrokerLogicalConnection_Expecter) SendClient(clientMessage any) *MockBrokerLogicalConnection_SendClient_Call {
483+ return &MockBrokerLogicalConnection_SendClient_Call{Call: _e.mock.On("SendClient", clientMessage)}
484+}
485+
486+func (_c *MockBrokerLogicalConnection_SendClient_Call) Run(run func(clientMessage protocol.ClientMessage)) *MockBrokerLogicalConnection_SendClient_Call {
487+ _c.Call.Run(func(args mock.Arguments) {
488+ var arg0 protocol.ClientMessage
489+ if args[0] != nil {
490+ arg0 = args[0].(protocol.ClientMessage)
491+ }
492+ run(
493+ arg0,
494+ )
495+ })
496+ return _c
497+}
498+
499+func (_c *MockBrokerLogicalConnection_SendClient_Call) Return(err error) *MockBrokerLogicalConnection_SendClient_Call {
500+ _c.Call.Return(err)
501+ return _c
502+}
503+
504+func (_c *MockBrokerLogicalConnection_SendClient_Call) RunAndReturn(run func(clientMessage protocol.ClientMessage) error) *MockBrokerLogicalConnection_SendClient_Call {
505+ _c.Call.Return(run)
506+ return _c
507+}
A internal/usecase/broker/pool.go +410 -0
@@ -0,0 +1,410 @@
1+package broker
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "errors"
7+ "sync"
8+ "time"
9+
10+ "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/ports"
12+)
13+
14+// PoolLimits bound live physical keys, clients, reservations (including I/O in
15+// flight), and logical streams. No unbounded queue or alias cache is retained.
16+type PoolLimits struct {
17+ Physical, Clients, Streams, StreamsPerClient int
18+ Idle time.Duration
19+}
20+
21+type poolKey struct {
22+ identity ports.BrokerDaemonIdentity
23+ policy ports.BrokerPolicy
24+}
25+type poolClient struct {
26+ high ports.BrokerStreamID
27+ streams map[ports.BrokerStreamID]*reservation
28+}
29+type reservation struct{ cancel context.CancelFunc }
30+type poolEntry struct {
31+ key poolKey
32+ ctx context.Context
33+ cancel context.CancelFunc
34+ ready, wake chan struct{}
35+ physical ports.BrokerPhysicalConnection
36+ err error
37+ refs int
38+ retiring bool
39+}
40+
41+// Pool is transport independent and is not production-composed in P2.2.
42+// mu protects bookkeeping only; no port I/O or Close runs under it.
43+type Pool struct {
44+ mu sync.Mutex
45+ epoch ports.BrokerEpoch
46+ resolver ports.BrokerEndpointResolver
47+ connector ports.BrokerEndpointConnector
48+ clock ports.Clock
49+ limits PoolLimits
50+ ctx context.Context
51+ cancel context.CancelFunc
52+ closed bool
53+ next uint64
54+ clients map[ports.BrokerConnectionID]*poolClient
55+ entries map[poolKey]*poolEntry
56+ streams int
57+ wg sync.WaitGroup
58+}
59+
60+func NewPool(epoch ports.BrokerEpoch, resolver ports.BrokerEndpointResolver, connector ports.BrokerEndpointConnector, clock ports.Clock, limits PoolLimits) (*Pool, error) {
61+ if epoch == 0 || resolver == nil || connector == nil || clock == nil || limits.Physical <= 0 || limits.Clients <= 0 || limits.Streams <= 0 || limits.StreamsPerClient <= 0 || limits.Idle <= 0 {
62+ return nil, ports.BrokerAdmissionInvalid
63+ }
64+ ctx, cancel := context.WithCancel(context.Background())
65+ return &Pool{epoch: epoch, resolver: resolver, connector: connector, clock: clock, limits: limits, ctx: ctx, cancel: cancel, clients: make(map[ports.BrokerConnectionID]*poolClient), entries: make(map[poolKey]*poolEntry)}, nil
66+}
67+
68+// RegisterClient issues epoch-scoped nonreusable identities. The bounded active
69+// map needs no retired-ID tombstones. Stream IDs must increase within a client.
70+func (p *Pool) RegisterClient() (ports.BrokerConnectionID, error) {
71+ p.mu.Lock()
72+ defer p.mu.Unlock()
73+ if p.closed {
74+ return ports.BrokerConnectionID{}, ports.BrokerAdmissionClosed
75+ }
76+ if len(p.clients) >= p.limits.Clients || p.next == ^uint64(0) {
77+ return ports.BrokerConnectionID{}, ports.BrokerAdmissionLimit
78+ }
79+ p.next++
80+ var id ports.BrokerConnectionID
81+ binary.BigEndian.PutUint64(id[:8], uint64(p.epoch))
82+ binary.BigEndian.PutUint64(id[8:], p.next)
83+ p.clients[id] = &poolClient{streams: make(map[ports.BrokerStreamID]*reservation)}
84+ return id, nil
85+}
86+
87+func (p *Pool) CloseClient(id ports.BrokerConnectionID) {
88+ p.mu.Lock()
89+ c := p.clients[id]
90+ delete(p.clients, id)
91+ var cancels []context.CancelFunc
92+ if c != nil {
93+ for _, r := range c.streams {
94+ cancels = append(cancels, r.cancel)
95+ }
96+ }
97+ p.mu.Unlock()
98+ for _, cancel := range cancels {
99+ cancel()
100+ }
101+}
102+
103+func (p *Pool) CloseStream(id ports.BrokerConnectionID, stream ports.BrokerStreamID) error {
104+ p.mu.Lock()
105+ c := p.clients[id]
106+ var r *reservation
107+ if c != nil {
108+ r = c.streams[stream]
109+ }
110+ p.mu.Unlock()
111+ if r == nil {
112+ return ports.BrokerAdmissionStale
113+ }
114+ r.cancel()
115+ return nil
116+}
117+
118+func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
119+ if err := req.Validate(); err != nil {
120+ return nil, errors.Join(ports.BrokerAdmissionInvalid, err)
121+ }
122+ if req.Epoch != p.epoch {
123+ return nil, ports.BrokerError{Code: ports.BrokerErrorStaleEpoch}
124+ }
125+ ctx, cancel := context.WithCancel(ctx)
126+ p.mu.Lock()
127+ c := p.clients[req.Connection]
128+ if p.closed {
129+ p.mu.Unlock()
130+ cancel()
131+ return nil, ports.BrokerAdmissionClosed
132+ }
133+ if c == nil || req.Stream <= c.high {
134+ p.mu.Unlock()
135+ cancel()
136+ return nil, ports.BrokerAdmissionStale
137+ }
138+ c.high = req.Stream
139+ if p.streams >= p.limits.Streams || len(c.streams) >= p.limits.StreamsPerClient {
140+ p.mu.Unlock()
141+ cancel()
142+ return nil, ports.BrokerAdmissionLimit
143+ }
144+ r := &reservation{cancel: cancel}
145+ c.streams[req.Stream] = r
146+ p.streams++
147+ p.wg.Add(1)
148+ p.mu.Unlock()
149+ stop := context.AfterFunc(p.ctx, cancel)
150+ var entry *poolEntry
151+ release := func() {
152+ stop()
153+ cancel()
154+ p.mu.Lock()
155+ delete(c.streams, req.Stream)
156+ p.streams--
157+ var abandon context.CancelFunc
158+ if entry != nil {
159+ entry.refs--
160+ if entry.refs == 0 {
161+ select {
162+ case <-entry.ready:
163+ default:
164+ entry.retiring = true
165+ abandon = entry.cancel
166+ }
167+ }
168+ select {
169+ case entry.wake <- struct{}{}:
170+ default:
171+ }
172+ }
173+ p.mu.Unlock()
174+ if abandon != nil {
175+ abandon()
176+ }
177+ p.wg.Done()
178+ }
179+ success := false
180+ defer func() {
181+ if !success {
182+ release()
183+ }
184+ }()
185+ req.Env = append([]string(nil), req.Env...)
186+ resolved, err := p.resolver.Resolve(ctx, req)
187+ if err != nil {
188+ return nil, normalizePoolError(err)
189+ }
190+ if err := resolved.Validate(); err != nil {
191+ return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: err}
192+ }
193+ if !resolved.Policy.Compatible(req.Policy) {
194+ return nil, ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}
195+ }
196+ if err = ctx.Err(); err != nil {
197+ return nil, normalizePoolError(err)
198+ }
199+ key := poolKey{resolved.Identity, resolved.Policy}
200+ p.mu.Lock()
201+ if p.closed {
202+ p.mu.Unlock()
203+ return nil, ports.BrokerAdmissionClosed
204+ }
205+ entry = p.entries[key]
206+ if entry != nil && entry.retiring {
207+ entry = nil
208+ p.mu.Unlock()
209+ return nil, ports.BrokerError{Code: ports.BrokerErrorUnavailable}
210+ }
211+ if entry == nil {
212+ if len(p.entries) >= p.limits.Physical {
213+ p.mu.Unlock()
214+ return nil, ports.BrokerAdmissionLimit
215+ }
216+ ectx, ecancel := context.WithCancel(p.ctx)
217+ entry = &poolEntry{key: key, ctx: ectx, cancel: ecancel, ready: make(chan struct{}), wake: make(chan struct{}, 1)}
218+ p.entries[key] = entry
219+ p.wg.Add(1)
220+ go p.runEntry(entry, resolved)
221+ }
222+ entry.refs++
223+ p.mu.Unlock()
224+ select {
225+ case <-ctx.Done():
226+ return nil, normalizePoolError(ctx.Err())
227+ case <-entry.ready:
228+ }
229+ if entry.err != nil {
230+ return nil, entry.err
231+ }
232+ openCtx, openCancel := context.WithCancel(ctx)
233+ stopEntry := context.AfterFunc(entry.ctx, openCancel)
234+ raw, err := entry.physical.OpenStream(openCtx, req)
235+ stopEntry()
236+ openCancel()
237+ if err != nil {
238+ if raw != nil {
239+ _ = raw.Close()
240+ }
241+ return nil, p.streamTerminal(ctx, entry, req, normalizePoolError(err))
242+ }
243+ if raw == nil {
244+ return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ports.BrokerAdmissionInvalid}
245+ }
246+ if terminal := p.streamTerminal(ctx, entry, req, nil); terminal != nil {
247+ _ = raw.Close()
248+ return nil, terminal
249+ }
250+ stream := &pooledStream{BrokerLogicalConnection: raw, done: make(chan struct{}), release: release}
251+ success = true
252+ go func() {
253+ var terminal error
254+ select {
255+ case <-ctx.Done():
256+ terminal = ports.BrokerError{Code: ports.BrokerErrorCancelled}
257+ case <-entry.ctx.Done():
258+ terminal = ports.BrokerError{Code: ports.BrokerErrorAttachmentLost}
259+ case <-entry.physical.Done():
260+ case <-raw.Done():
261+ terminal = raw.Err()
262+ case <-stream.done:
263+ return
264+ }
265+ stream.finish(p.streamTerminal(ctx, entry, req, terminal))
266+ }()
267+ return stream, nil
268+}
269+
270+func (p *Pool) runEntry(e *poolEntry, endpoint ports.BrokerResolvedEndpoint) {
271+ defer p.wg.Done()
272+ physical, err := p.connector.Connect(e.ctx, endpoint)
273+ if err == nil && (physical == nil || physical.Identity() != endpoint.Identity || !physical.Policy().Compatible(endpoint.Policy) || physical.Incarnation().Validate() != nil) {
274+ err = ports.BrokerError{Code: ports.BrokerErrorIncompatible}
275+ }
276+ e.physical = physical
277+ e.err = normalizePoolError(err)
278+ close(e.ready)
279+ if err == nil {
280+ timer := p.clock.NewTimer(p.limits.Idle)
281+ defer stopPoolTimer(timer)
282+ for {
283+ p.mu.Lock()
284+ idle := e.refs == 0
285+ p.mu.Unlock()
286+ stopPoolTimer(timer)
287+ if idle {
288+ timer.Reset(p.limits.Idle)
289+ }
290+ select {
291+ case <-e.ctx.Done():
292+ goto retire
293+ case <-physical.Done():
294+ goto retire
295+ case <-e.wake:
296+ continue
297+ case <-timer.C():
298+ p.mu.Lock()
299+ if e.refs == 0 {
300+ e.retiring = true
301+ p.mu.Unlock()
302+ goto retire
303+ }
304+ p.mu.Unlock()
305+ }
306+ }
307+ }
308+retire:
309+ p.mu.Lock()
310+ e.retiring = true
311+ p.mu.Unlock()
312+ e.cancel()
313+ if physical != nil {
314+ _ = physical.Close()
315+ }
316+ // Keep the key occupied until Close completes: never overlap physicals.
317+ p.mu.Lock()
318+ delete(p.entries, e.key)
319+ p.mu.Unlock()
320+}
321+
322+// Close cancels pending operations, closes transports, and joins all owned
323+// workers. Bounded completion depends on the explicit cancellation/Close port
324+// contract; callers must not supply adapters that ignore it.
325+func (p *Pool) Close() error {
326+ p.mu.Lock()
327+ p.closed = true
328+ p.mu.Unlock()
329+ p.cancel()
330+ p.wg.Wait()
331+ return nil
332+}
333+
334+type pooledStream struct {
335+ ports.BrokerLogicalConnection
336+ once sync.Once
337+ done chan struct{}
338+ err error
339+ closeErr error
340+ release func()
341+}
342+
343+func (s *pooledStream) Done() <-chan struct{} { return s.done }
344+func (s *pooledStream) Err() error {
345+ select {
346+ case <-s.done:
347+ return s.err
348+ default:
349+ return nil
350+ }
351+}
352+func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr }
353+func (s *pooledStream) finish(err error) {
354+ s.once.Do(func() { s.err = err; s.closeErr = s.BrokerLogicalConnection.Close(); s.release(); close(s.done) })
355+}
356+
357+// One goroutine owns this timer. Drain a buffered expiry before every reset,
358+// including clocks implementing pre-Go-1.23 timer semantics.
359+func stopPoolTimer(timer ports.Timer) {
360+ if !timer.Stop() {
361+ select {
362+ case <-timer.C():
363+ default:
364+ }
365+ }
366+}
367+
368+func normalizePoolError(err error) error {
369+ if err == nil {
370+ return nil
371+ }
372+ code := ports.BrokerErrorUnavailable
373+ var typed ports.BrokerError
374+ var typedPtr *ports.BrokerError
375+ switch {
376+ case errors.Is(err, context.DeadlineExceeded):
377+ code = ports.BrokerErrorTimeout
378+ case errors.Is(err, context.Canceled):
379+ code = ports.BrokerErrorCancelled
380+ case errors.As(err, &typed) && typed.Validate() == nil:
381+ code = typed.Code
382+ case errors.As(err, &typedPtr) && typedPtr != nil && typedPtr.Validate() == nil:
383+ code = typedPtr.Code
384+ }
385+ return ports.BrokerError{Code: code, Cause: err}
386+}
387+
388+// Precedence at terminal observation: pool shutdown, request/client cancellation,
389+// physical Done, entry retirement, logical result. A completed stream is immutable.
390+func (p *Pool) streamTerminal(ctx context.Context, e *poolEntry, req ports.BrokerOpenStreamRequest, fallback error) error {
391+ if p.ctx.Err() != nil {
392+ return ports.BrokerError{Code: ports.BrokerErrorCancelled, Cause: p.ctx.Err()}
393+ }
394+ if ctx.Err() != nil {
395+ return normalizePoolError(ctx.Err())
396+ }
397+ select {
398+ case <-e.physical.Done():
399+ kind := e.physical.FailureKind()
400+ if kind < domain.RemoteFailureTransport || kind > domain.RemoteFailureInvalidResponse {
401+ kind = domain.RemoteFailureTransport
402+ }
403+ return ports.BrokerStreamLost{Epoch: req.Epoch, Connection: req.Connection, Stream: req.Stream, Cause: kind, Err: e.physical.Err()}
404+ default:
405+ }
406+ if e.ctx.Err() != nil {
407+ return ports.BrokerError{Code: ports.BrokerErrorAttachmentLost}
408+ }
409+ return normalizePoolError(fallback)
410+}
@@ -0,0 +1,410 @@
1+package broker
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "errors"
7+ "sync"
8+ "time"
9+
10+ "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/ports"
12+)
13+
14+// PoolLimits bound live physical keys, clients, reservations (including I/O in
15+// flight), and logical streams. No unbounded queue or alias cache is retained.
16+type PoolLimits struct {
17+ Physical, Clients, Streams, StreamsPerClient int
18+ Idle time.Duration
19+}
20+
21+type poolKey struct {
22+ identity ports.BrokerDaemonIdentity
23+ policy ports.BrokerPolicy
24+}
25+type poolClient struct {
26+ high ports.BrokerStreamID
27+ streams map[ports.BrokerStreamID]*reservation
28+}
29+type reservation struct{ cancel context.CancelFunc }
30+type poolEntry struct {
31+ key poolKey
32+ ctx context.Context
33+ cancel context.CancelFunc
34+ ready, wake chan struct{}
35+ physical ports.BrokerPhysicalConnection
36+ err error
37+ refs int
38+ retiring bool
39+}
40+
41+// Pool is transport independent and is not production-composed in P2.2.
42+// mu protects bookkeeping only; no port I/O or Close runs under it.
43+type Pool struct {
44+ mu sync.Mutex
45+ epoch ports.BrokerEpoch
46+ resolver ports.BrokerEndpointResolver
47+ connector ports.BrokerEndpointConnector
48+ clock ports.Clock
49+ limits PoolLimits
50+ ctx context.Context
51+ cancel context.CancelFunc
52+ closed bool
53+ next uint64
54+ clients map[ports.BrokerConnectionID]*poolClient
55+ entries map[poolKey]*poolEntry
56+ streams int
57+ wg sync.WaitGroup
58+}
59+
60+func NewPool(epoch ports.BrokerEpoch, resolver ports.BrokerEndpointResolver, connector ports.BrokerEndpointConnector, clock ports.Clock, limits PoolLimits) (*Pool, error) {
61+ if epoch == 0 || resolver == nil || connector == nil || clock == nil || limits.Physical <= 0 || limits.Clients <= 0 || limits.Streams <= 0 || limits.StreamsPerClient <= 0 || limits.Idle <= 0 {
62+ return nil, ports.BrokerAdmissionInvalid
63+ }
64+ ctx, cancel := context.WithCancel(context.Background())
65+ return &Pool{epoch: epoch, resolver: resolver, connector: connector, clock: clock, limits: limits, ctx: ctx, cancel: cancel, clients: make(map[ports.BrokerConnectionID]*poolClient), entries: make(map[poolKey]*poolEntry)}, nil
66+}
67+
68+// RegisterClient issues epoch-scoped nonreusable identities. The bounded active
69+// map needs no retired-ID tombstones. Stream IDs must increase within a client.
70+func (p *Pool) RegisterClient() (ports.BrokerConnectionID, error) {
71+ p.mu.Lock()
72+ defer p.mu.Unlock()
73+ if p.closed {
74+ return ports.BrokerConnectionID{}, ports.BrokerAdmissionClosed
75+ }
76+ if len(p.clients) >= p.limits.Clients || p.next == ^uint64(0) {
77+ return ports.BrokerConnectionID{}, ports.BrokerAdmissionLimit
78+ }
79+ p.next++
80+ var id ports.BrokerConnectionID
81+ binary.BigEndian.PutUint64(id[:8], uint64(p.epoch))
82+ binary.BigEndian.PutUint64(id[8:], p.next)
83+ p.clients[id] = &poolClient{streams: make(map[ports.BrokerStreamID]*reservation)}
84+ return id, nil
85+}
86+
87+func (p *Pool) CloseClient(id ports.BrokerConnectionID) {
88+ p.mu.Lock()
89+ c := p.clients[id]
90+ delete(p.clients, id)
91+ var cancels []context.CancelFunc
92+ if c != nil {
93+ for _, r := range c.streams {
94+ cancels = append(cancels, r.cancel)
95+ }
96+ }
97+ p.mu.Unlock()
98+ for _, cancel := range cancels {
99+ cancel()
100+ }
101+}
102+
103+func (p *Pool) CloseStream(id ports.BrokerConnectionID, stream ports.BrokerStreamID) error {
104+ p.mu.Lock()
105+ c := p.clients[id]
106+ var r *reservation
107+ if c != nil {
108+ r = c.streams[stream]
109+ }
110+ p.mu.Unlock()
111+ if r == nil {
112+ return ports.BrokerAdmissionStale
113+ }
114+ r.cancel()
115+ return nil
116+}
117+
118+func (p *Pool) OpenStream(ctx context.Context, req ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
119+ if err := req.Validate(); err != nil {
120+ return nil, errors.Join(ports.BrokerAdmissionInvalid, err)
121+ }
122+ if req.Epoch != p.epoch {
123+ return nil, ports.BrokerError{Code: ports.BrokerErrorStaleEpoch}
124+ }
125+ ctx, cancel := context.WithCancel(ctx)
126+ p.mu.Lock()
127+ c := p.clients[req.Connection]
128+ if p.closed {
129+ p.mu.Unlock()
130+ cancel()
131+ return nil, ports.BrokerAdmissionClosed
132+ }
133+ if c == nil || req.Stream <= c.high {
134+ p.mu.Unlock()
135+ cancel()
136+ return nil, ports.BrokerAdmissionStale
137+ }
138+ c.high = req.Stream
139+ if p.streams >= p.limits.Streams || len(c.streams) >= p.limits.StreamsPerClient {
140+ p.mu.Unlock()
141+ cancel()
142+ return nil, ports.BrokerAdmissionLimit
143+ }
144+ r := &reservation{cancel: cancel}
145+ c.streams[req.Stream] = r
146+ p.streams++
147+ p.wg.Add(1)
148+ p.mu.Unlock()
149+ stop := context.AfterFunc(p.ctx, cancel)
150+ var entry *poolEntry
151+ release := func() {
152+ stop()
153+ cancel()
154+ p.mu.Lock()
155+ delete(c.streams, req.Stream)
156+ p.streams--
157+ var abandon context.CancelFunc
158+ if entry != nil {
159+ entry.refs--
160+ if entry.refs == 0 {
161+ select {
162+ case <-entry.ready:
163+ default:
164+ entry.retiring = true
165+ abandon = entry.cancel
166+ }
167+ }
168+ select {
169+ case entry.wake <- struct{}{}:
170+ default:
171+ }
172+ }
173+ p.mu.Unlock()
174+ if abandon != nil {
175+ abandon()
176+ }
177+ p.wg.Done()
178+ }
179+ success := false
180+ defer func() {
181+ if !success {
182+ release()
183+ }
184+ }()
185+ req.Env = append([]string(nil), req.Env...)
186+ resolved, err := p.resolver.Resolve(ctx, req)
187+ if err != nil {
188+ return nil, normalizePoolError(err)
189+ }
190+ if err := resolved.Validate(); err != nil {
191+ return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: err}
192+ }
193+ if !resolved.Policy.Compatible(req.Policy) {
194+ return nil, ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}
195+ }
196+ if err = ctx.Err(); err != nil {
197+ return nil, normalizePoolError(err)
198+ }
199+ key := poolKey{resolved.Identity, resolved.Policy}
200+ p.mu.Lock()
201+ if p.closed {
202+ p.mu.Unlock()
203+ return nil, ports.BrokerAdmissionClosed
204+ }
205+ entry = p.entries[key]
206+ if entry != nil && entry.retiring {
207+ entry = nil
208+ p.mu.Unlock()
209+ return nil, ports.BrokerError{Code: ports.BrokerErrorUnavailable}
210+ }
211+ if entry == nil {
212+ if len(p.entries) >= p.limits.Physical {
213+ p.mu.Unlock()
214+ return nil, ports.BrokerAdmissionLimit
215+ }
216+ ectx, ecancel := context.WithCancel(p.ctx)
217+ entry = &poolEntry{key: key, ctx: ectx, cancel: ecancel, ready: make(chan struct{}), wake: make(chan struct{}, 1)}
218+ p.entries[key] = entry
219+ p.wg.Add(1)
220+ go p.runEntry(entry, resolved)
221+ }
222+ entry.refs++
223+ p.mu.Unlock()
224+ select {
225+ case <-ctx.Done():
226+ return nil, normalizePoolError(ctx.Err())
227+ case <-entry.ready:
228+ }
229+ if entry.err != nil {
230+ return nil, entry.err
231+ }
232+ openCtx, openCancel := context.WithCancel(ctx)
233+ stopEntry := context.AfterFunc(entry.ctx, openCancel)
234+ raw, err := entry.physical.OpenStream(openCtx, req)
235+ stopEntry()
236+ openCancel()
237+ if err != nil {
238+ if raw != nil {
239+ _ = raw.Close()
240+ }
241+ return nil, p.streamTerminal(ctx, entry, req, normalizePoolError(err))
242+ }
243+ if raw == nil {
244+ return nil, ports.BrokerError{Code: ports.BrokerErrorIncompatible, Cause: ports.BrokerAdmissionInvalid}
245+ }
246+ if terminal := p.streamTerminal(ctx, entry, req, nil); terminal != nil {
247+ _ = raw.Close()
248+ return nil, terminal
249+ }
250+ stream := &pooledStream{BrokerLogicalConnection: raw, done: make(chan struct{}), release: release}
251+ success = true
252+ go func() {
253+ var terminal error
254+ select {
255+ case <-ctx.Done():
256+ terminal = ports.BrokerError{Code: ports.BrokerErrorCancelled}
257+ case <-entry.ctx.Done():
258+ terminal = ports.BrokerError{Code: ports.BrokerErrorAttachmentLost}
259+ case <-entry.physical.Done():
260+ case <-raw.Done():
261+ terminal = raw.Err()
262+ case <-stream.done:
263+ return
264+ }
265+ stream.finish(p.streamTerminal(ctx, entry, req, terminal))
266+ }()
267+ return stream, nil
268+}
269+
270+func (p *Pool) runEntry(e *poolEntry, endpoint ports.BrokerResolvedEndpoint) {
271+ defer p.wg.Done()
272+ physical, err := p.connector.Connect(e.ctx, endpoint)
273+ if err == nil && (physical == nil || physical.Identity() != endpoint.Identity || !physical.Policy().Compatible(endpoint.Policy) || physical.Incarnation().Validate() != nil) {
274+ err = ports.BrokerError{Code: ports.BrokerErrorIncompatible}
275+ }
276+ e.physical = physical
277+ e.err = normalizePoolError(err)
278+ close(e.ready)
279+ if err == nil {
280+ timer := p.clock.NewTimer(p.limits.Idle)
281+ defer stopPoolTimer(timer)
282+ for {
283+ p.mu.Lock()
284+ idle := e.refs == 0
285+ p.mu.Unlock()
286+ stopPoolTimer(timer)
287+ if idle {
288+ timer.Reset(p.limits.Idle)
289+ }
290+ select {
291+ case <-e.ctx.Done():
292+ goto retire
293+ case <-physical.Done():
294+ goto retire
295+ case <-e.wake:
296+ continue
297+ case <-timer.C():
298+ p.mu.Lock()
299+ if e.refs == 0 {
300+ e.retiring = true
301+ p.mu.Unlock()
302+ goto retire
303+ }
304+ p.mu.Unlock()
305+ }
306+ }
307+ }
308+retire:
309+ p.mu.Lock()
310+ e.retiring = true
311+ p.mu.Unlock()
312+ e.cancel()
313+ if physical != nil {
314+ _ = physical.Close()
315+ }
316+ // Keep the key occupied until Close completes: never overlap physicals.
317+ p.mu.Lock()
318+ delete(p.entries, e.key)
319+ p.mu.Unlock()
320+}
321+
322+// Close cancels pending operations, closes transports, and joins all owned
323+// workers. Bounded completion depends on the explicit cancellation/Close port
324+// contract; callers must not supply adapters that ignore it.
325+func (p *Pool) Close() error {
326+ p.mu.Lock()
327+ p.closed = true
328+ p.mu.Unlock()
329+ p.cancel()
330+ p.wg.Wait()
331+ return nil
332+}
333+
334+type pooledStream struct {
335+ ports.BrokerLogicalConnection
336+ once sync.Once
337+ done chan struct{}
338+ err error
339+ closeErr error
340+ release func()
341+}
342+
343+func (s *pooledStream) Done() <-chan struct{} { return s.done }
344+func (s *pooledStream) Err() error {
345+ select {
346+ case <-s.done:
347+ return s.err
348+ default:
349+ return nil
350+ }
351+}
352+func (s *pooledStream) Close() error { s.finish(nil); return s.closeErr }
353+func (s *pooledStream) finish(err error) {
354+ s.once.Do(func() { s.err = err; s.closeErr = s.BrokerLogicalConnection.Close(); s.release(); close(s.done) })
355+}
356+
357+// One goroutine owns this timer. Drain a buffered expiry before every reset,
358+// including clocks implementing pre-Go-1.23 timer semantics.
359+func stopPoolTimer(timer ports.Timer) {
360+ if !timer.Stop() {
361+ select {
362+ case <-timer.C():
363+ default:
364+ }
365+ }
366+}
367+
368+func normalizePoolError(err error) error {
369+ if err == nil {
370+ return nil
371+ }
372+ code := ports.BrokerErrorUnavailable
373+ var typed ports.BrokerError
374+ var typedPtr *ports.BrokerError
375+ switch {
376+ case errors.Is(err, context.DeadlineExceeded):
377+ code = ports.BrokerErrorTimeout
378+ case errors.Is(err, context.Canceled):
379+ code = ports.BrokerErrorCancelled
380+ case errors.As(err, &typed) && typed.Validate() == nil:
381+ code = typed.Code
382+ case errors.As(err, &typedPtr) && typedPtr != nil && typedPtr.Validate() == nil:
383+ code = typedPtr.Code
384+ }
385+ return ports.BrokerError{Code: code, Cause: err}
386+}
387+
388+// Precedence at terminal observation: pool shutdown, request/client cancellation,
389+// physical Done, entry retirement, logical result. A completed stream is immutable.
390+func (p *Pool) streamTerminal(ctx context.Context, e *poolEntry, req ports.BrokerOpenStreamRequest, fallback error) error {
391+ if p.ctx.Err() != nil {
392+ return ports.BrokerError{Code: ports.BrokerErrorCancelled, Cause: p.ctx.Err()}
393+ }
394+ if ctx.Err() != nil {
395+ return normalizePoolError(ctx.Err())
396+ }
397+ select {
398+ case <-e.physical.Done():
399+ kind := e.physical.FailureKind()
400+ if kind < domain.RemoteFailureTransport || kind > domain.RemoteFailureInvalidResponse {
401+ kind = domain.RemoteFailureTransport
402+ }
403+ return ports.BrokerStreamLost{Epoch: req.Epoch, Connection: req.Connection, Stream: req.Stream, Cause: kind, Err: e.physical.Err()}
404+ default:
405+ }
406+ if e.ctx.Err() != nil {
407+ return ports.BrokerError{Code: ports.BrokerErrorAttachmentLost}
408+ }
409+ return normalizePoolError(fallback)
410+}
A internal/usecase/broker/pool_idle_test.go +104 -0
@@ -0,0 +1,104 @@
1+package broker
2+
3+import (
4+ "context"
5+ "sync/atomic"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func waitPoolEmpty(t *testing.T, p *Pool) {
14+ t.Helper()
15+ require.Eventually(t, func() bool { p.mu.Lock(); defer p.mu.Unlock(); return len(p.entries) == 0 }, time.Second, time.Millisecond)
16+}
17+
18+func TestPoolIdleEvictionAndReconnect(t *testing.T) {
19+ var calls atomic.Int32
20+ p, clock := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
21+ calls.Add(1)
22+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
23+ })
24+ id, _ := p.RegisterClient()
25+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
26+ require.NoError(t, err)
27+ require.NoError(t, s.Close())
28+ require.Eventually(t, func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) == 1 }, time.Second, time.Millisecond)
29+ clock.Advance(time.Minute)
30+ waitPoolEmpty(t, p)
31+ s, err = p.OpenStream(context.Background(), poolRequest(id, 2))
32+ require.NoError(t, err)
33+ require.EqualValues(t, 2, calls.Load())
34+ require.NoError(t, s.Close())
35+}
36+
37+type retiringPhysical struct {
38+ *fakePhysical
39+ closing, allowClose chan struct{}
40+}
41+
42+func (f *retiringPhysical) Close() error {
43+ close(f.closing)
44+ <-f.allowClose
45+ return f.fakePhysical.Close()
46+}
47+
48+func TestPoolRetiringKeyUntilCloseCompletes(t *testing.T) {
49+ var calls atomic.Int32
50+ var first *retiringPhysical
51+ allow := make(chan struct{})
52+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
53+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
54+ if calls.Add(1) == 1 {
55+ first = &retiringPhysical{fakePhysical: f, closing: make(chan struct{}), allowClose: allow}
56+ return first, nil
57+ }
58+ return f, nil
59+ })
60+ defer func() {
61+ select {
62+ case <-allow:
63+ default:
64+ close(allow)
65+ }
66+ }()
67+ id, _ := p.RegisterClient()
68+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
69+ require.NoError(t, err)
70+ require.NoError(t, first.fakePhysical.Close())
71+ await(t, first.closing)
72+ await(t, s.Done())
73+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
74+ var typed ports.BrokerError
75+ require.ErrorAs(t, err, &typed)
76+ require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
77+ require.EqualValues(t, 1, calls.Load())
78+ close(allow)
79+ waitPoolEmpty(t, p)
80+ s, err = p.OpenStream(context.Background(), poolRequest(id, 3))
81+ require.NoError(t, err)
82+ require.EqualValues(t, 2, calls.Load())
83+ require.NoError(t, s.Close())
84+}
85+
86+func TestPoolFailedConnectRetry(t *testing.T) {
87+ var calls atomic.Int32
88+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
89+ if calls.Add(1) == 1 {
90+ return nil, context.DeadlineExceeded
91+ }
92+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
93+ })
94+ id, _ := p.RegisterClient()
95+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
96+ require.ErrorIs(t, err, context.DeadlineExceeded)
97+ waitPoolEmpty(t, p)
98+ _, err = p.OpenStream(context.Background(), poolRequest(id, 1))
99+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
100+ s, err := p.OpenStream(context.Background(), poolRequest(id, 2))
101+ require.NoError(t, err)
102+ require.NoError(t, s.Close())
103+ require.EqualValues(t, 2, calls.Load())
104+}
@@ -0,0 +1,104 @@
1+package broker
2+
3+import (
4+ "context"
5+ "sync/atomic"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func waitPoolEmpty(t *testing.T, p *Pool) {
14+ t.Helper()
15+ require.Eventually(t, func() bool { p.mu.Lock(); defer p.mu.Unlock(); return len(p.entries) == 0 }, time.Second, time.Millisecond)
16+}
17+
18+func TestPoolIdleEvictionAndReconnect(t *testing.T) {
19+ var calls atomic.Int32
20+ p, clock := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
21+ calls.Add(1)
22+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
23+ })
24+ id, _ := p.RegisterClient()
25+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
26+ require.NoError(t, err)
27+ require.NoError(t, s.Close())
28+ require.Eventually(t, func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) == 1 }, time.Second, time.Millisecond)
29+ clock.Advance(time.Minute)
30+ waitPoolEmpty(t, p)
31+ s, err = p.OpenStream(context.Background(), poolRequest(id, 2))
32+ require.NoError(t, err)
33+ require.EqualValues(t, 2, calls.Load())
34+ require.NoError(t, s.Close())
35+}
36+
37+type retiringPhysical struct {
38+ *fakePhysical
39+ closing, allowClose chan struct{}
40+}
41+
42+func (f *retiringPhysical) Close() error {
43+ close(f.closing)
44+ <-f.allowClose
45+ return f.fakePhysical.Close()
46+}
47+
48+func TestPoolRetiringKeyUntilCloseCompletes(t *testing.T) {
49+ var calls atomic.Int32
50+ var first *retiringPhysical
51+ allow := make(chan struct{})
52+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
53+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
54+ if calls.Add(1) == 1 {
55+ first = &retiringPhysical{fakePhysical: f, closing: make(chan struct{}), allowClose: allow}
56+ return first, nil
57+ }
58+ return f, nil
59+ })
60+ defer func() {
61+ select {
62+ case <-allow:
63+ default:
64+ close(allow)
65+ }
66+ }()
67+ id, _ := p.RegisterClient()
68+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
69+ require.NoError(t, err)
70+ require.NoError(t, first.fakePhysical.Close())
71+ await(t, first.closing)
72+ await(t, s.Done())
73+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
74+ var typed ports.BrokerError
75+ require.ErrorAs(t, err, &typed)
76+ require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
77+ require.EqualValues(t, 1, calls.Load())
78+ close(allow)
79+ waitPoolEmpty(t, p)
80+ s, err = p.OpenStream(context.Background(), poolRequest(id, 3))
81+ require.NoError(t, err)
82+ require.EqualValues(t, 2, calls.Load())
83+ require.NoError(t, s.Close())
84+}
85+
86+func TestPoolFailedConnectRetry(t *testing.T) {
87+ var calls atomic.Int32
88+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
89+ if calls.Add(1) == 1 {
90+ return nil, context.DeadlineExceeded
91+ }
92+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
93+ })
94+ id, _ := p.RegisterClient()
95+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
96+ require.ErrorIs(t, err, context.DeadlineExceeded)
97+ waitPoolEmpty(t, p)
98+ _, err = p.OpenStream(context.Background(), poolRequest(id, 1))
99+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
100+ s, err := p.OpenStream(context.Background(), poolRequest(id, 2))
101+ require.NoError(t, err)
102+ require.NoError(t, s.Close())
103+ require.EqualValues(t, 2, calls.Load())
104+}
A internal/usecase/broker/pool_review_test.go +235 -0
@@ -0,0 +1,235 @@
1+package broker
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestPoolAdapterFailures(t *testing.T) {
15+ for _, stage := range []string{"resolver", "connector", "open"} {
16+ for _, tc := range []struct {
17+ name string
18+ err error
19+ code ports.BrokerErrorCode
20+ }{
21+ {"unknown", errors.New("adapter failure"), ports.BrokerErrorUnavailable},
22+ {"cancel", context.Canceled, ports.BrokerErrorCancelled},
23+ {"deadline", context.DeadlineExceeded, ports.BrokerErrorTimeout},
24+ {"semantic", ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}, ports.BrokerErrorConflictingPolicy},
25+ } {
26+ t.Run(stage+"/"+tc.name, func(t *testing.T) {
27+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
28+ if stage == "connector" {
29+ return nil, tc.err
30+ }
31+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
32+ return nil, tc.err
33+ }}, nil
34+ })
35+ if stage == "resolver" {
36+ p.resolver = poolResolver(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
37+ return ports.BrokerResolvedEndpoint{}, tc.err
38+ })
39+ }
40+ id, _ := p.RegisterClient()
41+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
42+ require.ErrorIs(t, err, tc.err)
43+ var typed ports.BrokerError
44+ require.ErrorAs(t, err, &typed)
45+ require.Equal(t, tc.code, typed.Code)
46+ })
47+ }
48+ }
49+}
50+
51+func TestPoolRejectedPaths(t *testing.T) {
52+ for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} {
53+ t.Run(name, func(t *testing.T) {
54+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
55+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
56+ return nil, nil
57+ }}, nil
58+ })
59+ id, _ := p.RegisterClient()
60+ req := poolRequest(id, 1)
61+ switch name {
62+ case "closed":
63+ require.NoError(t, p.Close())
64+ case "epoch":
65+ req.Epoch++
66+ case "invalid endpoint":
67+ p.resolver = poolResolver(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
68+ return ports.BrokerResolvedEndpoint{}, nil
69+ })
70+ case "retiring":
71+ key := poolKey{"canonical", poolPolicy()}
72+ p.entries[key] = &poolEntry{key: key, retiring: true}
73+ }
74+ _, err := p.OpenStream(context.Background(), req)
75+ require.Error(t, err)
76+ if name == "closed" {
77+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
78+ _, err = p.RegisterClient()
79+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
80+ } else {
81+ var typed ports.BrokerError
82+ require.ErrorAs(t, err, &typed)
83+ want := ports.BrokerErrorIncompatible
84+ if name == "epoch" {
85+ want = ports.BrokerErrorStaleEpoch
86+ }
87+ if name == "retiring" {
88+ want = ports.BrokerErrorUnavailable
89+ }
90+ require.Equal(t, want, typed.Code)
91+ }
92+ p.mu.Lock()
93+ require.Zero(t, p.streams)
94+ p.mu.Unlock()
95+ })
96+ }
97+}
98+
99+func TestPoolPostOpenPhysicalLoss(t *testing.T) {
100+ raw := newFakeLogical()
101+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
102+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
103+ f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
104+ _ = f.Close()
105+ return raw, nil
106+ }
107+ return f, nil
108+ })
109+ id, _ := p.RegisterClient()
110+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
111+ var lost ports.BrokerStreamLost
112+ require.ErrorAs(t, err, &lost)
113+ require.NoError(t, lost.Validate())
114+ require.Equal(t, domain.RemoteFailureTransport, lost.Cause)
115+ await(t, raw.Done())
116+}
117+
118+func TestPoolTerminalPrecedence(t *testing.T) {
119+ for _, name := range []string{"physical over logical", "request over physical", "shutdown over physical"} {
120+ t.Run(name, func(t *testing.T) {
121+ p, _ := setupPool(t, func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
122+ panic("unused")
123+ })
124+ ctx, cancel := context.WithCancel(context.Background())
125+ defer cancel()
126+ f := &fakePhysical{done: make(chan struct{})}
127+ _ = f.Close()
128+ e := &poolEntry{ctx: context.Background(), physical: f}
129+ if name == "request over physical" {
130+ cancel()
131+ }
132+ if name == "shutdown over physical" {
133+ p.cancel()
134+ }
135+ err := p.streamTerminal(ctx, e, poolRequest(ports.BrokerConnectionID{1}, 1), nil)
136+ if name == "physical over logical" {
137+ var lost ports.BrokerStreamLost
138+ require.ErrorAs(t, err, &lost)
139+ require.NoError(t, lost.Validate())
140+ } else {
141+ var typed ports.BrokerError
142+ require.ErrorAs(t, err, &typed)
143+ require.Equal(t, ports.BrokerErrorCancelled, typed.Code)
144+ }
145+ })
146+ }
147+}
148+
149+// Reset deliberately does not drain, matching older buffered timer adapters.
150+type bufferedPoolTimer struct {
151+ ch chan time.Time
152+ active bool
153+}
154+
155+func (t *bufferedPoolTimer) C() <-chan time.Time { return t.ch }
156+func (t *bufferedPoolTimer) Stop() bool { was := t.active; t.active = false; return was }
157+func (t *bufferedPoolTimer) Reset(time.Duration) bool { was := t.active; t.active = true; return was }
158+func TestPoolTimerStopDrain(t *testing.T) {
159+ for _, name := range []string{"active", "expired buffered", "expired consumed", "stopped"} {
160+ t.Run(name, func(t *testing.T) {
161+ timer := &bufferedPoolTimer{ch: make(chan time.Time, 1), active: name == "active"}
162+ if name == "expired buffered" {
163+ timer.ch <- time.Time{}
164+ }
165+ stopPoolTimer(timer)
166+ timer.Reset(time.Minute)
167+ select {
168+ case <-timer.C():
169+ t.Fatal("stale expiry survived reset")
170+ default:
171+ }
172+ stopPoolTimer(timer)
173+ require.False(t, timer.active)
174+ })
175+ }
176+}
177+
178+type closeErrorLogical struct {
179+ *fakeLogical
180+ err error
181+}
182+
183+func (l *closeErrorLogical) Close() error { _ = l.fakeLogical.Close(); return l.err }
184+func TestPooledStreamPreservesCloseError(t *testing.T) {
185+ cause := errors.New("close failed")
186+ calls := 0
187+ s := &pooledStream{BrokerLogicalConnection: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}
188+ require.ErrorIs(t, s.Close(), cause)
189+ require.ErrorIs(t, s.Close(), cause)
190+ require.Equal(t, 1, calls)
191+}
192+
193+func TestPoolCaps(t *testing.T) {
194+ for _, name := range []string{"clients", "streams", "physical"} {
195+ t.Run(name, func(t *testing.T) {
196+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
197+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
198+ })
199+ p.limits.Clients = 1
200+ p.limits.Streams = 1
201+ p.limits.Physical = 1
202+ id, _ := p.RegisterClient()
203+ if name == "clients" {
204+ _, err := p.RegisterClient()
205+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
206+ return
207+ }
208+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
209+ require.NoError(t, err)
210+ defer s.Close()
211+ if name == "physical" {
212+ p.limits.Streams = 2
213+ p.resolver = poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
214+ return ports.BrokerResolvedEndpoint{Identity: "other", Policy: r.Policy, Address: "route"}, nil
215+ })
216+ }
217+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
218+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
219+ })
220+ }
221+}
222+
223+func TestPoolShutdownStreamsCancelled(t *testing.T) {
224+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
225+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
226+ })
227+ id, _ := p.RegisterClient()
228+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
229+ require.NoError(t, err)
230+ require.NoError(t, p.Close())
231+ await(t, s.Done())
232+ var typed ports.BrokerError
233+ require.ErrorAs(t, s.Err(), &typed)
234+ require.Equal(t, ports.BrokerErrorCancelled, typed.Code)
235+}
@@ -0,0 +1,235 @@
1+package broker
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/ports"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func TestPoolAdapterFailures(t *testing.T) {
15+ for _, stage := range []string{"resolver", "connector", "open"} {
16+ for _, tc := range []struct {
17+ name string
18+ err error
19+ code ports.BrokerErrorCode
20+ }{
21+ {"unknown", errors.New("adapter failure"), ports.BrokerErrorUnavailable},
22+ {"cancel", context.Canceled, ports.BrokerErrorCancelled},
23+ {"deadline", context.DeadlineExceeded, ports.BrokerErrorTimeout},
24+ {"semantic", ports.BrokerError{Code: ports.BrokerErrorConflictingPolicy}, ports.BrokerErrorConflictingPolicy},
25+ } {
26+ t.Run(stage+"/"+tc.name, func(t *testing.T) {
27+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
28+ if stage == "connector" {
29+ return nil, tc.err
30+ }
31+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
32+ return nil, tc.err
33+ }}, nil
34+ })
35+ if stage == "resolver" {
36+ p.resolver = poolResolver(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
37+ return ports.BrokerResolvedEndpoint{}, tc.err
38+ })
39+ }
40+ id, _ := p.RegisterClient()
41+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
42+ require.ErrorIs(t, err, tc.err)
43+ var typed ports.BrokerError
44+ require.ErrorAs(t, err, &typed)
45+ require.Equal(t, tc.code, typed.Code)
46+ })
47+ }
48+ }
49+}
50+
51+func TestPoolRejectedPaths(t *testing.T) {
52+ for _, name := range []string{"closed", "epoch", "invalid endpoint", "nil raw", "retiring"} {
53+ t.Run(name, func(t *testing.T) {
54+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
55+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
56+ return nil, nil
57+ }}, nil
58+ })
59+ id, _ := p.RegisterClient()
60+ req := poolRequest(id, 1)
61+ switch name {
62+ case "closed":
63+ require.NoError(t, p.Close())
64+ case "epoch":
65+ req.Epoch++
66+ case "invalid endpoint":
67+ p.resolver = poolResolver(func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
68+ return ports.BrokerResolvedEndpoint{}, nil
69+ })
70+ case "retiring":
71+ key := poolKey{"canonical", poolPolicy()}
72+ p.entries[key] = &poolEntry{key: key, retiring: true}
73+ }
74+ _, err := p.OpenStream(context.Background(), req)
75+ require.Error(t, err)
76+ if name == "closed" {
77+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
78+ _, err = p.RegisterClient()
79+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
80+ } else {
81+ var typed ports.BrokerError
82+ require.ErrorAs(t, err, &typed)
83+ want := ports.BrokerErrorIncompatible
84+ if name == "epoch" {
85+ want = ports.BrokerErrorStaleEpoch
86+ }
87+ if name == "retiring" {
88+ want = ports.BrokerErrorUnavailable
89+ }
90+ require.Equal(t, want, typed.Code)
91+ }
92+ p.mu.Lock()
93+ require.Zero(t, p.streams)
94+ p.mu.Unlock()
95+ })
96+ }
97+}
98+
99+func TestPoolPostOpenPhysicalLoss(t *testing.T) {
100+ raw := newFakeLogical()
101+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
102+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
103+ f.open = func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
104+ _ = f.Close()
105+ return raw, nil
106+ }
107+ return f, nil
108+ })
109+ id, _ := p.RegisterClient()
110+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
111+ var lost ports.BrokerStreamLost
112+ require.ErrorAs(t, err, &lost)
113+ require.NoError(t, lost.Validate())
114+ require.Equal(t, domain.RemoteFailureTransport, lost.Cause)
115+ await(t, raw.Done())
116+}
117+
118+func TestPoolTerminalPrecedence(t *testing.T) {
119+ for _, name := range []string{"physical over logical", "request over physical", "shutdown over physical"} {
120+ t.Run(name, func(t *testing.T) {
121+ p, _ := setupPool(t, func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
122+ panic("unused")
123+ })
124+ ctx, cancel := context.WithCancel(context.Background())
125+ defer cancel()
126+ f := &fakePhysical{done: make(chan struct{})}
127+ _ = f.Close()
128+ e := &poolEntry{ctx: context.Background(), physical: f}
129+ if name == "request over physical" {
130+ cancel()
131+ }
132+ if name == "shutdown over physical" {
133+ p.cancel()
134+ }
135+ err := p.streamTerminal(ctx, e, poolRequest(ports.BrokerConnectionID{1}, 1), nil)
136+ if name == "physical over logical" {
137+ var lost ports.BrokerStreamLost
138+ require.ErrorAs(t, err, &lost)
139+ require.NoError(t, lost.Validate())
140+ } else {
141+ var typed ports.BrokerError
142+ require.ErrorAs(t, err, &typed)
143+ require.Equal(t, ports.BrokerErrorCancelled, typed.Code)
144+ }
145+ })
146+ }
147+}
148+
149+// Reset deliberately does not drain, matching older buffered timer adapters.
150+type bufferedPoolTimer struct {
151+ ch chan time.Time
152+ active bool
153+}
154+
155+func (t *bufferedPoolTimer) C() <-chan time.Time { return t.ch }
156+func (t *bufferedPoolTimer) Stop() bool { was := t.active; t.active = false; return was }
157+func (t *bufferedPoolTimer) Reset(time.Duration) bool { was := t.active; t.active = true; return was }
158+func TestPoolTimerStopDrain(t *testing.T) {
159+ for _, name := range []string{"active", "expired buffered", "expired consumed", "stopped"} {
160+ t.Run(name, func(t *testing.T) {
161+ timer := &bufferedPoolTimer{ch: make(chan time.Time, 1), active: name == "active"}
162+ if name == "expired buffered" {
163+ timer.ch <- time.Time{}
164+ }
165+ stopPoolTimer(timer)
166+ timer.Reset(time.Minute)
167+ select {
168+ case <-timer.C():
169+ t.Fatal("stale expiry survived reset")
170+ default:
171+ }
172+ stopPoolTimer(timer)
173+ require.False(t, timer.active)
174+ })
175+ }
176+}
177+
178+type closeErrorLogical struct {
179+ *fakeLogical
180+ err error
181+}
182+
183+func (l *closeErrorLogical) Close() error { _ = l.fakeLogical.Close(); return l.err }
184+func TestPooledStreamPreservesCloseError(t *testing.T) {
185+ cause := errors.New("close failed")
186+ calls := 0
187+ s := &pooledStream{BrokerLogicalConnection: &closeErrorLogical{newFakeLogical(), cause}, done: make(chan struct{}), release: func() { calls++ }}
188+ require.ErrorIs(t, s.Close(), cause)
189+ require.ErrorIs(t, s.Close(), cause)
190+ require.Equal(t, 1, calls)
191+}
192+
193+func TestPoolCaps(t *testing.T) {
194+ for _, name := range []string{"clients", "streams", "physical"} {
195+ t.Run(name, func(t *testing.T) {
196+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
197+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
198+ })
199+ p.limits.Clients = 1
200+ p.limits.Streams = 1
201+ p.limits.Physical = 1
202+ id, _ := p.RegisterClient()
203+ if name == "clients" {
204+ _, err := p.RegisterClient()
205+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
206+ return
207+ }
208+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
209+ require.NoError(t, err)
210+ defer s.Close()
211+ if name == "physical" {
212+ p.limits.Streams = 2
213+ p.resolver = poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
214+ return ports.BrokerResolvedEndpoint{Identity: "other", Policy: r.Policy, Address: "route"}, nil
215+ })
216+ }
217+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
218+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
219+ })
220+ }
221+}
222+
223+func TestPoolShutdownStreamsCancelled(t *testing.T) {
224+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
225+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
226+ })
227+ id, _ := p.RegisterClient()
228+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
229+ require.NoError(t, err)
230+ require.NoError(t, p.Close())
231+ await(t, s.Done())
232+ var typed ports.BrokerError
233+ require.ErrorAs(t, s.Err(), &typed)
234+ require.Equal(t, ports.BrokerErrorCancelled, typed.Code)
235+}
A internal/usecase/broker/pool_test.go +338 -0
@@ -0,0 +1,338 @@
1+package broker
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync"
7+ "sync/atomic"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
14+ "github.com/stretchr/testify/require"
15+)
16+
17+// Stateful fakes make terminal races and blocked I/O explicit; no real carriage.
18+type poolResolver func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)
19+
20+func (f poolResolver) Resolve(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
21+ return f(c, r)
22+}
23+
24+type poolConnector func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)
25+
26+func (f poolConnector) Connect(c context.Context, r ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
27+ return f(c, r)
28+}
29+
30+type fakeLogical struct {
31+ ports.ClientConnection
32+ done chan struct{}
33+ once sync.Once
34+}
35+
36+func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }
37+func (f *fakeLogical) Done() <-chan struct{} { return f.done }
38+func (f *fakeLogical) Err() error { return nil }
39+func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
40+func (f *fakeLogical) SendClient(protocol.ClientMessage) error { <-f.done; return errors.New("closed") }
41+
42+type fakePhysical struct {
43+ endpoint ports.BrokerResolvedEndpoint
44+ done chan struct{}
45+ once sync.Once
46+ open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)
47+}
48+
49+func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity { return f.endpoint.Identity }
50+func (f *fakePhysical) Policy() ports.BrokerPolicy { return f.endpoint.Policy }
51+func (f *fakePhysical) Incarnation() ports.BrokerDaemonIncarnation {
52+ return ports.BrokerDaemonIncarnation{1}
53+}
54+func (f *fakePhysical) FailureKind() domain.RemoteFailureKind { return domain.RemoteFailureTransport }
55+func (f *fakePhysical) Done() <-chan struct{} { return f.done }
56+func (f *fakePhysical) Err() error { return errors.New("physical loss") }
57+func (f *fakePhysical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
58+func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
59+ if f.open != nil {
60+ return f.open(c, r)
61+ }
62+ return newFakeLogical(), nil
63+}
64+func poolPolicy() ports.BrokerPolicy {
65+ return ports.BrokerPolicy{ProtocolVersion: 1, CatalogSchemaVersion: 1, Transport: "fake", Trust: "trusted", Launch: "explicit", Isolation: "user", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
66+}
67+func poolRequest(id ports.BrokerConnectionID, n uint64) ports.BrokerOpenStreamRequest {
68+ return ports.BrokerOpenStreamRequest{Epoch: 1, Connection: id, Stream: ports.BrokerStreamID(n), Local: true, Purpose: ports.BrokerStreamControl, Policy: poolPolicy()}
69+}
70+func setupPool(t testing.TB, connector poolConnector) (*Pool, *manualClock) {
71+ t.Helper()
72+ clock := newManualClock(time.Unix(0, 0))
73+ resolver := poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
74+ return ports.BrokerResolvedEndpoint{Identity: "canonical", Policy: r.Policy, Address: "fake"}, nil
75+ })
76+ p, err := NewPool(1, resolver, connector, clock, PoolLimits{Physical: 2, Clients: 128, Streams: 128, StreamsPerClient: 128, Idle: time.Minute})
77+ require.NoError(t, err)
78+ t.Cleanup(func() { require.NoError(t, p.Close()) })
79+ return p, clock
80+}
81+func await(t *testing.T, ch <-chan struct{}) {
82+ t.Helper()
83+ select {
84+ case <-ch:
85+ case <-time.After(3 * time.Second):
86+ t.Fatal("timed out")
87+ }
88+}
89+
90+func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
91+ var calls atomic.Int32
92+ var physical *fakePhysical
93+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
94+ calls.Add(1)
95+ physical = &fakePhysical{endpoint: e, done: make(chan struct{})}
96+ return physical, nil
97+ })
98+ streams := make([]ports.BrokerLogicalConnection, 100)
99+ var wg sync.WaitGroup
100+ for i := range streams {
101+ id, err := p.RegisterClient()
102+ require.NoError(t, err)
103+ wg.Add(1)
104+ go func(i int) {
105+ defer wg.Done()
106+ s, e := p.OpenStream(context.Background(), poolRequest(id, 1))
107+ if e != nil {
108+ t.Error(e)
109+ return
110+ }
111+ streams[i] = s
112+ }(i)
113+ }
114+ wg.Wait()
115+ require.EqualValues(t, 1, calls.Load())
116+ blocked := make(chan struct{})
117+ go func() { _ = streams[0].SendClient(nil); close(blocked) }()
118+ require.NoError(t, streams[0].Close())
119+ await(t, blocked)
120+ select {
121+ case <-streams[1].Done():
122+ t.Fatal("sibling closed")
123+ default:
124+ }
125+ require.NoError(t, physical.Close())
126+ for _, s := range streams[1:] {
127+ await(t, s.Done())
128+ var loss ports.BrokerError
129+ require.ErrorAs(t, s.Err(), &loss)
130+ require.Equal(t, ports.BrokerErrorAttachmentLost, loss.Code)
131+ }
132+}
133+
134+func TestPoolAdmissionAndNonreuse(t *testing.T) {
135+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
136+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
137+ })
138+ p.limits.StreamsPerClient = 1
139+ id, err := p.RegisterClient()
140+ require.NoError(t, err)
141+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
142+ require.NoError(t, err)
143+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
144+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
145+ require.NoError(t, s.Close())
146+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
147+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
148+ s, err = p.OpenStream(context.Background(), poolRequest(id, 3))
149+ require.NoError(t, err)
150+ p.CloseClient(id)
151+ await(t, s.Done())
152+ _, err = p.OpenStream(context.Background(), poolRequest(id, 4))
153+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
154+}
155+
156+func TestPoolCoalescedCancellation(t *testing.T) {
157+ entered, proceed := make(chan struct{}), make(chan struct{})
158+ var calls atomic.Int32
159+ p, _ := setupPool(t, func(ctx context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
160+ calls.Add(1)
161+ close(entered)
162+ select {
163+ case <-ctx.Done():
164+ return nil, ctx.Err()
165+ case <-proceed:
166+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
167+ }
168+ })
169+ id1, _ := p.RegisterClient()
170+ id2, _ := p.RegisterClient()
171+ ctx, cancel := context.WithCancel(context.Background())
172+ result := make(chan error, 1)
173+ go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }()
174+ await(t, entered)
175+ result2 := make(chan ports.BrokerLogicalConnection, 1)
176+ go func() {
177+ s, err := p.OpenStream(context.Background(), poolRequest(id2, 1))
178+ if err != nil {
179+ t.Error(err)
180+ }
181+ result2 <- s
182+ }()
183+ require.Eventually(t, func() bool {
184+ p.mu.Lock()
185+ defer p.mu.Unlock()
186+ for _, e := range p.entries {
187+ return e.refs == 2
188+ }
189+ return false
190+ }, time.Second, time.Millisecond)
191+ cancel()
192+ require.ErrorIs(t, <-result, context.Canceled)
193+ close(proceed)
194+ s := <-result2
195+ require.NotNil(t, s)
196+ require.EqualValues(t, 1, calls.Load())
197+ require.NoError(t, s.Close())
198+}
199+
200+func TestPoolAbandonedAttemptAndShutdown(t *testing.T) {
201+ entered, exited := make(chan struct{}), make(chan struct{})
202+ p, _ := setupPool(t, func(ctx context.Context, _ ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
203+ close(entered)
204+ <-ctx.Done()
205+ close(exited)
206+ return nil, ctx.Err()
207+ })
208+ id, _ := p.RegisterClient()
209+ ctx, cancel := context.WithCancel(context.Background())
210+ result := make(chan error, 1)
211+ go func() { _, err := p.OpenStream(ctx, poolRequest(id, 1)); result <- err }()
212+ await(t, entered)
213+ cancel()
214+ require.ErrorIs(t, <-result, context.Canceled)
215+ await(t, exited)
216+ require.NoError(t, p.Close())
217+ _, err := p.RegisterClient()
218+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
219+}
220+
221+func TestPoolIdleEviction(t *testing.T) {
222+ physicals := make(chan *fakePhysical, 2)
223+ p, clock := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
224+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
225+ physicals <- f
226+ return f, nil
227+ })
228+ id, _ := p.RegisterClient()
229+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
230+ require.NoError(t, err)
231+ f := <-physicals
232+ require.NoError(t, s.Close())
233+ require.Eventually(t, func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) > 0 }, time.Second, time.Millisecond)
234+ clock.Advance(time.Minute)
235+ await(t, f.done)
236+ require.NoError(t, p.Close())
237+}
238+
239+func TestPoolPolicyAndIdentityVerification(t *testing.T) {
240+ for _, mode := range []string{"identity", "policy", "resolver"} {
241+ t.Run(mode, func(t *testing.T) {
242+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
243+ if mode == "identity" {
244+ e.Identity = "impostor"
245+ }
246+ if mode == "policy" {
247+ e.Policy.Trust = "other"
248+ }
249+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
250+ })
251+ if mode == "resolver" {
252+ p.resolver = poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
253+ policy := r.Policy
254+ policy.Trust = "other"
255+ return ports.BrokerResolvedEndpoint{Identity: "canonical", Policy: policy, Address: "fake"}, nil
256+ })
257+ }
258+ id, _ := p.RegisterClient()
259+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
260+ var typed ports.BrokerError
261+ require.ErrorAs(t, err, &typed)
262+ })
263+ }
264+}
265+
266+func BenchmarkPoolOpenClose(b *testing.B) {
267+ p, _ := setupPool(b, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
268+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
269+ })
270+ id, _ := p.RegisterClient()
271+ b.ReportAllocs()
272+ b.ResetTimer()
273+ for i := 0; i < b.N; i++ {
274+ s, err := p.OpenStream(context.Background(), poolRequest(id, uint64(i+1)))
275+ if err != nil {
276+ b.Fatal(err)
277+ }
278+ _ = s.Close()
279+ }
280+}
281+
282+func TestPoolAliasesAndPolicyPartition(t *testing.T) {
283+ var calls atomic.Int32
284+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
285+ calls.Add(1)
286+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
287+ })
288+ id, _ := p.RegisterClient()
289+ for i, alias := range []string{"user@first", "user@second"} {
290+ r := poolRequest(id, uint64(i+1))
291+ r.Local = false
292+ r.Endpoint = alias
293+ r.Registration = domain.RemoteRegistration{Endpoint: alias, Incarnation: [16]byte{1}, Generation: 1}
294+ s, err := p.OpenStream(context.Background(), r)
295+ require.NoError(t, err)
296+ defer s.Close()
297+ }
298+ require.EqualValues(t, 1, calls.Load())
299+ r := poolRequest(id, 3)
300+ r.Policy.Isolation = "other"
301+ s, err := p.OpenStream(context.Background(), r)
302+ require.NoError(t, err)
303+ defer s.Close()
304+ require.EqualValues(t, 2, calls.Load())
305+ r.Stream = 4
306+ r.Policy.Trust = "third"
307+ _, err = p.OpenStream(context.Background(), r)
308+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
309+}
310+
311+func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) {
312+ entered := make(chan struct{})
313+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
314+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
315+ if r.Stream == 1 {
316+ close(entered)
317+ <-ctx.Done()
318+ return nil, ctx.Err()
319+ }
320+ return newFakeLogical(), nil
321+ }}, nil
322+ })
323+ id, _ := p.RegisterClient()
324+ result := make(chan error, 1)
325+ go func() { _, err := p.OpenStream(context.Background(), poolRequest(id, 1)); result <- err }()
326+ await(t, entered)
327+ s, err := p.OpenStream(context.Background(), poolRequest(id, 2))
328+ require.NoError(t, err)
329+ require.NoError(t, p.CloseStream(id, 1))
330+ require.ErrorIs(t, <-result, context.Canceled)
331+ select {
332+ case <-s.Done():
333+ t.Fatal("sibling canceled")
334+ default:
335+ }
336+ require.NoError(t, p.Close())
337+ await(t, s.Done())
338+}
@@ -0,0 +1,338 @@
1+package broker
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync"
7+ "sync/atomic"
8+ "testing"
9+ "time"
10+
11+ "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
14+ "github.com/stretchr/testify/require"
15+)
16+
17+// Stateful fakes make terminal races and blocked I/O explicit; no real carriage.
18+type poolResolver func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error)
19+
20+func (f poolResolver) Resolve(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
21+ return f(c, r)
22+}
23+
24+type poolConnector func(context.Context, ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error)
25+
26+func (f poolConnector) Connect(c context.Context, r ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
27+ return f(c, r)
28+}
29+
30+type fakeLogical struct {
31+ ports.ClientConnection
32+ done chan struct{}
33+ once sync.Once
34+}
35+
36+func newFakeLogical() *fakeLogical { return &fakeLogical{done: make(chan struct{})} }
37+func (f *fakeLogical) Done() <-chan struct{} { return f.done }
38+func (f *fakeLogical) Err() error { return nil }
39+func (f *fakeLogical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
40+func (f *fakeLogical) SendClient(protocol.ClientMessage) error { <-f.done; return errors.New("closed") }
41+
42+type fakePhysical struct {
43+ endpoint ports.BrokerResolvedEndpoint
44+ done chan struct{}
45+ once sync.Once
46+ open func(context.Context, ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error)
47+}
48+
49+func (f *fakePhysical) Identity() ports.BrokerDaemonIdentity { return f.endpoint.Identity }
50+func (f *fakePhysical) Policy() ports.BrokerPolicy { return f.endpoint.Policy }
51+func (f *fakePhysical) Incarnation() ports.BrokerDaemonIncarnation {
52+ return ports.BrokerDaemonIncarnation{1}
53+}
54+func (f *fakePhysical) FailureKind() domain.RemoteFailureKind { return domain.RemoteFailureTransport }
55+func (f *fakePhysical) Done() <-chan struct{} { return f.done }
56+func (f *fakePhysical) Err() error { return errors.New("physical loss") }
57+func (f *fakePhysical) Close() error { f.once.Do(func() { close(f.done) }); return nil }
58+func (f *fakePhysical) OpenStream(c context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
59+ if f.open != nil {
60+ return f.open(c, r)
61+ }
62+ return newFakeLogical(), nil
63+}
64+func poolPolicy() ports.BrokerPolicy {
65+ return ports.BrokerPolicy{ProtocolVersion: 1, CatalogSchemaVersion: 1, Transport: "fake", Trust: "trusted", Launch: "explicit", Isolation: "user", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
66+}
67+func poolRequest(id ports.BrokerConnectionID, n uint64) ports.BrokerOpenStreamRequest {
68+ return ports.BrokerOpenStreamRequest{Epoch: 1, Connection: id, Stream: ports.BrokerStreamID(n), Local: true, Purpose: ports.BrokerStreamControl, Policy: poolPolicy()}
69+}
70+func setupPool(t testing.TB, connector poolConnector) (*Pool, *manualClock) {
71+ t.Helper()
72+ clock := newManualClock(time.Unix(0, 0))
73+ resolver := poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
74+ return ports.BrokerResolvedEndpoint{Identity: "canonical", Policy: r.Policy, Address: "fake"}, nil
75+ })
76+ p, err := NewPool(1, resolver, connector, clock, PoolLimits{Physical: 2, Clients: 128, Streams: 128, StreamsPerClient: 128, Idle: time.Minute})
77+ require.NoError(t, err)
78+ t.Cleanup(func() { require.NoError(t, p.Close()) })
79+ return p, clock
80+}
81+func await(t *testing.T, ch <-chan struct{}) {
82+ t.Helper()
83+ select {
84+ case <-ch:
85+ case <-time.After(3 * time.Second):
86+ t.Fatal("timed out")
87+ }
88+}
89+
90+func TestPoolHundredStreamsLossAndIsolation(t *testing.T) {
91+ var calls atomic.Int32
92+ var physical *fakePhysical
93+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
94+ calls.Add(1)
95+ physical = &fakePhysical{endpoint: e, done: make(chan struct{})}
96+ return physical, nil
97+ })
98+ streams := make([]ports.BrokerLogicalConnection, 100)
99+ var wg sync.WaitGroup
100+ for i := range streams {
101+ id, err := p.RegisterClient()
102+ require.NoError(t, err)
103+ wg.Add(1)
104+ go func(i int) {
105+ defer wg.Done()
106+ s, e := p.OpenStream(context.Background(), poolRequest(id, 1))
107+ if e != nil {
108+ t.Error(e)
109+ return
110+ }
111+ streams[i] = s
112+ }(i)
113+ }
114+ wg.Wait()
115+ require.EqualValues(t, 1, calls.Load())
116+ blocked := make(chan struct{})
117+ go func() { _ = streams[0].SendClient(nil); close(blocked) }()
118+ require.NoError(t, streams[0].Close())
119+ await(t, blocked)
120+ select {
121+ case <-streams[1].Done():
122+ t.Fatal("sibling closed")
123+ default:
124+ }
125+ require.NoError(t, physical.Close())
126+ for _, s := range streams[1:] {
127+ await(t, s.Done())
128+ var loss ports.BrokerError
129+ require.ErrorAs(t, s.Err(), &loss)
130+ require.Equal(t, ports.BrokerErrorAttachmentLost, loss.Code)
131+ }
132+}
133+
134+func TestPoolAdmissionAndNonreuse(t *testing.T) {
135+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
136+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
137+ })
138+ p.limits.StreamsPerClient = 1
139+ id, err := p.RegisterClient()
140+ require.NoError(t, err)
141+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
142+ require.NoError(t, err)
143+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
144+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
145+ require.NoError(t, s.Close())
146+ _, err = p.OpenStream(context.Background(), poolRequest(id, 2))
147+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
148+ s, err = p.OpenStream(context.Background(), poolRequest(id, 3))
149+ require.NoError(t, err)
150+ p.CloseClient(id)
151+ await(t, s.Done())
152+ _, err = p.OpenStream(context.Background(), poolRequest(id, 4))
153+ require.ErrorIs(t, err, ports.BrokerAdmissionStale)
154+}
155+
156+func TestPoolCoalescedCancellation(t *testing.T) {
157+ entered, proceed := make(chan struct{}), make(chan struct{})
158+ var calls atomic.Int32
159+ p, _ := setupPool(t, func(ctx context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
160+ calls.Add(1)
161+ close(entered)
162+ select {
163+ case <-ctx.Done():
164+ return nil, ctx.Err()
165+ case <-proceed:
166+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
167+ }
168+ })
169+ id1, _ := p.RegisterClient()
170+ id2, _ := p.RegisterClient()
171+ ctx, cancel := context.WithCancel(context.Background())
172+ result := make(chan error, 1)
173+ go func() { _, err := p.OpenStream(ctx, poolRequest(id1, 1)); result <- err }()
174+ await(t, entered)
175+ result2 := make(chan ports.BrokerLogicalConnection, 1)
176+ go func() {
177+ s, err := p.OpenStream(context.Background(), poolRequest(id2, 1))
178+ if err != nil {
179+ t.Error(err)
180+ }
181+ result2 <- s
182+ }()
183+ require.Eventually(t, func() bool {
184+ p.mu.Lock()
185+ defer p.mu.Unlock()
186+ for _, e := range p.entries {
187+ return e.refs == 2
188+ }
189+ return false
190+ }, time.Second, time.Millisecond)
191+ cancel()
192+ require.ErrorIs(t, <-result, context.Canceled)
193+ close(proceed)
194+ s := <-result2
195+ require.NotNil(t, s)
196+ require.EqualValues(t, 1, calls.Load())
197+ require.NoError(t, s.Close())
198+}
199+
200+func TestPoolAbandonedAttemptAndShutdown(t *testing.T) {
201+ entered, exited := make(chan struct{}), make(chan struct{})
202+ p, _ := setupPool(t, func(ctx context.Context, _ ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
203+ close(entered)
204+ <-ctx.Done()
205+ close(exited)
206+ return nil, ctx.Err()
207+ })
208+ id, _ := p.RegisterClient()
209+ ctx, cancel := context.WithCancel(context.Background())
210+ result := make(chan error, 1)
211+ go func() { _, err := p.OpenStream(ctx, poolRequest(id, 1)); result <- err }()
212+ await(t, entered)
213+ cancel()
214+ require.ErrorIs(t, <-result, context.Canceled)
215+ await(t, exited)
216+ require.NoError(t, p.Close())
217+ _, err := p.RegisterClient()
218+ require.ErrorIs(t, err, ports.BrokerAdmissionClosed)
219+}
220+
221+func TestPoolIdleEviction(t *testing.T) {
222+ physicals := make(chan *fakePhysical, 2)
223+ p, clock := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
224+ f := &fakePhysical{endpoint: e, done: make(chan struct{})}
225+ physicals <- f
226+ return f, nil
227+ })
228+ id, _ := p.RegisterClient()
229+ s, err := p.OpenStream(context.Background(), poolRequest(id, 1))
230+ require.NoError(t, err)
231+ f := <-physicals
232+ require.NoError(t, s.Close())
233+ require.Eventually(t, func() bool { clock.mu.Lock(); defer clock.mu.Unlock(); return len(clock.timers) > 0 }, time.Second, time.Millisecond)
234+ clock.Advance(time.Minute)
235+ await(t, f.done)
236+ require.NoError(t, p.Close())
237+}
238+
239+func TestPoolPolicyAndIdentityVerification(t *testing.T) {
240+ for _, mode := range []string{"identity", "policy", "resolver"} {
241+ t.Run(mode, func(t *testing.T) {
242+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
243+ if mode == "identity" {
244+ e.Identity = "impostor"
245+ }
246+ if mode == "policy" {
247+ e.Policy.Trust = "other"
248+ }
249+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
250+ })
251+ if mode == "resolver" {
252+ p.resolver = poolResolver(func(_ context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerResolvedEndpoint, error) {
253+ policy := r.Policy
254+ policy.Trust = "other"
255+ return ports.BrokerResolvedEndpoint{Identity: "canonical", Policy: policy, Address: "fake"}, nil
256+ })
257+ }
258+ id, _ := p.RegisterClient()
259+ _, err := p.OpenStream(context.Background(), poolRequest(id, 1))
260+ var typed ports.BrokerError
261+ require.ErrorAs(t, err, &typed)
262+ })
263+ }
264+}
265+
266+func BenchmarkPoolOpenClose(b *testing.B) {
267+ p, _ := setupPool(b, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
268+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
269+ })
270+ id, _ := p.RegisterClient()
271+ b.ReportAllocs()
272+ b.ResetTimer()
273+ for i := 0; i < b.N; i++ {
274+ s, err := p.OpenStream(context.Background(), poolRequest(id, uint64(i+1)))
275+ if err != nil {
276+ b.Fatal(err)
277+ }
278+ _ = s.Close()
279+ }
280+}
281+
282+func TestPoolAliasesAndPolicyPartition(t *testing.T) {
283+ var calls atomic.Int32
284+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
285+ calls.Add(1)
286+ return &fakePhysical{endpoint: e, done: make(chan struct{})}, nil
287+ })
288+ id, _ := p.RegisterClient()
289+ for i, alias := range []string{"user@first", "user@second"} {
290+ r := poolRequest(id, uint64(i+1))
291+ r.Local = false
292+ r.Endpoint = alias
293+ r.Registration = domain.RemoteRegistration{Endpoint: alias, Incarnation: [16]byte{1}, Generation: 1}
294+ s, err := p.OpenStream(context.Background(), r)
295+ require.NoError(t, err)
296+ defer s.Close()
297+ }
298+ require.EqualValues(t, 1, calls.Load())
299+ r := poolRequest(id, 3)
300+ r.Policy.Isolation = "other"
301+ s, err := p.OpenStream(context.Background(), r)
302+ require.NoError(t, err)
303+ defer s.Close()
304+ require.EqualValues(t, 2, calls.Load())
305+ r.Stream = 4
306+ r.Policy.Trust = "third"
307+ _, err = p.OpenStream(context.Background(), r)
308+ require.ErrorIs(t, err, ports.BrokerAdmissionLimit)
309+}
310+
311+func TestPoolPendingOpenCancellationDoesNotHoldLock(t *testing.T) {
312+ entered := make(chan struct{})
313+ p, _ := setupPool(t, func(_ context.Context, e ports.BrokerResolvedEndpoint) (ports.BrokerPhysicalConnection, error) {
314+ return &fakePhysical{endpoint: e, done: make(chan struct{}), open: func(ctx context.Context, r ports.BrokerOpenStreamRequest) (ports.BrokerLogicalConnection, error) {
315+ if r.Stream == 1 {
316+ close(entered)
317+ <-ctx.Done()
318+ return nil, ctx.Err()
319+ }
320+ return newFakeLogical(), nil
321+ }}, nil
322+ })
323+ id, _ := p.RegisterClient()
324+ result := make(chan error, 1)
325+ go func() { _, err := p.OpenStream(context.Background(), poolRequest(id, 1)); result <- err }()
326+ await(t, entered)
327+ s, err := p.OpenStream(context.Background(), poolRequest(id, 2))
328+ require.NoError(t, err)
329+ require.NoError(t, p.CloseStream(id, 1))
330+ require.ErrorIs(t, <-result, context.Canceled)
331+ select {
332+ case <-s.Done():
333+ t.Fatal("sibling canceled")
334+ default:
335+ }
336+ require.NoError(t, p.Close())
337+ await(t, s.Done())
338+}

Keyboard shortcuts

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

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

Branches 12

Tags 11

↑↓ moveenter openesc close