package runtime import ( "context" "sync" "sync/atomic" "time" "github.com/tegwick/fluid-core/internal/contract" ) // Sink receives normalized telemetry. Implementations may block; the emitter // exists precisely so that blocking never reaches a request. type Sink interface { Write(context.Context, contract.FluidTelemetry) error } // Emitter buffers telemetry and delivers it out of band. // // ArchitectureBlueprint.md section 34.2 is a hard invariant: telemetry // backpressure must not block normal API requests. The buffer is therefore // bounded and lossy by design. Losing evidence degrades learning; blocking a // request degrades the service, and the service is the thing that must not // degrade. type Emitter struct { sink Sink ch chan contract.FluidTelemetry dropped atomic.Int64 written atomic.Int64 failed atomic.Int64 stopOnce sync.Once done chan struct{} wg sync.WaitGroup } // EmitterOptions configures buffering and delivery. type EmitterOptions struct { // Buffer is the number of events held in memory. When it fills, new events // are dropped rather than queued. Buffer int // Workers is the number of concurrent deliveries. Workers int // WriteTimeout bounds a single sink write. WriteTimeout time.Duration } // NewEmitter starts an emitter delivering into sink. func NewEmitter(sink Sink, opts EmitterOptions) *Emitter { if opts.Buffer <= 0 { opts.Buffer = 4096 } if opts.Workers <= 0 { opts.Workers = 2 } if opts.WriteTimeout <= 0 { opts.WriteTimeout = 5 * time.Second } e := &Emitter{ sink: sink, ch: make(chan contract.FluidTelemetry, opts.Buffer), done: make(chan struct{}), } for i := 0; i < opts.Workers; i++ { e.wg.Add(1) go e.run(opts.WriteTimeout) } return e } // Emit queues an event. It never blocks and never returns an error: a caller on // the request path has no useful response to a telemetry failure, and giving it // one invites handling that blocks. func (e *Emitter) Emit(ev contract.FluidTelemetry) { select { case e.ch <- ev: default: e.dropped.Add(1) } } func (e *Emitter) run(timeout time.Duration) { defer e.wg.Done() for { select { case ev, ok := <-e.ch: if !ok { return } ctx, cancel := context.WithTimeout(context.Background(), timeout) if err := e.sink.Write(ctx, ev); err != nil { e.failed.Add(1) } else { e.written.Add(1) } cancel() case <-e.done: // Drain what is already buffered, then stop. for { select { case ev := <-e.ch: ctx, cancel := context.WithTimeout(context.Background(), timeout) if err := e.sink.Write(ctx, ev); err != nil { e.failed.Add(1) } else { e.written.Add(1) } cancel() default: return } } } } } // Close stops delivery after draining the buffer. func (e *Emitter) Close() { e.stopOnce.Do(func() { close(e.done) }) e.wg.Wait() } // Stats reports emitter health. // // FLUID must observe itself (ArchitectureBlueprint.md section 40), and a // silently lossy telemetry path would make every downstream pressure count // quietly wrong. Dropped events are a first-class operational metric. type Stats struct { Written int64 Dropped int64 Failed int64 } // Stats returns a snapshot of delivery counters. func (e *Emitter) Stats() Stats { return Stats{ Written: e.written.Load(), Dropped: e.dropped.Load(), Failed: e.failed.Load(), } }