shard.ts 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. /**
  2. * Deterministic domain sharding for spreading the domain-alive check across
  3. * multiple GitHub Actions runners.
  4. *
  5. * GitHub-hosted runners are provisioned across many Azure regions, so each
  6. * matrix job gets a distinct egress IP. Splitting the workload by a stable
  7. * hash of the domain lets every runner enumerate the *same* source files but
  8. * only check the subset that belongs to its shard, which:
  9. *
  10. * - spreads DoH load across N distinct egress IPs (fewer per-server rate
  11. * limits), and
  12. * - makes assignment deterministic — the same domain always lands on the
  13. * same shard, so duplicates across source files collapse to one check and
  14. * re-runs are reproducible.
  15. *
  16. * FNV-1a (52-bit, via foxts/fnv1a52) is used because it is fast, has good
  17. * distribution for load balancing, and stays within JavaScript's safe integer
  18. * range. It is NOT cryptographic — never use it where collision resistance
  19. * matters.
  20. */
  21. import { fnv1a52 } from 'foxts/fnv1a52';
  22. import process from 'node:process';
  23. export interface ShardConfig {
  24. /** 0-based index of this shard. */
  25. index: number,
  26. /** Total number of shards. */
  27. total: number
  28. }
  29. /**
  30. * Read shard configuration from the environment. Defaults to a single shard
  31. * (index 0 of 1), i.e. a full run — so local invocations and the non-matrix
  32. * path behave exactly like before.
  33. */
  34. export function getShardConfigFromEnv(env: NodeJS.ProcessEnv = process.env): ShardConfig {
  35. const total = Number.parseInt(env.SHARD_TOTAL ?? '1', 10);
  36. const index = Number.parseInt(env.SHARD_INDEX ?? '0', 10);
  37. if (!Number.isSafeInteger(total) || total < 1) {
  38. throw new RangeError(`Invalid SHARD_TOTAL: ${JSON.stringify(env.SHARD_TOTAL)} (must be an integer >= 1)`);
  39. }
  40. if (!Number.isSafeInteger(index) || index < 0 || index >= total) {
  41. throw new RangeError(`Invalid SHARD_INDEX: ${JSON.stringify(env.SHARD_INDEX)} (must be an integer in [0, ${total - 1}])`);
  42. }
  43. return { index, total };
  44. }
  45. /** Whether the given domain belongs to this shard. */
  46. export function isInShard(domain: string, { index, total }: ShardConfig): boolean {
  47. if (total === 1) return true;
  48. return fnv1a52(domain) % total === index;
  49. }