191 lines
8.5 KiB
Python
191 lines
8.5 KiB
Python
import threading
|
|
import traceback
|
|
import io
|
|
import contextlib
|
|
|
|
from django.utils import timezone
|
|
|
|
from .models import RotaRun, RotaSchedule
|
|
|
|
|
|
def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_builder: bool = True, builder_solve: bool = True):
|
|
"""Background job that executes the scheduler and updates RotaRun.
|
|
|
|
If `use_external_generator` is True, callers should implement a project
|
|
level `generate_rota_for_schedule` function and import it here. By
|
|
default a minimal placeholder result is recorded so the UI and workflow
|
|
can be tested without wiring the full solver.
|
|
"""
|
|
|
|
run = RotaRun.objects.get(pk=run_id)
|
|
try:
|
|
run.mark_running()
|
|
|
|
rota: RotaSchedule = run.rota
|
|
|
|
# Placeholder: integrate your generator here.
|
|
# Example integration point (not implemented in this scaffold):
|
|
# from proc.integration import generate_rota_for_schedule
|
|
# result = generate_rota_for_schedule(rota)
|
|
|
|
if use_external_generator:
|
|
# Try to import a project-level generator hook. This allows you to
|
|
# call into existing code (e.g. your `rota.run2` logic) without
|
|
# hard-coding it into the Django app.
|
|
try:
|
|
from project_rota_hook import generate_rota_for_schedule # type: ignore
|
|
|
|
# Capture any stdout/stderr produced by the external generator
|
|
out_buf = io.StringIO()
|
|
err_buf = io.StringIO()
|
|
with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr(err_buf):
|
|
result = generate_rota_for_schedule(rota)
|
|
# Attach captured output to the result for debugging
|
|
try:
|
|
result = result or {}
|
|
result["generator_stdout"] = out_buf.getvalue()
|
|
result["generator_stderr"] = err_buf.getvalue()
|
|
except Exception:
|
|
pass
|
|
# Also persist captured generator output into the run.log so
|
|
# it's visible from the run detail page when executed in the
|
|
# background thread.
|
|
try:
|
|
captured = "\n[GENERATOR STDOUT]\n" + (result.get("generator_stdout") or "") + "\n[GENERATOR STDERR]\n" + (result.get("generator_stderr") or "")
|
|
run.log = (run.log or "") + captured
|
|
run.save(update_fields=["log"])
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
tb = traceback.format_exc()
|
|
run.mark_failed(message=tb)
|
|
return
|
|
else:
|
|
# Minimal demo result for initial testing
|
|
result = {
|
|
"rota": rota.name,
|
|
"worker_count": rota.workers.count(),
|
|
"generated_at": timezone.now().isoformat(),
|
|
}
|
|
|
|
# Optionally build the RotaBuilder and attach generated HTML to the
|
|
# result. This allows the web UI to present the generated HTML for a
|
|
# run without requiring a separate on-demand build.
|
|
if generate_builder:
|
|
# Defensive: if no workers are assigned to the rota, skip the
|
|
# builder step and return a clear message rather than raising an
|
|
# exception from deep inside the builder.
|
|
try:
|
|
worker_count = rota.workers.count()
|
|
except Exception:
|
|
worker_count = 0
|
|
|
|
if worker_count == 0:
|
|
result["builder_error"] = "No workers assigned to rota; add workers before running the builder."
|
|
else:
|
|
try:
|
|
# Capture stdout/stderr from the builder so debugging info
|
|
# is available in the RotaRun result and log.
|
|
out_buf = io.StringIO()
|
|
err_buf = io.StringIO()
|
|
with contextlib.redirect_stdout(out_buf), contextlib.redirect_stderr(err_buf):
|
|
rb = rota.to_rota_builder()
|
|
# If the rota has solver options saved in rota.options, prefer
|
|
# those when invoking the builder. `solver_options` is expected
|
|
# to be a dict of solver-specific options; `solver` may be a
|
|
# string naming the solver plugin.
|
|
opts = rota.options or {}
|
|
solver_options = opts.get("solver_options") or {}
|
|
solver_name = opts.get("solver") or None
|
|
|
|
# build the model and optionally solve. If solver_name is
|
|
# provided, pass it through; otherwise let builder default.
|
|
if solver_name:
|
|
rb.build_and_solve(options=solver_options, solve=builder_solve, solver=solver_name)
|
|
else:
|
|
rb.build_and_solve(options=solver_options, solve=builder_solve)
|
|
|
|
html = rb.get_worker_timetable_html(include_html_tag=True)
|
|
|
|
# store under result so it's accessible from UI
|
|
result["builder_html"] = html
|
|
result["builder_stdout"] = out_buf.getvalue()
|
|
result["builder_stderr"] = err_buf.getvalue()
|
|
|
|
# Persist the HTML on the RotaRun record for easy retrieval
|
|
try:
|
|
run.export_html = html
|
|
run.save(update_fields=["export_html"])
|
|
except Exception:
|
|
# best-effort: if saving fails, include a log entry
|
|
run.log = (run.log or "") + "\nFailed to persist export_html"
|
|
# Append captured output to run.log for convenient viewing
|
|
try:
|
|
captured = "\n[BUILDER STDOUT]\n" + result.get("builder_stdout", "") + "\n[BUILDER STDERR]\n" + result.get("builder_stderr", "")
|
|
run.log = (run.log or "") + captured
|
|
# persist the appended log so it is visible to users
|
|
# when the run finishes
|
|
run.save(update_fields=["log"])
|
|
except Exception:
|
|
pass
|
|
except Exception as exc:
|
|
# include failure information in run.log and result
|
|
tb = traceback.format_exc()
|
|
run.log = (run.log or "") + "\n" + tb
|
|
result["builder_error"] = str(exc)
|
|
# Persist the log immediately so UI can show it even if
|
|
# the outer flow later marks the run failed.
|
|
try:
|
|
run.save(update_fields=["log"])
|
|
except Exception:
|
|
pass
|
|
|
|
run.mark_finished(result=result)
|
|
|
|
except Exception:
|
|
tb = traceback.format_exc()
|
|
# Ensure the traceback is persisted into the run log and result (if
|
|
# available) before marking the run failed so the UI can display the
|
|
# diagnostic information when viewing the run details.
|
|
try:
|
|
run.log = (run.log or "") + "\n" + tb
|
|
# If we have a partially constructed `result`, attach it so the
|
|
# failure page can show any captured stdout/stderr
|
|
try:
|
|
if 'result' in locals() and isinstance(result, dict):
|
|
run.result = result
|
|
run.save(update_fields=["log", "result"])
|
|
else:
|
|
run.save(update_fields=["log"])
|
|
except Exception:
|
|
# best-effort: ignore persistence errors here
|
|
pass
|
|
except Exception:
|
|
# ignore log assignment errors and continue to mark failed
|
|
pass
|
|
try:
|
|
run.mark_failed(message=tb)
|
|
except Exception:
|
|
# last resort: log to stdout
|
|
print("Failed to update RotaRun with failure:")
|
|
print(tb)
|
|
|
|
|
|
def run_rota_async(rota_id: int, use_external_generator: bool = False, generate_builder: bool = True, builder_solve: bool = True) -> RotaRun:
|
|
"""Create a RotaRun and start background thread to run it.
|
|
|
|
Returns the created RotaRun instance.
|
|
"""
|
|
|
|
rota = RotaSchedule.objects.get(pk=rota_id)
|
|
run = RotaRun.objects.create(rota=rota)
|
|
|
|
thread = threading.Thread(
|
|
target=_run_rota_job,
|
|
args=(run.id, use_external_generator, generate_builder, builder_solve),
|
|
daemon=True,
|
|
)
|
|
thread.start()
|
|
|
|
return run
|