Gyredocs
Depth Practice
API

Gyre/Software Engineering

Service architecture

In one minuteThe service is an asyncio API process that owns no numerical state, plus solver subprocesses of two kinds — one-shot job workers and long-lived sessions — over a file-based store that also holds the meshes clients upload. This page describes lifecycles, failure handling, caching and security.

Written at L2 Practice goes to L3 Mechanics 1708 words Markdown
  • One API process, many solver processes. The service process validates requests, keeps queues and serves the documentation; it never computes a flow. Every solve runs in a subprocess — a one-shot worker per job, or a long-lived session — so a crashing or diverging solve cannot take the service down.
  • State is files. Jobs, results, the result cache, uploaded meshes and pipeline records live under the data directory (gyre serve --data DIR). There is no database. Finished jobs and the cache survive a restart; running jobs become interrupted and sessions are lost.
  • Limits are environment variables (GYRE_MAX_SESSIONS, GYRE_MAX_QUEUED_JOBS, GYRE_MAX_DOF, the mesh-store limits); see the deployment guide.
  • The network is the trust boundary. The service binds all interfaces; without GYRE_API_TOKEN anyone who can reach the port can run jobs and upload meshes.
  • Health and identity: GET /api/v1/health, GET /api/v1/info (revision, process id, start time), GET /api/v1/metrics (Prometheus).

Components#

Component Module Responsibility
API application gyre.service.app routing, validation, error shaping, static docs, MCP mount, metrics, auth
Job manager gyre.service.jobs queue, worker subprocesses, cancellation, result cache, event streams
Worker gyre.service.worker run one case; write status, events, artefacts
Session manager gyre.service.sessions spawn/command/reap warm solver processes
Store gyre.service.store atomic JSON files, event logs
Mesh store gyre.service.meshes uploaded mesh files: names, validation, limits
Pipeline manager gyre.service.pipelines definitions, runs as subprocesses in their own process group
MCP server gyre.agent.server agent tools over the REST API

Job lifecycle#

text
queued ──▶ running ──▶ succeeded
   │           ├─────▶ failed        (exception in the solver; traceback in error.txt)
   └──────────▶└─────▶ cancelled     (DELETE /jobs/{id})
                       interrupted   (service restarted while the job was active)
  1. The request is resolved to a CaseSpec (preset + dotted overrides, or a full document) and validated; errors return a structured body with a hint. A string case is only looked up among the presets and is never opened as a file. A CaseSpec object with mesh.generator: file names a mesh of the mesh store in mesh.params.path: the name is checked against the store’s pattern before anything is opened, the stored file is read for the size estimate, and its boundary groups are compared with discretization.boundaries. An adapt request that cannot start is refused here as well. All of this file work runs in a worker thread, not on the event loop.
  2. A cache key is computed from the canonical spec JSON, the gradient, error-estimate and adapt requests, the SHA-256 of a mesh file’s contents and the code fingerprint (gyre.io.code_fingerprint: version, revision and a hash of the package sources). If a successful job with that key exists, its artefacts are copied and the new job is returned already succeeded with cached_from set. A worker recomputes the key when it starts and records it, so a result is filed under the code that produced it.
  3. Otherwise the job directory is created (request.json, status.json) and the id queued.
  4. A dispatcher task starts python -m gyre.service.worker <dir> with GYRE_DEVICE chosen from the estimated problem size (GPU above 60 000 degrees of freedom, if one is present; GPU jobs are serialised).
  5. The worker appends progress to events.jsonl (one JSON object per line) and rewrites status.json atomically; on completion it writes result.json and artefacts (solution.npz, flow.vtu, field.json). With adapt the worker runs the order selection (gyre.adapt.adaptive_order) through the same run_case: one adapt event per degree, and the result of the final degree with an adaptation section.

Mesh store#

The store is the directory <data_dir>/meshes, one file per name. PUT /meshes/{name} checks the name against ^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}\.(msh|npz)$, refuses a body above GYRE_MESH_MAX_BYTES from the Content-Length header and again while it streams the body into a temporary file of the same directory, and then validates that file in a worker thread with gyre.meshio.inspect_mesh, the reader every solver process uses. Validations run one at a time. An .npz archive whose members would expand to more than four times the size limit is refused from its directory, before any array is read. Only an accepted file is moved to its name (os.replace), so an existing mesh is replaced by a valid one or not at all, and a refused upload leaves nothing behind. The store is bounded in number and total size (GYRE_MESH_STORE_MAX_FILES, GYRE_MESH_STORE_MAX_BYTES; 507 MeshStoreFull).

A ValueError of the reader carries a message written for the client and is returned as 422 InvalidMesh, with the temporary file name replaced by the client’s name. Any other exception of the reader is also a 422 InvalidMesh, with the message <name> could not be read as a mesh (<ExceptionType>); the detail goes to the log.

When the application starts it sets GYRE_MESH_DIR to the store (absolute path) and GYRE_MESH_RESTRICT=1 in its own environment and restores the previous values at shutdown. Job workers, session processes and pipeline runs inherit both, and gyre.meshio.resolve_mesh_path then accepts only a bare file name inside that directory. The confinement therefore holds in the processes that read the mesh, not only in the request handler. A request for a case on a stored mesh that reaches an application without this environment is answered 503 MeshStoreInactive; the name check (422 InvalidMeshName) and the lookup in the store (404 UnknownMesh) come before that test.

GET /jobs/{id}/events?stream=true is a Server-Sent-Events stream that tails the event log until the job reaches a final state; ?since=N returns a JSON page for polling clients (and agents). Because the log is a file, a client can reconnect and resume from any sequence number.

Sessions#

A design loop evaluates one case many times at nearby inputs. A session process builds the case once, keeps its compiled kernels, warm-starts each solve from the previous state in the Newton regime and caches the linearisation between gradient calls. A session on a mesh file reads the file once, at start-up, and reports the case digest of that moment; replacing or deleting the mesh in the store afterwards does not affect it. Commands (evaluate, gradients, adjoint, error_estimate, surface, distributions, field, state) travel over a pipe; one command at a time per session. Idle sessions are reaped after 30 minutes (GYRE_SESSION_IDLE_S), and the number of sessions is capped (GYRE_MAX_SESSIONS, default 4).

Failure handling#

Failure Behaviour
invalid case, override or request 4xx with {"error": {"type", "message", "hint"}}, the shape of every error of the API; nothing is queued
unexpected exception in the API 500 InternalError in the same shape, with an incident id as the only detail; the exception type and text are logged (logger gyre.service) under that id, because they can contain server paths
solver does not converge job succeeded with converged: false, the reason and the outputs of the last iterate; requested gradients and error estimates are not computed and listed under skipped. A session reports it, keeps its last converged state as warm start, and refuses derivative, error-estimate, distribution and field calls (422 RuntimeError) until an evaluation converges
uploaded mesh unusable, too large, or the store is full 422 InvalidMesh, 413 MeshTooLarge, 507 MeshStoreFull; nothing is stored
mesh deleted while a job on it is being submitted 404 UnknownMesh
mesh deleted while a job on it is queued the job fails when its worker builds the case
exception in a worker job failed; traceback in the error.txt artefact
worker killed or crashed in native code dispatcher marks the job failed (“exited without a result”)
cancel before the worker process exists (queued, waiting for the GPU, being spawned) the job stays cancelled; no worker runs it
service restart active jobs become interrupted; finished jobs and the cache persist
session process dies the call that meets the death (or the first call after it) returns 410 SessionDead; the session is removed, so later calls return 404 NotFound
session process dies while starting 500 SessionStartFailed
command error in a session reported with its type; the session stays usable

Security and exposure#

The service binds to all interfaces so that tailnet machines can reach it; the tailnet is the trust boundary. Setting GYRE_API_TOKEN requires Authorization: Bearer … on /api/v1 (except /health and /info) and on /mcp. Artefact downloads are restricted to a fixed allow-list of file names; job ids are validated before touching the file system, and case strings are never treated as paths. A CaseSpec object with mesh.generator: file names a mesh of the mesh store; the service sets GYRE_MESH_RESTRICT=1 and GYRE_MESH_DIR for itself and its subprocesses, so mesh.params.path is a bare file name inside the store and never a path. Mesh uploads are as open as the rest of the API: without a token, anyone on the tailnet can store files up to the size and number limits, and each file is parsed by the reader. The reader checks declared counts against the contents of a file before it allocates, and measures an .npz archive before it reads an array. Pipeline runs started through the service load a case from a preset name or an inline specification only (GYRE_PIPELINE_CASE_FILES=0). Pipelines with shell stages run arbitrary commands as the service user and are therefore excluded from the service and MCP by default; GYRE_PIPELINES_ALLOW_SHELL=1 in the service’s environment opts in. The MCP endpoint disables Host-header pinning because tailnet clients address the service by tailnet names and addresses.

Observability#

GET /api/v1/metrics exposes Prometheus counters and gauges (jobs submitted / succeeded / failed / cancelled, cache hits, queue length, running jobs, solve seconds, sessions, evaluations, gradient calls, uptime, build info). GET /api/v1/info identifies the deployment: version, commit, process id, start time, deployment name, whether documentation is built and from which revision.

The agent interface is a client#

The MCP server holds no logic of its own: each tool is a described wrapper that calls the REST API — in-process through an ASGI transport when mounted at /mcp, or over the network in the standalone stdio proxy (python -m gyre.agent --service URL). Tool results summarise large arrays (shape, extrema, norm) unless full_arrays is requested, and API errors are passed through with their hints so an agent can correct itself. The mesh tools check the name against the store’s pattern themselves, because a name with a / would address another URL, and upload_mesh decodes its content (text or base64) into the request body of the PUT.

    ↑ ↓ moveEnter open[ ] change depthResults show section and depth level