Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/10757-request-rounds-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/core': patch
---

perf(core): an authenticated request resolves its grants in one round fewer, and a write to an unrelated object no longer retires the localization cache

Clause-②: no (no accept set, served body or authz answer moves: fewer queries and rounds for the same answer; owes the seat's review of record before the queue, at `CONTRACT_REVIEW_TIER` by the seat's choice, since it changes when authz and localization caches are trusted)

- **The Default Organization read and the activation-ledger read are one wave.** `resolveUserAuthzGrants` read `sys_organization` (by slug) and then `sys_metadata_activation`, each awaited after the other. Both depend only on the first wave of reads, so they now run together. While the organization read is still open, the ledger is asked about every permission-set name that may apply. In one case that adds the name `admin_full_access` to the ledger's `name IN` list: an organization-owned `admin_full_access` grant that turns out not to anchor platform standing. The envelope is unchanged in every case: positions, permissions, `org_user_ids`, `accessible_org_ids` and posture. The organization read's filter is unchanged, and so are the organization-less grant readings.
- **The localization success cache is retired only by a write that can change its answer.** That is a write to `sys_setting` or `sys_platform_setting`, observed after it settles by an engine middleware. A non-`write` epoch reason (a peer node's `remote` hint, `metadata`, `manual`), a `localization` settings change or `OS_LOCALIZATION_CACHE_TTL_MS` also retires it. Before, any engine write to any object retired it, so on an idle boot the outbox pollers' claim updates made warm requests read the settings again. A success is cached only on an engine that has both the write epoch and `registerMiddleware`. With `OS_LOCALIZATION_CACHE_TTL_MS=0`, nothing is registered on the engine. The failure memo for a missing `sys_setting` table is unchanged: it is retired only by its TTL.
10 changes: 10 additions & 0 deletions .changeset/10757-request-rounds-metadata-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/metadata-protocol': patch
---

perf(metadata-protocol): the served by-name read takes its stored rows from the overlay cache, and only a `sys_metadata` write retires that cache

Clause-②: no (no accept set, served body or authz answer moves: fewer queries and rounds for the same answer; owes the seat's review of record before the queue, at `CONTRACT_REVIEW_TIER` by the seat's choice, since it changes when authz and localization caches are trusted)

- **`getMetaItem` reads the item's active `sys_metadata` rows through the overlay cache.** The list read already used that cache. The REST API-exposure gate reads the object by name on every `/data` request, and that read was its one uncached `sys_metadata` round trip. The cached value is the raw row set. Parsing, merging and registry hydration still run on every call, hit or miss. Only the served face reads through the cache. The protocol's own authoring checks, such as the destructive-change check before an object save, still read the store on every call.
- **Only a write to `sys_metadata` retires the cache.** The write is observed by an engine middleware after it settles. Every non-`write` epoch reason also retires it, including the metadata cluster bridge's `remote` bump, and `OS_METADATA_OVERLAY_CACHE_TTL_MS` still bounds it. Before, any engine write to any object retired it. A row set is cached only on an engine that has both the write epoch and `registerMiddleware`. With the TTL set to `0`, nothing is registered on the engine.
19 changes: 12 additions & 7 deletions content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,18 @@ The hash is `sha256:` + 64-hex of a canonical JSON serialization of the body, wi
> writes.
>
> ⚠️ **"Peers re-read from the shared database" has one bound worth naming.** The
> `sys_metadata` overlay read inside `protocol.ts:getMetaItems` is cached across
> requests, keyed on the engine write epoch. A **local** write advances that epoch
> before its own middleware chain runs, so read-your-writes is exact on the node
> that made the write. A **peer's** write does not: `metadata.changed` invalidates
> the MetadataManager caches this note is about, but it does not retire the
> overlay-read cache. What retires that on a peer is either of two receipt
> paths that bump the same local write epoch: the `metadata.mutated`
> `sys_metadata` overlay rows `protocol.ts` reads — the list read inside
> `getMetaItems`, and the served by-name read inside `getMetaItem` — are cached
> across requests. A **local** `sys_metadata` write retires them when its
> statement completes (an engine middleware sees it), so read-your-writes is
> exact on the node that made the write; a write to any other object does not
> retire them. Inside an engine transaction the statement completes before the
> commit, so a read landing between the two can keep the pre-commit rows until
> the next `sys_metadata` write or the TTL. A **peer's**
> write does not either: `metadata.changed` invalidates the MetadataManager
> caches this note is about, but it does not retire the overlay-read cache. What
> retires that on a peer is either of two receipt paths that bump the local
> write epoch for a non-`write` reason, which retires the cache wholesale: the `metadata.mutated`
> channel's own — `protocol.ts`'s `applyRemoteMetadataMutation` calls
> `bumpWriteEpoch(engine, 'remote')` right after its registry converges and
> before local listeners run (#13609, 2026-09-03) — or the `authz.invalidated`
Expand Down
4 changes: 2 additions & 2 deletions content/docs/deployment/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ the hosted ObjectOS Cloud control plane.
| `OS_SANDBOX_HOOK_TIMEOUT_MS` | number | `250` | Default **CPU-time** budget for a sandboxed **hook** body (QuickJS, ADR-0102): how much *VM-active* time a body may burn — idle host-await time and a nested hook's own run are NOT charged. A loaded/slow host rarely needs to raise this now (it is not wall-clock), but the knob remains. Only a positive integer is honored; unset / non-numeric / non-positive keeps the 250ms default. A hook body's own declared `timeoutMs` still wins over this. |
| `OS_SANDBOX_ACTION_TIMEOUT_MS` | number | `5000` | Default **CPU-time** budget for a sandboxed **action** body (QuickJS). Same resolution rules as the hook variant above (positive integer only; an action body's own `timeoutMs` still wins). |
| `OS_SANDBOX_WALL_CEILING_MS` | number | `30000` | Wall-clock ceiling (ADR-0102) — the backstop that cuts a hook/action body stuck on a host call that never settles (which burns no CPU, so the CPU budget alone would never fire). The effective ceiling is `max(this, cpuBudget)`, so it can never cut a body still inside its CPU budget. Positive integer only; unset keeps 30s. |
| `OS_LOCALIZATION_CACHE_TTL_MS` | number | `30000` | Staleness bound, in milliseconds, for the cross-request cache of a workspace's reference localization (`timezone` / `locale` / `currency`, read from `sys_setting`) — leg C of #11633. `0` means **off**, a real path that restores the uncached query pattern exactly. Unlike `OS_AUTHZ_GRANTS_CACHE_TTL_MS` (which is off by default) this one ships **on**, because its invalidation is synchronous and in-process rather than TTL-bound: a `localization` settings change and any engine write both retire a cached answer immediately, so the TTL only bounds what neither seam can see — a write made on another replica with no cluster bridge attached at all: neither the `metadata.mutated` channel's receipt path (`applyRemoteMetadataMutation` calls `bumpWriteEpoch`, #13609) nor the `authz.invalidated` bridge, both of which bump this same epoch when attached. ⚠️ A malformed value reads as `0` (off), the opposite arm from the grants variable and deliberately so: there `0` is also the default, whereas here folding `3OOO` (letter O) into the default would hand you a **longer** window than the one you were setting. Deployment config only — never a settings row, because `sys_setting` is the table this cache caches. |
| `OS_METADATA_OVERLAY_CACHE_TTL_MS` | number | `30000` | Staleness bound, in milliseconds, for the cross-request cache of the `sys_metadata` overlay read inside `getMetaItems` — leg D of #11633. `0` means **off**, a real path that restores the uncached query pattern exactly. Ships **on**, for the same reason as `OS_LOCALIZATION_CACHE_TTL_MS`: invalidation is synchronous and in-process, because every `sys_metadata` write goes through the engine and so advances the write epoch that retires the entry before the next read. What is cached is the overlay ROW SET only — never the merged answer — so the SchemaRegistry, the MetadataService and the artifact table are re-consulted on every call, cached or not, and the read-side registry hydration keeps running on a cache hit. The TTL therefore bounds one thing: a write made on **another replica** with no cluster bridge attached at all — neither the `metadata.mutated` channel's own receipt path (`applyRemoteMetadataMutation` calls `bumpWriteEpoch` right after registry convergence, #13609) nor the `authz.invalidated` bridge, either of which retires this entry the moment it is attached and fires. ⚠️ A malformed value reads as `0` (off) — same arm and same reason as `OS_LOCALIZATION_CACHE_TTL_MS`. Deployment config only, never a settings row. |
| `OS_LOCALIZATION_CACHE_TTL_MS` | number | `30000` | Staleness bound, in milliseconds, for the cross-request cache of a workspace's reference localization (`timezone` / `locale` / `currency`, read from `sys_setting`) — leg C of #11633. `0` means **off**, a real path that restores the uncached query pattern exactly. Unlike `OS_AUTHZ_GRANTS_CACHE_TTL_MS` (which is off by default) this one ships **on**, because its invalidation is synchronous and in-process rather than TTL-bound: a `localization` settings change, a write to `sys_setting` or `sys_platform_setting` (the tables the answer is read from), and a non-`write` bump of the engine write epoch each retire a cached answer immediately. A write to any other object does not. So the TTL only bounds what none of these seams can see — a write made on another replica with no cluster bridge attached at all: neither the `metadata.mutated` channel's receipt path (`applyRemoteMetadataMutation` calls `bumpWriteEpoch`, #13609) nor the `authz.invalidated` bridge, both of which bump this same epoch when attached. ⚠️ A malformed value reads as `0` (off), the opposite arm from the grants variable and deliberately so: there `0` is also the default, whereas here folding `3OOO` (letter O) into the default would hand you a **longer** window than the one you were setting. Deployment config only — never a settings row, because `sys_setting` is the table this cache caches. |
| `OS_METADATA_OVERLAY_CACHE_TTL_MS` | number | `30000` | Staleness bound, in milliseconds, for the cross-request cache of the `sys_metadata` overlay rows read inside `getMetaItems` (the list read) and `getMetaItem` (the served by-name read the REST API-exposure gate makes on every `/data` request) — leg D of #11633. `0` means **off**, a real path that restores the uncached query pattern exactly. Ships **on**, for the same reason as `OS_LOCALIZATION_CACHE_TTL_MS`: invalidation is synchronous and in-process, because every `sys_metadata` write goes through the engine, whose middleware retires the entry when the write's statement completes, before the next read. Outside a transaction that is the commit. Inside an engine transaction it is earlier than the commit: a read that lands between the statement and the commit can keep the pre-commit rows until the next `sys_metadata` write or the TTL. A write to any other object does not retire it; a non-`write` bump of the engine write epoch retires it wholesale. What is cached is the overlay ROW SET only — never the merged answer — so the SchemaRegistry, the MetadataService and the artifact table are re-consulted on every call, cached or not, and the read-side registry hydration keeps running on a cache hit. The TTL therefore bounds one thing: a write made on **another replica** with no cluster bridge attached at all — neither the `metadata.mutated` channel's own receipt path (`applyRemoteMetadataMutation` calls `bumpWriteEpoch` right after registry convergence, #13609) nor the `authz.invalidated` bridge, either of which retires this entry the moment it is attached and fires. ⚠️ A malformed value reads as `0` (off) — same arm and same reason as `OS_LOCALIZATION_CACHE_TTL_MS`. Deployment config only, never a settings row. |
| `OS_INLINE_SEED_BUDGET_MS` | number | `8000` | Time budget for synchronous seed execution at boot before deferring to a worker. |
| `OS_TENANT_AUDIT` | flag | `1` | Set to `0` to silence the tenant-isolation audit warnings emitted by the SQL driver. |

Expand Down
Loading
Loading