run_job.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. """Detached, restart-surviving reconstruction runner.
  2. Launched by app.py with start_new_session=True and, with `KillMode=process` on the
  3. systemd unit, it OUTLIVES a `systemctl restart genrecon-app` — so redeploying or
  4. restarting the web service never kills an in-flight reconstruction. Concurrent
  5. uploads are serialized on the single GPU by an flock. app.py's startup re-attaches
  6. to a live runner (worker.pid) or relaunches a dead one, instead of failing the job.
  7. """
  8. from __future__ import annotations
  9. import fcntl
  10. import json
  11. import os
  12. import sys
  13. import time
  14. import traceback
  15. from pathlib import Path
  16. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  17. import config # noqa: E402
  18. import pipeline # noqa: E402
  19. def main(job_id: str) -> None:
  20. job = config.JOBS_DIR / job_id
  21. status = job / "status.json"
  22. pid_file = job / "worker.pid"
  23. def write(**patch) -> None:
  24. st: dict = {"id": job_id}
  25. try:
  26. st = json.loads(status.read_text())
  27. except Exception:
  28. pass
  29. st.update(patch)
  30. st["updated"] = time.time()
  31. tmp = status.with_suffix(".json.tmp")
  32. tmp.write_text(json.dumps(st))
  33. tmp.replace(status) # atomic — a reader never sees a half-written status
  34. def log_line(line: str) -> None:
  35. with open(job / "pipeline.log", "a") as f:
  36. f.write(line + "\n")
  37. # Claim the job immediately (before the GPU flock) so app.py's startup sees a
  38. # live runner and re-attaches rather than relaunching.
  39. pid_file.write_text(str(os.getpid()))
  40. lock = open(config.JOBS_DIR / ".gpu.lock", "w")
  41. try:
  42. write(status="running", stage=None, progress=1, message="w kolejce (GPU)…", error=None)
  43. fcntl.flock(lock, fcntl.LOCK_EX) # one reconstruction at a time on the single GPU
  44. write(status="running", stage=config.STAGES[0][0], progress=1, message="start", error=None)
  45. pipeline.run_pipeline(
  46. job,
  47. log=log_line,
  48. set_progress=lambda stage, pct, msg: write(status="running", stage=stage, progress=pct, message=msg),
  49. )
  50. write(status="done", stage="glb", progress=100, message="ready")
  51. except Exception as e: # noqa: BLE001 — surface any stage failure to the client
  52. log_line(f"ERROR: {e}")
  53. traceback.print_exc()
  54. write(status="error", message="failed", error=str(e))
  55. finally:
  56. try:
  57. fcntl.flock(lock, fcntl.LOCK_UN)
  58. except OSError:
  59. pass
  60. try:
  61. pid_file.unlink()
  62. except OSError:
  63. pass
  64. if __name__ == "__main__":
  65. if len(sys.argv) != 2:
  66. print("usage: run_job.py <job_id>", file=sys.stderr)
  67. sys.exit(2)
  68. main(sys.argv[1])