Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion doc/dev-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ It holds a `channelRef`, an atomically replaceable reference to the node's curre
There are three channel kinds:

* `OutboundChannel` runs over streams this node dials; it opens a new `session` for each stream and requeues pending calls when a stream is lost, as far as the send queue has space.
A stream that the server refuses is not lost: it ends with a refusal code (`Unauthenticated`, `PermissionDenied`, `InvalidArgument`, or `Unimplemented`) before it delivered a frame.
Its pending calls fail with the refusal, and the channel backs off before it opens the next stream, failing requests at once with the refusal until then.
A stream opened after a refusal is suspect: it counts as up only once it delivers a frame or lives 2 s, and until then its sends do not clear `LastErr`.
* `InboundChannel` runs over one stream that the server accepted; its single `session` ends with the stream.
* `LocalChannel` serves requests in-process through the handler, with no stream.

Expand All @@ -173,7 +176,7 @@ A `session` sends queued requests on its `BidiStream`, records two-way calls in
The dispatcher runs handlers one at a time, in arrival order.
A handler that returns without calling `Release` hands its goroutine to the next queued handler, so a busy stream keeps one goroutine and the stack it has grown; an early `Release` starts the next handler on a new goroutine.
On the server side, `Server.NodeStream` asks a `PeerAcceptor` for the `InboundChannel` of each accepted stream.
An `OutboundChannel` with a `PeerVerifier` checks the remote end of each new stream against the channel's node ID, with the stream's context, before it sends requests on the stream; a stream that fails the check is closed, and the waiting request fails with `codes.Unauthenticated`.
An `OutboundChannel` with a `PeerVerifier` checks the remote end of each new stream against the channel's node ID, with the stream's context, before it sends requests on the stream; a stream that fails the check is closed, the waiting request fails with `codes.Unauthenticated`, and the channel backs off as for a refused stream.

```mermaid
classDiagram
Expand Down Expand Up @@ -242,6 +245,28 @@ classDiagram
PeerAcceptor ..> InboundChannel : returns
```

Only the receive loop of an outbound session learns whether the server refused its stream: when the server ends a stream, a concurrent `Send` returns `io.EOF`, and only `Recv` returns the server's status.
So a send that fails with `io.EOF` before the stream delivered a frame only cancels the stream, and the receive loop disposes of the pending calls.
The session's `handoff` state ensures that exactly one goroutine disposes of them.
The session's `verdict` channel closes when the stream delivers its first frame or receiving ends; the run loop waits for it after a session ends, and then reads whether the server refused the stream.

```mermaid
stateDiagram-v2
[*] --> Fresh: stream opened
Fresh --> Accepted: first frame arrives
Fresh --> SendFailed: a send fails with io.EOF
Fresh --> Closed: Recv fails
SendFailed --> Accepted: a buffered frame arrives, so retry the pending calls
SendFailed --> Closed: Recv fails
Accepted --> Closed: Recv fails
Closed --> [*]
note right of Closed
From Fresh or SendFailed with a refusal code:
fail the pending calls with the refusal.
Otherwise: retry them.
end note
```

### Connectivity Layer (`internal/conn`)

A `Config` is a slice of `*Node`, and each `Node` has a fixed `stream.Transport`.
Expand Down
16 changes: 12 additions & 4 deletions doc/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2404,12 +2404,20 @@ Use `TLSPeerVerifier` to bind node IDs to mTLS certificates; see [Binding Node I
| --- | --- | --- |
| Claims the node ID of a configured peer | `verify(ctx, claimed ID)` before the stream is attached to the peer | The stream ends with `codes.Unauthenticated`; no connect callback or handler runs |
| Claims a node ID outside the peer configuration | None; there is no identity to check | The stream ends with `codes.Unauthenticated` |
| Opened by this server to a configured peer | `verify(ctx, dialed ID)` before any request is sent | The waiting call fails with `codes.Unauthenticated`; the server tries again later |
| Opened by this server to a configured peer | `verify(ctx, dialed ID)` before any request is sent | The waiting call fails with `codes.Unauthenticated`; the server backs off before it opens a new stream to the peer, as a refused peer does |
| Claims no peer node ID (a client) | None | Served as without the option |

A peer that this server refuses sees the refusal, with the verifier's text, in `Node.LastErr` of its node for this server.
Its pending two-way calls to this server are resent on new streams until their contexts end, so they fail with the context's error, not with `codes.Unauthenticated`; give calls between peers a deadline.
While the refused peer keeps retrying, its `ConnectedPeers` view can briefly include this server, and under `WithStreamDedup` its `WaitForAll` can return before this server has accepted any of its streams.
A peer that this server refuses sees the refusal, with the verifier's text, in `Node.LastErr` of its node for this server, until one of its streams to this server succeeds.
Its calls on the refused stream fail with the refusal's status; they are not resent.
A node treats a stream as refused when the stream ends with `codes.Unauthenticated`, `codes.PermissionDenied`, `codes.InvalidArgument`, or `codes.Unimplemented` before it delivered any message.
After a refusal, the refused peer backs off before it opens a new stream to this server: 50 ms after the first refusal, doubling after each further refusal up to 2 s.
Until the backoff ends, its calls to this server fail at once with the refusal.
The refused peer's first stream counts as connected until the refusal arrives, about one round trip later.
After a refusal, a new stream counts as connected only once it delivers a message or has lived 2 s.
After the refusal of its first stream arrives, its `ConnectedPeers` view no longer includes this server while this server keeps refusing it, and under `WithStreamDedup` its `WaitForAll` does not return.
A `WaitForAll` that is already waiting when the first stream opens can still return during that round trip.
Once this server accepts the peer, an idle stream counts as connected 2 s after it opened, or earlier if a message arrives on it.
When a stream that this server opened fails this server's own check, the peer at the other end counts this server as connected until the stream is closed, about one round trip, once per backoff period.

With `WithPeerVerifier`, `ServerContext.SenderID` is authenticated for every configured peer, with and without stream deduplication:

Expand Down
6 changes: 4 additions & 2 deletions internal/conn/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,10 @@ func (n *Node) Detail() string {
//
// It reports current node health, not the outcome of any particular call: it is
// last-write-wins across concurrent requests and reverts to nil once traffic
// flows to the node again. Use the [ByLastError] comparator with [Config.Sort]
// to order nodes by whether they are currently failing.
// flows to the node again. After the node refused a stream, it reverts to nil
// only once a stream proves viable: the stream delivers a message, or a send
// succeeds on a stream that has lived 2 s. Use the [ByLastError] comparator
// with [Config.Sort] to order nodes by whether they are currently failing.
func (n *Node) LastErr() error {
if ch := n.activeChannel(); ch != nil {
return ch.LastErr()
Expand Down
8 changes: 6 additions & 2 deletions internal/stream/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ type Channel interface {
StreamUp() bool
// LastErr returns [ErrSendStalled] while a send has been blocked for
// [stallReportDelay] or longer, and otherwise the error of the channel's
// most recent stream operation, or nil if it succeeded.
// most recent stream operation, or nil if it succeeded. After the server
// refused a stream, LastErr reports the refusal until a stream proves
// viable: it delivers a frame, or a send succeeds on a stream that has
// lived [eagerReconnectMaxDelay].
LastErr() error
// DroppedReplies returns the number of handler replies dropped because
// the send queue was full or closed.
// the send queue was full or closed, or because they failed during the
// backoff after the server refused a stream.
DroppedReplies() int64
// PendingCount returns the number of two-way calls awaiting responses.
// The count may include calls whose callers are done but that have not
Expand Down
Loading
Loading