terminal.go RawHistoryFolder
1package daemonmux
2
3import (
4 "errors"
5 "sync"
6
7 "github.com/bnema/vev/internal/domain"
8)
9
10// ErrTerminalFailure is the stable package sentinel published by Fail when it
11// is called with a nil cause. A failure always pairs a non-nil Err() with a
12// nonzero RemoteFailureKind, so Fail substitutes this sentinel for a nil cause
13// rather than publish an inconsistent terminal outcome.
14var ErrTerminalFailure = errors.New("daemonmux: terminal failure without a cause")
15
16// terminalState is the publish-once terminal authority shared by a physical
17// daemon connection and every logical stream it owns. It mirrors the
18// Done/Err/FailureKind contract of ports.BrokerPhysicalConnection: once the
19// connection reaches a terminal outcome, every getter returns the same stable
20// values and Done is closed exactly once, independent of reads.
21//
22// Exactly one publication wins. The first concurrent Fail or Close decides Err
23// and FailureKind; later publishers observe the winner and never change it. An
24// orderly local Close publishes a nil error and RemoteFailureNone. A failure
25// publishes its cause and a nonzero valid RemoteFailureKind, preserves the
26// error identity for errors.Is/As, and normalizes any kind outside the
27// transport-failure taxonomy (including RemoteFailureNone) to
28// RemoteFailureTransport. A failure never publishes a nil error: a nil cause is
29// replaced with the ErrTerminalFailure sentinel.
30type terminalState struct {
31 once sync.Once
32 done chan struct{}
33 err error
34 kind domain.RemoteFailureKind
35}
36
37// newTerminalState returns an open terminal state. Done stays open until a
38// publisher wins.
39func newTerminalState() *terminalState {
40 return &terminalState{done: make(chan struct{})}
41}
42
43// Done returns a channel closed exactly once when the terminal outcome is
44// published. The channel is stable and this method is safe before or after
45// publication.
46func (s *terminalState) Done() <-chan struct{} { return s.done }
47
48// Err returns the published failure cause: nil while open and for an orderly
49// local Close. The result is stable once Done is closed.
50func (s *terminalState) Err() error {
51 select {
52 case <-s.done:
53 return s.err
54 default:
55 return nil
56 }
57}
58
59// FailureKind returns the published failure classification: RemoteFailureNone
60// while open and for an orderly local Close. The result is stable once Done is
61// closed.
62func (s *terminalState) FailureKind() domain.RemoteFailureKind {
63 select {
64 case <-s.done:
65 return s.kind
66 default:
67 return domain.RemoteFailureNone
68 }
69}
70
71// Fail publishes a failure terminal outcome. It wins only while the state is
72// open, so at most one publisher ever applies. The kind must be a nonzero
73// valid RemoteFailureKind; any other value, including RemoteFailureNone, is
74// normalized to RemoteFailureTransport. A non-nil error is stored verbatim so
75// callers keep errors.Is/As identity; a nil error is replaced with the
76// ErrTerminalFailure package sentinel so a failure never publishes a nil Err()
77// alongside a nonzero kind.
78func (s *terminalState) Fail(kind domain.RemoteFailureKind, err error) {
79 s.publishFail(kind, err)
80}
81
82// publishFail applies a failure outcome exactly once and reports whether this
83// call won the publication. It is the primitive behind Fail and the callers
84// that must know they are the one publisher - for example, so a stream signals
85// its peer with exactly one Reset instead of one per observer. The first
86// publisher's values win; a later caller observes false and must not apply its
87// own outcome.
88func (s *terminalState) publishFail(kind domain.RemoteFailureKind, err error) bool {
89 if kind < domain.RemoteFailureTransport || kind > domain.RemoteFailureInvalidResponse {
90 kind = domain.RemoteFailureTransport
91 }
92 if err == nil {
93 err = ErrTerminalFailure
94 }
95 won := false
96 s.once.Do(func() {
97 s.err = err
98 s.kind = kind
99 close(s.done)
100 won = true
101 })
102 return won
103}
104
105// Close publishes an orderly local terminal outcome: a nil error and
106// RemoteFailureNone. It wins only while the state is open.
107func (s *terminalState) Close() {
108 s.publish(nil, domain.RemoteFailureNone)
109}
110
111// publish applies the first outcome exactly once and then closes Done. The
112// winning values are stored before close(done), so every reader that observes
113// the closed channel sees them.
114func (s *terminalState) publish(err error, kind domain.RemoteFailureKind) {
115 s.once.Do(func() {
116 s.err = err
117 s.kind = kind
118 close(s.done)
119 })
120}