feat: Explicit DB handle threading for complex _context() wrappers (Phase 7)#453
Conversation
…hase 7)
Convert the five remaining save/swap/restore _context() wrappers in
db_format.c to pass the database handle explicitly through new _hdb
functions, eliminating all databasedata global mutation from the
wrapper layer.
Layer 2: dbseteof_fnum, dbreadheader_fnum, dbwriteheader_fnum,
dbwritetrailer_fnum — header/trailer I/O with explicit fnum
Layer 3: dballocate_hdb, dbrelease_hdb — block allocator with explicit
database handle (includes ~15 internal helper _hdb variants)
Layer 4: dbassign_hdb, dbcopy_hdb, dbsavehandle_hdb, dbrefhandle_hdb,
dbassignhandle_hdb — high-level operations with explicit handle
Layer 5: dbassign_context, dbcopy_context, dbsavehandle_context,
dbassignhandle_context, dbreference_handle_context now call
_hdb variants directly instead of mutating globals
Key design decisions:
- _hdb functions derive fnum and format (v6/v7) from (**hdb).headerLength
- dbcopy_hdb reads source data from dbsaveas_source during Save As
- dbassign_hdb checks fldatabasesaveas to force fresh allocation
- 4 new unit tests exercise allocate/read, assign, save, copy roundtrips
295 unit tests pass, 1704 integration tests pass, migration verified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Code Review: Phase 7 — Explicit DB Handle ThreadingOverall this is a solid, well-structured continuation of the Bugs1. The function returns This compiles due to C's implicit-return allowance but is undefined behavior when the caller reads the return value (even if no current caller does). The legacy 2. Ignored return value in At the bottom of dbsetavailshadow_hdb (ixshadow, newadr, newsize, hdb); /* return value silently dropped */Every other call to if (!dbsetavailshadow_hdb (ixshadow, newadr, newsize, hdb))
return (false);Design Concerns3. The comment acknowledges this: "a rare path that will be fully converted in a future phase." That's fine, but the PR description claims "all 4. This exported function is a one-liner that calls Minor5. Repeated test setup — four identical open/new/save/restore blocks Each of the four new tests contains identical ~15-line setup and cleanup sequences. A What's working well
SummaryTwo bugs need fixing before merge: the missing 🤖 Generated with Claude Code |
- Change dbclearshadowavaillist_hdb from static boolean to static void (matches legacy dbclearshadowavaillist signature, had no return statement) - Check return value of dbsetavailshadow_hdb in dbmergeleft_hdb (was silently discarded unlike all other call sites) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review — PR #453: Explicit DB Handle Threading (Phase 7)Overall assessment: Strong, well-structured work. The layered architecture is clear, the _context() simplifications are a genuine improvement, and test coverage is solid. A few items worth addressing before merge. What is Working Well
IssuesMedium: "No global access" comments overstated for dbcopy_hdb and dbassign_hdbdballocate_hdb and dbrelease_hdb doc comments say "no global databasedata access", which is true. But dbassign_hdb and dbcopy_hdb both silently read non-databasedata globals: // dbassign_hdb:
if (fldatabasesaveas || (adr == nildbaddress)) { ...
// dbcopy_hdb:
hdldatabaserecord source_hdb = (fldatabasesaveas && dbsaveas_source != nil) ? dbsaveas_source : hdb;The distinction between "no databasedata access" and "no global access" is meaningful. A future caller who takes the comment at face value could misuse these functions in a context where fldatabasesaveas/dbsaveas_source are not in the expected state. Suggest updating the doc comments to say something like:
Medium: dbclearshadowavaillist_hdb still swaps databasedata (acknowledged, but needs a tracking comment)The comment explains the two temporary swaps and notes this will be "fully converted in a future phase." Given the GIL model this is safe today — but the swap happens twice in sequence (once for dbflushheader, once for dbrelease_internal), and the correctness guarantee depends on no yield points occurring between save and restore. The comment should say this explicitly so future maintainers know why the pattern is safe. A TODO tag would help track it: /* TODO(Phase 8): Convert dbflushheader and dbrelease_internal to _hdb
so this function no longer needs to touch databasedata.
Safe under GIL: no yield points between save and restore. */Low: dbreadheader_fnum is a trivial forwarding wrapperboolean dbreadheader_fnum (...) {
return dbreadheader_core (...);
}This adds a public API symbol for zero logic. Either rename dbreadheader_core to dbreadheader_fnum directly, or have internal callers call dbreadheader_core themselves. One less exported symbol. Low: ixshadow + 1 array access in dbmergeright_hdb / dbmergeleft_hdbassert ((*(hdlavaillistshadow)(**hdb).u.extensions.availlistshadow.data) [ixshadow + 1].adr == nextavail);assert is stripped in release builds. If the avail list is corrupt, ixshadow + 1 could be an out-of-bounds read in production. This mirrors the legacy pattern, but since this is new code it is worth noting — a bounds check before the assert would make this safe in production too. Low: hsize computed then gethandlesize(h) called again in dbassignhandle_hdblong hsize = (h != nil) ? gethandlesize(h) : 0;
if (*adr == nildbaddress) {
...
fl = dballocate_hdb ((long) gethandlesize (h), *h, adr, hdb); /* redundant call */hsize is never used in the nildbaddress branch. Either use hsize consistently or drop the early computation. Minor redundancy. Low: Test temp files leak on assertion failureAll four new tests use assert() for setup steps before the cleanup block: assert(dbnew(fnum, true));
/* ...many asserts... */
remove(scratch_path); /* never reached if any assert fires */If any assert() fires mid-test, the /tmp/hdb_*.db file is left on disk. A goto cleanup pattern would be more robust for CI environments. Nits
Summary
The medium items are documentation/comment accuracy rather than behavioral bugs — the code appears correct given the GIL model and existing Save As semantics. None of the issues are blocking, but the "no global access" comment precision is worth fixing before this lands so future phases do not inherit a misleading invariant claim. Generated with Claude Code |
- Clarify doc comments on dbassign_hdb and dbcopy_hdb: they read fldatabasesaveas/dbsaveas_source globals but do not touch databasedata - Add TODO(Phase 8) tag to dbclearshadowavaillist_hdb with GIL safety note - Add comment to dbreadheader_fnum explaining why the indirection over dbreadheader_core is intentional (public/private API boundary) - Add bounds checks before ixshadow+1 array access in dbmergeright_hdb and dbmergeleft_hdb (production-safe, not just assert) - Remove redundant gethandlesize(h) in dbassignhandle_hdb - Add "format detection only" comments to dbwriteheader_fnum and dbwritetrailer_fnum hdb parameters - Refactor 4 test functions to use shared open_scratch_v7_db/ close_scratch_v7_db helpers with goto cleanup (no temp file leaks) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: Phase 7 — Explicit DB Handle ThreadingThis is a well-structured refactor with clear layering, good documentation of design decisions, and solid test coverage. The goal—eliminating What's Working Well
Issues and Concerns1. Ignored return value in
|
| Category | Status |
|---|---|
| Correctness | ✅ Good — the core conversion is correct |
| Global elimination | ✅ Achieved in all 5 wrappers |
| Remaining globals | dbclearshadowavaillist_hdb still swaps (acknowledged) |
| Test coverage | ✅ 4 new roundtrip tests, all passing |
| Return value checking | dbdeleteavailshadow_hdb unchecked in shadow allocator path |
| Design invariants | context->mode vs headerLength invariant undocumented |
| Test robustness | /tmp paths |
The unchecked dbdeleteavailshadow_hdb return value (#1) is the most actionable item before merge. The rest are documentation/robustness concerns that could be addressed in follow-on work.
🤖 Generated with Claude Code
- Check return value of dbdeleteavailshadow_hdb in dballocate_hdb shadow fast-path (was silently ignored, could corrupt shadow list) - Verify and document GIL yield-point safety in dbclearshadowavaillist_hdb: dbflushheader and dbrelease_internal do only file I/O, no yield points - Add invariant comment in db_format.c noting that _hdb functions derive format from headerLength, not context->mode (mode field is ignored) - Use PID-based scratch file paths in tests for parallel-safe execution - Suppress unused fnum warning in dbfindpreviousavail_hdb shadow path Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: Phase 7 — Explicit DB Handle Threading for
|
| Severity | Count |
|---|---|
| Low (test fix) | 1 |
| Low (duplication/ordering) | 1 |
| Low (silent failure on corruption) | 2 |
| Nit/informational | 2 |
None of the issues are blockers. The most actionable items are the file descriptor cleanup in open_scratch_v7_db (#1) and adding error logging before the OOB avail-list returns (#5). The architectural work itself is correct and well-executed — this is exactly the kind of incremental global-state elimination that makes the codebase safer for concurrent and multi-database use.
🤖 Generated with Claude Code
- Check return value of dbdeleteavailshadow_hdb in dballocate_hdb shadow fast-path (was silently ignored) - Add forward-reference comments for duplicated use64 logic in dbwriteheader_fnum/dbwritetrailer_fnum (references db_hdb_use64) - Convert assert(availlistblock == nil) to checked error return with dbfreelisterror in dbwriteavailnode_hdb and dbsetavaillink_hdb - Add (void)flfree suppression in dbmergeleft_hdb for unused variable - Add dblogerror(dbmergeinvalidblockerror) before OOB avail-list bounds check returns in dbmergeright_hdb and dbmergeleft_hdb - Fix FD leak in open_scratch_v7_db if dbnew fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Code Review: Phase 7 — Explicit DB Handle ThreadingThis is a well-structured, bottom-up refactor that makes real progress toward eliminating Architecture / Correctness1. The two brief This is acceptable as a staged approach, but consider adding an assertion or compile-time note that the 2. Shadow list bounds checks outside In Wrapping these blocks in // In dbmergeright_hdb, after dbfindpreviousavail_hdb():
#ifdef dbshadow
{
long ctavail = (**hdb).u.extensions.availlistshadow.eof / sizeof (tyavailnodeshadow);
if (ixshadow + 1 >= ctavail) { ... }
assert (...);
}
#endif3. if (newsize > cttotal) {
dbrelease_hdb (adr, hdb); /*ignore return — don't abort saving*/
return dballocate_hdb (newsize, pdata, padr, hdb);
}The comment acknowledges this matches the legacy Code Quality4. Format detection logic duplicated before Both /* Same logic as db_hdb_use64() defined below in Layer 3+ helpers section */
boolean use64 = (hdb != nil && (**hdb).headerLength == (long) sizeof (tydatabaserecord_64));Since 5. Public API exposure of internal-only functions
6. Minor comment inconsistency
Test Coverage7. assert(open_scratch_v7_db(&ctx, "alloc"));If if (!open_scratch_v7_db(&ctx, "alloc")) {
log_error(..., "setup failed");
goto cleanup;
}8. if (!dbrefhandle_hdb(adr, &href, ctx.hdb)) goto cleanup;
assert(href != nil); // reachable only if dbrefhandle_hdb returned true
assert(GetHandleSize(href) == sizeof(payload));
lockhandle(href);
assert(memcmp(*href, payload, sizeof(payload)) == 0);
unlockhandle(href);
disposehandle(href);If assert(href != nil && GetHandleSize(href) == sizeof(payload));Minor point — the existing Positive Highlights
SummaryThe implementation is correct for the intended scope. The two items worth addressing before merge are (2) the shadow list guard consistency and (4) the format detection duplication — both are style/defensive issues rather than bugs given the current always-defined 🤖 Generated with Claude Code |
- Wrap shadow list bounds checks and asserts in dbmergeright_hdb and dbmergeleft_hdb with #ifdef dbshadow to match legacy code pattern - Log dbrelease_hdb failure in dbassign_hdb instead of silently ignoring (still continues with allocation per legacy behavior) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 7 Code ReviewThis is a well-executed continuation of the global-state elimination project. The layered architecture (L2 Issues1.
/* Same logic as db_hdb_use64() defined below in Layer 3+ helpers section */
boolean use64 = (hdb != nil && (**hdb).headerLength == (long) sizeof (tydatabaserecord_64));The comment correctly explains why it's duplicated (forward reference — Suggested fix: Move the four 2. The Phase 8 TODO for Minor / Test Code3. if (ctx->path != NULL) remove(ctx->path);
4. Handle leak on partial failure in tests (minor) In e.g. Confirmed-Correct Observations
SummaryThe one substantive fix worth applying before merge is moving the four 🤖 Generated with Claude Code |
Summary
_context()wrappers indb_format.cto pass the database handle explicitly through new_hdbfunctionsdatabasedataglobal mutation is now eliminated from the_context()wrapper layer_fnum), Layer 3 (allocator_hdb), Layer 4 (high-level_hdb), Layer 5 (wrapper conversion)Key Design Decisions
_hdbfunctions derive fnum and v6/v7 format from(**hdb).headerLength— no dependency ondb_format_mode_current()dbcopy_hdbreads source data fromdbsaveas_sourceduring Save As (the source block lives in the original DB, not the destination)dbassign_hdbchecksfldatabasesaveasto force fresh allocation in destination_hdbvariants (allstatictodb.c)Test plan
databasedatais never mutated by wrappers_hdbtests prove explicit handle threading works without touching globals🤖 Generated with Claude Code