service.ts 3.1 KB

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