Acquire, use, and release
An acquisition borrows one live worker context for an application task. The context registry performs the final assignment atomically and the returned ContextHandle remembers the user, client, context, client type, and task that won that assignment.
Use a new task ID for an independent acquisition when an older handle could still exist. Several contexts may deliberately share one task ID for a fan-out operation, but task identity is not a durable attempt number and is not required to be globally unique.
Keep the ordinary path explicit
from servercheetah.services import WorkerContextLeaseError
from servercheetah.types.contexts import ClientType
user_id = "usr_11111111-1111-4111-8111-111111111111"
task_id = "capture-catalog-page-42"
handle = await wcm.acquire(
user_id=user_id,
task_id=task_id,
client_type=ClientType.browser,
)
if handle is None:
return no_worker_available()
try:
resolution = await handle.send_and_wait(
{
"name": "navigate",
"params": {"url": "https://example.com/catalog/42"},
},
timeout_ms=20_000,
)
return await read_retained_result(user_id, resolution.command_id)
except WorkerContextLeaseError:
return assignment_was_lost()
finally:
await wcm.release(handle)
acquire() returning None means that no context was assigned. It can result from no connected eligible client, no idle worker, a required placement miss, exhausted capacity, failed creation, or a created context that did not appear in a state report during the polling window. It does not enqueue the product task. Waiting, backoff, alternative placement, and rejection belong to the application.
A handle guards dispatch as well as release
ContextHandle.send() returns the admitted command ID. send_and_wait() forces RPC response mode and returns RpcResolution, including command and trace identity and terminal correlation status. It does not inline the complete retained result payload. Read history by command ID when the application needs returned progress, result, error, event, state, or capture content.
Before either send method dispatches, the handle re-reads the exact context and confirms that it is still a live worker assigned to the same task. If the context disappeared, changed ownership, became non-live, or belongs to another task, dispatch raises WorkerContextLeaseError. A stale handle therefore cannot quietly issue new work through a later assignment.
The handle injects target.tab_id when the command has no explicit tab target. The protocol uses that field for both browser tabs and console flows. An explicit target is preserved, so code that overrides it also owns the consistency of that decision.
Caller-supplied SendOptions are preserved. send_and_wait() changes only response mode and, when the method receives timeout_ms, the timeout. Parser-definition delivery, authorization metadata, capture requests, and other supported options remain intact.
Renewal extends assignment time, not command truth
The default manager timeout is five minutes. renew_task(handle) refreshes the assignment timestamp only if the same context is still live, worker-owned, and assigned to the handle's task. It raises WorkerContextLeaseError with a specific reason when those conditions no longer hold.
Renew long work before it reaches the configured assignment timeout. Sending a command does not renew the assignment automatically. Renewal also says nothing about whether an earlier command completed; it only preserves the manager's current ownership record.
A timeout sweep can release an assignment while client work or a late result still exists. Never interpret release as cancellation, rollback, or permission to replay an uncertain external effect. Reconcile history and product-owned evidence before retrying.
Release is ownership-checked
release(handle) calls the registry's handle-safe release operation. It clears the task only when the exact context still carries the same task ID. It returns False when the context is gone, already idle, or assigned to another task. A late cleanup from an old handle cannot release a newer assignment.
release_all(handles) applies the same check to each handle and returns the number released. It is a convenience for fan-out cleanup, not a transaction: another process can change individual assignments while the batch proceeds.
Put release in finally, including when an RPC times out. If the product uses a browser overlay, attempt to remove or update it before release, but keep release in an outer cleanup path so a content-script failure does not strand the assignment.
Query the pool within one user
list_available(user_id, client_type=None) returns current idle worker snapshots for one canonical user. It is an observation; acquire() still performs the authoritative assignment.
get_task_contexts(task_id, user_id) returns handles for the worker contexts currently carrying that task ID inside the required user boundary. Cross-user inspection belongs to explicit administrative registry or diagnostics APIs, not to this application helper.
Continue with Matching and selection to control which eligible worker wins, or Browser placement and protection when the manager may create or choose browser resources.