Some checks failed
ci / build (push) Has been cancelled
Completes FLUID-WP-0007. The seven minimal-conformance requirements and the mechanically checkable architectural invariants are asserted as tests rather than claimed in a README, because a conformance claim nobody re-checks is one that quietly stops being true. Only the checkable subset of the invariants is asserted; pretending a test can settle the rest would be worse than leaving them to review. TestFirstVerticalSlice runs all eleven steps of Blueprint 50 with no human steps: two revisions, explicit routing, telemetry, a cohort dimension, detected pressure, a hypothesis, a candidate, a 90/10 experiment, fitness comparison, promotion, and a complete audit trail. Requests per completed task fall from 5.65 to 1.00 against a 1.20 target. A companion test runs the loop twice and requires the same verdict, since a loop whose conclusion depended on run order would be measuring the harness rather than the interface. The failure-containment matrix covers Blueprint 34 directly: the data plane keeps serving with the evidence store closed, with telemetry wedged against a sink that never returns, after a failed build, after an experiment rollback, and with the adaptive concurrency limit saturated. Fixes a real bug the suite exposed. Drain closed the emitter outright, so every request after the first flush emitted into a dead emitter and was silently lost -- the kind of fault that makes a later measurement quietly wrong rather than loudly broken. Emitter.Flush now waits for delivery without stopping it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
166 lines
4.2 KiB
Go
166 lines
4.2 KiB
Go
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
|
|
inflight 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:
|
|
e.inflight.Add(1)
|
|
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
|
|
}
|
|
e.deliver(ev, timeout)
|
|
case <-e.done:
|
|
// Drain what is already buffered, then stop.
|
|
for {
|
|
select {
|
|
case ev := <-e.ch:
|
|
e.deliver(ev, timeout)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// deliver writes one event and settles its in-flight accounting.
|
|
func (e *Emitter) deliver(ev contract.FluidTelemetry, timeout time.Duration) {
|
|
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()
|
|
e.inflight.Add(-1)
|
|
}
|
|
|
|
// Flush waits for queued telemetry to reach the sink without stopping delivery.
|
|
//
|
|
// It exists for tests and for operational tooling that needs to read back what
|
|
// it just emitted. Close would also flush, but closing an emitter that is still
|
|
// serving traffic silently drops everything emitted afterwards -- which is the
|
|
// kind of bug that makes a later measurement quietly wrong rather than loudly
|
|
// broken.
|
|
//
|
|
// It returns false if the deadline passes with work still outstanding, so a
|
|
// caller can tell a slow sink from an empty one.
|
|
func (e *Emitter) Flush(timeout time.Duration) bool {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if e.inflight.Load() == 0 && len(e.ch) == 0 {
|
|
return true
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
return e.inflight.Load() == 0 && len(e.ch) == 0
|
|
}
|
|
|
|
// 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(),
|
|
}
|
|
}
|