Skip to content

Commit 00ea494

Browse files
docs(billing/crons): jitter & sharding guidance (#3579)
* docs(billing/crons): add jitter & sharding guidance to README * docs(billing/crons): fix jitter & sharding guidance accuracy - Remove misleading "persisted across restarts" prose — K8s CronJob scripts are invoked once per execution, so jitter is naturally per-invocation - Replace cron.schedule() snippet (no-cron-dep model) with a self-executing top-level await delay pattern matching the actual entrypoint structure - Add optional stable per-pod jitter derivation from HOSTNAME hash - Add SHARD_INDEX/SHARD_TOTAL env-var example in CronJob manifest + script filter snippet to complete sharding implementation guidance * docs(billing/crons): replace $where shard filter with safe client-side hash $where executes JS in MongoDB and is deprecated/disabled in newer versions. Replace with a client-side hash on _id strings — same O(n) complexity, no server-side JS dependency. * docs(billing/crons): fix CJS compatibility + variable shadowing + scalability note - Wrap jitter snippet in async IIFE (CJS entrypoints don't support top-level await) - Rename stable-jitter variables (hostHash/stableJitterMs) to avoid const redeclaration if both snippets appear in the same file - Add scalability caveat on in-memory shard filter; point toward server-side $mod for high tenant counts
1 parent 3b5aada commit 00ea494

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

modules/billing/crons/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,69 @@ spec:
5353
5454
Repeat the manifest for `billing.extrasExpiration.js` and `billing.dunningSweep.js`, adjusting `name` and `schedule`.
5555

56+
## Jitter & sharding
57+
58+
Devkit-shipped crons run on identical UTC schedules across all consumer deployments. To avoid thundering-herd against a shared DB or external API:
59+
60+
### Recommended pattern — startup jitter
61+
62+
These scripts are invoked once per CronJob execution and exit immediately after. Add a random delay at the top of your entrypoint to spread load across deployments:
63+
64+
```js
65+
// Wrap in an async IIFE — cron entrypoints are CommonJS, so top-level await is not available.
66+
// Jitter is re-randomized on each CronJob invocation — this is intentional for K8s CronJobs.
67+
// For a stable per-pod offset, derive from process.env.HOSTNAME instead (see note below).
68+
(async () => {
69+
const jitterMs = Math.floor(Math.random() * 60_000); // 0–60s window
70+
await new Promise(r => setTimeout(r, jitterMs));
71+
await BillingResetService.resetAllDue();
72+
})();
73+
```
74+
75+
> **Stable per-pod jitter (optional):** If you want the same pod to always fire at the same offset within the window, derive jitter from the pod hostname instead of `Math.random()`. Use a distinct variable name to avoid shadowing if both snippets appear in the same file:
76+
> ```js
77+
> const seed = process.env.HOSTNAME ?? 'default';
78+
> const hostHash = [...seed].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0);
79+
> const stableJitterMs = Math.abs(hostHash) % 60_000;
80+
> ```
81+
82+
### When to shard
83+
84+
If your tenant count > 10k OR the operation touches a single table that doesn't tolerate concurrent writes well:
85+
- Shard by `organizationId` modulo N (e.g. 8 shards, each at a different hour offset: `0 2-9 * * 1`)
86+
- Or use a per-tenant queue with worker pool
87+
88+
To implement shard-based filtering, pass a `SHARD_INDEX` and `SHARD_TOTAL` env vars in the CronJob manifest:
89+
90+
```yaml
91+
env:
92+
- name: SHARD_INDEX
93+
value: "0" # 0..N-1
94+
- name: SHARD_TOTAL
95+
value: "8"
96+
```
97+
98+
Then filter in the script by hashing a stable field (e.g. the string representation of `_id`) against the shard count:
99+
100+
```js
101+
const shardIndex = parseInt(process.env.SHARD_INDEX ?? '0', 10);
102+
const shardTotal = parseInt(process.env.SHARD_TOTAL ?? '1', 10);
103+
// Only process orgs assigned to this shard (stable hash on _id string).
104+
// Note: this loads all _id values into memory. For very large tenant counts,
105+
// prefer a server-side filter (e.g. MongoDB $expr + $mod on a numeric shard key).
106+
const allOrgs = await Org.find({}, '_id').lean();
107+
const orgs = allOrgs.filter(o => {
108+
const id = o._id.toString();
109+
const h = [...id].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0);
110+
return Math.abs(h) % shardTotal === shardIndex;
111+
});
112+
```
113+
114+
### Constraints
115+
116+
- Don't jitter more than the operation's idempotency window — if reset is idempotent within 1h, jitter ≤ 30min. Beyond that, late-running jobs miss their window.
117+
- Don't jitter critical SLA-bound jobs (alerts, notifications) — jitter undermines time-sensitivity.
118+
56119
## Dependency: meterMode flag
57120

58121
All scripts check `config.billing.meterMode` at startup. Downstream projects must set this flag to `true` in their project config to activate billing crons. The devkit default is `false` — all crons are no-ops until explicitly enabled.

0 commit comments

Comments
 (0)