Add connector, response policy, telemetry emitter and gateway
Some checks failed
ci / build (push) Failing after 23s
Some checks failed
ci / build (push) Failing after 23s
Completes the request path for FLUID-WP-0003 T01-T03 and T05-T07. The gateway resolves a revision, routes to the adapter process, and records what happened, without ever depending on the control plane to serve. Three behaviours carry tests because the architecture rests on them: Emit never blocks against a stalled sink (Blueprint 34.2), the gateway keeps serving after control-plane loss (invariant 2), and backend internals do not leak into error responses (5.7). The connector retries only idempotent methods, so a slow adapter cannot cause a hall-of-helix entry to be published twice, and breakers are per-revision so a broken candidate does not take the stable revision down with 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
This commit is contained in:
parent
42891e08a2
commit
791e419973
7 changed files with 1088 additions and 6 deletions
143
internal/runtime/telemetry.go
Normal file
143
internal/runtime/telemetry.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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(),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue