From 3ea81d0fbc5b93295f291315c1d05fe7a1911948 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Tue, 10 Jun 2025 15:54:27 -0500 Subject: Provide generalised success and error handling for the jobs. --- uploader/background_jobs.py | 107 +++++++++++++++++++++++++++++---- uploader/templates/jobs/job-error.html | 17 ++++++ 2 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 uploader/templates/jobs/job-error.html (limited to 'uploader') diff --git a/uploader/background_jobs.py b/uploader/background_jobs.py index ac47ff2..09ea0c0 100644 --- a/uploader/background_jobs.py +++ b/uploader/background_jobs.py @@ -1,14 +1,86 @@ import uuid +import logging +import importlib +from typing import Callable +from functools import partial -from flask import request, Blueprint, render_template, current_app as app +from flask import ( + request, + url_for, + redirect, + Response, + Blueprint, + render_template, + current_app as app) from gn_libs import jobs -from gn_libs.jobs.jobs import JobNotFound from gn_libs import sqlite3 +from gn_libs.jobs.jobs import JobNotFound from uploader.authorisation import require_login background_jobs_bp = Blueprint("background-jobs", __name__) +HandlerType = Callable[[dict], Response] + + +def __default_error_handler__(job: dict) -> Response: + return redirect(url_for("background-jobs.job_error", job_id=job["job_id"])) + +def register_handlers( + job_type: str, + success_handler: HandlerType, + error_handler: HandlerType = __default_error_handler__ +) -> str: + if not bool(app.config.get("background-jobs")): + app.config["background-jobs"] = {} + + if not bool(app.config["background-jobs"].get(job_type)): + app.config["background-jobs"][job_type] = { + "success": success_handler, + "error": error_handler + } + + return job_type + + +def register_job_handlers(job: str): + """Related to register handlers above.""" + def __load_handler__(absolute_function_path): + _parts = absolute_function_path.split(".") + app.logger.debug("THE PARTS ARE: %s", _parts) + assert len(_parts) > 1, f"Invalid path: {absolute_function_path}" + function = _parts[-1] + module = importlib.import_module(f".{_parts[-2]}", + package=".".join(_parts[0:-2])) + return getattr(module, _parts[-1]) + + metadata = job["metadata"] + if metadata["success_handler"]: + success_handler = __load_handler__(metadata["success_handler"]) + try: + error_handler = __load_handler__(metadata["error_handler"]) + except Exception as _exc: + error_handler = __default_error_handler__ + register_handlers(metadata["job-type"], success_handler, error_handler) + + +def handler(job: dict, handler_type: str) -> HandlerType: + """Fetch a handler for the job.""" + _job_type = job["metadata"]["job-type"] + _handler = app.config.get( + "background-jobs", {} + ).get( + _job_type, {} + ).get(handler_type) + if bool(_handler): + return _handler(job) + raise Exception( + f"No '{handler_type}' handler registered for job type: {_job_type}") + + +error_handler = partial(handler, handler_type="error") +success_handler = partial(handler, handler_type="success") + @background_jobs_bp.route("/status/") @require_login @@ -17,19 +89,28 @@ def job_status(job_id: uuid.UUID): with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: try: job = jobs.job(conn, job_id, fulldetails=True) - stdout = "" - stderr = "" - # with (open(job["metadata"]["stdout-file"], encoding="utf-8") as stdout_file, - # open(job["metadata"]["stderr-file"], encoding="utf-8") as stderr_file): - # stdout = stdout_file.read() - # stderr = stderr_file.read() + status = job["metadata"]["status"] - return render_template( - f"jobs/job-status.html", - job=job, - stdout=stdout, - stderr=stderr) + register_job_handlers(job) + if status == "error": + return error_handler(job) + + if status == "completed": + return success_handler(job) + + return render_template(f"jobs/job-status.html", job=job) except JobNotFound as jnf: return render_template( "jobs/job-not-found.html", job_id=job_id) + + +@background_jobs_bp.route("/error/") +@require_login +def job_error(job_id: uuid.UUID): + with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: + try: + job = jobs.job(conn, job_id, fulldetails=True) + return render_template("jobs/job-error.html", job=job) + except JobNotFound as jnf: + return render_template("jobs/job-not-found.html", job_id=job_id) diff --git a/uploader/templates/jobs/job-error.html b/uploader/templates/jobs/job-error.html new file mode 100644 index 0000000..b3015fc --- /dev/null +++ b/uploader/templates/jobs/job-error.html @@ -0,0 +1,17 @@ +{%extends "base.html"%} + +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Background Jobs: Error{%endblock%} + +{%block pagetitle%}Background Jobs: Error{%endblock%} + +{%block contents%} + +

Background Jobs: Error

+

Job {{job["job_id"]}} failed!

+

The error details are in the "STDERR" section below.

+ +

STDERR

+
{{job["stderr"]}}
+{%endblock%} -- cgit v1.2.3