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.
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 jsonThe 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
| Domain | Commit and failure | Boundary |
|---|---|---|
| AtomicState | One owner publishes a complete candidate; precommit failure preserves old data | No state sharing across runtimes or workers |
| Single file | Atomic replacement under a stable sidecar lock; durability may be unknown after publication | Cooperating writers only; no multi-file transaction or network-filesystem guarantee |
| Database | Server COMMIT; distinguish rollback from unknown commit | Isolation, SQL, constraints and idempotency determine consistency |
| task_map | Ordered return after success; failure terminates and reaps direct tasks | Previously performed external effects are not rolled back |
| Ordinary variables | Existing semantics and isolated captures remain | Variables 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
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.
ACTIVE -> COMMITTING -> COMMITTED
| |----> ABORTED
| `----> UNKNOWN
`----> ABORTEDCooperative single-file updates
fn increment(text) { encode_json(to_int(text) + 1) }
atomic_file_update(path(args[0]), 3s, increment) |> printprintf '0' > /tmp/hhy-counter.txt
HHY_CONCURRENCY_EXPERIMENTS=1 hhy run atomic-file.hhy /tmp/hhy-counter.txtThe 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
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.0Bounded tasks, cancellation and existing parallel
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.
