service.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import { appendFile, rename, writeFile } from "node:fs/promises"
  2. const [registration, mode, delay] = process.argv.slice(2)
  3. if (registration === undefined || mode === undefined) throw new Error("Missing service fixture arguments")
  4. if (mode === "failed") process.exit(1)
  5. if (mode === "record-start") {
  6. await writeFile(registration + ".started", "")
  7. process.exit(1)
  8. }
  9. if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
  10. if (
  11. mode === "delayed" ||
  12. mode === "delayed-failed" ||
  13. mode === "coordinated" ||
  14. mode === "coordinated-failed-loser"
  15. ) {
  16. await appendFile(registration + ".starts", process.pid + "\n")
  17. const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
  18. .then(() => true)
  19. .catch(() => false)
  20. if (!owner) process.exit(mode === "coordinated-failed-loser" ? 1 : 0)
  21. if (mode === "coordinated" || mode === "coordinated-failed-loser") {
  22. while ((await Bun.file(registration + ".starts").text()).trim().split("\n").length < 2) await Bun.sleep(10)
  23. if (mode === "coordinated-failed-loser") await Bun.sleep(1_500)
  24. } else await Bun.sleep(Number(delay))
  25. if (mode === "delayed-failed") process.exit(1)
  26. }
  27. let requests = 0
  28. const version = mode === "old" || mode === "reject-stop" ? "old" : "test"
  29. const id = crypto.randomUUID()
  30. const server = Bun.serve({
  31. port: 0,
  32. async fetch(request) {
  33. const pathname = new URL(request.url).pathname
  34. if (pathname === "/api/service/stop" && mode === "reject-stop") {
  35. await writeFile(registration + ".stop-attempt", "")
  36. return Response.json({ accepted: false })
  37. }
  38. if (pathname === "/api/service/stop" && mode === "graceful") {
  39. const body = await request.json()
  40. if (typeof body !== "object" || body === null || body.instanceID !== id) return Response.json({ accepted: false })
  41. await writeFile(registration + ".stop", JSON.stringify(body))
  42. setTimeout(shutdown, 25)
  43. return Response.json({ accepted: true })
  44. }
  45. if (pathname !== "/api/health") return new Response(null, { status: 404 })
  46. requests += 1
  47. if (mode === "modern" && requests === 1) {
  48. await writeFile(registration + ".first-request", "")
  49. while (!(await Bun.file(registration + ".release").exists())) await Bun.sleep(5)
  50. return new Response(null, { status: 503 })
  51. }
  52. if (mode === "legacy") return Response.json({ healthy: true })
  53. if (mode === "starting" && !(await Bun.file(registration + ".release").exists()))
  54. return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 })
  55. if (mode === "failed-owner")
  56. return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
  57. if (mode === "starting" || mode === "graceful" || mode === "reject-stop")
  58. return Response.json({ healthy: true, version, pid: process.pid })
  59. return Response.json({ healthy: true, version, pid: process.pid })
  60. },
  61. })
  62. await writeFile(
  63. registration + ".tmp",
  64. JSON.stringify({
  65. id,
  66. version: mode === "legacy" ? undefined : version,
  67. url: server.url.toString(),
  68. pid: process.pid,
  69. }),
  70. { mode: 0o600 },
  71. )
  72. await rename(registration + ".tmp", registration)
  73. function shutdown() {
  74. server.stop(true)
  75. process.exit()
  76. }
  77. process.on("SIGTERM", shutdown)
  78. process.on("SIGINT", shutdown)