Skip to content

refactor(dispatch): extract DispatchService from the HTTP handlers#167

Merged
seandavi merged 3 commits into
mainfrom
feat/dispatch-service
Jul 10, 2026
Merged

refactor(dispatch): extract DispatchService from the HTTP handlers#167
seandavi merged 3 commits into
mainfrom
feat/dispatch-service

Conversation

@seandavi

Copy link
Copy Markdown
Owner

What

Pulls the dispatch orchestration out of routers/dispatch.py into a new services/dispatch.py (@dataclass DispatchService(engine)), following the service-injected-router convention (services/sample.py + routers/samples.py) — the one clean wiring the design review praised. Roadmap "Architecture deepening" candidate #4, built directly on lifecycle.claim() from #166.

Design (agreed in the #3 grilling)

The seam was drawn during #3: lifecycle owns the state mutation; DispatchService owns the pick-then-lock and response assembly.

Router drops to a thin call-and-map; Pydantic models stay inline (repo convention); all OpenAPI strings unchanged.

Behaviour

Behaviour-preserving — the concurrency-critical pick-then-lock SQL is byte-identical, and the 204/404 conditions and response shapes are unchanged.

Tests

New tests/test_dispatch_service.py drives the service directly at the seam (claim-then-empty, report_submitted true/false/unknown, requeue_expired recycling a stale claim). Existing test_integration.py/test_e2e.py dispatch coverage is unchanged and green.

  • just typecheck: clean (60 files)
  • just test: 177 passed

(Also includes a roadmap doc update marking #3 done / #4 next.)

🤖 Generated with Claude Code

seandavi and others added 2 commits July 10, 2026 15:46
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pulls the dispatch orchestration out of routers/dispatch.py into a new
services/dispatch.py (@DataClass DispatchService(engine)), following the
service-injected-router convention (services/sample.py + routers/samples.py) —
the one clean wiring the design review praised. Deepening candidate #4.

- claim_batch(): the two-phase pick-then-lock (SELECT ... FOR UPDATE SKIP
  LOCKED, issue #74), run_name minting, lifecycle.claim(), and sample-metadata
  fetch — moved verbatim. Returns a plain ClaimedBatch/ClaimedJob dataclass or
  None; the service is HTTP/Pydantic-agnostic (no FastAPI imports).
- report_submitted() -> bool, requeue_expired() -> int; CLAIM_TTL_MINUTES and
  the uuid7 version-guard move here too.
- The state writes still go through services/lifecycle.py (the claim seam from
  #166); this owns only the orchestration around them.

Router drops to a thin call-and-map: parse request -> service -> map None to
204 / False to 404. Pydantic models stay inline (repo convention); all OpenAPI
strings unchanged. Behaviour-preserving.

New tests/test_dispatch_service.py drives the service directly at the seam
(claim-then-empty, submitted true/false, requeue-expired). typecheck clean;
full suite 177 passed (existing dispatch integration/e2e unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors dispatch orchestration by extracting the concurrency-critical claim/submit/requeue logic out of the FastAPI router into a dedicated DispatchService, aligning dispatch with the repo’s “thin router + service layer” architecture and keeping lifecycle state transitions centralized in services/lifecycle.py.

Changes:

  • Added DispatchService (services/dispatch.py) to own batch-claim orchestration, submitted confirmation, and expired-claim requeue, returning HTTP-agnostic dataclasses/bool/int.
  • Simplified routers/dispatch.py into a thin adapter that calls the service and maps None/False to 204/404 without changing response shapes.
  • Added direct seam tests for the new service (tests/test_dispatch_service.py) and updated the roadmap doc to mark lifecycle (#166) done and DispatchService as next.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/test_dispatch_service.py Adds unit-style service seam tests for claim/submitted/requeue behavior against testcontainers Postgres.
src/nextflow_telemetry/services/dispatch.py Introduces DispatchService and dataclass return types; encapsulates pick-then-lock query and lifecycle calls.
src/nextflow_telemetry/routers/dispatch.py Refactors router handlers to delegate orchestration to DispatchService and keep HTTP mapping logic in the router.
docs/roadmap.md Updates roadmap status notes to reflect lifecycle extraction completion and DispatchService as the next step.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_dispatch_service.py Outdated
Comment on lines +188 to +196
job_id = await _seed_job(
engine, workflow_pk=wf_pk, workflow_id=wf_id, sample_id=sample_id, status="claimed"
)
async with engine.begin() as conn:
await conn.execute(
jobs_tbl.update()
.where(jobs_tbl.c.id == job_id)
.values(run_name=stale_run_name)
)
…pilot)

Copilot review nit on #167: the requeue-expired test seeded a claimed job
with run_name=NULL then set it in a second transaction. Seed it in one
insert via a new run_name param on the _seed_job helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seandavi seandavi merged commit 25b6fa8 into main Jul 10, 2026
@seandavi seandavi deleted the feat/dispatch-service branch July 10, 2026 22:03
seandavi added a commit that referenced this pull request Jul 10, 2026
…emon (#168)

* docs(roadmap): mark deepening candidate #4 (DispatchService) done (#167)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(nf-client): scheduler Submitter seam; de-duplicate submit/daemon

The SLURM/PBS submission path (build the same Jinja context, render the
template, dispatch via sbatch/qsub with retries) was copied verbatim across
cli.py's submit() and daemon(). Extract it behind one seam in submission.py:

- build_submission_context(batch, cfg, sample_ids) — the context dict, once.
- SCHEDULER_SUBMITTERS registry {slurm, pbs} + submit_to_scheduler() entry
  point; lsf is deliberately absent (raises UnwiredSchedulerError).
- Two named exceptions (TemplatePathMissingError, UnwiredSchedulerError) let
  each caller keep its own control flow: submit() exits nonzero for both;
  daemon() exits for a missing template but skips-and-continues for an unwired
  mode. A genuine submission failure is NOT wrapped — it propagates as the
  SubprocessError/OSError submit_with_retry already raised.

local mode is untouched — it's deliberately different per caller (non-blocking
Popen in submit(); blocking subprocess.run in daemon() to serialise runs).

Behaviour-preserving. One faithfulness fix over a naive extraction: the
daemon's submission-failure catch is narrowed from `except Exception` to
`except (SubprocessError, OSError)` so a template/render error (a config bug)
still stops the daemon as before, rather than being swallowed into an infinite
skip loop mislabelled "submission failed after retries".

New tests/test_submitters.py (10 tests): context keys, both dispatch commands,
export_none toggle, the two can't-try signals, and failure-propagates-as-is.
nf_client suite 60 passed; server suite 177 unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(nf-client): sync comments + narrow template_path type (Copilot)

Copilot review on #168:
- Update the seam's module comment and the propagation test's docstring to
  reflect the daemon's narrowed `except (SubprocessError, OSError)` catch
  (they still described the old bare `except Exception`).
- submit_to_scheduler: bind `template_path` locally and check `is None` so the
  Optional is narrowed to Path before render_submission_script (clearer for
  type-checkers than the truthiness check).

nf_client suite still 60 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants