#!/usr/bin/env bash # # End-to-end check: two Python workers process separate coordinator shards. # # Requires Docker, curl, python3, and an installed scimesh-worker (normally # from this repository's .venv). The test uses its own Compose project, ports, # volumes, and temporary worker directories, leaving a developer stack alone. set -euo pipefail ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) COORDINATOR_DIR="$ROOT_DIR/coordinator" COMPOSE_PROJECT=${COMPOSE_PROJECT:-scimesh-two-worker-smoke} COORDINATOR_PORT=${COORDINATOR_PORT:-18081} POSTGRES_PORT=${POSTGRES_PORT:-55434} HOST="http://127.0.0.1:${COORDINATOR_PORT}" TOKEN=${SCIMESH_SMOKE_TOKEN:-two-worker-smoke-token} WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/scimesh-two-worker-smoke.XXXXXX") WORKER_ONE_PID="" WORKER_TWO_PID="" WORKER_PYTHON=${SCIMESH_WORKER_PYTHON:-"$ROOT_DIR/.venv/bin/python"} cleanup() { local exit_code=$? if [[ "$exit_code" -ne 0 ]]; then printf '\nTwo-worker smoke failed; worker logs follow.\n' >&2 sed -n '1,200p' "$WORK_DIR/worker-a.log" >&2 || true sed -n '1,200p' "$WORK_DIR/worker-b.log" >&2 || true ( cd "$COORDINATOR_DIR" POSTGRES_PORT="$POSTGRES_PORT" COORDINATOR_PORT="$COORDINATOR_PORT" \ docker compose -p "$COMPOSE_PROJECT" logs coordinator >&2 || true ) fi if [[ -n "$WORKER_ONE_PID" ]]; then kill "$WORKER_ONE_PID" 2>/dev/null || true; fi if [[ -n "$WORKER_TWO_PID" ]]; then kill "$WORKER_TWO_PID" 2>/dev/null || true; fi if [[ -n "$WORKER_ONE_PID" ]]; then wait "$WORKER_ONE_PID" 2>/dev/null || true; fi if [[ -n "$WORKER_TWO_PID" ]]; then wait "$WORKER_TWO_PID" 2>/dev/null || true; fi ( cd "$COORDINATOR_DIR" POSTGRES_PORT="$POSTGRES_PORT" COORDINATOR_PORT="$COORDINATOR_PORT" \ docker compose -p "$COMPOSE_PROJECT" down -v --remove-orphans >/dev/null 2>&1 || true ) rm -rf "$WORK_DIR" exit "$exit_code" } trap cleanup EXIT INT TERM require() { command -v "$1" >/dev/null || { printf 'missing required command: %s\n' "$1" >&2 exit 2 } } for command in docker curl python3; do require "$command"; done [[ -x "$WORKER_PYTHON" ]] || { printf 'worker Python is not executable: %s\n' "$WORKER_PYTHON" >&2 printf 'Set SCIMESH_WORKER_PYTHON to a Python environment with SciMesh and RDKit.\n' >&2 exit 2 } printf 'Starting isolated coordinator on %s (project %s)\n' "$HOST" "$COMPOSE_PROJECT" ( cd "$COORDINATOR_DIR" POSTGRES_PORT="$POSTGRES_PORT" COORDINATOR_PORT="$COORDINATOR_PORT" \ WORKER_AUTH_TOKEN="$TOKEN" UI_AUTH_TOKEN= \ docker compose -p "$COMPOSE_PROJECT" up -d --build ) for _ in $(seq 1 45); do if curl -fsS "$HOST/health" >/dev/null; then break; fi sleep 1 done curl -fsS "$HOST/health" >/dev/null || { printf 'coordinator did not become healthy\n' >&2 exit 1 } start_worker() { local worker_name=$1 local worker_dir=$2 SCIMESH_COORDINATOR_URL="$HOST" \ SCIMESH_BEARER_TOKEN="$TOKEN" \ SCIMESH_WORKER_NAME="$worker_name" \ SCIMESH_POLL_INTERVAL=0.2 \ "$WORKER_PYTHON" -m scimesh.worker.cli --work-dir "$worker_dir" --max-tasks 2 >"$worker_dir.log" 2>&1 & STARTED_WORKER_PID=$! } start_worker two-worker-smoke-a "$WORK_DIR/worker-a" WORKER_ONE_PID=$STARTED_WORKER_PID start_worker two-worker-smoke-b "$WORK_DIR/worker-b" WORKER_TWO_PID=$STARTED_WORKER_PID for _ in $(seq 1 30); do registered=$(docker compose -p "$COMPOSE_PROJECT" -f "$COORDINATOR_DIR/docker-compose.yml" \ exec -T postgres psql -U scimesh -d scimesh -Atc "SELECT count(*) FROM workers" 2>/dev/null || printf '0') [[ "$registered" == "2" ]] && break sleep 1 done [[ "${registered:-0}" == "2" ]] || { printf 'workers did not register; logs follow\n' >&2 sed -n '1,160p' "$WORK_DIR/worker-a.log" >&2 || true sed -n '1,160p' "$WORK_DIR/worker-b.log" >&2 || true exit 1 } DATASET="$WORK_DIR/fixture.tsv" printf '%s\n' \ $'chembl_id\tcanonical_smiles' \ $'TEST001\tCC' $'TEST002\tCCC' $'TEST003\tCCCC' $'TEST004\tCCCO' $'TEST005\tCCN' \ $'TEST006\tCCCl' $'TEST007\tCCBr' $'TEST008\tCCF' $'TEST009\tCC=O' $'TEST010\tCC#N' \ $'TEST011\tCO' $'TEST012\tCOC' $'TEST013\tCOCC' $'TEST014\tCN' $'TEST015\tCNC' \ $'TEST016\tO=C=O' $'TEST017\tC1CC1' $'TEST018\tc1ccccc1' $'TEST019\tCC(C)O' $'TEST020\tCC(C)N' \ >"$DATASET" response=$(curl -fsS -H "Authorization: Bearer $TOKEN" -X POST "$HOST/jobs/upload" \ -F 'workload=similarity-search' \ -F 'parameters={"query_smiles":"CCO","top_k":5,"progress_every":0}' \ -F 'chunk_rows=5' \ -F 'max_rows=20' \ -F "file=@${DATASET};type=text/tab-separated-values") job_id=$(printf '%s' "$response" | python3 -c 'import json,sys; print(json.load(sys.stdin)["job_id"])') task_count=$(printf '%s' "$response" | python3 -c 'import json,sys; print(json.load(sys.stdin)["task_count"])') [[ "$task_count" == "4" ]] || { printf 'expected four shards, got response: %s\n' "$response" >&2 exit 1 } printf 'Submitted job %s with four shards\n' "$job_id" for _ in $(seq 1 90); do job=$(curl -fsS -H "Authorization: Bearer $TOKEN" "$HOST/jobs/$job_id") status=$(printf '%s' "$job" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])') [[ "$status" == "completed" || "$status" == "failed" || "$status" == "cancelled" ]] && break sleep 1 done printf '%s' "$job" | python3 -c ' import json, sys job = json.load(sys.stdin) assert job["status"] == "completed", job assert job["total"] == 4, job assert job["completed"] == 4, job assert job["failed"] == 0, job ' task_check=$(docker compose -p "$COMPOSE_PROJECT" -f "$COORDINATOR_DIR/docker-compose.yml" \ exec -T postgres psql -U scimesh -d scimesh -Atc \ "SELECT count(*) FROM tasks WHERE job_id = '$job_id'::uuid AND status = 'completed' AND attempt = 1 AND result_artifact_id IS NOT NULL") [[ "$task_check" == "4" ]] || { printf 'expected four first-attempt tasks with coordinator artifacts, got %s\n' "$task_check" >&2 exit 1 } worker_one_results=$(find "$WORK_DIR/worker-a" -name result.csv -type f | wc -l | tr -d ' ') worker_two_results=$(find "$WORK_DIR/worker-b" -name result.csv -type f | wc -l | tr -d ' ') [[ "$worker_one_results" -ge 1 && "$worker_two_results" -ge 1 ]] || { printf 'both workers must process at least one shard (a=%s, b=%s)\n' \ "$worker_one_results" "$worker_two_results" >&2 exit 1 } printf 'PASS: 4/4 shards completed; worker-a=%s, worker-b=%s\n' \ "$worker_one_results" "$worker_two_results"