How to sequence cloud work and clean up resources
Use the client from cloud commands.
Raw ol.smolvm.cloud builders describe one request. invoke sends it once.
The separate workflow namespace sequences requests and returns a
CompletableFuture of the final value:
(require ' [ol.smolvm.cloud :as cloud]
' [ol.smolvm.cloud.workflow :as workflow])
Replace example IDs, names and paths before use. These operations can create billable resources. The recipes describe API contracts, not proof that every capability has passed live-service validation.
Find a machine and wait explicitly
connect tries an exact ID, then an exact name only after not-found. It does not
start or wait for a machine. Start it separately and wait for readiness:
(def machine @ (workflow/connect client "replace-with-machine-id-or-name" ))
(def machine-id (:id machine))
@ (cloud/invoke client (cloud/start machine-id))
@ (workflow/wait-until-ready client machine-id
{:timeout-ms 120000 :interval-ms 1000 })
The wait uses server readiness, with a running-state fallback only when the ready field is absent. It does not test your application’s health. A stopped, deleted or error-state machine fails when not ready. Timeout does not delete it. Agent probing is optional and off by default.
For one guest-port change, use workflow/publish or workflow/unpublish:
@ (workflow/publish client machine-id 8080 )
@ (workflow/unpublish client machine-id 8080 )
These read and replace the port set and return updated machine info.
Serialize port changes across all callers. The update is not atomic and is not
retried. This is different from granting public access with cloud/port-links.
Run a command in a temporary branch
Prepare a branchable source with :branchable? true on both cloud/create and
cloud/start. Use a distinct child name. The helper creates the child, waits,
executes, then requests deletion:
(def source-id "replace-with-branchable-source-id" )
(def task
(workflow/branch-exec client source-id "temporary-trial" ["echo" "hello" ]
{:exec {:timeout-ms 10000 }
:wait {:timeout-ms 120000 }
:cleanup-timeout-ms 60000 }))
@ task
Nonzero exit is an ordinary exec result. The source is never deleted. Cleanup targets only a child ID returned by successful creation, never a guessed name. Normal success/failure waits for the cleanup attempt. Cleanup failure after successful execution fails the future. If execution also fails, that error is primary and cleanup failure is suppressed.
For children that must survive the call, use cloud/branch or
cloud/branch-batch through invoke instead. These do not wait or delete children.
Batch names must be distinct. Multiple children with published ports require
host port zero for allocation. Track every returned ID for later cleanup.
Distinguish a wait timeout from cancellation
For a still-pending task, use a limited wait instead of @task:
(deref task 1000 ::still-running )
;; Use only if you intend to cancel local sequencing:
(.cancel task true )
Timed dereference does not cancel work. Explicit cancellation marks the future cancelled immediately, stops further ordinary scheduling and requests cancellation of active work where possible. Required cleanup can continue. A late branch response containing a child ID still triggers deletion.
A cancelled future is not proof of remote rollback, command termination or finished cleanup. It cannot later deliver cleanup errors or newly learned IDs. Await normal completion when you need a cleanup outcome. After uncertain mutations, inspect remote resources and explicitly reconcile them.
Wait for a job and collect bounded output
Use a job when remote cancellation is part of the task:
(def job @ (cloud/invoke client
(cloud/job-start machine-id ["echo" "hello" ])))
(def job-id (:id job))
@ (workflow/job-wait client machine-id job-id {:timeout-ms 120000 })
(def stdout
@ (workflow/job-read-all client machine-id job-id
{:stream :stdout :max-bytes 16777216 }))
job-wait returns final info for exited jobs, including nonzero exits. A lost
job fails. job-read-all returns bytes for one stream through EOF. Its default
aggregate limit is 16 MiB, with pages of at most 1 MiB. It does not merge stdout
and stderr. Use raw job-output pages for larger output. Empty nonterminal
replies pause before another long poll.
Timeout/cancellation of either helper does not cancel the remote job. Invoke
cloud/job-cancel explicitly when needed. Before using :user, invoke
cloud/user-check. That verifies exec user support, not that jobs honor the
account.
To upload a file and apply permissions, use the two-request helper:
@ (workflow/file-write client machine-id "/tmp/input" "sample" {:mode 0600 })
Without :mode, chmod is skipped. Zero explicitly removes permissions.
The sequence is not atomic. Failed chmod leaves the uploaded file in place.
Use raw cloud/file-write when no chmod is required.
Capture and restore durable checkpoints
Capture returns checkpoint metadata, not a downloaded file:
(def checkpoint @ (cloud/invoke client (cloud/checkpoint machine-id)))
(def checkpoint-id (:id checkpoint))
(def restored
@ (cloud/invoke client
(cloud/checkpoint-restore checkpoint-id "restored-trial" )))
@ (cloud/invoke client (cloud/start (:id restored)))
@ (workflow/wait-until-ready client (:id restored))
Restore without an egress policy uses blocked networking, not the checkpoint’s
previous policy. Supply :egress-policy explicitly if access is required.
cloud/checkpoints lists captures for a machine. Treat download URLs as secrets.
After deleting machines that reference the checkpoint, request
cloud/checkpoint-delete. Referenced checkpoints can produce HTTP 409.
Upload a local checkpoint automatically
Keep the nonempty checkpoint file unchanged during transfer. The helper checks file access before allocation, uploads exact file sections, then completes:
(def progress (atom {:read-bytes 0 :total-bytes 0 }))
(def upload
(workflow/checkpoint-upload
client "replace-with-checkpoint-file"
{:progress (fn [read-bytes total-bytes]
(reset! progress {:read-bytes read-bytes
:total-bytes total-bytes}))}))
(def uploaded-checkpoint @ upload)
Defaults match Go’s part policy: four concurrent parts, four attempts per part, and retry delays of 2, 4 and 8 seconds. Transport failures and every non-2xx part response can retry, within the total deadline and attempt limit. Allocation and completion are not retried. File preparation, validation and callback errors do not become HTTP retries. The default total budget is 30 minutes.
Progress counts sender reads, not storage acknowledgements. Callbacks are serialized across parts. A failed attempt resets that part’s counter to zero, so aggregate progress can move backward. Reaching total does not mean storage accepted the data. Wait for normal future completion. Callback failure stops further scheduling. Keep callbacks short.
The helper closes local part streams. Cancellation is immediate, but active work/resource cleanup can finish later. No partial upload is automatically deleted. Normal failures include a known checkpoint ID. Incomplete-upload abort/deletion semantics are not established.
Control checkpoint upload one request at a time
Use raw steps when the application must record an allocation ID before transfer or supply application scheduling. Determine file size before allocation:
(def checkpoint-path
(.toPath (java.io.File. "replace-with-checkpoint-file" )))
(def size-bytes (java.nio.file.Files/size checkpoint-path))
(def plan @ (cloud/invoke client (cloud/checkpoint-upload-create size-bytes)))
(def allocated-id (get-in plan [:checkpoint :id ]))
Save allocated-id securely before sending parts. Do not log signed URLs.
Validate plan geometry against file size. This fragment sends only the first
part, not a complete upload:
@ (cloud/invoke client
(cloud/checkpoint-upload-part
(first (:upload-urls plan)) checkpoint-path
{:offset 0 :length (min size-bytes (:part-size-bytes plan))}))
The builder performs no file I/O. Invocation opens a fresh bounded section and
sends one raw PUT with exact Content-Length. It closes that section afterward.
Storage requests receive no API bearer or cookies and follow no redirects.
Do not forward authenticated API headers to storage URLs. These are raw parts,
not multipart/form-data. Raw invoke does not retry.
Send every exact section to its corresponding URL and wait for every success before completion:
@ (cloud/invoke client (cloud/checkpoint-upload-complete allocated-id))
Completion sends no ETags. If any part fails, do not complete until your application has resolved the failure. Use the automatic helper unless you need this control.