610 lines
20 KiB
YAML
610 lines
20 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: SciMesh Coordinator API
|
|
version: 1.0.0
|
|
description: >
|
|
Durable task-queue server for SciMesh. Workers register, claim tasks one at a
|
|
time, heartbeat, upload partial-result artifacts, and complete or fail tasks.
|
|
Submitters create jobs — either with pre-chunked input URIs or by uploading a
|
|
dataset the coordinator chunks itself.
|
|
|
|
|
|
Machine-readable mirror of `docs/api-contract.md` (v1). All timestamps are
|
|
UTC, RFC 3339. Every endpoint except `GET /health` requires a bearer token.
|
|
Unknown JSON fields are rejected with 400.
|
|
|
|
servers:
|
|
- url: "{scheme}://{host}"
|
|
variables:
|
|
scheme:
|
|
default: http
|
|
enum: [http, https]
|
|
host:
|
|
default: localhost:8080
|
|
|
|
security:
|
|
- bearerAuth: []
|
|
|
|
tags:
|
|
- name: health
|
|
- name: workers
|
|
- name: jobs
|
|
- name: tasks
|
|
- name: artifacts
|
|
|
|
paths:
|
|
/health:
|
|
get:
|
|
tags: [health]
|
|
summary: Readiness (probes the database)
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: The coordinator and its database are ready.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Health" }
|
|
"503":
|
|
description: The database is unreachable.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Health" }
|
|
|
|
/workers/register:
|
|
post:
|
|
tags: [workers]
|
|
summary: Register a worker
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RegisterRequest" }
|
|
responses:
|
|
"201":
|
|
description: Registered.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/RegisterResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/jobs:
|
|
post:
|
|
tags: [jobs]
|
|
summary: Create a job from pre-chunked input URIs
|
|
description: >
|
|
The submitter supplies each chunk's input URI and checksum. To have the
|
|
coordinator split a dataset instead, use `POST /jobs/upload`.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CreateJobRequest" }
|
|
responses:
|
|
"201":
|
|
description: Job and its tasks were created transactionally.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/JobCreated" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/jobs/upload:
|
|
post:
|
|
tags: [jobs]
|
|
summary: Upload a dataset; the coordinator chunks it into shard tasks
|
|
description: >
|
|
multipart/form-data. The text fields (`workload`, `parameters`,
|
|
`chunk_rows`, `max_rows`) MUST precede the `file` part: the file is streamed, not
|
|
buffered, so the fields have to be parsed before it arrives. Currently
|
|
only `similarity-search` with `parameters.query_smiles` is accepted.
|
|
When every shard succeeds, the coordinator merges their candidates into
|
|
one final CSV; distributed graph planning is not implemented.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema: { $ref: "#/components/schemas/UploadJobForm" }
|
|
encoding:
|
|
file:
|
|
contentType: text/tab-separated-values
|
|
responses:
|
|
"201":
|
|
description: Job, input artifact, shard artifacts, and shard tasks created.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/UploadJobResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/jobs/{job_id}:
|
|
get:
|
|
tags: [jobs]
|
|
summary: Aggregate job progress
|
|
parameters:
|
|
- $ref: "#/components/parameters/JobID"
|
|
responses:
|
|
"200":
|
|
description: >
|
|
Progress counts and derived status. A completed similarity-search
|
|
response includes `result_uri` for its final CSV.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/JobProgress" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/jobs/{job_id}/result:
|
|
get:
|
|
tags: [jobs]
|
|
summary: Download a completed job's final result
|
|
parameters:
|
|
- $ref: "#/components/parameters/JobID"
|
|
responses:
|
|
"200":
|
|
description: Final coordinator-owned CSV.
|
|
content:
|
|
text/csv:
|
|
schema: { type: string, format: binary }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/jobs/{job_id}/cancel:
|
|
post:
|
|
tags: [jobs]
|
|
summary: Cancel a job and invalidate all unfinished task leases
|
|
parameters:
|
|
- $ref: "#/components/parameters/JobID"
|
|
responses:
|
|
"200":
|
|
description: The job is cancelled. Completed and terminally failed tasks remain unchanged.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/CancelJobResponse" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/tasks/claim:
|
|
post:
|
|
tags: [tasks]
|
|
summary: Atomically lease one task
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClaimRequest" }
|
|
responses:
|
|
"200":
|
|
description: A task was leased.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClaimedTask" }
|
|
"204":
|
|
description: No compatible task is available.
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
|
|
/tasks/{task_id}/heartbeat:
|
|
post:
|
|
tags: [tasks]
|
|
summary: Renew the caller's lease
|
|
description: >
|
|
The response carries a renewed `lease_expires_at`. Schedule the next
|
|
heartbeat before half of the remaining TTL, never on a fixed interval alone.
|
|
parameters:
|
|
- $ref: "#/components/parameters/TaskID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/IdentityRequest" }
|
|
responses:
|
|
"200":
|
|
description: Lease renewed.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ClaimedTask" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/tasks/{task_id}/input:
|
|
get:
|
|
tags: [tasks]
|
|
summary: Download the task's input shard
|
|
description: >
|
|
Streams the shard bytes for an uploaded-dataset task. The worker verifies
|
|
the `X-Checksum-SHA256` header (also delivered as `input.sha256` on claim)
|
|
before executing. URI-based tasks have no coordinator-stored input and
|
|
return 404.
|
|
parameters:
|
|
- $ref: "#/components/parameters/TaskID"
|
|
responses:
|
|
"200":
|
|
description: The shard bytes.
|
|
headers:
|
|
X-Checksum-SHA256:
|
|
schema: { type: string }
|
|
description: SHA-256 of the shard.
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
/tasks/{task_id}/artifacts/{filename}:
|
|
put:
|
|
tags: [tasks, artifacts]
|
|
summary: Upload a partial-result artifact
|
|
description: >
|
|
Streams the body into blob storage. Identity travels in headers, not the
|
|
body. The coordinator measures the size and SHA-256 itself and returns them.
|
|
parameters:
|
|
- $ref: "#/components/parameters/TaskID"
|
|
- name: filename
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- name: X-Worker-ID
|
|
in: header
|
|
required: true
|
|
schema: { type: string }
|
|
- name: X-Task-Attempt
|
|
in: header
|
|
required: true
|
|
schema: { type: integer }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
text/csv:
|
|
schema: { type: string, format: binary }
|
|
responses:
|
|
"200":
|
|
description: Artifact stored.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ArtifactUploaded" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/tasks/{task_id}/result:
|
|
post:
|
|
tags: [tasks]
|
|
summary: Complete a task with an uploaded result artifact
|
|
description: >
|
|
References an artifact previously uploaded for THIS task. The coordinator
|
|
verifies ownership before accepting it. Idempotent: replaying the same
|
|
artifact_id succeeds; a different one for a completed task is a 409.
|
|
parameters:
|
|
- $ref: "#/components/parameters/TaskID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/ResultRequest" }
|
|
responses:
|
|
"200":
|
|
description: Recorded.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TaskState" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/tasks/{task_id}/failure:
|
|
post:
|
|
tags: [tasks]
|
|
summary: Report a task failure
|
|
description: >
|
|
`retryable: true` returns the task to the queue while attempts remain;
|
|
otherwise it fails terminally. Send only sanitized error fields — never a
|
|
traceback, token, or absolute worker path.
|
|
parameters:
|
|
- $ref: "#/components/parameters/TaskID"
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/FailureRequest" }
|
|
responses:
|
|
"200":
|
|
description: Recorded.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/TaskState" }
|
|
"400": { $ref: "#/components/responses/BadRequest" }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
"409": { $ref: "#/components/responses/Conflict" }
|
|
|
|
/artifacts/{artifact_id}/download:
|
|
get:
|
|
tags: [artifacts]
|
|
summary: Download an artifact by id
|
|
parameters:
|
|
- name: artifact_id
|
|
in: path
|
|
required: true
|
|
schema: { type: string, format: uuid }
|
|
responses:
|
|
"200":
|
|
description: The artifact bytes.
|
|
headers:
|
|
X-Checksum-SHA256:
|
|
schema: { type: string }
|
|
content:
|
|
application/octet-stream:
|
|
schema: { type: string, format: binary }
|
|
"401": { $ref: "#/components/responses/Unauthorized" }
|
|
"404": { $ref: "#/components/responses/NotFound" }
|
|
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
|
|
parameters:
|
|
JobID:
|
|
name: job_id
|
|
in: path
|
|
required: true
|
|
schema: { type: string, format: uuid }
|
|
TaskID:
|
|
name: task_id
|
|
in: path
|
|
required: true
|
|
schema: { type: string, format: uuid }
|
|
|
|
responses:
|
|
BadRequest:
|
|
description: Invalid input.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
Unauthorized:
|
|
description: Missing or invalid bearer token.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
NotFound:
|
|
description: The referenced job, task, or artifact does not exist.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
Conflict:
|
|
description: Lease not held, stale attempt, or a different result already recorded.
|
|
content:
|
|
application/json:
|
|
schema: { $ref: "#/components/schemas/Error" }
|
|
|
|
schemas:
|
|
Health:
|
|
type: object
|
|
properties:
|
|
status: { type: string, example: ok }
|
|
|
|
Error:
|
|
type: object
|
|
properties:
|
|
error: { type: string, example: "invalid input" }
|
|
request_id: { type: string, description: Correlates with the server logs. }
|
|
|
|
RegisterRequest:
|
|
type: object
|
|
required: [capabilities]
|
|
properties:
|
|
name: { type: string, example: lab-worker-01 }
|
|
capabilities:
|
|
type: array
|
|
minItems: 1
|
|
items: { type: string }
|
|
example: [similarity-search]
|
|
cpu_count:
|
|
type: integer
|
|
description: Accepted for forward compatibility; not yet persisted.
|
|
memory_mb:
|
|
type: integer
|
|
description: Accepted for forward compatibility; not yet persisted.
|
|
|
|
RegisterResponse:
|
|
type: object
|
|
properties:
|
|
worker_id: { type: string, format: uuid }
|
|
heartbeat_interval_seconds: { type: integer, example: 15 }
|
|
|
|
ChunkSpec:
|
|
type: object
|
|
required: [chunk_index, input_uri, input_sha256]
|
|
properties:
|
|
chunk_index: { type: integer }
|
|
workload:
|
|
type: string
|
|
description: Empty inherits the job's workload.
|
|
input_uri: { type: string }
|
|
input_sha256: { type: string }
|
|
parameters: { type: object, additionalProperties: true }
|
|
max_attempts: { type: integer }
|
|
|
|
CreateJobRequest:
|
|
type: object
|
|
required: [workload, input_uri, chunks]
|
|
properties:
|
|
workload: { type: string, example: similarity-search }
|
|
input_uri: { type: string }
|
|
parameters: { type: object, additionalProperties: true }
|
|
chunks:
|
|
type: array
|
|
minItems: 1
|
|
items: { $ref: "#/components/schemas/ChunkSpec" }
|
|
|
|
JobCreated:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
status: { $ref: "#/components/schemas/JobStatus" }
|
|
|
|
UploadJobForm:
|
|
type: object
|
|
required: [workload, file]
|
|
properties:
|
|
workload: { type: string, enum: [similarity-search], example: similarity-search }
|
|
parameters:
|
|
type: string
|
|
description: JSON object, sent as a string form field.
|
|
example: '{"query_smiles":"CCO","top_k":10}'
|
|
chunk_rows:
|
|
type: integer
|
|
description: Data rows per shard. Default 1000.
|
|
example: 1000
|
|
max_rows:
|
|
type: integer
|
|
minimum: 1
|
|
description: Optional leading data-row limit for a small pipeline check.
|
|
example: 500
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: The dataset (TSV; header repeated into each shard).
|
|
|
|
UploadJobResponse:
|
|
type: object
|
|
properties:
|
|
job_id: { type: string, format: uuid }
|
|
task_count: { type: integer, example: 3 }
|
|
input_artifact_id: { type: string, format: uuid }
|
|
|
|
CancelJobResponse:
|
|
type: object
|
|
properties:
|
|
job_id: { type: string, format: uuid }
|
|
status: { type: string, enum: [cancelled] }
|
|
cancelled_tasks: { type: integer }
|
|
|
|
JobProgress:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
status: { $ref: "#/components/schemas/JobStatus" }
|
|
total: { type: integer }
|
|
pending: { type: integer }
|
|
leased: { type: integer }
|
|
completed: { type: integer }
|
|
failed: { type: integer }
|
|
cancelled: { type: integer }
|
|
result_uri:
|
|
type: string
|
|
description: Present only when the final result is available.
|
|
example: /jobs/5a4c3a7f-ccfc-47d6-b78d-2d1fa565bafd/result
|
|
error_code:
|
|
type: string
|
|
description: Sanitized terminal reducer failure code, when applicable.
|
|
|
|
ClaimRequest:
|
|
type: object
|
|
required: [worker_id]
|
|
properties:
|
|
worker_id: { type: string, format: uuid, description: Registered worker identity. }
|
|
capabilities:
|
|
type: array
|
|
items: { type: string }
|
|
description: Accepted for compatibility only; registration capabilities decide eligibility.
|
|
max_concurrency:
|
|
type: integer
|
|
description: Accepted; the coordinator leases one task per call.
|
|
|
|
InputRef:
|
|
type: object
|
|
properties:
|
|
uri:
|
|
type: string
|
|
description: >
|
|
For an uploaded shard, a coordinator path `/tasks/{id}/input`. For a
|
|
URI-based task, the external input URI.
|
|
sha256: { type: string }
|
|
|
|
ClaimedTask:
|
|
type: object
|
|
properties:
|
|
task_id: { type: string, format: uuid }
|
|
job_id: { type: string, format: uuid }
|
|
chunk_index: { type: integer }
|
|
workload: { type: string }
|
|
input: { $ref: "#/components/schemas/InputRef" }
|
|
parameters: { type: object, additionalProperties: true }
|
|
attempt: { type: integer }
|
|
lease_expires_at: { type: string, format: date-time }
|
|
|
|
IdentityRequest:
|
|
type: object
|
|
required: [worker_id, attempt]
|
|
properties:
|
|
worker_id: { type: string }
|
|
attempt: { type: integer }
|
|
|
|
ResultManifest:
|
|
type: object
|
|
required: [artifact_id]
|
|
properties:
|
|
artifact_id:
|
|
type: string
|
|
format: uuid
|
|
description: An artifact previously uploaded for this task.
|
|
sha256:
|
|
type: string
|
|
description: Accepted for the worker's own cross-check; the coordinator trusts its stored metadata.
|
|
content_type: { type: string }
|
|
|
|
ResultRequest:
|
|
type: object
|
|
required: [worker_id, attempt, result]
|
|
properties:
|
|
worker_id: { type: string }
|
|
attempt: { type: integer }
|
|
result: { $ref: "#/components/schemas/ResultManifest" }
|
|
metrics: { type: object, additionalProperties: true }
|
|
|
|
FailureRequest:
|
|
type: object
|
|
required: [worker_id, attempt, error_code]
|
|
properties:
|
|
worker_id: { type: string }
|
|
attempt: { type: integer }
|
|
error_code: { type: string, example: download_failed }
|
|
error_message: { type: string }
|
|
retryable: { type: boolean }
|
|
|
|
ArtifactUploaded:
|
|
type: object
|
|
properties:
|
|
artifact_id: { type: string, format: uuid }
|
|
uri:
|
|
type: string
|
|
description: Coordinator download path, `/artifacts/{id}/download`.
|
|
sha256: { type: string }
|
|
size_bytes: { type: integer, format: int64 }
|
|
|
|
TaskState:
|
|
type: object
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
job_id: { type: string, format: uuid }
|
|
status: { $ref: "#/components/schemas/TaskStatus" }
|
|
|
|
JobStatus:
|
|
type: string
|
|
enum: [pending, running, reducing, completed, failed, cancelled]
|
|
|
|
TaskStatus:
|
|
type: string
|
|
enum: [pending, leased, running, completed, failed, cancelled]
|