feat(broker): add bounded connection pooling
- commit
- a3b975e6903dc579d16950d15b3dae9abbffb01c
- author
- brice · · 2026-09-16 10:20 UTC
- parent
- ad3de98
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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. | |||
| 102 | 102 | Never edit generated `*.pb.go`; never add manual IDs or dispatch tables. | |
| 103 | 103 | - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`. | |
| 104 | 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. | |
| @@ -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 ( | |||
| 65 | 65 | BrokerMaxTransportBytes = 64 | |
| 66 | 66 | // BrokerMaxIdentityBytes bounds the authenticated daemon identity token. | |
| 67 | 67 | BrokerMaxIdentityBytes = 256 | |
| 68 | + | // BrokerMaxPolicyTokenBytes bounds each authoritative policy identity. | |
| 69 | + | BrokerMaxPolicyTokenBytes = 256 | |
| 68 | 70 | ) | |
| 69 | 71 | ||
| 70 | 72 | // BrokerEpoch identifies one broker process incarnation. Every broker | |
| @@ -147,9 +149,18 @@ type BrokerPolicy struct { | |||
| 147 | 149 | CatalogSchemaVersion uint16 | |
| 148 | 150 | EnvironmentPolicy protocol.EnvironmentPolicy | |
| 149 | 151 | 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 | 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 | 164 | if p.ProtocolVersion == 0 { | |
| 154 | 165 | return errors.New("ports: broker policy has no protocol version") | |
| 155 | 166 | } | |
| @@ -168,10 +179,7 @@ func (p BrokerPolicy) Validate() error { | |||
| 168 | 179 | // Compatible reports whether two validated policies may share a pooled | |
| 169 | 180 | // physical transport. It requires exact equality across every field. | |
| 170 | 181 | 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 | |
| 175 | 183 | } | |
| 176 | 184 | ||
| 177 | 185 | // BrokerHostTombstone fences a removed host registration so a stale probe, | |
| @@ -322,6 +330,9 @@ func BrokerCompletionIsStale(currentRegistration domain.RemoteRegistration, curr | |||
| 322 | 330 | // session environment; it is never inherited from the broker process | |
| 323 | 331 | // environment. | |
| 324 | 332 | type BrokerOpenStreamRequest struct { | |
| 333 | + | Epoch BrokerEpoch | |
| 334 | + | Purpose BrokerStreamPurpose | |
| 335 | + | Local bool | |
| 325 | 336 | Connection BrokerConnectionID | |
| 326 | 337 | Stream BrokerStreamID | |
| 327 | 338 | Endpoint string | |
| @@ -332,23 +343,38 @@ type BrokerOpenStreamRequest struct { | |||
| 332 | 343 | } | |
| 333 | 344 | ||
| 334 | 345 | func (r BrokerOpenStreamRequest) Validate() error { | |
| 346 | + | if r.Epoch == 0 { | |
| 347 | + | return errors.New("ports: missing broker epoch") | |
| 348 | + | } | |
| 335 | 349 | if err := r.Connection.Validate(); err != nil { | |
| 336 | 350 | return err | |
| 337 | 351 | } | |
| 338 | 352 | if err := r.Stream.Validate(); err != nil { | |
| 339 | 353 | return err | |
| 340 | 354 | } | |
| 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 | + | } | |
| 349 | 366 | } | |
| 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") | |
| 352 | 378 | } | |
| 353 | 379 | if uint64(len(r.Env)) > BrokerMaxEnvEntries { | |
| 354 | 380 | return errors.New("ports: broker open stream has too many environment entries") | |
| @@ -492,6 +518,8 @@ func (c BrokerErrorCode) Validate() error { | |||
| 492 | 518 | type BrokerError struct { | |
| 493 | 519 | Code BrokerErrorCode | |
| 494 | 520 | Text string | |
| 521 | + | // Cause is local-only diagnostic authority, never serialized as display text. | |
| 522 | + | Cause error | |
| 495 | 523 | } | |
| 496 | 524 | ||
| 497 | 525 | func (e BrokerError) Error() string { | |
| @@ -501,6 +529,8 @@ func (e BrokerError) Error() string { | |||
| 501 | 529 | return "vev: broker " + e.Code.String() + ": " + e.Text | |
| 502 | 530 | } | |
| 503 | 531 | ||
| 532 | + | func (e BrokerError) Unwrap() error { return e.Cause } | |
| 533 | + | ||
| 504 | 534 | func (e BrokerError) Validate() error { | |
| 505 | 535 | if err := e.Code.Validate(); err != nil { | |
| 506 | 536 | return err | |
| @@ -522,9 +552,19 @@ type BrokerStreamLost struct { | |||
| 522 | 552 | Stream BrokerStreamID | |
| 523 | 553 | Epoch BrokerEpoch | |
| 524 | 554 | 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 | 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 | 568 | if err := e.Connection.Validate(); err != nil { | |
| 529 | 569 | return err | |
| 530 | 570 | } | |
| @@ -555,7 +595,7 @@ type BrokerService interface { | |||
| 555 | 595 | ConnectionID() BrokerConnectionID | |
| 556 | 596 | Snapshot() BrokerSnapshot | |
| 557 | 597 | Subscribe() (BrokerSubscription, error) | |
| 558 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error) | |
| 598 | + | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) | |
| 559 | 599 | CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error | |
| 560 | 600 | AddHost(ctx context.Context, target string) error | |
| 561 | 601 | RemoveHost(ctx context.Context, target string) (bool, error) | |
| @@ -602,10 +642,17 @@ type BrokerHostProbe interface { | |||
| 602 | 642 | // state, identity, geometry, input, output, cancellation, or application | |
| 603 | 643 | // errors between streams. | |
| 604 | 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 | 652 | Identity() BrokerDaemonIdentity | |
| 606 | 653 | Incarnation() BrokerDaemonIncarnation | |
| 607 | 654 | Policy() BrokerPolicy | |
| 608 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error) | |
| 655 | + | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) | |
| 609 | 656 | Close() error | |
| 610 | 657 | } | |
| 611 | 658 | ||
| @@ -613,7 +660,7 @@ type BrokerPhysicalConnection interface { | |||
| 613 | 660 | // one endpoint under an exact compatible policy. Conflicting policy is | |
| 614 | 661 | // rejected; the first launching client never becomes policy authority. | |
| 615 | 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 | 666 | // BrokerListener accepts client connections to the per-user broker | |
| @@ -65,6 +65,8 @@ const ( | |||||
| 65 | BrokerMaxTransportBytes = 64 | 65 | BrokerMaxTransportBytes = 64 | ||
| 66 | // BrokerMaxIdentityBytes bounds the authenticated daemon identity token. | 66 | // BrokerMaxIdentityBytes bounds the authenticated daemon identity token. | ||
| 67 | BrokerMaxIdentityBytes = 256 | 67 | BrokerMaxIdentityBytes = 256 | ||
| 68 | + | // BrokerMaxPolicyTokenBytes bounds each authoritative policy identity. | |||
| 69 | + | BrokerMaxPolicyTokenBytes = 256 | |||
| 68 | ) | 70 | ) | ||
| 69 | 71 | ||||
| 70 | // BrokerEpoch identifies one broker process incarnation. Every broker | 72 | // BrokerEpoch identifies one broker process incarnation. Every broker | ||
| @@ -147,9 +149,18 @@ type BrokerPolicy struct { | |||||
| 147 | CatalogSchemaVersion uint16 | 149 | CatalogSchemaVersion uint16 | ||
| 148 | EnvironmentPolicy protocol.EnvironmentPolicy | 150 | EnvironmentPolicy protocol.EnvironmentPolicy | ||
| 149 | Transport string | 151 | 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 pooled | 179 | // 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 process | 330 | // 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 BrokerConnectionID | 336 | Connection BrokerConnectionID | ||
| 326 | Stream BrokerStreamID | 337 | Stream BrokerStreamID | ||
| 327 | Endpoint string | 338 | 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 err | 350 | return err | ||
| 337 | } | 351 | } | ||
| 338 | if err := r.Stream.Validate(); err != nil { | 352 | if err := r.Stream.Validate(); err != nil { | ||
| 339 | return err | 353 | 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 BrokerErrorCode | 519 | Code BrokerErrorCode | ||
| 494 | Text string | 520 | 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.Text | 529 | 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 err | 536 | return err | ||
| @@ -522,9 +552,19 @@ type BrokerStreamLost struct { | |||||
| 522 | Stream BrokerStreamID | 552 | Stream BrokerStreamID | ||
| 523 | Epoch BrokerEpoch | 553 | Epoch BrokerEpoch | ||
| 524 | Cause domain.RemoteFailureKind | 554 | 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 err | 569 | return err | ||
| 530 | } | 570 | } | ||
| @@ -555,7 +595,7 @@ type BrokerService interface { | |||||
| 555 | ConnectionID() BrokerConnectionID | 595 | ConnectionID() BrokerConnectionID | ||
| 556 | Snapshot() BrokerSnapshot | 596 | 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) error | 599 | CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error | ||
| 560 | AddHost(ctx context.Context, target string) error | 600 | 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 application | 642 | // 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() BrokerDaemonIdentity | 652 | Identity() BrokerDaemonIdentity | ||
| 606 | Incarnation() BrokerDaemonIncarnation | 653 | Incarnation() BrokerDaemonIncarnation | ||
| 607 | Policy() BrokerPolicy | 654 | Policy() BrokerPolicy | ||
| 608 | - | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error) | 655 | + | OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (BrokerLogicalConnection, error) |
| 609 | Close() error | 656 | Close() error | ||
| 610 | } | 657 | } | ||
| 611 | 658 | ||||
| @@ -613,7 +660,7 @@ type BrokerPhysicalConnection interface { | |||||
| 613 | // one endpoint under an exact compatible policy. Conflicting policy is | 660 | // 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 broker | 666 | // 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 { | |||
| 32 | 32 | CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, | |
| 33 | 33 | EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, | |
| 34 | 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 | 267 | "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }), | |
| 267 | 268 | "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }), | |
| 268 | 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 | 274 | for name, conflict := range conflicts { | |
| 271 | 275 | if base.Compatible(conflict) { | |
| @@ -273,11 +277,10 @@ func TestBrokerPolicyCompatibility(t *testing.T) { | |||
| 273 | 277 | } | |
| 274 | 278 | } | |
| 275 | 279 | 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 = "" }), | |
| 281 | 284 | } | |
| 282 | 285 | for i, policy := range invalid { | |
| 283 | 286 | if err := policy.Validate(); err == nil { | |
| @@ -289,6 +292,7 @@ func TestBrokerPolicyCompatibility(t *testing.T) { | |||
| 289 | 292 | func TestBrokerOpenStreamRequestValidate(t *testing.T) { | |
| 290 | 293 | valid := func() BrokerOpenStreamRequest { | |
| 291 | 294 | return BrokerOpenStreamRequest{ | |
| 295 | + | Epoch: 1, Purpose: BrokerStreamAttachment, | |
| 292 | 296 | Connection: BrokerConnectionID{1}, | |
| 293 | 297 | Stream: 2, | |
| 294 | 298 | Endpoint: "user@arch", | |
| @@ -466,6 +470,8 @@ func TestBrokerInterfacesArePorts(t *testing.T) { | |||
| 466 | 470 | // adapters can implement them without importing use cases. | |
| 467 | 471 | for name, iface := range map[string]reflect.Type{ | |
| 468 | 472 | "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(), | |
| 473 | + | "BrokerEndpointResolver": reflect.TypeOf((*BrokerEndpointResolver)(nil)).Elem(), | |
| 474 | + | "BrokerLogicalConnection": reflect.TypeOf((*BrokerLogicalConnection)(nil)).Elem(), | |
| 469 | 475 | "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(), | |
| 470 | 476 | "BrokerHostProbe": reflect.TypeOf((*BrokerHostProbe)(nil)).Elem(), | |
| 471 | 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 | + | } | |
| @@ -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 | |||
| 350 | 350 | } | |
| 351 | 351 | ||
| 352 | 352 | // 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 | 354 | ret := _mock.Called(ctx, request) | |
| 355 | 355 | ||
| 356 | 356 | if len(ret) == 0 { | |
| 357 | 357 | panic("no return value specified for OpenStream") | |
| 358 | 358 | } | |
| 359 | 359 | ||
| 360 | - | var r0 ports.ClientConnection | |
| 360 | + | var r0 ports.BrokerLogicalConnection | |
| 361 | 361 | 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 | 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 | 366 | r0 = returnFunc(ctx, request) | |
| 367 | 367 | } else { | |
| 368 | 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 | 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 | 407 | return _c | |
| 408 | 408 | } | |
| 409 | 409 | ||
| 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) | |
| 412 | 412 | 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 | 416 | _c.Call.Return(run) | |
| 417 | 417 | return _c | |
| 418 | 418 | } | |
| @@ -917,6 +917,140 @@ func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error | |||
| 917 | 917 | 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 | 1054 | // Identity provides a mock function for the type MockBrokerPhysicalConnection | |
| 921 | 1055 | func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity { | |
| 922 | 1056 | ret := _mock.Called() | |
| @@ -1008,23 +1142,23 @@ func (_c *MockBrokerPhysicalConnection_Incarnation_Call) RunAndReturn(run func() | |||
| 1008 | 1142 | } | |
| 1009 | 1143 | ||
| 1010 | 1144 | // 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 | 1146 | ret := _mock.Called(ctx, request) | |
| 1013 | 1147 | ||
| 1014 | 1148 | if len(ret) == 0 { | |
| 1015 | 1149 | panic("no return value specified for OpenStream") | |
| 1016 | 1150 | } | |
| 1017 | 1151 | ||
| 1018 | - | var r0 ports.ClientConnection | |
| 1152 | + | var r0 ports.BrokerLogicalConnection | |
| 1019 | 1153 | 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 | 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 | 1158 | r0 = returnFunc(ctx, request) | |
| 1025 | 1159 | } else { | |
| 1026 | 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 | 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 | 1199 | return _c | |
| 1066 | 1200 | } | |
| 1067 | 1201 | ||
| 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) | |
| 1070 | 1204 | 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 | 1208 | _c.Call.Return(run) | |
| 1075 | 1209 | return _c | |
| 1076 | 1210 | } | |
| @@ -1147,8 +1281,8 @@ func (_m *MockBrokerEndpointConnector) EXPECT() *MockBrokerEndpointConnector_Exp | |||
| 1147 | 1281 | } | |
| 1148 | 1282 | ||
| 1149 | 1283 | // 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) | |
| 1152 | 1286 | ||
| 1153 | 1287 | if len(ret) == 0 { | |
| 1154 | 1288 | panic("no return value specified for Connect") | |
| @@ -1156,18 +1290,18 @@ func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint | |||
| 1156 | 1290 | ||
| 1157 | 1291 | var r0 ports.BrokerPhysicalConnection | |
| 1158 | 1292 | 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) | |
| 1161 | 1295 | } | |
| 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) | |
| 1164 | 1298 | } else { | |
| 1165 | 1299 | if ret.Get(0) != nil { | |
| 1166 | 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 { | |
| 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) | |
| 1171 | 1305 | } else { | |
| 1172 | 1306 | r1 = ret.Error(1) | |
| 1173 | 1307 | } | |
| @@ -1181,30 +1315,24 @@ type MockBrokerEndpointConnector_Connect_Call struct { | |||
| 1181 | 1315 | ||
| 1182 | 1316 | // Connect is a helper method to define mock.On call | |
| 1183 | 1317 | // - 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)} | |
| 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 | 1324 | _c.Call.Run(func(args mock.Arguments) { | |
| 1192 | 1325 | var arg0 context.Context | |
| 1193 | 1326 | if args[0] != nil { | |
| 1194 | 1327 | arg0 = args[0].(context.Context) | |
| 1195 | 1328 | } | |
| 1196 | - | var arg1 string | |
| 1329 | + | var arg1 ports.BrokerResolvedEndpoint | |
| 1197 | 1330 | 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) | |
| 1203 | 1332 | } | |
| 1204 | 1333 | run( | |
| 1205 | 1334 | arg0, | |
| 1206 | 1335 | arg1, | |
| 1207 | - | arg2, | |
| 1208 | 1336 | ) | |
| 1209 | 1337 | }) | |
| 1210 | 1338 | return _c | |
| @@ -1215,7 +1343,7 @@ func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnect | |||
| 1215 | 1343 | 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 | 1347 | _c.Call.Return(run) | |
| 1220 | 1348 | return _c | |
| 1221 | 1349 | } | |
| @@ -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 MockBrokerService | 352 | // 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.ClientConnection | 360 | + | var r0 ports.BrokerLogicalConnection |
| 361 | var r1 error | 361 | 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 _c | 407 | 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 _c | 412 | 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 _c | 417 | return _c | ||
| 418 | } | 418 | } | ||
| @@ -917,6 +917,140 @@ func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error | |||||
| 917 | return _c | 917 | 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 MockBrokerPhysicalConnection | 1054 | // 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 MockBrokerPhysicalConnection | 1144 | // 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.ClientConnection | 1152 | + | var r0 ports.BrokerLogicalConnection |
| 1019 | var r1 error | 1153 | 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 _c | 1199 | 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 _c | 1204 | 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 _c | 1209 | 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 MockBrokerEndpointConnector | 1283 | // 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.BrokerPhysicalConnection | 1291 | var r0 ports.BrokerPhysicalConnection | ||
| 1158 | var r1 error | 1292 | 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 call | 1316 | // Connect is a helper method to define mock.On call | ||
| 1183 | // - ctx context.Context | 1317 | // - ctx context.Context | ||
| 1184 | - | // - endpoint string | 1318 | + | // - endpoint ports.BrokerResolvedEndpoint |
| 1185 | - | // - policy ports.BrokerPolicy | 1319 | + | 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.Context | 1325 | 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 string | 1329 | + | 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 _c | 1338 | return _c | ||
| @@ -1215,7 +1343,7 @@ func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnect | |||||
| 1215 | return _c | 1343 | 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 _c | 1348 | 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 | + | } | |||