diff --git a/reference/components/scheduler.md b/reference/components/scheduler.md index 1b3832b19..4fa1a2571 100644 --- a/reference/components/scheduler.md +++ b/reference/components/scheduler.md @@ -111,7 +111,7 @@ export async function syncExchangeRates(context) { } ``` -**Handlers should be idempotent.** Harper's clustering has no distributed lock, so leadership failover and daylight-saving fall-back can occasionally deliver the same logical occurrence twice. Design handlers so that running twice for one occurrence is harmless. +**Handlers should be idempotent.** Scheduler delivery does not acquire a distributed lock for each occurrence, so leadership failover and daylight-saving fall-back can occasionally deliver the same logical occurrence twice. Design handlers so that running twice for one occurrence is harmless. Harper's [native record locks](../database/concurrency.md#native-record-locks) can protect participating record updates within their supported scope; they do not make scheduler delivery or external side effects exactly once. **A failed run is not retried.** If a handler throws (or its outbound request times out), the run is recorded with `lastStatus: error` and the occurrence is not made up - the job simply fires again at its next scheduled time. Handlers that need delivery guarantees should implement their own bounded retry/backoff inside the handler. diff --git a/reference/configuration/options.md b/reference/configuration/options.md index d12128410..e83bc74df 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -281,6 +281,7 @@ replication: - `hostname` — This instance's hostname within the cluster. Subject to the same bare hostname or IP literal requirement as [`node.hostname`](#node) — a URL, `host:port`, or non-string value fails startup. When both are set, `node.hostname` wins. - `url` — WebSocket URL peers use to connect to this instance - `databases` — Databases to replicate; _Default_: `"*"` (all). Each entry supports `name` and `sharded` +- `recordLocks` - Boolean enabling cluster record locks in Harper Pro on RocksDB; _Default_: `false`. Set the YAML boolean `true`, not a quoted string, and restart for the change to take effect. An agreed home map is also required; the flag alone does not establish cluster protection. See [native record locks](../database/concurrency.md#native-record-locks) for setup and limits. - `routes` — Peer nodes; URL strings or `{hostname, port, startTime, revokedCertificates}` objects - `port` — Replication port - `securePort` — Secure replication port; _Default_: `9933` (changed from `9925` in v4.5.0) diff --git a/reference/database/api.md b/reference/database/api.md index 6197ec4a6..423068ae7 100644 --- a/reference/database/api.md +++ b/reference/database/api.md @@ -153,18 +153,20 @@ On normal callback completion the transaction is committed automatically. If the ### Transaction Scope and Atomicity -Transactions span a single database. All tables within the same database share a single transactional context: reads return a consistent snapshot, and writes across multiple tables are committed atomically. If code accesses tables in different databases, each database gets its own transaction with no cross-database atomicity guarantee. +Transactions span a single database. All tables within the same database share a single transactional context: reads normally return a consistent snapshot, and writes across multiple tables are committed atomically. Lock acquisition can refresh the read state, as described below. If code accesses tables in different databases, each database gets its own transaction with no cross-database atomicity guarantee. For deeper background on Harper's transaction model, see [Storage Algorithm](./storage-algorithm.md). ## Read Consistency -Each request reads from a consistent point-in-time snapshot. Within a single request: +Ordinary reads in a request use a consistent point-in-time snapshot. Within a single request: - a query response never mixes partially-written records; - a long or streamed scan holds the snapshot it started with, so it does not observe writes that land mid-scan; - multiple reads in the same request transaction see the same snapshot. +Native `Table.lock()` is an exception: it reloads committed data and can release an earlier read snapshot. Acquire the lock before reading the value used in a condition; do not rely on a pre-lock read remaining repeatable after acquisition. See [Concurrency and Consistency](./concurrency.md#native-record-locks) for lock scope and limits. + Across separate requests there is no shared snapshot. A common pattern — querying for matching rows in one request, then fetching each by id in a later request — runs the two reads at two different points in time. If the data changes in between (a row is deleted or expires, say), the second read can disagree with the first; a query may return an id whose subsequent fetch returns a 404. This is expected: it is two independent snapshots, not a consistency bug. For repeatable-read consistency across a query and a fetch, perform both reads within a single resource method or `transaction()` callback. diff --git a/reference/database/concurrency.md b/reference/database/concurrency.md new file mode 100644 index 000000000..5eed2e266 --- /dev/null +++ b/reference/database/concurrency.md @@ -0,0 +1,124 @@ +--- +title: Concurrency and Consistency +--- + +# Concurrency and Consistency + +Harper provides request transactions, atomic increments and decrements, storage conflict resolution, and native record locks. Choose the primitive that protects your application's requirement before adding client write queues, JavaScript mutexes, version columns, compare-and-swap (CAS) retry loops, lock tables, external coordinators, or sticky routing. Application coordination is needed only for a requirement those primitives do not satisfy. + +Harper 5.3.2 is the verification baseline for this page, not an upper version limit. Check the deployed version, storage engine, topology, and Harper Pro configuration before selecting a lock scope. + +## Choose a Primitive + +| Requirement | Harper primitive | Boundary | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| Several writes succeed or fail together in one HTTP request | Automatic request transaction; await the writes | Atomicity spans one database, not different databases or separate requests. | +| A timer or background task needs a transaction | `transaction(async () => { ... })` | Background work needs an explicit transaction scope. | +| Concurrent increments or decrements | `Table.update(id)` followed by `addTo()` or `subtractFrom()` | Atomic deltas preserve concurrent increments. A preceding read/check is not part of the delta. | +| Read a record, check a condition, and update it while excluding competing writers | Server-side `Table.lock(id, options?)` | Every conflicting writer must participate. Scope, timestamp, and recovery limits apply. | +| Resolve storage-level optimistic transaction conflicts | Harper's internal conflict resolution and commit retries | These do not rerun application checks or external side effects. | +| Reject an edit because a form is stale | A server-enforced stale-edit precondition | This is an application policy, separate from storage conflict handling. | + +Transactions normally provide a consistent read snapshot and atomic writes across tables in one database. Lock acquisition reloads committed data and can release an earlier read snapshot, so acquire the lock before reading the condition. A transaction alone does not make a read/check/write sequence exclusive against another request. See [`transaction()`](./api.md#transactioncontext-callback) and the [Resource instance methods](../resources/resource-api.md#resource-instance-methods). + +Atomic deltas are appropriate for counters. A stock decrement is safe as a delta, but "stock must never go below zero" also needs protection for the check. The same distinction applies to balance floors, capacity limits, claim-once behavior, and ordered ticket allocation. + +## Native Record Locks + + + + +`await Table.lock(id, options?, context?)` acquires a lock and returns a mutable record joined to the supplied or ambient transaction. Acquire it before other writes and before reading the value used by the condition. Mutate the returned record and call `await record.save()`. With the default `hold: false`, the transaction releases the lock on commit or abort. Do not call `record.unlock()` before the transaction commits. + +For a missing id, `lock()` returns a mutable record without stored values; setting its properties and saving creates the record. Initialize missing counter values explicitly. In the reservation example below, a missing quantity fails the stock check. + +`lock()` is an in-process API for server Resources and Components. It has no authorization check of its own and is not a REST `LOCK` method. Authenticate and authorize the caller before using it. Ordinary requests do not automatically acquire record locks. + +### Options and Lifetime + +| Option | Type | Default | Meaning | +| --------- | ----------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `scope` | `'node'` or `'cluster'` | Cluster for replicated tables when supported; node for `replicate: false` | Node scope coordinates the node's worker threads. Cluster scope also coordinates nodes through Harper Pro. | +| `lease` | `number` (milliseconds) | `30000` | Maximum time the lock can remain held. Keep the critical section within this lease. | +| `timeout` | `number` (milliseconds) | `30000` | Maximum wait for acquisition. Choose a value within the request's latency budget. The option is `timeout`, not `timeoutMs`. | +| `hold` | `boolean` | `false` | `true` keeps the lock beyond the transaction until explicit `record.unlock()` or lease expiry. | + +`lease` must be a finite number from `100` to `300000`; `timeout` must be a finite number from `1` to `300000`. Invalid options reject with `400`. In particular, `timeout: 0` is not a non-blocking acquisition option. + +Prefer transaction-scoped locks. A lock acquired outside a request or explicit `transaction()` can remain held until unlock or expiry. Keep external network calls outside the critical section: aborting a Harper transaction cannot undo a payment or another external side effect. + +When correctness requires cross-node coordination, set `scope: 'cluster'` explicitly. This fails closed if the Harper Pro cluster-lock transport and home-map configuration are unavailable. With an implicit default, a core-only deployment can fall back to node scope; the existence of `lock()` alone does not establish cluster protection. Revisit an explicit `scope: 'node'` before adding nodes. + +Cluster locks require RocksDB, Harper Pro with [`replication.recordLocks: true`](../configuration/options.md#replication), and an agreed home map for the database. The map names every participating node; a record's **home** is the node that arbitrates its locks. A **generation** identifies one agreed version of that map. Enabling the configuration flag alone does not create a map. Pro's `record_lock_apply_homes` operation coordinates the transition for an operator-supplied participant list. Follow its versioned [home-map setup and transition procedure](https://github.com/HarperFast/harper-pro/blob/v5.3.2/replication/RECORD_LOCK_HOMES_DESIGN.md#the-operation): agree the complete participant list, stage and stop the old generation, drain outstanding locks, then activate the agreed generation. Do not derive separate maps from each node's current view of reachable peers. + +### Participating Writers + +Locks exclude other `lock()` callers on the same key. Plain `put`, `patch`, `delete`, and `create` calls are not blocked. Unlocked `update()` calls using `addTo()` or `subtractFrom()` are not blocked either. Every writer that can violate the condition must use the same locking path, including automatic REST writes and delta updates to guarded fields. Use [table extensions](../resources/overview.md#extending-a-table) to enforce that path, or restrict endpoints that bypass it. + +### Example: Reserve One Item + +This example is for one RocksDB node and a non-replicated table. The table has no `@export`, so it does not expose an automatic REST write endpoint. Call `reserveOne()` from an authorized server path. All other writers of `Inventory.quantity` must use the same lock. The explicit transaction also permits calling the function from a background task. + +```graphql +type Inventory @table(replicate: false) { + id: ID @primaryKey + quantity: Int +} +``` + +```javascript +import { tables, transaction } from 'harper'; + +export async function reserveOne(id) { + return transaction(async () => { + const record = await tables.Inventory.lock(id, { scope: 'node', timeout: 1000 }); + if (!(record.quantity >= 1)) { + throw Object.assign(new Error('Insufficient stock'), { statusCode: 409 }); + } + record.subtractFrom('quantity', 1); + await record.save(); + return { id, reserved: 1 }; + }); +} +``` + +The condition failure throws, so the transaction aborts and releases its lock. Returning an error-shaped object would instead commit. With stock `1`, two competing calls should produce one reservation and one insufficient-stock error, leaving stock `0`. This single-record example does not establish the multi-record or cluster recovery guarantees discussed below. + +Inside a request handler, `transaction()` joins the active request transaction. Returning from `reserveOne()` does not commit the decrement or release the lock; that happens when the request transaction completes. Keep slow or external work outside that transaction. A later handler failure rolls the reservation back. In a background task without an active transaction, the callback's completion commits and releases the lock. + +### Errors + +Lock rejections expose `error.statusCode`: + +| Status | Meaning | Handling | +| ------ | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `409` | A write through an expired/released lock, or a commit submitted after lease expiry | Abort the operation. An application stale-edit response must be distinguishable from this failure. | +| `423` | Lock contention exceeded the wait budget | Propagate it or retry within a bounded policy for an operation safe to repeat. | +| `503` | Coordination is unavailable | Fail the operation; do not perform the write unlocked. | +| `501` | The storage engine is LMDB | Use a supported deployment or a design that satisfies the requirement without this primitive. | + +Application retries are separate from Harper's internal commit retries. Account for repeated requests and external side effects. Lease checks reject expired-lock writes and commits submitted after expiry; they do not guarantee cancellation of a native commit already in progress. + +### Timestamp and Recovery Limits + +Locks leave normal conflict resolution unchanged in both node and cluster scope. Earlier writes or another lock in the same transaction can fix its timestamp before a later locked update. That update can lose to an intervening commit even after reading its value, with all writers participating and no clock skew or crash. Acquiring several locks in one request therefore does not establish serializable multi-record read/check/write. Atomic deltas help only where the invariant permits them. Clock skew or a deliberately future transaction timestamp can also let a predecessor's write outrank its successor's under last-write-wins. + +Cluster locks provide exclusion, not global serializable transactions or quorum durability. On a clean handoff, the home retains **release dependencies**, the replication positions the next holder must receive before using the record. Home restart, generation change, or eviction from the home's bounded dependency cache can lose those dependencies even after a clean release. Recovery then uses a **barrier**, a check that earlier writes have settled and replicated. An unreachable predecessor can block acquisition; a native commit that settles after that barrier can leave a successor reading stale data. Client retries, replication headers, and origin pinning do not establish a stronger guarantee. Define and verify any stronger cross-record or cross-node requirement before designing additional coordination. + +The versioned [lock contract](https://github.com/HarperFast/harper/blob/v5.3.2/resources/record-locks.md) and [Table implementation](https://github.com/HarperFast/harper/blob/v5.3.2/resources/Table.ts) describe these boundaries. + +## Optimistic Concurrency and Stale Edits + +Harper handles storage-level optimistic transaction conflicts and retries commits internally. An application version field, browser CAS loop, or whole-request retry wrapper is not needed to duplicate that work. The [commit implementation](https://github.com/HarperFast/harper/blob/v5.3.2/resources/DatabaseTransaction.ts) does not rerun the handler's business logic. + +For a conditional update, evaluate native `lock()` first within its supported scope. A separate version comparison followed by `put()` or `patch()` is not atomic CAS. In Harper 5.3.2, application-facing table writes do not expose an `ifVersion` precondition, and automatic REST writes do not enforce `If-Match`. Conditional GET and ETag support does not change that. + +"Warn or reject if this form is stale" is a separate product requirement. A product-specific revision token can serve that policy, but needs an atomic server-enforced comparison and write. For a single record, acquire the lock first in the transaction, compare the client's token with the locked record's revision, reject a mismatch with `409`, otherwise advance the revision and save. All conflicting writers must participate, and the scope, timestamp, and recovery limits must satisfy the requirement. Verify the revision representation and supported precondition mechanism for the deployed version before implementing it. + +Give a stale-edit response a distinguishable application error code or body. A bare `409` can also mean lease expiry and must not cause a client to discard unsaved edits. Automatic storage conflict handling does not promise to reject stale forms. + +## Request, Node, and Browser Origin + +One browser origin does not imply one Harper worker, node, or transaction. A browser queue serializes only that client's work. Separate HTTP requests do not share a request snapshot, and using one URL does not create a cluster-wide snapshot or remove replication lag. + +For a coherent unit of work, put related reads and writes in one request transaction within one database. For counters, use atomic deltas. For participating conditional writers, evaluate native locks and their scope. Sticky routing or a single-origin architecture should not be the default answer to concurrency. State the required cross-request or cross-node freshness and check the deployment's guarantees before adding coordination. diff --git a/reference/database/overview.md b/reference/database/overview.md index 406b251a9..df60913a0 100644 --- a/reference/database/overview.md +++ b/reference/database/overview.md @@ -108,6 +108,7 @@ For deeper coverage of each database feature, see the dedicated pages in this se - **[Schema](./schema.md)** — Defining table structure, types, indexes, relationships, and computed properties using GraphQL schema syntax - **[API](./api.md)** — The `tables`, `databases`, `transaction()`, and `createBlob()` globals for interacting with the database from code +- [Concurrency and Consistency](./concurrency.md) - Transactions, atomic deltas, native record locks, and their consistency limits - **[Data Loader](./data-loader.md)** — Loading seed or initial data into tables as part of component deployment - **[Storage Algorithm](./storage-algorithm.md)** — How Harper stores data using LMDB with universal indexing and ACID compliance - **[Storage Tuning](./storage-tuning.md)** — Tuning `storage.*` options for production: durability vs. throughput, compression, blob paths, reclamation diff --git a/reference/mcp/configuration.md b/reference/mcp/configuration.md index 2915d5d82..20ec2d4b9 100644 --- a/reference/mcp/configuration.md +++ b/reference/mcp/configuration.md @@ -189,7 +189,7 @@ Semantics to know: - The handler runs **after** the in-memory buckets admit the call, so rate-limited clients cannot spam a table-backed handler. - **Fail-closed**: a handler that throws **denies** the call. Cost protection that silently disables itself on a bug is worse than a hard failure. The raw error is written to the server log only; the client sees a sanitized message. - Harper calls the handler once per attempted tool call; counting strategy (increment on check vs on success) is the handler's business. -- **Race-safety is the handler's business too.** It can run concurrently for the same identity — within a worker (interleaving across `await` boundaries) and across workers. A naive read-then-write counter like the example above can undercount under concurrency and admit calls past the limit; make the read-modify-write atomic (a transaction that serializes conflicting writers, a compare-and-set retry loop, or a store with native atomic increments) for production use. +- **Race-safety is the handler's business too.** It can run concurrently for the same identity, within a worker and across workers. The illustrative read-then-write counter above can undercount and admit calls past the limit. On RocksDB, for a strict limit on one record, use an explicit `transaction()` and acquire `Table.lock()` before reading the count, initialize a missing count to zero, check the limit, then increment and save the locked record. Every conflicting writer must use that path. Select node or cluster scope for the deployment and check the timestamp and recovery limits in [Concurrency and Consistency](../database/concurrency.md#native-record-locks). Locks reject with `501` on LMDB, so this pattern requires RocksDB. A transaction alone does not serialize these checks, and an atomic increment alone does not enforce a ceiling. ## `mcp.session.*` diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 21815c98a..335fd70a5 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1623,16 +1623,17 @@ Operations for configuring and managing Harper cluster replication. Detailed documentation: [Replication & Clustering](../replication/clustering.md) -| Operation | Description | Role Required | -| ----------------------- | --------------------------------------------------------------- | ------------- | -| `add_node` | Adds a Harper instance to the cluster | super_user | -| `update_node` | Modifies an existing node's subscriptions | super_user | -| `remove_node` | Removes a node from the cluster | super_user | -| `cluster_status` | Returns current cluster connection status | super_user | -| `configure_cluster` | Bulk-creates/resets cluster subscriptions across multiple nodes | super_user | -| `cluster_set_routes` | Adds routes to the replication routes config (PATCH/upsert) | super_user | -| `cluster_get_routes` | Returns the current replication routes config | super_user | -| `cluster_delete_routes` | Removes routes from the replication routes config | super_user | +| Operation | Description | Role Required | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| `add_node` | Adds a Harper instance to the cluster | super_user | +| `update_node` | Modifies an existing node's subscriptions | super_user | +| `remove_node` | Removes a node from the cluster | super_user | +| `cluster_status` | Returns current cluster connection status | super_user | +| `configure_cluster` | Bulk-creates/resets cluster subscriptions across multiple nodes | super_user | +| `cluster_set_routes` | Adds routes to the replication routes config (PATCH/upsert) | super_user | +| `cluster_get_routes` | Returns the current replication routes config | super_user | +| `cluster_delete_routes` | Removes routes from the replication routes config | super_user | +| `record_lock_apply_homes` | Applies an agreed record-lock home map in Harper Pro on RocksDB; see [native record locks](../database/concurrency.md#native-record-locks) | super_user | ### `add_node` diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index da064a259..834dc3551 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -218,6 +218,19 @@ product.save(); --- +### `lock(target: RequestTarget | Id, options?, context?): Promise` + + + + +On a table, acquires a native record lock and returns a mutable Resource instance in the supplied or ambient transaction. Lock before other writes and before reading the condition, then update the returned record and `await record.save()`. The default transaction-scoped lock releases on commit or abort. + +Options are `scope` (`'node'` or `'cluster'`), `lease` and `timeout` (milliseconds, each defaulting to `30000`), and `hold` (default `false`). Cluster coordination requires Harper Pro; explicit `scope: 'cluster'` fails closed when unavailable. Locks are cooperative: ordinary unlocked writes still proceed. This server API does not perform authorization or expose a REST method. + +See [Concurrency and Consistency](../database/concurrency.md#native-record-locks) for option defaults, participating writers, `record.unlock()`, errors, a worked example, and timestamp and recovery limits. + +--- + ### `publish(target: RequestTarget | Id, message: object, context?: Resource | Context): Promise` Publishes a message to a record/topic using the same primary key structure as records. @@ -519,7 +532,7 @@ Post.sourcedFrom(BlogSource); #### Read consistency across databases -A resolver's reads are snapshot-consistent as long as every table it reads lives in the same [database](../database/overview.md#databases) — Harper pins a single transaction snapshot per database, so multiple `get()` calls into tables in that database always see the same point-in-time view, no matter how the resolver interleaves them. +A resolver's ordinary reads use one transaction snapshot per [database](../database/overview.md#databases), so multiple `get()` calls into tables in that database see the same point-in-time view. Native `Table.lock()` can reload committed data and release an earlier snapshot; acquire the lock before reading the value used in a condition. See [native record locks](../database/concurrency.md#native-record-locks) for this exception and its limits. That snapshot is scoped per database. If a resolver reads from one database, `await`s something (an upstream fetch, another async call), and then reads from a second database, the second read is not guaranteed to reflect the same point in time as the first — it can observe writes that landed on the second database during the `await`. @@ -941,21 +954,21 @@ The arguments these instance methods receive depend on [`static loadAsInstance`] ## Concurrency and Safe Concurrent Writes -When multiple writers may touch the same record concurrently, only atomic deltas are safe. Harper resolves deltas at commit time, so the committed result is exact regardless of how the writers interleave: +Use atomic deltas for concurrent increments and decrements. Harper resolves deltas at commit time, preserving competing increments without an application read/modify/write loop: ```javascript record.addTo('counter', 1); // increment record.subtractFrom('stock', 1); // decrement ``` -Read-then-write patterns — "compare-and-set" — are not safe under concurrency. This includes version-guarded updates (`ifVersion`), create-only "first write wins", reading a value to decide and then writing it back, and HTTP `If-Match` / conditional writes. All of these read a value _before_ commit and are not re-validated _at_ commit, so two concurrent writers can both pass the check. Modeling state as commutative deltas avoids the problem entirely. +An unprotected read/check/write is not atomic compare-and-swap. Two requests can pass the same check before either commits. In Harper 5.3.2, application-facing table writes do not expose an `ifVersion` precondition, and automatic REST writes do not enforce `If-Match`. A preceding read or conditional GET does not establish create-only "first write wins" behavior. For a condition that must hold while updating a record, evaluate [`Table.lock()`](#locktarget-requesttarget--id-options-context-promiseresource) and require every conflicting writer to participate. A few limitations to design around: - `addTo` and `subtractFrom` return no value, and a read-back immediately after a write is not guaranteed to reflect your own committed result. You cannot use them to read "your position" for an exactly-once claim or a hard limit. - `subtractFrom` has no floor; concurrent decrements can drive a value negative. -For non-commutative operations — claim-once, hard caps, inventory floors, state-machine transitions — serialize the operation through a single owning process, or use an external lock or coordinator. This matters more in a cluster: a counter replicates and converges eventually, but a decision made on one node's not-yet-converged view can be wrong during the replication window. A rate limiter built this way, for example, can transiently over-admit when requests are spread across nodes. +For claim-once behavior, inventory floors, or guarded state transitions, start with Harper's native locks and check their scope and limits before introducing an external coordinator. Harper also handles storage-level optimistic transaction conflicts internally; rejecting a stale form is a separate application policy. See [Concurrency and Consistency](../database/concurrency.md) for these distinctions, transaction boundaries, and the multi-record and cluster recovery limits. ## Returning Errors and Status Codes diff --git a/release-notes/v5-lincoln/5.3.md b/release-notes/v5-lincoln/5.3.md index 195c6baa9..66ee8ea04 100644 --- a/release-notes/v5-lincoln/5.3.md +++ b/release-notes/v5-lincoln/5.3.md @@ -61,6 +61,8 @@ Isolation requires worker threads (a `threads.count` above `0`), `http.securePor ## Record Locks +See [Concurrency and Consistency](/reference/v5/database/concurrency#native-record-locks) for the API, examples, setup requirements, and limits. + `table.lock(id)` gives an application an exclusive lock on one record, serialized across worker threads ([harper#2462](https://github.com/HarperFast/harper/pull/2462)) and, in a Harper Pro cluster, across nodes ([harper-pro#822](https://github.com/HarperFast/harper-pro/pull/822)). Ownership of a record's lock is held per record across calls, so repeatedly locking a busy record does not cost a network round trip each time ([harper#2498](https://github.com/HarperFast/harper/pull/2498)). When a wait for a lock runs out, the response reports what the lock's owning node answered: `423` means the lock is held and the request can be retried, and `503` means coordination between nodes failed ([harper#2685](https://github.com/HarperFast/harper/pull/2685)). ## Models: `models.decide` diff --git a/sidebarsReference.ts b/sidebarsReference.ts index f997c1810..055854e74 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -29,6 +29,11 @@ const sidebars: SidebarsConfig = { id: 'database/api', label: 'API', }, + { + type: 'doc', + id: 'database/concurrency', + label: 'Concurrency and Consistency', + }, { type: 'doc', id: 'database/data-loader',