Skip to content

Instantly share code, notes, and snippets.

@gusost
Last active January 29, 2025 18:08
Show Gist options
  • Save gusost/79948c1c9ed4e9348a22bb71ea20d54a to your computer and use it in GitHub Desktop.
Save gusost/79948c1c9ed4e9348a22bb71ea20d54a to your computer and use it in GitHub Desktop.
Offline re-connection simulation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backoff Strategy Visualization</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: Helvetica, sans-serif;
}
.bar {
fill: steelblue;
}
.axis-label {
font-size: 14px;
}
</style>
</head>
<body>
<h2>Backoff Strategy Visualization</h2>
<label>Calls: <input type="number" id="calls" value="100000"></label>
<label>Failure Rate (0-1): <input type="number" step="0.01" id="failureRate" value="1"></label>
<label>Jitter Factor: <input type="number" step="0.1" id="jitter" value="10"></label>
<label>Min Backoff (ms): <input type="number" id="minBackoff" value="20000"></label>
<label>Max Backoff (ms): <input type="number" id="maxBackoff" value="60000"></label>
<label>Max server latency (ms): <input type="number" id="maxServerLatency" value="15000"></label>
<button onclick="updateUIinputGraph()">Run Simulation</button>
<div>
<svg id="ui-input" width="750" height="300"></svg>
<svg id="ui-input-expo" width="750" height="300"></svg>
<svg id="blip" width="750" height="300"></svg>
<svg id="half" width="750" height="300"></svg>
<svg id="outage" width="750" height="300"></svg>
</div>
<script>
const maxRetryPlotTime = 60 * 60 * 1000 // 1 hour
let calls = parseInt(document.getElementById('calls').value)
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value)
const transitionTime = 0 // animation time in ms
const jitter = 1
const minBackoff = 60e3
const maxBackoff = 600e3
const variant = `Exponetial backoff, jitter ${jitter}, min ${minBackoff / 1000}s, max ${maxBackoff / 1000}s`
//const reqDelay = productionOriginalBackoff(jitter, minBackoff, maxBackoff)
const reqDelay = exponentialBackoff(jitter, minBackoff, maxBackoff)
function productionOriginalBackoff(jitter, minBackoff, maxBackoff) {
return function reqDelay(attempt) {
let delay = Math.min(fib(attempt) * minBackoff, maxBackoff)
delay += delay * jitter * Math.random()
return delay
}
}
function exponentialBackoff(jitter, minBackoff, maxBackoff) {
return function reqDelay(attempt) {
let delay = Math.min(minBackoff * Math.pow(2, attempt - 1), maxBackoff)
let jitterAmount = jitter * delay * (Math.random() * 2 - 1)
return Math.max(0, delay + jitterAmount)
}
}
function simulateBlip() {
let failureRate = 0.05
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay })
}
function simulateHalf() {
let failureRate = 0.5
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay })
}
function simulateOutage() {
let failureRate = 1
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay })
}
function simulateRequestsUsingUIinput() {
let calls = parseInt(document.getElementById('calls').value)
let failureRate = parseFloat(document.getElementById('failureRate').value)
let jitter = parseFloat(document.getElementById('jitter').value)
let minBackoff = parseInt(document.getElementById('minBackoff').value)
let maxBackoff = parseInt(document.getElementById('maxBackoff').value)
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value)
let reqDelay = productionOriginalBackoff(jitter, minBackoff, maxBackoff)
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay })
}
function simulateRequestsUsingUIinputExpo() {
let calls = parseInt(document.getElementById('calls').value)
let failureRate = parseFloat(document.getElementById('failureRate').value)
let jitter = parseFloat(document.getElementById('jitter').value)
let minBackoff = parseInt(document.getElementById('minBackoff').value)
let maxBackoff = parseInt(document.getElementById('maxBackoff').value)
let maxServerLatency = parseInt(document.getElementById('maxServerLatency').value)
const reqDelay = exponentialBackoff(jitter, minBackoff, maxBackoff)
return simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay })
}
function simulateRequests({ calls, failureRate, minBackoff, maxBackoff, maxServerLatency, reqDelay }) {
let requestTimes = []
let now = 0
for (let i = 0; i < calls; i++) {
let attempt = 1
let connectDelay = reqDelay(attempt)
requestTimes.push(connectDelay)
while (Math.random() < failureRate && connectDelay < maxRetryPlotTime) {
connectDelay += reqDelay(++attempt)
// Fixed server latency. (all reconnection attempts are due to the same server timeout)
connectDelay += maxServerLatency
// Variable server latency. (reconnection attempts are due to server asking to back off and not due to timeout)
//connectDelay += Math.random() * maxServerLatency
requestTimes.push(connectDelay)
}
}
// Make request times seconds integers
requestTimes = requestTimes.map(t => Math.floor(t / 1000))
let requestCounts = {}
requestTimes.forEach(t => requestCounts[t] = (requestCounts[t] || 0) + 1)
const plotArray = Object.entries(requestCounts).map(([t, count]) => ({ time: +t, count }))
// Count backwards and cut the array at a low sum count. This avoids a long tail of insignificant data in the graph
let sum = calls * 0.002
for (let i = plotArray.length - 1; i >= 0; i--) {
sum -= plotArray[i].count
if (sum <= 0) {
plotArray.length = i + 1
break
}
}
return plotArray
}
function updateUIinputGraph() {
let data = simulateRequestsUsingUIinput()
let svg = d3.select("svg#ui-input")
updateGraph(data, svg, "Production algo - UI Input")
let dataExpo = simulateRequestsUsingUIinputExpo()
let svgExpo = d3.select("svg#ui-input-expo")
updateGraph(dataExpo, svgExpo, "Exponetial backoff - UI Input")
}
function updateGraph(data, svg, title) {
let width = +svg.attr("width")
let height = +svg.attr("height")
let margin = { top: 20, right: 20, bottom: 20, left: 50 }
let innerWidth = width - margin.left - margin.right
let innerHeight = height - margin.top - margin.bottom
// Add title text to the top right corner
svg.selectAll(".title").data([null])
.join("text")
.attr("class", "title")
.attr("x", width)
.attr("y", margin.top * 1.5)
.attr("text-anchor", "end")
.attr("font-size", "16px")
.text(title)
let x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.time)])
.range([0, innerWidth])
let y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.count)])
.range([innerHeight, 0])
let g = svg.selectAll("g").data([null])
g = g.enter().append("g").merge(g)
.attr("transform", `translate(${margin.left},${margin.top})`)
let bars = g.selectAll("rect").data(data, d => d.time)
bars.enter().append("rect")
.merge(bars)
.transition().duration(transitionTime)
.attr("class", "bar")
.attr("x", d => x(d.time))
.attr("width", innerWidth / data.length)
.attr("y", d => y(d.count))
.attr("height", d => innerHeight - y(d.count))
bars.exit().remove()
let xAxis = d3.axisBottom(x)
let yAxis = d3.axisLeft(y)
g.selectAll(".x-axis").data([null])
.join("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`)
.call(xAxis)
.append("text")
.attr("fill", "black")
.attr("x", innerWidth + margin.right)
.attr("y", 0)
.attr("text-anchor", "end")
.text("[s]")
g.selectAll(".y-axis").data([null])
.join("g")
.attr("class", "y-axis")
.call(yAxis)
.append("text")
.attr("fill", "black")
.attr("x", -innerHeight / 2)
.attr("y", -40)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.text("[requests / s]")
}
updateUIinputGraph()
let blipSvg = d3.select("svg#blip")
let blipData = simulateBlip()
updateGraph(blipData, blipSvg, variant + " - 5% fail reconnect")
let halfSvg = d3.select("svg#half")
let halfData = simulateHalf()
updateGraph(halfData, halfSvg, variant + " - 50% fail reconnect")
let outageData = simulateOutage()
let outageSvg = d3.select("svg#outage")
updateGraph(outageData, outageSvg, variant + " - Outage")
// Linear time algoritm from https://wiki.c2.com/?FibonacciSequence
function fib(n) {
// This algortim starts of as 1, 1, 0, 1, 1, 2, 3, 5, etc.
// By adding two we can realign it to start as expected
n += 2
let m = 1
let k = 0
for (let i = 1; i < n; i++) {
const tmp = m + k
m = k
k = tmp
}
return m
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment