executeConsensus decouples two distinct concerns: 1. Caller-visible latency: the caller must return promptly when its context is cancelled (HTTP disconnect, upstream deadline, shutdown). 2. Analysis completeness: misbehavior tracking and metrics must see every participant's response, even ones that
( ctx context.Context, lg *zerolog.Logger, originalReq *common.NormalizedRequest, labels metricsLabels, in inner, startTime time.Time, consensusSpan trace.Span, )
| 178 | // which is already bounded by failsafe policy timeouts and HTTP client |
| 179 | // timeouts — no new magic-number budget is required. |
| 180 | func (e *executor) executeConsensus( |
| 181 | ctx context.Context, |
| 182 | lg *zerolog.Logger, |
| 183 | originalReq *common.NormalizedRequest, |
| 184 | labels metricsLabels, |
| 185 | in inner, |
| 186 | startTime time.Time, |
| 187 | consensusSpan trace.Span, |
| 188 | ) *slotResult { |
| 189 | ctx, collectionSpan := common.StartDetailSpan(ctx, "Consensus.CollectResponses") |
| 190 | // NOTE: collectionSpan.End() is owned by runAnalyzer (in its deferred |
| 191 | // cleanup), not this function. The analyzer outlives executeConsensus on |
| 192 | // the caller-cancel path, and ending the span here would drop late |
| 193 | // attributes (short_circuited, responses.collected). |
| 194 | |
| 195 | // For fire-and-forget mode, detach from parent context cancellation so background |
| 196 | // requests continue even after the HTTP response is sent. This is critical for |
| 197 | // transaction broadcasting where we want all nodes to receive the transaction. |
| 198 | // For normal mode, inherit parent cancellation for proper resource cleanup. |
| 199 | var baseCtx context.Context |
| 200 | if e.config.fireAndForget { |
| 201 | baseCtx = context.WithoutCancel(ctx) |
| 202 | } else { |
| 203 | baseCtx = ctx |
| 204 | } |
| 205 | |
| 206 | cancellableCtx, cancelFunc := context.WithCancel(baseCtx) |
| 207 | var cancelOnce sync.Once |
| 208 | cancelRemaining := func() { |
| 209 | cancelOnce.Do(cancelFunc) |
| 210 | } |
| 211 | |
| 212 | // Cancel remaining requests on exit, unless fire-and-forget mode is enabled. |
| 213 | // In fire-and-forget mode we want background requests to complete naturally. |
| 214 | // sync.Once ensures cancel is called at most once even if called explicitly earlier. |
| 215 | defer func() { |
| 216 | if !e.config.fireAndForget { |
| 217 | cancelRemaining() |
| 218 | } |
| 219 | }() |
| 220 | |
| 221 | // Spawn only as many participants as configured by policy |
| 222 | maxToSpawn := e.maxParticipants |
| 223 | if maxToSpawn <= 0 { |
| 224 | maxToSpawn = 1 |
| 225 | } |
| 226 | // Record on the request's ExecState that THIS request went through |
| 227 | // the consensus executor and how many participants we spawned. Read |
| 228 | // downstream by diagnostic surfaces (admin endpoints, response |
| 229 | // headers, the simulator's lifecycle drawer) so operators can tell |
| 230 | // "this request was a consensus race" from "this was a hedge race" |
| 231 | // — the two look similar in the per-attempt log otherwise. |
| 232 | if st := originalReq.ExecState(); st != nil { |
| 233 | st.ConsensusSlots.Add(int32(maxToSpawn)) |
| 234 | } |
| 235 | responseChan := make(chan *execResult, maxToSpawn) |
| 236 | // Per-slot cancellable child contexts let us cancel losers explicitly. |
| 237 | // Each slot inherits the shared cancellableCtx (which is cancelled |
no test coverage detected