The outbound half went behind `Bus` and a node's two statements stopped naming a transport. This is the other half, and where the transport reached furthest: the run loop selected on a channel of the client library's own delivery type, so every part of holding a node in its mesh knew which bus it was on. `Link` is dialling, hearing and saying in one interface, because dialling is where the transport is chosen and choosing it twice is how one half of a node ends up on a different bus from the other. `Declaration` has one way of being done rather than two: a declaration set aside for a newer one is settled exactly as an applied one is, on both buses, and the difference is a fact the report carries. Four things this settled. **The host declares nothing on the new bus.** On the bus the mesh has it declares its own queue, because a queue that is not there means a node that hears nothing. Here it binds to a consumer the mesh made when the node enrolled, and a missing one is said as the mesh's to answer rather than quietly created with whatever this client happens to default to. **The pin is easier here than in the tool runtime, not harder.** The Go client takes a *tls.Config, so the same PinnedConfig with the same VerifyPeerCertificate does the work — the subject-alternative-name constraint recorded against the runtime's client is that client's, because it takes PEM strings with no verify hook. A host checks the fingerprint and nothing else. **Binding needs the subject as well as the consumer.** An empty subject is refused rather than taken to mean "whatever that consumer delivers", which the server said plainly and only when asked. **Reconnection stays the caller's.** Hold already decides when to try again and how long to wait; a client reconnecting underneath it would make that reasoning a duplicate of the library's. The drain keeps its live half and loses its catch-up half, as it said it would: verified that three declarations pushed to an absent node leave one on the stream, and it is the newest. One test-harness lesson worth the comment it got: delete-then-add is not a reset. A test that did that inherited the previous test's messages, and the symptom was a declaration counted as delivered twice — which reads as a redelivery bug in the code under test rather than as a dirty stream.
85 lines
3.4 KiB
Go
85 lines
3.4 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// What a host hears, as the host's own words for it.
|
|
//
|
|
// The outbound half went behind `Bus` (bus.go) and a node's two statements stopped naming a
|
|
// transport. This is the other half — dialling, and the declarations that arrive — and it is where
|
|
// the transport reached furthest: the run loop selected on a channel of the client library's own
|
|
// delivery type, so every part of holding a node in its mesh knew which bus it was on.
|
|
//
|
|
// **The host still imports nothing of the mesh's own** (novox/hq ADR 0005). This is its own
|
|
// interface over its own libraries, and it agrees with the controller only because a conformance
|
|
// fixture holds both to one envelope.
|
|
|
|
// Link is this node's live connection to its mesh: what it hears, and what it says.
|
|
//
|
|
// One interface rather than two, because **dialling is where the transport is chosen** and choosing
|
|
// it twice is how one half of a node ends up on a different bus from the other.
|
|
type Link interface {
|
|
// Bus is what this node says: what it applied, and that it is here.
|
|
Bus
|
|
|
|
// Declarations is what the mesh tells this node to be.
|
|
Declarations() <-chan Declaration
|
|
|
|
// Lost says the link ended, and why.
|
|
//
|
|
// **Read rather than discovered.** A node that finds out by noticing silence is a node that
|
|
// believed it was in the mesh for as long as the silence lasted, which is the one state ADR
|
|
// 0004 says must never look like being connected.
|
|
Lost() <-chan error
|
|
|
|
// Close lets go of whatever was dialled.
|
|
Close()
|
|
}
|
|
|
|
// Declaration is one thing the mesh told this node to be.
|
|
//
|
|
// **Handled, once — after the report is published.** A node that dies between applying and
|
|
// reporting leaves the declaration with the mesh and applies it again on return, which is safe
|
|
// because applying is reconciliation: it converges rather than repeating.
|
|
//
|
|
// There is one way of being done rather than two. A declaration set aside because a newer arrived
|
|
// with it is settled exactly as an applied one is, on both buses, and the difference between them
|
|
// is a fact the *report* carries — a second method here would be a distinction the transport does
|
|
// not make.
|
|
type Declaration interface {
|
|
// Body is the signed declaration as it arrived, bytes unchanged: a node verifies what it
|
|
// received rather than what it re-encoded.
|
|
Body() []byte
|
|
|
|
// Handled settles it. Called after the report for it has been published, either way.
|
|
Handled() error
|
|
}
|
|
|
|
// Open opens this node's link to its mesh.
|
|
//
|
|
// Named Open rather than Dial because Dial is this package's raw TLS dial, which the enrolment path
|
|
// uses to see a certificate before it trusts anything.
|
|
//
|
|
// **Both transports ship and this is the one place that chooses** (novox/hq ADR 0116: nothing moves
|
|
// a node's bus before step 5). Until then every membership names the bus the mesh runs on today,
|
|
// and the rollout is this switch and the credential behind it — not a change anywhere in the loop
|
|
// that reads from what comes back.
|
|
func Open(ctx context.Context, m Membership, timeout time.Duration) (Link, error) {
|
|
switch m.Transport {
|
|
case OnNATS:
|
|
return dialNats(ctx, m, timeout)
|
|
default:
|
|
return dialCurrent(ctx, m, timeout)
|
|
}
|
|
}
|
|
|
|
// The buses a node can be on. Empty is the one the mesh runs on today, which is every node until
|
|
// the rollout — so a membership recorded before any of this existed reads as correct rather than as
|
|
// unset.
|
|
const (
|
|
OnCurrent = ""
|
|
OnNATS = "nats"
|
|
)
|