fix: Renew presence on sub/pub
Chart Release / chart (push) Successful in 18s
Server Deploy / deploy (push) Successful in 56s

This commit is contained in:
2026-07-31 16:58:50 +01:00
parent 71240f183c
commit 8699dc5b7e
3 changed files with 57 additions and 3 deletions
+34 -2
View File
@@ -42,6 +42,15 @@ type CommandEnvelope struct {
Command *pb.ServerCommand `json:"command"`
ReplyTo string `json:"reply_to"`
Log *LogRequest `json:"log,omitempty"`
// Node names the pod this envelope is for: the presence holder at the time
// it was published. The command channel is a fan-out, so during a reconnect
// two pods can be subscribed for one agent — the pod with the live stream,
// and a pod whose stream is half-open and has not yet noticed. Both would
// receive the envelope, and the first to ack wins the request. If that is
// the stale one, the command is queued onto a dead stream and acked OK: the
// operator is told it worked and the agent never sees it.
Node string `json:"node,omitempty"`
}
// LogRequest asks the owner pod to open a step log before it dispatches.
@@ -108,8 +117,13 @@ func (d *commandDispatcher) Serve(ctx context.Context, serverID string) (<-chan
case <-runCtx.Done():
return
case <-t.C:
if err := bus.SetPresence(runCtx, serverID, presenceTTL); err != nil {
log.Printf("dispatch: renew presence for %s: %v", serverID, err)
// Renew only while this pod still holds the claim. Losing it
// means a newer stream for the same agent was established
// elsewhere, and this one is a half-open leftover: it must stop
// renewing rather than overwrite the live owner every 10s.
if !bus.RenewPresence(runCtx, serverID, presenceTTL) {
log.Printf("dispatch: presence for %s is held elsewhere, stopping renewal", serverID)
return
}
}
}
@@ -152,6 +166,15 @@ func (d *commandDispatcher) handleEnvelope(ctx context.Context, raw []byte, out
return
}
// Not addressed to this pod: stay silent rather than ack. Answering would
// win the race against the pod that actually holds the agent's stream, and
// the caller would be told a command succeeded that was queued onto a
// stream nobody is reading. Silence lets the real owner answer, or lets the
// request time out as ErrNoResponder, which fails loudly and correctly.
if env.Node != "" && env.Node != bus.NodeID() {
return
}
ack := CommandAck{OK: true, Node: bus.NodeID()}
if env.Log != nil {
@@ -195,6 +218,15 @@ func (d *commandDispatcher) send(env CommandEnvelope) (CommandAck, error) {
ctx, cancel := context.WithTimeout(context.Background(), dispatchAckTimeout)
defer cancel()
// Resolved once, here, and carried in the envelope. Reading it at publish
// time rather than letting subscribers self-select is what makes a stale
// subscriber harmless: it will see an envelope addressed elsewhere and
// ignore it.
env.Node = bus.PresenceHolder(ctx, env.ServerID)
if env.Node == "" {
return CommandAck{}, fmt.Errorf("%w: %s", ErrAgentNotConnected, env.ServerID)
}
raw, err := bus.Request(ctx, bus.CommandChannel+env.ServerID, env.ReplyTo, env, dispatchAckTimeout)
if err != nil {
if errors.Is(err, bus.ErrNoResponder) {