Data-SpatialHash-Shared
view release on metacpan or search on metacpan
FROZEN (READ-ONLY) MODE
A file-backed spatial hash can be frozen and then shipped to other
machines, where consumers open it read-only and query it with no locking
at all.
# producer: build, freeze, ship the file
my $s = Data::SpatialHash::Shared->new("/tmp/world.sph", 100_000, 0, 1.0);
$s->insert($_, $_, $_) for 1 .. 1000;
$s->freeze; # seal: now immutable, and $s itself is read-only
# ... copy /tmp/world.sph to another host ...
# consumer (any process, same architecture): read-only, lock-free
my $ro = Data::SpatialHash::Shared->new_readonly("/tmp/world.sph");
my @near = $ro->query_radius(5, 5, 10);
"freeze" takes the write lock, marks the spatial hash permanently
immutable (there is no unfreeze -- rebuild the file to change it), and
flushes the seal to disk. A frozen spatial hash rejects every mutator
("insert", "insert_many", "insert_geo", "move", "move_many", "move_geo",
"remove", "set_value", "set_radius", "clear") with a croak, and a
read-write reopen ("new($path, ...)" or "new_from_fd") of a sealed file is
refused -- so a shipped artifact can never be silently mutated out from
under its readers.
new_readonly($path) maps the file "O_RDONLY" / "PROT_READ" and requires it
to be frozen (it croaks on a file that was never "freeze"d). Because a
sealed spatial hash's entries and geometry are immutable, every accessor
and query method -- "has", "value", "get_radius", "position",
"position_geo", "count", "query_cell", "query_aabb", "query_radius",
"query_radius_many", "query_knn", "query_geo_radius", "each_in_radius",
"each_pair_within", "each_colliding_pair", and "stats" -- reads it
directly, taking no reader lock. The mapping is never written, so a
read-only view works from a read-only file descriptor or a read-only
filesystem, and any number of processes can share one "PROT_READ" mapping.
"sync" is a silent no-op on a read-only view. "frozen" and "readonly"
report the two states.
Portability. The on-disk format is native binary (native-endian 64-bit
words), so a frozen file may be copied only between machines of the same
architecture; a wrong-endian file is rejected at open by the magic check.
Copy the file to each consumer -- do not share one file over a network
filesystem: the lock is a Linux futex (process-local to one kernel), and
the "no live writer" contract assumes a static copy. Linux-only; 64-bit
Perl.
SECURITY
Backing files are created with mode 0600 (owner-only) by default, so only
the creating user can open and attach them. To share a backing file across
users, pass an explicit octal file mode such as 0660 via a "mode => 0660"
option to "new"; the mode is applied when the file is created, and when a
file left behind by an interrupted create is re-initialized (see "CRASH
SAFETY"); a file already in use keeps its own permissions. The file is
opened with "O_NOFOLLOW", so a symlink planted at the path is refused, and
created with "O_EXCL"; the on-disk header is validated when the file is
attached. Any process you grant write access to a shared mapping is
trusted not to corrupt its contents while other processes are using it.
CRASH SAFETY
The write lock is a futex-based rwlock with PID-encoded ownership. If the
writer process dies while holding the lock, the next writer that cannot
acquire the lock checks whether the owner PID is still alive and, if not,
recovers the lock. Reader slots are similarly reclaimed when a dead
reader's slot is detected.
Limitation: PID reuse is not detected. If a new process acquires the same
PID as a dead lock holder before recovery runs, the stale lock may not be
released automatically. This edge case requires the kernel to reassign
PIDs faster than lock-recovery attempts, which is very unlikely in
practice but cannot be ruled out.
Reader-slot exhaustion (slotless readers): dead-process recovery
attributes a crashed lock holder's contribution through its reader-slot.
The slot table holds 1024 entries (one per concurrent reader process). If
more than that many reader processes share one mapping at once, a reader
that cannot claim a slot proceeds "slotless" -- it still takes the read
lock but leaves no per-process record. If such a slotless reader is then
killed while holding the read lock, its share of the lock cannot be
attributed to a dead process, so writer recovery cannot reclaim it and
writers may block until the mapping is recreated. Reaching this needs more
than 1024 concurrent reader processes on one mapping plus a crash in the
brief read-lock window; the dead-process slot reclaim keeps the table from
filling with stale entries, so in practice it is very unlikely.
An interrupted create is recovered too. A creator killed after the backing
file is sized but before its header is committed leaves a full-size,
all-zero file. "new" re-initializes such a file automatically, but only
when it is exactly the size the requested geometry needs, is owned by your
effective uid, and is still entirely zero -- a file holding data is never
re-initialized. If the creator got as far as writing part of the header,
the file cannot be told apart from a corrupt one and "new" croaks with
"incomplete spatial hash file left by an interrupted create; remove it and
retry". A file left behind by an interrupted create never held data, so
removing it is safe -- but a file whose header was corrupted after the
fact reaches the same croak, so confirm it is an abandoned create before
deleting anything you care about.
SEE ALSO
Data::Graph::Shared - directed weighted graph
Data::Heap::Shared - priority queue (for Dijkstra, Prim, etc.)
Data::Pool::Shared - fixed-size object pool
Data::HashMap::Shared - concurrent hash table
Data::Buffer::Shared - typed shared array
Data::Queue::Shared - FIFO queue
Data::Stack::Shared - LIFO stack
Data::Deque::Shared - double-ended queue
Data::Log::Shared - append-only log
Data::Sync::Shared - synchronization primitives
Data::PubSub::Shared - publish-subscribe ring
Data::ReqRep::Shared - request-reply
( run in 1.933 second using v1.01-cache-2.11-cpan-14f38c9f855 )