fix(CLI-01): Go Events() emits terminal ErrSlowConsumer on overflow
Events()/EventsAfter() previously closed the channel silently when the 16-slot buffer overflowed, indistinguishable from a graceful server end. Reserve one slot and emit a terminal EventResult wrapping the new exported ErrSlowConsumer so overflow is always observable. The blocking SubscribeEvents path (gRPC flow-controlled) is unchanged. archreview: CLI-01 (P0). Verified: gofmt clean, go build ./..., go test ./... (+ -race) pass.
This commit is contained in:
@@ -18,6 +18,15 @@ import (
|
||||
|
||||
const maxBulkItems = 1000
|
||||
|
||||
// eventBufferSize is the number of buffered event slots on the
|
||||
// Events/EventsAfter (cancel-when-full) results channel.
|
||||
const eventBufferSize = 16
|
||||
|
||||
// eventBufferReservedSlots is the extra capacity reserved beyond
|
||||
// eventBufferSize so a terminal ErrSlowConsumer result can always be enqueued
|
||||
// non-blockingly on overflow, even when all data slots are full.
|
||||
const eventBufferReservedSlots = 1
|
||||
|
||||
// EventResult carries either the next ordered event or a terminal stream error.
|
||||
type EventResult struct {
|
||||
// Event is the next event from the stream when Err is nil.
|
||||
@@ -674,11 +683,22 @@ func (s *Session) Write2Raw(ctx context.Context, serverHandle, itemHandle int32,
|
||||
|
||||
// Events streams ordered session events until the server ends the stream,
|
||||
// context cancellation stops Recv, or a terminal error is sent.
|
||||
//
|
||||
// The returned channel is buffered. If the consumer falls behind and the buffer
|
||||
// overflows, the stream is terminated and a final EventResult carrying a
|
||||
// GatewayError that wraps ErrSlowConsumer is delivered before the channel
|
||||
// closes. Callers must match it with errors.Is(res.Err, ErrSlowConsumer) to
|
||||
// distinguish a slow-consumer drop from a graceful server end. Use
|
||||
// SubscribeEvents for a blocking, backpressured stream that never drops.
|
||||
func (s *Session) Events(ctx context.Context) (<-chan EventResult, error) {
|
||||
return s.EventsAfter(ctx, 0)
|
||||
}
|
||||
|
||||
// EventsAfter streams ordered session events after the given worker sequence.
|
||||
//
|
||||
// Like Events, the returned channel is buffered and terminates with a final
|
||||
// EventResult wrapping ErrSlowConsumer (matchable via errors.Is) if the consumer
|
||||
// falls behind and the buffer overflows, rather than silently closing.
|
||||
func (s *Session) EventsAfter(ctx context.Context, afterWorkerSequence uint64) (<-chan EventResult, error) {
|
||||
subscription, err := s.subscribeEventsAfter(ctx, afterWorkerSequence, true)
|
||||
if err != nil {
|
||||
@@ -708,7 +728,7 @@ func (s *Session) subscribeEventsAfter(ctx context.Context, afterWorkerSequence
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(chan EventResult, 16)
|
||||
results := make(chan EventResult, eventBufferSize+eventBufferReservedSlots)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(results)
|
||||
@@ -756,14 +776,30 @@ func sendEventResult(
|
||||
cancel context.CancelFunc,
|
||||
) bool {
|
||||
if cancelWhenBufferFull {
|
||||
// Treat the channel as full once the eventBufferSize data slots are
|
||||
// occupied, keeping eventBufferReservedSlots free for the terminal
|
||||
// error. This goroutine is the sole producer, so len(results) only
|
||||
// grows by our own sends and shrinks as the consumer reads; the reserve
|
||||
// therefore always survives to carry ErrSlowConsumer. A plain buffered
|
||||
// send would instead consume the reserved slot as ordinary data.
|
||||
if len(results) >= eventBufferSize {
|
||||
// The consumer fell behind and the data slots are full. Cancel the
|
||||
// stream, then use the reserved terminal slot to enqueue a loud
|
||||
// ErrSlowConsumer result so overflow is always observable rather
|
||||
// than a silently closed channel indistinguishable from a graceful
|
||||
// server end.
|
||||
cancel()
|
||||
select {
|
||||
case results <- EventResult{Err: &GatewayError{Op: "stream events", Err: ErrSlowConsumer}}:
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case results <- result:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user