Readers can observe a consistent pair of related indexes if a writer builds both replacements privately and publishes one snapshot only after validation. This program copies a bay-owner map and repair-window map under a lock, removes retired bays from both, applies additions, checks that every window belongs to an existing bay, and swaps one current-snapshot reference. A reader acquires the lock briefly to capture that reference, then can inspect immutable mapping views after the lock is released. Existing readers retain the prior snapshot. The sample values are immutable strings and interval tuples; a shallow mapping view would not freeze a mutable value object stored inside either map.
Index snapshots: publish related maps as one in-memory version
Operational case
Version 1 contains bays 47 and 52, with [19, 26) booked at 47. A release retires 47 and adds bay 61 with [47, 58). The old snapshot still reports version 1 and its bay 47 window; the newly returned snapshot reports version 2 and bays 52 and 61. A caller supplies the version it planned against. If another writer has published since then, the operation raises rather than silently overwriting that later change. Invalid orphan windows also fail before the pointer changes. The lock makes one in-process publication coherent, but no data has been written durably.
Working Python program
from dataclasses import dataclass
from threading import Lock
from types import MappingProxyType
@dataclass(frozen=True)
class InventorySnapshot:
version: int
bay_owners: object
repair_windows: object
class InventoryIndexStore:
def __init__(self, bay_owners, repair_windows):
self.lock = Lock()
self.current = self._build(1, dict(bay_owners), dict(repair_windows))
@staticmethod
def _build(version, bay_owners, repair_windows):
if not set(repair_windows) <= set(bay_owners):
raise ValueError("window has no bay")
if any(start >= end for start, end in repair_windows.values()):
raise ValueError("invalid repair window")
return InventorySnapshot(
version,
MappingProxyType(bay_owners),
MappingProxyType(repair_windows),
)
def read(self):
with self.lock:
return self.current
def publish(self, expected_version, added_bays, removed_bays, changed_windows):
with self.lock:
if self.current.version != expected_version:
raise ValueError("stale inventory version")
bay_owners = dict(self.current.bay_owners)
repair_windows = dict(self.current.repair_windows)
for bay_id in removed_bays:
bay_owners.pop(bay_id, None)
repair_windows.pop(bay_id, None)
bay_owners.update(added_bays)
repair_windows.update(changed_windows)
replacement = self._build(expected_version + 1, bay_owners, repair_windows)
self.current = replacement
return replacement
store = InventoryIndexStore({47: "north", 52: "east"}, {47: (19, 26), 52: (31, 42)})
before = store.read()
after = store.publish(before.version, {61: "west"}, (47,), {61: (47, 58)})
print(before.version, sorted(before.bay_owners), before.repair_windows[47])
print(after.version, sorted(after.bay_owners), after.repair_windows[61])Output
1 [47, 52] (19, 26)
2 [52, 61] (47, 58)Time, space, and tradeoff
With B bay entries and W windows, each publication copies and validates O(B + W) entries and temporarily uses O(B + W) additional memory. Retaining multiple versions costs that amount per full copied version, unlike a path-copying persistent tree that shares untouched structure. Reading the current snapshot takes O(1) lock-protected pointer work; individual dictionary lookups are expected O(1) with normal hashing. The writer holds the same lock during copying and validation, so large indexes delay readers. This is one-process visibility, not disk durability, cross-process transactions, or a lock-free design.
Common Mistakes
- Do not publish one map before the matching map is validated.
- Do not assume a mapping view recursively freezes mutable values.
- Do not ignore an expected-version mismatch from a stale writer.
- Do not equate in-memory atomic visibility with crash-safe persistence.
Connected lessons
- Hashing
- Data Structures
- Hash maps: keyed lookup with collision and load costs
- Persistent segment trees: retain old range-sum versions
- Project: test mutation invariants across four indexes
- Projects
- Quizzes
Apply this contract in the reversible depot release project, then check the operations quiz.
Persistent ordered indexes: copy search paths, share subtrees examines the next boundary.
Merkle trees: verify an indexed scan record adds a related operation contract.
Eytzinger arrays: store a search tree in breadth-first order adds a distinct structure contract to compare.
Persistent two-list queues: fork FIFO dispatch history adds a distinct structure contract to compare.
CSR delta overlays: stage road edits before compaction adds a related structure with a different operation boundary.
