Skip to main content
Version: 13 - TBD

Settings and Locks

With settings it is possible to store structured data in Engine. Locks can be used to synchronize the processing of parallel running executions.

Use Cases

Settings can be used

Concept

Each setting has a name and can contain any JSON-serializable data structure. Comment lines (lines starting with a hash sign) will be discarded and the order of key-value pairs will be sorted alphabetically upon saving. Settings are stored as JSON data structures and therefore behave differently than strings.

Settings can be accessed by ID or name. The value of a setting can only be read or written as a whole.

Settings also double as lock objects.

note

A setting's name may be at most 255 characters long (this also bounds lock names, since a lock is acquired on a setting). When a lock name embeds a dynamic identifier — for example system.setting(f'transfer-lock-{table_name}') — keep the resulting name within this limit.

Using Settings

Settings can be manipulated via the user interface, via the REST API, and via flow scripts. The examples in this document are limited to one method per use case. The method described is interchangeable with any of the other methods.

To manipulate Settings using the command line you need an authorization token. Please see the Authentication documentation on how to obtain an authorization token.

note

Since settings are stored as JSON data structures, you can't just save any value. For example lines starting with the # symbol get discarded upon saving.

Store Configuration Parameters

You can manually or automatically store configuration parameters in settings which can be read by executions.

example

Store configuration parameters using the command line:

$ curl -X POST 'https://<my-workspace-name>.cloudomation.com/api/latest/setting' -d '{"name":"notification_emails","value":["toni@example.com","cory@example.com"]}' -H "Authorization: $TOKEN"

Read the configuration parameter in a flow:

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
# we use the setting named "notification_emails" and access its value
emails = system.setting('notification_emails').get('value')
this.connect(
'my-smtp-server',
name='send notification',
mode={
'mode_name': 'send_email',
'from_': 'noreply@example.com',
'to': emails,
'subject': 'notification from Engine',
'text': 'test email content',
},
)
return this.success('all done')

If you choose to change the emails which should receive notifications you only need to update it in one place: the setting value:

$ curl -X PATCH 'https://<my-workspace-name>.cloudomation.com/api/latest/setting/notification_emails?by=name' -d '{"value":["toni@example.com","cory@example.com","tracy@example.com"]}' -H "Authorization: $TOKEN"

and with the next execution your flow scripts will read and use the new value.

Store Outputs/Logging/Reports

Your flow scripts can write the value of a setting to store the result of some processing, store logging of some processing, or store a report which was generated:

example

Storing the result of some processing in a setting:

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
# do some processing
result = 42

# store the result
system.setting('occurrences_found').save(value=result)

return this.success('all done')
danger

The save method overwrites existing records with the same name and type, unless they're read-only. If that is not intended, you can first check for the existence of a record with the same name and type, before calling the save method.

Other flow scripts can read the value and adapt their behaviour accordingly:

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
count = system.setting('occurrences_found').get('value')
if count > 32:
this.connect(
connector_type='SMTP',
host='mail.example.com',
mode={
'mode_name': 'send_email',
'from_': 'no-reply@example.com',
'to': ['kevin@example.com'],
'subject': 'counter alert',
'text': f'found {count} occurrences',
},
)
return this.success('all done')

The value can also be retrieved using the REST API:

$ curl 'https://<my-workspace-name>.cloudomation.com/api/latest/setting/occurrences_found?by=name' -H "Authorization: $TOKEN" | jq .
{
"setting": {
"name": "occurences_found",
"value": 42,
...
}
}

:::

Lock Objects

It is possible for an execution to acquire a lock on a setting.

Each setting can be locked by one execution at a time. Other executions waiting to acquire a lock on the same setting will wait in the status WAITING_LOCK until it becomes available or a timeout occurs.

note

Only exclusive locks on settings can be acquired. There is no shared-lock mechanism.

example

Make sure only one cloud-vm is provisioned at once.

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
# we try to acquire the lock
system.setting('cloud-vm-lock').acquire()

# start the flow which launches the cloud-vm
system.flow('create-cloud-vm').run()

# use the cloud-vm
this.connect(
'cloud-vm',
mode={'mode_name': 'execute_script', 'script': 'sleep 30'},
)

# delete the cloud-vm
system.flow('remove-cloud-vm').run()

# free the lock
system.setting('cloud-vm-lock').release()

return this.success('all done')

If the cloud-vm-lock is free when an execution of this flow runs, the execution will acquire the lock and continue its processing.

When a second execution of the flow is started during the processing, it cannot acquire the lock and waits in the status WAITING_LOCK. A lock wait timeout of 60 seconds is used by default. If the lock cannot be acquired in this time, the second execution fails with a LockTimeoutError exception. Pass a different wait_timeout (in seconds) to acquire() or lock() to change this limit, or wait_timeout=None to wait without a timeout.

If instead the lock becomes free within the timeout, the second execution immediately acquires it and continues its processing.

Lock Acquisition Order

When several executions wait for the same lock, they acquire it in best-effort first-in-first-out (FIFO) order: the execution that started waiting earliest is granted the lock first. The ordering is best-effort — under heavy contention the exact grant order can vary slightly — but no waiting execution is starved.

Priority

The acquire method accepts an optional integer priority. A waiting execution with a higher priority acquires the lock before waiting executions with a lower priority, regardless of the order in which they started waiting. Executions that share the same priority fall back to FIFO order among themselves. An execution that does not pass a priority has the lowest priority.

example

Give an urgent execution precedence over routine ones that wait for the same lock:

import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
# a higher priority is granted the lock ahead of any waiter with a lower
# (or unset) priority, regardless of arrival order
system.setting('report-lock').acquire(priority=10)

# ... critical section ...

system.setting('report-lock').release()

return this.success('all done')

Priority affects only the order in which waiting executions acquire the lock. It does not relax mutual exclusion: the setting is held by exactly one execution at a time.

Releasing a lock automatically

lock() returns a context manager that acquires the lock when the with block is entered and releases it when the block exits, including when the block raises an exception. This is the recommended way to hold a lock, because the lock is released even if the code between acquiring and releasing fails:

example
import flow_api

def handler(system: flow_api.System, this: flow_api.Execution, inputs: dict):
with system.setting('cloud-vm-lock').lock() as setting:
system.flow('create-cloud-vm').run()
this.connect(
'cloud-vm',
mode={'mode_name': 'execute_script', 'script': 'sleep 30'},
)
system.flow('remove-cloud-vm').run()
# the lock is released here, even if the block above raised
return this.success('all done')

lock() accepts the same wait_timeout argument as acquire().

Checking whether a setting is locked

is_locked() returns True when the setting is held by an execution and False otherwise:

if system.setting('cloud-vm-lock').is_locked():
return this.success('a cloud-vm is already being provisioned')

The result reflects the setting data loaded on the object at the time it was fetched; re-read the setting to check the current state.

Re-entrancy and automatic release

An execution that already holds a lock can acquire the same lock again without waiting. The lock is tracked per execution, and a single release() frees it.

A lock is also released automatically when the execution holding it ends, in any end status. An execution that fails or is cancelled while holding a lock does not leave the setting locked — the executions waiting for it are resumed once the holder ends.

Deadlocks

A deadlock occurs when executions form a wait-for cycle that can never resolve. The most common form is a cycle of lock waits: each execution in the cycle holds a lock that the next one waits for. The cycle can also route through a synchronous execution dependency — an execution that holds a lock while waiting for a child execution (for example through run(), or a wait_for that returns once all of its children succeed or end) that in turn waits to acquire that lock. Instead of letting every execution in the cycle wait until its timeout elapses, the platform detects the cycle as soon as it forms and fails the execution whose lock acquire closed the cycle with a DeadlockError.

Because it is raised the moment the cyclic wait is detected rather than after a timeout, DeadlockError is distinct from LockTimeoutError. Acquiring locks in a consistent order across flows avoids this class of deadlock.

note

A dependency-routed cycle is a deadlock only when the lock holder cannot proceed until the lock-wanting execution ends. An execution that holds a lock while running a child asynchronously — without waiting for it — is not part of a wait-for cycle: it can release the lock independently, so the child acquires the lock once it is free instead of failing with a DeadlockError. A wait_for that returns as soon as any one child ends is likewise not a hard dependency and does not form a cycle.

Learn More

Exceptions