@@ -45,13 +45,18 @@ func main() {
4545 dur := flag .Duration ("dur" , 10 * time .Second , "driver: run duration" )
4646 profile := flag .String ("profile" , "light" , "driver: light | heavy" )
4747 readLatency := flag .Duration ("read-latency" , 0 , "driver: simulated S2 read-hop latency; models the server-side pusher substrates (shapes 2/3)" )
48+ pauseAt := flag .Duration ("pause-at" , 0 , "driver: when to take the delivery path down (fault injection)" )
49+ pauseFor := flag .Duration ("pause-for" , 0 , "driver: how long the delivery path stays down" )
4850 flag .Parse ()
4951
52+ fault := fault {at : * pauseAt , dur : * pauseFor }
5053 switch * mode {
5154 case "receiver" :
5255 runReceiver (* addr , * delay )
5356 case "relay" :
54- runRelay (* endpoint , * path , * profile , * rate , * dur , * readLatency )
57+ runRelay (* endpoint , * path , * profile , * rate , * dur , * readLatency , fault )
58+ case "durable" :
59+ runDurable (* endpoint , * path , * profile , * rate , * dur , fault )
5560 default :
5661 fmt .Fprintf (os .Stderr , "unknown mode %q\n " , * mode )
5762 os .Exit (2 )
@@ -64,6 +69,7 @@ func runReceiver(addr string, delay time.Duration) {
6469 total int
6570 byteCount int64
6671 lags []time.Duration
72+ seen = map [int64 ]struct {}{}
6773 )
6874
6975 http .HandleFunc ("/" , func (w http.ResponseWriter , r * http.Request ) {
@@ -90,6 +96,11 @@ func runReceiver(addr string, delay time.Duration) {
9096 if lr .TimeUnixNano > 0 {
9197 lags = append (lags , time .Duration (now - int64 (lr .TimeUnixNano )))
9298 }
99+ for _ , kv := range lr .Attributes {
100+ if kv .Key == "kernel.event.seq" {
101+ seen [kv .Value .GetIntValue ()] = struct {}{}
102+ }
103+ }
93104 }
94105 }
95106 }
@@ -130,22 +141,41 @@ func runReceiver(addr string, delay time.Duration) {
130141 mu .Lock ()
131142 defer mu .Unlock ()
132143 fmt .Println ("\n === otlpbench receiver summary ===" )
133- fmt .Printf ("records: %d\n " , total )
134- fmt .Printf ("bytes: %d\n " , byteCount )
135- fmt .Printf ("lag p50: %s\n " , pct (lags , 0.50 ))
136- fmt .Printf ("lag p99: %s\n " , pct (lags , 0.99 ))
144+ fmt .Printf ("records: %d\n " , total )
145+ fmt .Printf ("unique: %d\n " , len (seen ))
146+ fmt .Printf ("duplicates: %d\n " , total - len (seen ))
147+ fmt .Printf ("bytes: %d\n " , byteCount )
148+ fmt .Printf ("lag p50: %s\n " , pct (lags , 0.50 ))
149+ fmt .Printf ("lag p99: %s\n " , pct (lags , 0.99 ))
150+ }
151+
152+ // fault describes a delivery-path outage injected mid-run.
153+ type fault struct {
154+ at time.Duration
155+ dur time.Duration
156+ }
157+
158+ func (f fault ) active (elapsed time.Duration ) bool {
159+ return f .dur > 0 && elapsed >= f .at && elapsed < f .at + f .dur
160+ }
161+
162+ func newWriter (es * events.EventStream , endpoint , path string ) * events.OTLPStorageWriter {
163+ return events .NewOTLPStorageWriter (es , events.OTLPConfig {
164+ Endpoint : endpoint , URLPath : path , Insecure : true ,
165+ ServiceName : "kernel-browser" , InstanceName : "bench" , Metro : "dev-local" ,
166+ }, slog .Default ())
137167}
138168
139- func runRelay (endpoint , path , profile string , rate int , dur time.Duration , readLatency time.Duration ) {
140- es , err := events .NewEventStream (events.EventStreamConfig {RingCapacity : 4096 })
169+ func runRelay (endpoint , path , profile string , rate int , dur time.Duration , readLatency time.Duration , f fault ) {
170+ // The relay's source is the volatile in-VM ring (1024 slots). While the
171+ // delivery path is down it keeps filling and overflows; those records are
172+ // lost with no replay.
173+ es , err := events .NewEventStream (events.EventStreamConfig {RingCapacity : 1024 })
141174 if err != nil {
142175 panic (err )
143176 }
144177 wctx , wcancel := context .WithCancel (context .Background ())
145- writer := events .NewOTLPStorageWriter (es , events.OTLPConfig {
146- Endpoint : endpoint , URLPath : path , Insecure : true ,
147- ServiceName : "kernel-browser" , InstanceName : "bench" , Metro : "dev-local" ,
148- }, slog .Default ())
178+ writer := newWriter (es , endpoint , path )
149179 if err := writer .Start (wctx ); err != nil {
150180 panic (err )
151181 }
@@ -156,19 +186,34 @@ func runRelay(endpoint, path, profile string, rate int, dur time.Duration, readL
156186 perTick = 1
157187 }
158188 published := 0
159- deadline := time .Now ().Add (dur )
189+ start := time .Now ()
190+ deadline := start .Add (dur )
191+ paused , resumed := false , false
160192 t := time .NewTicker (tick )
161193 defer t .Stop ()
162194 for now := range t .C {
163195 if ! now .Before (deadline ) {
164196 break
165197 }
198+ elapsed := now .Sub (start )
199+ if f .active (elapsed ) && ! paused {
200+ wcancel ()
201+ sc , c := context .WithTimeout (context .Background (), 2 * time .Second )
202+ _ = writer .Stop (sc )
203+ c ()
204+ paused = true
205+ fmt .Printf (" [fault] relay delivery DOWN at %s\n " , elapsed .Round (time .Millisecond ))
206+ }
207+ if paused && ! resumed && elapsed >= f .at + f .dur {
208+ wctx , wcancel = context .WithCancel (context .Background ())
209+ writer = newWriter (es , endpoint , path )
210+ _ = writer .Start (wctx )
211+ resumed = true
212+ fmt .Printf (" [fault] relay delivery RESTORED at %s\n " , elapsed .Round (time .Millisecond ))
213+ }
166214 for i := 0 ; i < perTick ; i ++ {
167215 env := makeEnvelope (profile , published )
168216 if readLatency > 0 {
169- // Models the server-side substrates: the record is emitted now
170- // but only reaches the pusher after the S2 read hop, so the
171- // measured end-to-end lag includes it.
172217 time .AfterFunc (readLatency , func () { es .Publish (env ) })
173218 } else {
174219 es .Publish (env )
@@ -185,13 +230,104 @@ func runRelay(endpoint, path, profile string, rate int, dur time.Duration, readL
185230 defer cancel ()
186231 _ = writer .Stop (stopCtx )
187232
188- shape := "relay (in-VM)"
233+ shape := "relay (in-VM, best-effort )"
189234 if readLatency > 0 {
190235 shape = fmt .Sprintf ("server-side pusher (modeled, read-latency=%s)" , readLatency )
191236 }
192237 fmt .Printf ("%s: published=%d over %s (target %d/s, profile=%s)\n " , shape , published , dur , rate , profile )
193238}
194239
240+ // runDurable models the server-side durable pusher: a producer appends every
241+ // record to an unbounded persistent log (this models the existing reliable
242+ // VM->S2 write), and a pusher reads from a cursor and delivers. While the
243+ // delivery path is down the log keeps growing, so on resume the pusher catches
244+ // up from the cursor and loses nothing.
245+ func runDurable (endpoint , path , profile string , rate int , dur time.Duration , f fault ) {
246+ es , err := events .NewEventStream (events.EventStreamConfig {RingCapacity : 262144 })
247+ if err != nil {
248+ panic (err )
249+ }
250+ wctx , wcancel := context .WithCancel (context .Background ())
251+ writer := newWriter (es , endpoint , path )
252+ if err := writer .Start (wctx ); err != nil {
253+ panic (err )
254+ }
255+
256+ const tick = 10 * time .Millisecond
257+ perTick := rate * int (tick ) / int (time .Second )
258+ if perTick < 1 {
259+ perTick = 1
260+ }
261+
262+ var (
263+ mu sync.Mutex
264+ logq []events.Envelope
265+ produced int
266+ )
267+ start := time .Now ()
268+ prodDone := make (chan struct {})
269+ go func () {
270+ defer close (prodDone )
271+ t := time .NewTicker (tick )
272+ defer t .Stop ()
273+ deadline := start .Add (dur )
274+ for now := range t .C {
275+ if ! now .Before (deadline ) {
276+ return
277+ }
278+ mu .Lock ()
279+ for i := 0 ; i < perTick ; i ++ {
280+ logq = append (logq , makeEnvelope (profile , produced ))
281+ produced ++
282+ }
283+ mu .Unlock ()
284+ }
285+ }()
286+
287+ cursor := 0
288+ producing := true
289+ loggedDown := false
290+ for producing || func () bool { mu .Lock (); defer mu .Unlock (); return cursor < len (logq ) }() {
291+ elapsed := time .Since (start )
292+ if f .active (elapsed ) {
293+ if ! loggedDown {
294+ fmt .Printf (" [fault] pusher delivery DOWN at %s (log keeps growing)\n " , elapsed .Round (time .Millisecond ))
295+ loggedDown = true
296+ }
297+ time .Sleep (tick )
298+ goto checkProd
299+ }
300+ if loggedDown {
301+ fmt .Printf (" [fault] pusher RESUMED at %s, catching up from cursor\n " , elapsed .Round (time .Millisecond ))
302+ loggedDown = false
303+ }
304+ for {
305+ mu .Lock ()
306+ if cursor >= len (logq ) {
307+ mu .Unlock ()
308+ break
309+ }
310+ env := logq [cursor ]
311+ mu .Unlock ()
312+ es .Publish (env )
313+ cursor ++
314+ }
315+ checkProd:
316+ select {
317+ case <- prodDone :
318+ producing = false
319+ default :
320+ time .Sleep (tick )
321+ }
322+ }
323+
324+ wcancel ()
325+ stopCtx , cancel := context .WithTimeout (context .Background (), 15 * time .Second )
326+ defer cancel ()
327+ _ = writer .Stop (stopCtx )
328+ fmt .Printf ("durable pusher (at-least-once): produced=%d delivered-from-log=%d (profile=%s)\n " , produced , cursor , profile )
329+ }
330+
195331var heavyBody = `{"status":200,"url":"https://example.com/api","mime_type":"application/json","body":"` +
196332 stringOfLen (8 * 1024 ) + `"}`
197333
0 commit comments