Files
OpenNest/docs/server-nesting.md
T
ajandClaude Opus 5.5 54ad7ee84d feat(server): nest Auto Nest jobs on the server's worker
Large nests tie up the workstation that runs them. In Database mode, Auto Nest
can now queue the job on the nest server, where a worker container solves one
job at a time under its own CPU and memory limits. The job has no time limit,
so a throttled worker finds the same nest a fast one does, only later.

- Server: /api/nest-jobs queue (schema v15). The worker holds a job on a
  heartbeat lease. When the lease lapses, the job goes back to the queue,
  or fails after three attempts. Only one job runs at a time. Packages and
  results are opaque to the HTTP server, which stays off Engine and IO.
- Worker: OpenNest.Server.Worker in the same image (OPENNEST_ROLE=worker),
  reaching the server over HTTP only. It records each engine answer with
  RecordingNestingEngine and gives its job back on shutdown.
- Desktop: "Nest on the server" in the Auto Nest dialog, for built-in
  engines only. The client replays the recorded answers with
  RemoteNestSubmission, so validation, the review and Keep run locally
  against the client's own drawings, exactly as after a local run. A
  drawing edited after sending refuses the result. If the server cannot
  take the job, the user may nest locally instead.
- compose.server.yaml adds the throttled worker service. The Dockerfile,
  .dockerignore, CI paths and the deploy script now include the worker's
  projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:18:09 -05:00

7.8 KiB

Server nesting

In Database mode, Auto Nest can send the job to the nest server instead of nesting on the workstation. A worker container solves queued jobs one at a time under its own CPU and memory limits, and the workstation reviews and keeps the result exactly as it would a local run. Local nesting stays the default and the fallback when the server cannot take a job.

Desktop

The Auto Nest dialog's Plates tab shows Nest on the server in Database mode only. It is enabled for built-in engines only: the server loads no plug-ins from Engines/. The dialog remembers the last choice (AutoNestOnServer in the user settings).

With the box ticked, ServerAutoNest:

  1. Snapshots the request (RemoteNestSubmission) and writes its package (NestJobPackage).
  2. Submits it. If the server cannot take it (offline, too old, refused), the user may nest on this PC instead; declining discards the run.
  3. Polls every two seconds behind the same owned modal progress window a local run uses, showing the queue position or that the job is running. Transient poll failures only show a waiting message; the job keeps running on the server. A live plan strip is not shown yet.
  4. On success, replays the result here (below) and continues with the usual validation report, review, Keep or Discard and commit. A failed job shows its error.

Stop (or closing the progress window) cancels the server job, best effort, and discards the proposal, exactly like stopping a local run. The input drawings stay locked by the modal progress for the whole wait.

What travels

The package (OpenNest.Api.NestJobPackage) is a ZIP of:

  • job.json: NestJobDocument: engine name, each item's quantity, priority, rotation range and drawing index, the drawing names in order (a guard against reordering), every stock entry as NestStockJson (size, quantity, spacing, quadrant, cost, clamp zones in plate coordinates, clamp rail, irregular outline), and the job options. A server job never sets SearchUntilCancelled and never runs under a DeadlineNestingEngine: it has no time limit, so a throttled worker finds the same nest a fast one does, later.
  • drawings.nest: a throwaway nest of exactly the items' drawings, written by NestWriter. The caller's nest is never written. Drawings are uploaded rather than referenced from the drawing library or sets, because the nest being edited may hold unsaved edits and drawings that are in neither; the server must nest what the workstation has.

The result is a NestSolveTranscript: every engine solve the pipeline made on the worker, in order, as what was asked (part and stock ids with quantities) and the raw NestJobResult returned. Poses keep their IEEE values, nonfinite ones included. No bound plates or validation travel back.

Replay on the desktop

RemoteNestSubmission.Complete first checks that each item's drawing still has the geometry it had when the job was sent; a changed drawing refuses the result. It then runs NestPipeline.Run with a ReplayNestingEngine that answers each solve with the next recorded result, after checking it is asked the same question. Cutout bundling, the ordinary-solve comparison and clamp-removal recovery therefore happen here exactly as on the worker, and the result is validated by NestLayoutCheck and bound to the caller's own drawings like a local run. A recorded sheet that matches one of the request's stock entries uses that entry itself.

A replay that asks a different question, or more or fewer solves than were recorded, fails with NestReplayMismatchException. That happens only if the drawings read back on the worker differ enough to change a geometric decision, which .nest coordinates (10 decimal places) make unlikely. The user then runs Auto Nest again; nothing partial is kept.

Server

/api/nest-jobs (NestJobRoutes, NestDatabase.Jobs, schema version 15) stores packages and results as opaque bytes. The HTTP server never opens either and references neither the engine nor OpenNest.IO.

Route Who Does
POST /api/nest-jobs desktop multipart metadata (NestJobSubmission) and file (package, at most 256 MB); returns the queued job
GET /api/nest-jobs/{id} desktop state, attempts, error, queue position
GET /api/nest-jobs/{id}/result desktop the transcript; 409 until the job succeeded
POST /api/nest-jobs/{id}/cancel desktop queued: cancelled at once; running: flagged for the worker
POST /api/nest-jobs/claim worker the oldest queued job on a fresh lease, or 204
GET /api/nest-jobs/{id}/package worker the package while queued or running
POST /api/nest-jobs/{id}/heartbeat worker renews the lease and reports a cancel request; 409 once the worker no longer holds it
PUT /api/nest-jobs/{id}/result?workerId= worker stores the transcript (at most 64 MB); a job cancelled meanwhile ends Cancelled
POST /api/nest-jobs/{id}/fail worker ends the job Failed with a reason
POST /api/nest-jobs/{id}/cancelled worker confirms a requested stop
POST /api/nest-jobs/{id}/release worker gives the job back without spending an attempt (worker shutting down)

States: Queued, Running, then one of Succeeded, Failed and Cancelled. Only one job runs at a time: a claim returns nothing while any job is running, so another worker container never takes a second job. Claims are serialized by the database's monitor and transaction.

Crash recovery. A running job holds a two-minute lease (NestJobRules.Lease), which the worker renews every 20 seconds. Each claim first settles lapsed leases: a job someone asked to cancel ends Cancelled; otherwise it goes back to the front of the queue, or fails after three attempts (a job that keeps killing its worker, for example by running out of memory). A server restart keeps every job; leases lapse and recover the same way.

Finished jobs drop their package at once, and are deleted with their result seven days after they finish. Like the rest of the API, these routes have no authentication; keep the server on a trusted LAN.

Worker and container

OpenNest.Server.Worker (Generic Host) polls OPENNEST_SERVER_URL every OPENNEST_WORKER_POLL_SECONDS (default 5) and goes straight to the next job after one finishes. It runs each job through NestPipeline with the named built-in engine wrapped in a RecordingNestingEngine, at below-normal process priority. It talks to the server over HTTP and never opens the SQLite file, so it may run anywhere that reaches the server, including a host whose Docker data sits on NFS. On stop it gives a running job back (release), within the compose file's 30-second grace period.

The server image contains both processes. OPENNEST_ROLE picks one: server (default) or worker (/app/entrypoint.sh). The image health check (/app/healthcheck.sh) asks /healthz for the server; for the worker it requires the alive file (OPENNEST_WORKER_ALIVE_FILE, default /tmp/opennest-worker.alive), touched every poll round and every accepted heartbeat, to be under two minutes old.

compose.server.yaml adds opennest-worker with cpus: ${OPENNEST_WORKER_CPUS:-2} and mem_limit: ${OPENNEST_WORKER_MEMORY:-4g}. .NET reads the CPU quota for Environment.ProcessorCount, so the engines' parallel loops scale down to it. Fewer CPUs only make a job slower. Leave enough CPU and memory for the server itself.

Verification

OpenNest.Engine.Tests RemoteNestReplayTests: replay equals the local proposal (including the cutout trial plus ordinary solve), edited drawings and different jobs refuse, and stock and nonfinite poses survive the wire. OpenNest.Server.Tests NestJobQueueTests (states, single running job, concurrent claims, cancellation, lease lapse, retry limit, release, retention, restart) and NestJobApiTests (HTTP plus a real worker run end to end).