mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 04:41:09 -05:00
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>
127 lines
7.8 KiB
Markdown
127 lines
7.8 KiB
Markdown
# 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).
|