# 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.

The seven APIs below are experimental and disabled by default. HHY uses synchronous runtimes and process tasks. This release adds no coroutine scheduler, async/await, shared-memory STM or global atomicity. Concurrent calls to the same HhyContext from multiple threads are not guaranteed safe.

```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

| 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

- `atomic_state(data)` — Create state owned by one Runtime/PID.
- `atomic_read(state)` — Read an immutable snapshot.
- `atomic_update(state, callback)` — Validate a pure callback candidate and publish synchronously; precommit errors preserve the old value.
- `atomic_close(state)` — Close the handle; later access and repeated close fail.
- `atomic_file_update(path, timeout, callback)` — Read and replace an existing regular file under a cooperative lock; timeout bounds lock waiting only.
- `transaction_strict(config, options?, callback)` — Bind SQL to the current tx; caught errors still prevent commit; reconcile unknown commits.
- `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

```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.

AtomicState is Runtime/PID-bound and cannot be JSON-encoded, sent to tasks or captured across workers. Every access validates owner, PID, closed and busy status; repeated close also fails. Precommit errors, cancellation and quota failure preserve old data without automatic retry.

## 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
```

Catching a callback error leaves the scope rollback-only and cannot restore commit eligibility. The checker rejects known violations in direct closures; the Runtime validates indirect calls. AST and Bytecode share the commit core, while HIR/MIR retain effect, exception and cancellation barriers. This is not a whole-program purity proof.

## Cooperative single-file updates

```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.

HHY_PUBLISHED_DURABILITY_UNKNOWN means replacement was published but directory durability is unknown. HHY_PUBLISHED_CLEANUP_INCOMPLETE means no-replace published but source cleanup failed. Do not blindly retry as if nothing was written. Re-read business state after process failure or postcommit resource exhaustion as well.

## Restricted database transactions and durable idempotency

```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.

DB_COMMIT_UNKNOWN does not mean rolled back. Store a unique business request key and the business update in the same transaction for durable idempotency; query that key and reconcile after an unknown commit. There is no automatic retry middleware or combined rollback across database, files and HTTP.

[Database installation and configuration](DATABASE_1.0.0_README.md) — Database remains independently versioned at 1.0.0

## Bounded tasks, cancellation and existing parallel

```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.

Each child also has RLIMIT_FSIZE; pending result files can total parallelism × result limit, while input/output Lists remain subject to memory limits. Later-task failures are detected without waiting for the first slow task. Shutdown sends TERM to all direct children, shares an approximately 500ms grace period, then KILLs and reaps as needed. This is not a real-time guarantee, does not guarantee cleanup of detached descendants, and does not roll back external effects.

## 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 CI](https://github.com/hh696-wq/hhy-vm/actions/runs/34805271378) — f0fec9d · 2026-09-14

[Real-database acceptance](https://github.com/hh696-wq/hhy-vm/actions/runs/34805271372) — MySQL / PostgreSQL

[1.7.1 release notes and artifacts](https://github.com/hh696-wq/hhy-vm/releases/tag/v1.7.1) — Published; remaining 1.7.x candidates retain their individual acceptance gates
