All chapters21
On this page

Guide · Chapter 12

Controlled Concurrency and Atomicity

Seven experimental APIs in 1.7.1: ownership, effects, transaction and cancellation boundaries, with runnable examples.

Scope and opt-in activation

Enabled by default: long-lived Context environments remain GC roots; Stream close releases payload references; supervised children have independent quota-failure recovery and teardown; portable no-replace uses exclusive publication. Language specification stays 1.0 and Database stays 1.0.0.

sh
HHY_CONCURRENCY_EXPERIMENTS=1 hhy run --engine bytecode atomic-state.hhy
HHY_CONCURRENCY_EXPERIMENTS=1 hhy run --engine ast atomic-state.hhy
hhy contracts --format json

The shell variable above applies to one command. In PowerShell set $env:HHY_CONCURRENCY_EXPERIMENTS="1" and remove it afterward with Remove-Item Env:HHY_CONCURRENCY_EXPERIMENTS. Dry-run records contracts and returns Null; it neither executes callbacks nor validates their actual results.

Atomicity and consistency guarantees

DomainCommit and failureBoundary
AtomicStateOne owner publishes a complete candidate; precommit failure preserves old dataNo state sharing across runtimes or workers
Single fileAtomic replacement under a stable sidecar lock; durability may be unknown after publicationCooperating writers only; no multi-file transaction or network-filesystem guarantee
DatabaseServer COMMIT; distinguish rollback from unknown commitIsolation, SQL, constraints and idempotency determine consistency
task_mapOrdered return after success; failure terminates and reaps direct tasksPreviously performed external effects are not rolled back
Ordinary variablesExisting semantics and isolated captures remainVariables do not become implicitly atomic

Seven experimental APIs

funcatomic_state#

atomic_state(data)

Create state owned by one Runtime/PID.

funcatomic_read#

atomic_read(state)

Read an immutable snapshot.

funcatomic_update#

atomic_update(state, callback)

Validate a pure callback candidate and publish synchronously; precommit errors preserve the old value.

funcatomic_close#

atomic_close(state)

Close the handle; later access and repeated close fail.

funcatomic_file_update#

atomic_file_update(path, timeout, callback)

Read and replace an existing regular file under a cooperative lock; timeout bounds lock waiting only.

functransaction_strict#

transaction_strict(config, options?, callback)

Bind SQL to the current tx; caught errors still prevent commit; reconcile unknown commits.

functask_map#

task_map(data, parallelism, result_limit, callback)

Execute isolated processes, return an ordered List, and bound direct children and accepted results.

State updates and handle ownership

atomic-state.hhy
let state = atomic_state({left: 0, right: 100})
fn transfer(old) { {left: old.left + 1, right: old.right - 1} }
atomic_update(state, transfer)
print(atomic_read(state))
atomic_close(state)

After this example, left is 1 and right is 99. atomic_update returns the new value and atomic_read reads an immutable snapshot. Data is limited to Null, Bool, Int, finite Float, String and recursive List/Map values with a depth limit of 128. Functions, Streams and handles cannot enter state.

Effect contracts and commit state machine

strict_capability is pure, bound_transaction or external, with the Runtime contracts as the closed allowlist. Strict callbacks reject I/O, tasks, Streams, clocks, randomness, imports, assignments (including local mut) and nested strict scopes. Database callbacks additionally allow query/execute bound to the current tx. Third-party extensions cannot bypass restrictions by declaring themselves pure.

text
ACTIVE -> COMMITTING -> COMMITTED
   |           |----> ABORTED
   |           `----> UNKNOWN
   `----> ABORTED

Cooperative single-file updates

atomic-file.hhy
fn increment(text) { encode_json(to_int(text) + 1) }
atomic_file_update(path(args[0]), 3s, increment) |> print
sh
printf '0' > /tmp/hhy-counter.txt
HHY_CONCURRENCY_EXPERIMENTS=1 hhy run atomic-file.hhy /tmp/hhy-counter.txt

The file must already exist and the callback maps String to String. Lock-wait Duration must be positive and at most 300s; it is not a callback deadline. All writers must use the same path and <target>.hhy-lock protocol. Symlinks, multiple hard links and .hhy-lock target names are rejected. Keep the lock file while writers are active. Inode, ACL and extended-attribute preservation are not guaranteed.

Restricted database transactions and durable idempotency

strict-database.hhy
import database
let cfg = read_text(path(args[0])) |> parse_json
let n = cfg |> transaction_strict { tx ->
    database.execute(tx, "UPDATE counters SET n = n + 1 WHERE id = 1", [])
    return database.query(tx, "SELECT n FROM counters WHERE id = 1", []).rows[0].n
}
print(n)

Install Database 1.0.0 first, create a counters table with an existing id=1 row and integer n, then pass the database configuration JSON path as args[0]. Follow the database extension configuration format and keep credentials in local configuration. Options support the existing isolation/read_only settings.

Callbacks cannot use another tx, a pool, streaming handles, manual commit/savepoint, or return tx even nested in a result. MySQL implicit-commit statements are rejected. Effects inside stored procedures and triggers remain governed by the server; the Runtime cannot prove arbitrary SQL free of external effects.

Database installation and configurationDatabase remains independently versioned at 1.0.0

Bounded tasks, cancellation and existing parallel

bounded-task.hhy
let results = [1, 2, 3] |> task_map(2, 1mib) { n -> n * 2 }
print(results)

The example returns [2, 4, 6] in input order. task_map eagerly maps a List to a List using isolated subprocesses; parallel keeps its existing Stream behavior. Parallelism is a positive integer bounded by Runtime limits. The 1b–256mib result budget limits cumulative accepted serialized results.

Validation evidence and remaining limits

  • 69 local engine/configuration cases; 10,000 commits per engine; GC, owner, quota and effect-denial tests.
  • 80 competing file increments; native/portable no-replace races; MySQL/PostgreSQL tests with 32 requests, eight idempotency keys and dropped commit-acknowledgement reconciliation.
  • A 120-second soak and 100,000-request Web acceptance passed; these are not a 24-hour soak, real power-loss test or general shared-memory linearizability proof.
  • Four-platform CI and release validation passed; Windows artifacts use MSYS2 and exclude Database.
  • HHY_BYTECODE_BINDING_CACHE=1 and HHY_STREAM_SOURCE_FUSION=1 remain separate, disabled-by-default experiments. Evidence does not justify default enablement or general speedup claims.
1.7.1 four-platform CIf0fec9d · 2026-09-14Real-database acceptanceMySQL / PostgreSQL1.7.1 release notes and artifactsPublished; remaining 1.7.x candidates retain their individual acceptance gates

Offline Markdown guide

Download the English guideMarkdown version of this guide