Skip to content

Commit 5224d3f

Browse files
kriszypclaude
andcommitted
docs(design): document blob/put streaming race + system-table bootstrap pattern
Two non-obvious findings from #641 Slice A that future agents should know: 1. createBlob(readable) + table.put() doesn't synchronously drain the source. The blob's saveBlob runs concurrently; calling hash.digest() after the put resolves can race a still-flushing Transform and throw ERR_CRYPTO_HASH_FINALIZED. 2. Adding a new system table requires three changes: systemSchema.json (fresh installs), SYSTEM_TABLE_NAMES (constant), and an upgrade directive registered in directivesController.ts. The directive shape was undocumented after the .ts refactor cleared old directives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bacdd54 commit 5224d3f

1 file changed

Lines changed: 24 additions & 0 deletions

File tree

DESIGN.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,27 @@ The cross-thread subscription path (default `crossThreads`) drives every `Table.
6363
- **`databaseSubscriptions.activeCount`** is the count of live `Subscription` instances on a database. It is incremented at the end of `addSubscription` (after the Subscription is created, so the `scope: 'full-database'` early-return path correctly skips counting) and decremented in `Subscription.end()`. `notifyFromTransactionData` short-circuits when this is zero — the reusable rocksdb iterator stays put and resumes from its position the next time a subscriber arrives. Without this short-circuit, an idle database with no subscribers still pays the audit-log iteration cost on every commit during replication backlog catch-up.
6464
- **`notifyScheduled` + `setImmediate`** in the `'committed'` listener defers the iteration off the commit microtask. Multiple `'committed'` events that land in the same event-loop turn collapse into one notify pass. `notifyScheduled` stays set for the entire drain — including across yield-and-resume turns — so a re-entry from a new `'committed'` event cannot spawn a second concurrent notify on the same iterator.
6565
- **Batched yielding** in `notifyFromTransactionData` (`NOTIFY_BATCH_SIZE`) is gated by `allowYield`. The `'committed'` path passes `allowYield = true`; the `listenToCommits` (same-thread `aftercommit`) path does not, because that path holds an inter-thread `'thread-local-writes'` lock that must not span event-loop turns. `subscribersWithTxns` is carried across yields via `subscriptions.pendingTxnSubscribers` so the `end_txn` signal fires exactly once when the iterator truly drains. When `activeCount` drops to zero mid-yield, the next continuation drops the carry-over to avoid invoking ended subscribers' listeners.
66+
67+
## `createBlob(readable)` and `table.put()` don't synchronously drain the source
68+
69+
When a blob attribute is created from a Node `Readable` (e.g. `createBlob(stream)` then `row.payload_blob = blob; await table.put(row)`), the put does **not** wait for the underlying stream to fully drain into the file before resolving. Internally `saveBlob` kicks off a `writeBlobWithStream` pipeline whose `storageInfo.saving` promise is tracked separately. The put resolves once encoding has captured the blob reference; the bytes finish writing concurrently.
70+
71+
Consequence for callers that wrap the source in a hashing `Transform`: calling `hash.digest('hex')` after `await table.put()` is unsafe — more `chunk.update()` calls can still fire as the stream drains, producing `Error [ERR_CRYPTO_HASH_FINALIZED]: Digest already called`. Options:
72+
73+
- Buffer first, then hash + put (what `components/deploymentRecorder.ts` does for Slice A — small payloads only).
74+
- Hash via Transform while extraction reads the stream, and only finalize the hash on the Transform's `'end'` event before any second put with the final hash.
75+
- Await `storageInfo.saving` directly if you have a handle to the FileBackedBlob (the cleanest path for streaming).
76+
77+
Future agents touching `components/deploymentRecorder.ts` for Slice B's streaming variant should pick one of the latter two patterns.
78+
79+
## System table bootstrap: `systemSchema.json` + upgrade directive
80+
81+
Adding a new system table (e.g. `hdb_deployment` in #641 Slice A) requires three changes:
82+
83+
1. **`json/systemSchema.json`** — the table entry. Fresh installs auto-create it via `utility/mount_hdb.ts:createTables()`, which iterates `Object.keys(systemSchema)` on first boot.
84+
2. **`utility/hdbTerms.ts`** — add the table name to `SYSTEM_TABLE_NAMES`.
85+
3. **`upgrade/directives/<version>.ts`** — provisions the table on existing installs that already have a system schema. Registered in `upgrade/directives/directivesController.ts` (which is otherwise empty — its `versions` Map gets populated by these imports). The directive shape is `{ version, sync_functions, async_functions }`; copy `5-2-0.ts` for the canonical pattern (uses `bridge.createTable` to match what `mount_hdb` does on a fresh install).
86+
87+
System tables replicate by default. To opt out, add the name to `NON_REPLICATING_SYSTEM_TABLES` in `resources/databases.ts`. The check happens after table init and sets `table.replicate = false` per-node.
88+
89+
If the table needs `audit: true`, set it both in the schema (for fresh installs) **and** on the `CreateTableObject` instance in the directive (for upgrades) — otherwise the two paths diverge.

0 commit comments

Comments
 (0)