Skip to content

Record readers after a disk rewrite

Disk sort and trim publish a complete data.bin by atomic rename. Existing mmaps still refer to the previous inode. All cooperating clients must run the current ownership, recovery and reader-refresh protocol before publication is enabled. Disabling SHAREDDATA_LOCKFREE_READ does not provide compatibility with older clients: that flag controls index lookups only.

Historical upgrade context is in the changelog.

SharedNumpy slicing, records, count, recordssize, mtime, minchgid, next_bson_ptr and df now use a records-only read view. This view has its own header and record maps opened through one file descriptor. Readers publish only a single bundle reference; they never attach or remap a writer's Table. Two Table objects for one path maintain independent, EPOCH-validated bundles.

Stable non-owner reads check the always-mapped mutex EPOCH and the mapped capacity, with no path syscalls and no writer acquire. STRUCT/DIRTY can span the rename before the final EPOCH increment, including a writer death immediately after rename. Every read in these modes drops the superseded stable cache, reopens the current path and checks its device/inode. An overlapping read may retain its image, but the next read validates again. Four racing attempts raise RewriteError("record image changed repeatedly; retry read"); the reader never spins indefinitely or falls back to an old image. This bounds retries, not filesystem I/O latency. Plain reads do not repair indexes or take the exclusive write lock.

Old returned NumPy/DataFrame views remain valid while referenced. They are views of that image, not subscriptions. Call the SharedNumpy interface again for fresh data. Raw np.asarray(records), .view(np.ndarray), cached Table.hdr, cached pointers, and numeric locs held across a rewrite are outside this refresh API. HTTP GET, DataAPI table reads and .loc first wait out STRUCT/DIRTY as an index lookup does (capped waiters; a dead or released holder's DIRTY is repaired through acquire), so a sort or trim delays these reads rather than failing them. They retry the entire lookup and materialization up to four times when EPOCH changes, then raise with the last error chained. A lock holder inside its own STRUCT/DIRTY reads its writer image directly. This closes the new flag-off lookup-to-fresh-slice generation race on those paths. Numeric positions retained by external callers still require their own generation check. Row updates within one image are not atomic snapshots. Writable array views retain the existing access permissions but are not a supported substitute for locked upsert/extend/insert.

Held aliases refresh before writes, lookups, plain reads and decorated structural operations. An operation already in STRUCT skips recursive recovery. The per-thread ownership token still dies with Python thread state; dead-thread takeover is deliberately unsupported. A C caller must release in the same Python thread state in which it acquired.

Consumers and refresh paths

The repository's long-lived record pollers are IO/SyncTable.py (init_client, get_ids2send, socket/websocket publish loops), IO/ClientAPI.py (subscribe polling and HTTP publishing), and IO/ClientSocket.py (count/mtime handshake). IO/ServerSocket.py and IO/ServerWebSocket.py expose metadata and dispatch SyncTable publishers. These use the refreshed SharedNumpy properties/slicing without requiring changes at each call site. Socket and websocket payload selection shares the same tested get_ids2send path.

API/routes/tables.py GET without key filters (last, since, full scans) can keep a Table cached in SharedData.data and never acquire again. Its HEAD path previously accessed tbl.table.hdr directly; it now copies the current read header. Filtered GET and DataAPI/readers.py::read_table additionally use index lookups. Both HTTP GET and DataAPI check lookup and materialization together. SharedData.load_table metadata polling uses the refreshed properties; its hasindex field is invariant across sort/trim. SharedNumpy.df now invalidates its mmap-backed frame when the record address or count changes, including same-count sorts. Public callers retaining SharedNumpy references are covered by the same paths. Other raw record/index mmap accesses in this repository are writer/recovery internals under ownership. Cached raw pointers in other repositories/native code require a separate client inventory.

Platform limits

Windows disk-table use for these paths is claimed but unvalidated: the package declares OS independence, the quick start and development docs include Windows, and the repository ships Windows shared-mutex code, but the crash-safe record-replacement path has not been exercised on native Windows.

fsync_directory is a no-op on Windows. sync_file opens with write access there because Windows file flushing requires it. Neither behavior establishes Windows directory-entry power-loss durability or proves cold-open/rebuild compatibility: native Windows execution of these paths is unvalidated, including existing mapped index replacement sharing restrictions.

On non-Linux, ordinary SharedNumpy.write() skips automatic disk record sorting and saves the unsorted image. Explicit sort/trim and pending-rewrite recovery require Linux and the publisher's POSIX APIs. Refusal of an explicit structural operation can leave derived index flags invalid and mode DIRTY; it preserves record bytes. No crash-safe record publication guarantee is made off Linux.

Owner property reads call _sync_held_mapping, including a pending-descriptor existence check (a path stat) and possible remap, several times during upsert. Only stable non-owner reads avoid path syscalls. All reads currently consult the writer-lock registry, whose setdefault constructs a lock even for an existing entry. These costs and the exceptional DIRTY reopen cost require measurements.

A never-acquiring Table retains its original writer mmap: at most one deleted record generation can remain pinned until acquire/free, with disk usage not visible to du. Raw table.records.size and raw views retain old capacity. Disk read slices/records return np.ndarray, including with the index flag off; external dependencies on the previous SharedNumpy subclass are unverified.

Linux process-kill tests cannot prove host/power-loss durability. A disposable target-filesystem power-loss lab and coordinated old/native client upgrades remain release gates. No production lifecycle operation is part of this patch.