Resource Locking
This tutorial shows how to use SOVD resource locking (ISO 17978-3, Section 7.17) to prevent concurrent modification of entity resources by multiple clients.
Overview
When multiple clients interact with the same entity - for example, two diagnostic tools both trying to reconfigure a motor controller - their changes can conflict. Resource locking solves this by granting a client exclusive access to an entity’s resource collections (data, configurations, operations, etc.) for a bounded time period.
Key concepts:
Client identification: Each client generates a UUID and sends it via the
X-Client-Idheader on every requestScoped locks: A lock can protect specific collections (e.g., only
configurations) or all collections when no scopes are specifiedParent propagation: A lock on a component also protects its child apps - the gateway walks up the entity hierarchy when checking access
Lock breaking: A client can forcefully replace an existing lock by setting
break_lock: true(unless the entity is configured as non-breakable)Automatic expiry: Locks expire after a TTL and are cleaned up periodically
Locking is supported on components and apps only. Areas cannot be locked directly.
Quick Example
The examples below use curl against a gateway running on localhost:8080.
Pick a client ID (any unique string) and use it consistently.
CLIENT_ID="my-diagnostic-tool-$(uuidgen)"
1. Acquire a lock
Lock the configurations and operations collections on a component for
5 minutes:
curl -X POST http://localhost:8080/api/v1/components/motor_controller/locks \
-H "Content-Type: application/json" \
-H "X-Client-Id: $CLIENT_ID" \
-d '{
"lock_expiration": 300,
"scopes": ["configurations", "operations"]
}'
Response (201 Created):
{
"id": "lock_1",
"owned": true,
"scopes": ["configurations", "operations"],
"lock_expiration": "2026-03-21T15:05:00Z"
}
Save the id value - you need it to extend or release the lock.
2. Perform work while holding the lock
Other clients that try to modify configurations or operations on
motor_controller (or any of its child apps) will receive a 409 Conflict
response until the lock is released or expires.
# This succeeds because we hold the lock
curl -X PUT http://localhost:8080/api/v1/components/motor_controller/configurations/max_speed \
-H "Content-Type: application/json" \
-H "X-Client-Id: $CLIENT_ID" \
-d '{"value": 1500}'
3. Extend the lock
If the work takes longer than expected, extend the lock before it expires:
curl -X PUT http://localhost:8080/api/v1/components/motor_controller/locks/lock_1 \
-H "Content-Type: application/json" \
-H "X-Client-Id: $CLIENT_ID" \
-d '{"lock_expiration": 600}'
Response: 204 No Content
The lock now expires 600 seconds from the time of the extend request (not from the original acquisition time).
4. Release the lock
When done, release the lock so other clients can proceed:
curl -X DELETE http://localhost:8080/api/v1/components/motor_controller/locks/lock_1 \
-H "X-Client-Id: $CLIENT_ID"
Response: 204 No Content
5. List locks (optional)
Check what locks exist on an entity:
curl http://localhost:8080/api/v1/components/motor_controller/locks \
-H "X-Client-Id: $CLIENT_ID"
The owned field in each lock item indicates whether the requesting client
holds that lock.
Lock Enforcement
By default, locking is opt-in - clients can acquire locks, but the gateway
does not require them. To make locking mandatory for certain resource
collections, configure lock_required_scopes.
When required scopes are set, any mutating request to a listed collection is
rejected with 409 Conflict unless the requesting client holds a valid lock
on the entity.
Example: Require a lock before modifying configurations or operations on any component:
ros2_medkit_gateway:
ros__parameters:
locking:
enabled: true
defaults:
components:
lock_required_scopes: [configurations, operations]
apps:
lock_required_scopes: [configurations]
With this configuration, a PUT to
/components/motor_controller/configurations/max_speed without first
acquiring a lock returns:
{
"error_code": "invalid-request",
"message": "Lock required for 'configurations' on entity 'motor_controller'"
}
The two-phase access check works as follows:
Lock-required check - If
lock_required_scopesincludes the target collection, the client must hold a valid (non-expired) lock on the entity. If not, access is denied immediately.Lock-conflict check - If another client holds a lock covering the target collection, access is denied. This check walks up the parent chain (app -> component -> area) so a component lock also protects child apps.
Per-Entity Configuration
The manifest lock: section lets you override lock behavior for individual
components or apps. This is useful when certain entities have stricter
requirements than the global defaults.
# manifest.yaml
components:
- id: safety_controller
name: Safety Controller
lock:
required_scopes: [configurations, operations, data]
breakable: false
max_expiration: 7200
- id: telemetry
name: Telemetry
lock:
breakable: true
apps:
- id: motor_driver
name: Motor Driver
component: safety_controller
lock:
required_scopes: [configurations]
breakable: false
The three manifest lock fields are:
required_scopes- Collections that require a lock before mutation (overrides the type-levellock_required_scopesdefault)breakable- Whether other clients can usebreak_lock: trueto replace an existing lock on this entity (default:true)max_expiration- Maximum lock TTL in seconds for this entity (0= use the globaldefault_max_expiration)
Configuration is resolved with the following priority:
Per-entity manifest override (
lock:section on the entity)Per-type default (
locking.defaults.componentsorlocking.defaults.apps)Global default (
locking.default_max_expiration)
Lock Expiry
Every lock has a TTL set by lock_expiration at acquisition time. The maximum
allowed value is capped by default_max_expiration (global) or
max_expiration (per-entity override).
A background timer runs every cleanup_interval seconds (default: 30) and
removes all expired locks. When a lock expires, the gateway also cleans up
associated temporary resources:
Cyclic subscriptions: If the expired lock’s scopes include
cyclic-subscriptions(or the lock had no scopes, meaning all collections), any cyclic subscriptions for that entity are removed. This prevents orphaned subscriptions from accumulating after a client disconnects without cleaning up.
The cleanup timer logs each expiration:
[INFO] Lock lock_3 expired on entity motor_controller
[INFO] Removed subscription sub_42 on lock expiry
To avoid lock expiry during long operations, clients should periodically extend
their locks using the PUT endpoint.
Plugin Integration
Gateway plugins receive a PluginContext reference that provides lock-aware
methods. Plugins should check locks before performing mutating operations on
entity resources.
Checking lock access:
// In a plugin provider method
auto result = context.check_lock(entity_id, client_id, "configurations");
if (!result.allowed) {
return tl::make_unexpected("Blocked by lock: " + result.denied_reason);
}
check_lock delegates to LockManager::check_access and performs the same
two-phase check (lock-required + lock-conflict) used by the built-in handlers.
If locking is disabled on the gateway, check_lock always returns
allowed = true.
Acquiring a lock from a plugin:
auto lock_result = context.acquire_lock(entity_id, client_id, {"configurations"}, 300);
if (!lock_result) {
// lock_result.error() contains LockError with code, message, status_code
return tl::make_unexpected(lock_result.error().message);
}
auto lock_info = lock_result.value();
// lock_info.lock_id, lock_info.expires_at, etc.
Configuration Reference
All locking parameters are documented in the server configuration reference:
Server Configuration -
Lockingsection for gateway parametersLocking API - Full REST API endpoint reference with request/response schemas
Parameter |
Default |
Description |
|---|---|---|
|
|
Enable the lock manager and lock endpoints |
|
|
Maximum lock TTL in seconds |
|
|
Seconds between expired lock cleanup sweeps |
|
|
Collections requiring a lock on components (empty = no requirement) |
|
|
Whether component locks can be broken |
|
|
Collections requiring a lock on apps (empty = no requirement) |
|
|
Whether app locks can be broken |
Valid lock scopes: data, operations, configurations, faults,
bulk-data, modes, scripts, logs, cyclic-subscriptions.
See Also
Locking API - Locking REST API reference
Server Configuration - Server configuration (Locking section)
Configuring Authentication - JWT authentication (complementary to locking)
Manifest-Based Discovery - Manifest YAML for per-entity lock config
Plugin System - Writing gateway plugins