The Jitter Shootout
The Jitter Shootout: none vs equal (+/-25%, the default) vs full (uniform over [0, delay], new this round) - same forty workers, same synchronized failure, three retry-arrival histograms. Pick your herd size with your eyes open.
Reliability & Recovery
Round 6
Mike Perham
exit 0
bundle exec ruby examples/jitter_shootout.rb
a real captured run
JITTER SHOOTOUT: 40 workers, synchronized failure, 150ms base backoff (seed 20260707)
none (jitter: false):
325- 350ms ### 3
350- 375ms ##################################### 37
peak 37 workers per bucket, spread 25ms
equal +/-25% (the default):
100- 125ms ## 2
125- 150ms ################## 18
150- 175ms ########## 10
175- 200ms ########## 10
peak 18 workers per bucket, spread 76ms
full [0, delay] (jitter: :full):
0- 25ms ### 3
25- 50ms ############## 14
50- 75ms ###### 6
75- 100ms ##### 5
100- 125ms #### 4
125- 150ms ####### 7
150- 175ms # 1
peak 14 workers per bucket, spread 150ms
scoreboard (peak herd, smaller is safer):
none (jitter: false) 37 of 40
equal +/-25% (the default) 18 of 40
full [0, delay] (jitter: :full) 14 of 40
full jitter trades punctuality for survival: retries land anywhere
in [0, delay], so some come back early and few come back TOGETHER.
when the upstream is already hurting, together is the only wrong time.
source
# frozen_string_literal: true # The Jitter Shootout: none vs equal (+/-25%, the default) vs full # (uniform over [0, delay], new this round) - same forty workers, same # synchronized failure, three retry-arrival histograms. Pick your # herd size with your eyes open. # # bundle exec ruby examples/jitter_shootout.rb [seed] # # Runs offline and deterministically. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal WORKERS = 40 BACKOFF = 0.15 BUCKET_MS = 25 def run_mode(jitter, seed) srand(seed) started = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) arrivals = [] attempts = Hash.new(0) orchestrator = Agentic:class="y">:PlanOrchestrator.new( concurrency_limit: WORKERS, retry_policy: { max_retries: 2, retryable_errors: [class="s">"RuntimeError"], backoff_strategy: class="y">:constant, backoff_constant: BACKOFF, backoff_jitter: jitter } ) WORKERS.times do |i| orchestrator.add_task(Agentic:class="y">:Task.new( description: class="s">"worker-#{i}", agent_spec: {class="s">"name" => class="s">"worker", class="s">"instructions" => class="s">"call"} ), agent: ->(t) { attempts[t.description] += 1 raise class="s">"hiccup" if attempts[t.description] == 1 arrivals << Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - started class="y">:ok }) end orchestrator.execute_plan arrivals end def report(label, arrivals) buckets = arrivals.group_by { |t| (t * 1000 / BUCKET_MS).floor } peak = buckets.values.map(&class="y">:size).max spread = ((arrivals.max - arrivals.min) * 1000).round puts class="s">" #{label}:" buckets.sort.each do |bucket, hits| puts format(class="s">" %4d-%4dms %-40s %d", bucket * BUCKET_MS, (bucket + 1) * BUCKET_MS, class="s">"#" * hits.size, hits.size) end puts format(class="s">" peak %d workers per bucket, spread %dms", peak, spread) puts peak end seed = (ARGV.first || 20260707).to_i puts class="s">"JITTER SHOOTOUT: #{WORKERS} workers, synchronized failure, " \ class="s">"#{(BACKOFF * 1000).round}ms base backoff (seed #{seed})" puts peaks = { class="s">"none (jitter: false)" => run_mode(false, seed), class="s">"equal +/-25% (the default)" => run_mode(true, seed), class="s">"full [0, delay] (jitter: class="y">:full)" => run_mode(class="y">:full, seed) }.map { |label, arrivals| [label, report(label, arrivals)] } puts class="s">"scoreboard (peak herd, smaller is safer):" peaks.each { |label, peak| puts format(class="s">" %-32s %2d of %d", label, peak, WORKERS) } puts puts class="s">"full jitter trades punctuality for survival: retries land anywhere" puts class="s">"in [0, delay], so some come back early and few come back TOGETHER." puts class="s">"when the upstream is already hurting, together is the only wrong time."