Skip to content

Commit 4a2e2b5

Browse files
committed
Add durable mode and fault injection to otlpbench for crash A/B
Adds a durable-pusher mode (unbounded log + resumable cursor) and a delivery outage knob, plus unique-record counting in the receiver, so the relay's best-effort loss can be measured head-to-head against at-least-once recovery.
1 parent 650c039 commit 4a2e2b5

1 file changed

Lines changed: 152 additions & 16 deletions

File tree

server/cmd/otlpbench/main.go

Lines changed: 152 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
195331
var heavyBody = `{"status":200,"url":"https://example.com/api","mime_type":"application/json","body":"` +
196332
stringOfLen(8*1024) + `"}`
197333

0 commit comments

Comments
 (0)