service.ts 3.4 KB

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