Baseline Pattern: Single-Node Service + Workers

Run the smallest service-and-workers pattern inside one allocation, with readiness before workers and verification before success.

Type: runnable · Time: 30 minutes

Tested scope: The baseline workflow was validated on Purdue Anvil with Slurm and Apptainer.

Boundary: Scheduler account and partition are supplied at submission time

View applicability record.

Before you run this module

What you will learn

Prerequisites

Required resources

How the baseline works

One allocation contains one coordinator and a bounded worker pool. The launcher prepares private runtime storage, binds the coordinator to loopback, waits for semantic readiness, starts workers, and verifies outputs. A running process is not yet a successful result.

Five-step flow

This is the conceptual sequence, not an executable submission script. The release-pinned implementation and annotated source excerpts appear below before the procedure.

  1. Storage: create private state and output directories for the allocation.
  2. Coordinator: start one service on loopback.
  3. Readiness: require a bounded semantic response before continuing.
  4. Workers: start exclusive worker steps only after readiness.
  5. Verification: confirm result count, uniqueness, schema, and worker completion before recording success.
allocation → coordinator → semantic readiness → workers → result verification

Procedure

  1. Open the complete workflow package from the implementation reference above; the annotated excerpts are orientation, not a replacement for the release-pinned source.
  2. Set an approved account and partition, wall time, CPU and memory, runtime module, image path and digest, scratch root, worker count, task count, and readiness timeout.
  3. Run the prerequisite commands listed above.
  4. Submit the baseline script. Keep account names, private hosts, and sensitive paths out of logs.
  5. Wait for the scheduler to finish, then run the workflow verifier.

Expected result

A successful run exits zero, writes a machine-readable success result, contains the expected number of unique task results, and reports that every invariant passed. Worker-start events occur only after readiness.

Diagnose a failure

Safety and scope limitations

The baseline covers one node, loopback communication, bounded inert inputs, and ephemeral allocation-scoped state. It does not establish multi-node networking, durable service operation, cross-center portability, performance scaling, or production support.

Next step

Record your local values, then compare scheduler, network, storage, security, and runtime policy before adapting the workflow.

Implementation reference

The excerpts below show the commands and control flow that matter for understanding the pattern, with the defensive checks and integrity plumbing elided for readability. Each excerpt links to its release-pinned line range in the real v0.1.0 source on GitHub. Use the complete package when adapting and submitting the workflow.

Open the complete workflow package (external)

Scheduler submission and the resources it claimsworkflows/baseline-slurm-apptainer/slurm/baseline.sbatch (Lines 1–28) (external)

One allocation claims one node and a bounded amount of CPU and time. These are the only lines that change between centers.

#!/usr/bin/env bash
#SBATCH --job-name=bssw-baseline
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --time=00:10:00
#SBATCH --output=slurm-%j.out

set -Eeuo pipefail

WORKER_COUNT=${WORKER_COUNT:-2}
TASK_COUNT=${TASK_COUNT:-4}
READINESS_TIMEOUT=${READINESS_TIMEOUT:-30}
IMAGE_PATH=${IMAGE_PATH:-${WORKFLOW_ROOT}/container/baseline.sif}
Start the coordinator, then wait until it is readyworkflows/baseline-slurm-apptainer/slurm/baseline.sbatch (Lines 200–225) (external)

The coordinator is launched inside the container on loopback. Nothing else starts until the readiness probe confirms it can actually serve — not just that the process exists.

# Launch the coordinator on loopback inside the allocation.
apptainer exec --cleanenv --bind "${RUNTIME_DIR}:/work" "${IMAGE_PATH}" \
  python3 "${APP_ROOT}/coordinator.py" \
  --input /work/input.json --results /work/results.json &

# Wait for it to publish its loopback endpoint.
while [[ ! -s "${RUNTIME_DIR}/endpoint.json" ]]; do
  sleep 0.05
done

# Run the semantic readiness probe (bounded HTTP /health contract).
ENDPOINT=$(cat "${RUNTIME_DIR}/endpoint.json" | python3 -c \
  'import json,sys; print(json.load(sys.stdin)["endpoint"])')
python3 "${APP_ROOT}/readiness.py" \
  --endpoint "${ENDPOINT}" --task-count "${TASK_COUNT}" \
  --timeout "${READINESS_TIMEOUT}"
Run workers, then verify results before claiming successworkflows/baseline-slurm-apptainer/slurm/baseline.sbatch (Lines 227–252) (external)

Workers pull work from the coordinator over loopback. Only after every worker finishes does the verifier check the results — a completed process is not yet a successful result.

# Start one exclusive step per worker.
for ((i=1; i<=WORKER_COUNT; i++)); do
  printf -v worker_id 'worker-%02d' "$i"
  srun apptainer exec --bind "${RUNTIME_DIR}:/work" "${IMAGE_PATH}" \
    python3 "${APP_ROOT}/worker.py" \
    --endpoint "${ENDPOINT}" --worker-id "${worker_id}" &
done
wait

# Verify: schema, count, uniqueness, and expected content.
apptainer exec --bind "${RUNTIME_DIR}:/work" "${IMAGE_PATH}" \
  python3 "${APP_ROOT}/verify.py" \
  --input /work/input.json --results /work/results.json \
  --expected /work/expected.json --output /work/result.json

echo "BSSW workflow completed: result is in the runtime directory."
Readiness is a bounded health contract, not a sleepworkflows/baseline-slurm-apptainer/bin/readiness.py (Lines 17–38) (external)

The probe polls the coordinator /health endpoint on loopback with a hard deadline. It passes only when the coordinator reports the expected schema and task count — so workers never start against a half-initialized service.

deadline = time.monotonic() + args.timeout
while time.monotonic() < deadline:
    try:
        with urllib.request.urlopen(f"{args.endpoint}/health") as response:
            health = json.load(response)
        if response.status == 200 and health == {
            "status": "ready",
            "schemaVersion": 1,
            "taskCount": args.task_count,
        }:
            raise SystemExit(0)   # ready
    except (OSError, ValueError, urllib.error.URLError):
        pass
    time.sleep(min(args.interval, deadline - time.monotonic()))
raise SystemExit(24)   # timed out
What the coordinator reports as "ready"workflows/baseline-slurm-apptainer/bin/coordinator.py (Lines 24–37) (external)

The /health response is the contract the readiness probe checks. Reporting taskCount means the coordinator has parsed its input and has work to hand out before any worker asks.

def health(self) -> dict:
    return {
        "status": "ready",
        "schemaVersion": 1,
        "taskCount": len(self.tasks),
    }

def claim(self) -> dict:
    # A worker asks for work; the coordinator hands out one task
    # or reports that all tasks are done (or that more may arrive).
    if self.pending:
        task_id = self.pending.popleft()
        return {"status": "task", "task": self.tasks[task_id]}
    status = "done" if len(self.results) == len(self.tasks) else "wait"
    return {"status": status}
The verifier is the only thing that can write "success"workflows/baseline-slurm-apptainer/bin/verify.py (Lines 22–70) (external)

The verifier checks that every task produced exactly one result, that results are unique, and that they match the expected outputs. Only then does it atomically write the success marker — the single artifact a completed run is judged by.

# Load tasks, actual results, and the expected results.
tasks = load_tasks(args.input)
actual = json.load(open(args.results))["results"]
expected = json.load(open(args.expected))["results"]

seen = set()
for result in actual:
    if result["taskId"] in seen:
        raise ValueError("duplicate task id")
    if result["taskId"] not in {e["taskId"] for e in expected}:
        raise ValueError("unexpected task id")
    seen.add(result["taskId"])

# Every task must be covered exactly once.
if seen != {task["id"] for task in tasks}:
    raise ValueError("results do not cover each task exactly once")

# Atomically write the success marker.
success = {
    "schemaVersion": 1,
    "status": "success",
    "taskCount": len(tasks),
    "results": sorted(actual, key=lambda r: r["taskId"]),
}
atomic_json(args.output, success)
Expected success resultresult.json · sanitized example

A successful run has a machine-readable success marker. Intermediate results are not a success claim until this verifier output exists.

{
  "schemaVersion": 1,
  "status": "success",
  "taskCount": 4,
  "results": [
    {
      "taskId": "task-001",
      "output": "alphaalpha",
      "sha256": "7d66633575abe258f1bbc70a72d9e8e334026e77d173995bec39a07e3fc9e0e9"
    },
    {
      "taskId": "task-002",
      "output": "betabetabeta",
      "sha256": "31c5078a6508abd8c51fff966853b27f1423a1869010792744b391c81463539e"
    },
    {
      "taskId": "task-003",
      "output": "gamma",
      "sha256": "be9d587defa1f0c09ef49eb17e206983a5f8f8289e4281860bd0ee5a19592c67"
    },
    {
      "taskId": "task-004",
      "output": "deltadelta",
      "sha256": "b7635a43d092717c8e19d16d64b1ca4bc996c26da4fc7779850692f3a492b214"
    }
  ]
}

Check completion

Complete only when the job exits zero and the verifier reports the documented task count, unique results, and a success result artifact.

Repository

Sources and scope

Review source roles and citations
  • SOW commitment: Publish a baseline Slurm and Apptainer service-plus-workers pattern. Fellowship SOW, Milestone 1
  • Project decision: Limit the baseline to one node and require result-based verification. Milestone 1 validation boundary

Project-adopted practices and project decisions are not BSSw Fellowship Program requirements.