Skip to content

Commit 5c2518f

Browse files
authored
Add production durability check
Add production durability check
2 parents 755ec2e + f519519 commit 5c2518f

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,11 @@ jobs:
4444
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
4545
SEED_TOKEN: ${{ secrets.SEED_TOKEN }}
4646
run: npx wrangler deploy --var "SEED_TOKEN:$SEED_TOKEN"
47+
48+
- name: Wait for deployment to propagate
49+
run: sleep 10
50+
51+
- name: Run production durability canary
52+
env:
53+
COUNTER_BASE_URL: https://counter.avikalp.workers.dev
54+
run: npm run test:production-durability

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"test": "vitest run",
88
"test:watch": "vitest",
9+
"test:production-durability": "node scripts/production-durability-check.mjs",
910
"deploy": "wrangler deploy"
1011
},
1112
"devDependencies": {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
3+
const DEFAULT_WAIT_MS = 130_000;
4+
5+
function usage() {
6+
console.error(`Usage:
7+
COUNTER_BASE_URL=https://counter.example.workers.dev npm run test:production-durability
8+
9+
Optional env:
10+
COUNTER_NAMESPACE namespace to use, default: production-test.local
11+
COUNTER_KEY key to use, default: durability-<timestamp>-<random>
12+
COUNTER_WAIT_MS wait before final readOnly check, default: ${DEFAULT_WAIT_MS}
13+
`);
14+
}
15+
16+
function sleep(ms) {
17+
return new Promise((resolve) => setTimeout(resolve, ms));
18+
}
19+
20+
async function fetchJson(url) {
21+
const res = await fetch(url, { cache: 'no-store' });
22+
const text = await res.text();
23+
24+
let body;
25+
try {
26+
body = JSON.parse(text);
27+
} catch {
28+
throw new Error(`Expected JSON from ${url}, got status ${res.status}: ${text.slice(0, 200)}`);
29+
}
30+
31+
if (!res.ok) {
32+
throw new Error(`Request failed for ${url}: ${res.status} ${JSON.stringify(body)}`);
33+
}
34+
35+
return body;
36+
}
37+
38+
const baseUrl = process.env.COUNTER_BASE_URL?.replace(/\/+$/, '');
39+
if (!baseUrl) {
40+
usage();
41+
process.exit(2);
42+
}
43+
44+
const namespace = process.env.COUNTER_NAMESPACE || 'production-test.local';
45+
const key = process.env.COUNTER_KEY || `durability-${Date.now()}-${Math.random().toString(36).slice(2)}`;
46+
const waitMs = Number(process.env.COUNTER_WAIT_MS || DEFAULT_WAIT_MS);
47+
48+
if (!Number.isFinite(waitMs) || waitMs < 0) {
49+
throw new Error(`COUNTER_WAIT_MS must be a non-negative number, got ${process.env.COUNTER_WAIT_MS}`);
50+
}
51+
52+
const path = `/api/${encodeURIComponent(namespace)}/views/${encodeURIComponent(key)}`;
53+
const incrementUrl = `${baseUrl}${path}`;
54+
const readOnlyUrl = `${baseUrl}${path}?readOnly=true`;
55+
56+
console.log(`Incrementing ${incrementUrl}`);
57+
const increment = await fetchJson(incrementUrl);
58+
59+
if (increment.value !== 1) {
60+
throw new Error(`Expected first increment to return 1, got ${increment.value}`);
61+
}
62+
63+
const immediateRead = await fetchJson(readOnlyUrl);
64+
if (immediateRead.value < increment.value) {
65+
throw new Error(`Immediate readOnly value regressed from ${increment.value} to ${immediateRead.value}`);
66+
}
67+
68+
console.log(`Waiting ${waitMs}ms before durability read...`);
69+
await sleep(waitMs);
70+
71+
const durableRead = await fetchJson(readOnlyUrl);
72+
if (durableRead.value < increment.value) {
73+
throw new Error(
74+
`Durability check failed: increment returned ${increment.value}, final readOnly returned ${durableRead.value}`
75+
);
76+
}
77+
78+
console.log(JSON.stringify({
79+
ok: true,
80+
namespace,
81+
key,
82+
incrementValue: increment.value,
83+
immediateReadValue: immediateRead.value,
84+
durableReadValue: durableRead.value,
85+
}, null, 2));

0 commit comments

Comments
 (0)