NAME Data::IntervalTree::Shared - shared-memory interval tree (overlap / stabbing queries) SYNOPSIS use Data::IntervalTree::Shared; # up to 100_000 intervals my $it = Data::IntervalTree::Shared->new(undef, 100_000); $it->add($start, $end, $booking_id) for @bookings; # each interval carries an id # which intervals contain a point? my @at = $it->stab($t); # e.g. "what's booked at time $t" # which intervals overlap a range? my @ov = $it->overlaps($lo, $hi); # e.g. "any booking touching [$lo,$hi]" printf "id %d: [%d, %d]\n", $_->{id}, $_->{lo}, $_->{hi} for @ov; # share the index across processes via a backing file my $shared = Data::IntervalTree::Shared->new("/tmp/bookings.it", 100_000); DESCRIPTION An interval tree in shared memory: a set of integer intervals "[lo, hi]" that answers overlap and stabbing queries far faster than scanning every interval -- "which stored intervals contain point "p"?" and "which overlap the range "[lo, hi]"?". It complements Data::SegmentTree::Shared (range-aggregate over indexed positions) and Data::KDTree::Shared (multi-dimensional points): this one indexes a set of intervals for containment/overlap. Classic uses: scheduling and calendar conflict detection, IP-range to owner lookup, genomic feature overlap, and "what is active at time "t"". Endpoints are signed 64-bit integers (timestamps, IP addresses, genomic coordinates -- exact, with no floating-point edge cases). Each interval carries a user-supplied 64-bit id (defaulting to its insertion index), returned with every match. Internally it is an augmented balanced binary search tree keyed by the low endpoint, each node caching the maximum high endpoint of its subtree so a query prunes whole subtrees that end before it. Intervals are appended in O(1) and the balanced tree is bulk-built on the first query after any insert, so query recursion is O(log n + k) deep (k = matches) regardless of insertion order -- no risk of a degenerate, deep tree. Because the intervals live in a shared mapping, several processes build and query one index: any process that opens the same backing file, inherits the anonymous mapping across "fork", or reopens a passed memfd sees the same intervals. A write-preferring futex rwlock with dead-process recovery guards mutation; once the tree is built, queries take only the read lock. Linux-only. Requires 64-bit Perl. The index has a fixed capacity; adding beyond it croaks. Memory is "capacity * 40" bytes for the intervals plus a build scratch of "capacity * 4" bytes and a fixed header. METHODS Constructors my $it = Data::IntervalTree::Shared->new($path, $capacity, $mode); my $it = Data::IntervalTree::Shared->new(undef, $capacity); my $it = Data::IntervalTree::Shared->new_memfd($name, $capacity); my $it = Data::IntervalTree::Shared->new_from_fd($fd); my $ro = Data::IntervalTree::Shared->new_readonly($path); # frozen file, read-only $capacity is the maximum number of intervals (1..2^24). "new" and "new_memfd" croak on an out-of-range $capacity. When reopening an existing file or memfd the stored geometry wins and the caller's argument does not resize it, though it is still range-checked and an out-of-range value croaks; reopening a sealed (frozen) file read-write is refused -- use "new_readonly" instead (see "FROZEN (READ-ONLY) MODE"). An optional file mode may be passed as the last argument to "new" (e.g. 0660) for cross-user sharing; it defaults to 0600 (owner-only). Adding intervals my $i = $it->add($lo, $hi); # id defaults to the insertion index my $i = $it->add($lo, $hi, $id); # attach an explicit 64-bit id $it->build; # (optional) force a rebuild now "add" appends one interval with integer endpoints "$lo <= $hi" (croaks otherwise) and an optional integer $id, returning its insertion index; it croaks if the tree is full. Intervals are treated as closed (both endpoints inclusive). "build" forces the balanced tree to be (re)built immediately; you rarely need it, since queries build automatically after inserts. Queries my @at = $it->stab($point); # intervals containing $point (lo <= p <= hi) my @ov = $it->overlaps($lo, $hi); # intervals overlapping [$lo, $hi] "stab" returns every stored interval that contains the point $point. "overlaps" returns every stored interval that intersects the closed range "[$lo, $hi]" (i.e. "interval.lo <= $hi" and "interval.hi >= $lo"); "$lo > $hi" croaks. Both return a list of hash references "{ id => ..., lo => ..., hi => ... }", sorted by "lo" ascending. A point stab is exactly "overlaps($p, $p)". Introspection and lifecycle $it->count; # number of intervals added $it->capacity; # maximum number of intervals $it->clear; # remove all intervals $it->stats; # { count, capacity, dirty, ops, mmap_size, frozen, readonly } $it->frozen; # 1 if sealed by freeze, else 0 $it->readonly; # 1 if this handle is a read-only view, else 0 $it->path; $it->memfd; $it->sync; $it->unlink; "clear" empties the index. "sync" flushes the mapping to its backing store (a no-op for anonymous and memfd trees, and for any read-only view); "unlink" removes the backing file (also callable as "Class->unlink($path)"); "path" returns the backing path ("undef" for anonymous, memfd, or fd-reopened trees) and "memfd" the backing descriptor. "frozen" and "readonly" report whether the tree has been sealed and whether this handle is a read-only view, respectively (see "FROZEN (READ-ONLY) MODE"). SHARING ACROSS PROCESSES The index lives in a shared mapping, shared the same three ways as the rest of the family: a backing file, an anonymous mapping inherited across "fork", or a memfd passed to an unrelated process and reopened with new_from_fd($fd). The descriptor you pass is duplicated ("F_DUPFD_CLOEXEC"), so it stays yours to close and closing it does not disturb the handle. Any process can add intervals; the first query after an add rebuilds the shared tree once (under the write lock), and subsequent queries run concurrently under the read lock. FROZEN (READ-ONLY) MODE A file-backed tree 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 $it = Data::IntervalTree::Shared->new("/tmp/bookings.it", 100_000); $it->add($_->[0], $_->[1], $_->[2]) for @known; $it->freeze; # seal: now immutable, and $it itself is read-only # ... copy /tmp/bookings.it to another host ... # consumer (any process, same architecture): read-only, lock-free my $ro = Data::IntervalTree::Shared->new_readonly("/tmp/bookings.it"); $ro->stab($t); # or $ro->overlaps($lo, $hi) "freeze" takes the write lock, force-completes any balanced-tree build still pending (so a frozen tree is never left "dirty"), marks the tree permanently immutable (there is no unfreeze -- rebuild the file to change it), and flushes the seal to disk. A frozen tree rejects every mutator ("add", "build", "clear") with a croak, and a read-write reopen ("new($path, ...)") 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 "freeze" guarantees the balanced tree is already built, and a sealed tree's intervals and links are immutable, "stab", "overlaps", "count" and "stats" read them directly, taking no reader lock and never rebuilding -- 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. "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; pass an explicit octal mode (e.g. 0660) as the last argument to "new" for cross-user sharing. The file is opened with "O_NOFOLLOW" and "O_EXCL", and the header is validated on attach. Any process granted write access is trusted not to corrupt the mapping. CRASH SAFETY Mutation is guarded by a futex-based write-preferring rwlock with PID-encoded ownership and dead-owner recovery. Adds are short bounded appends and the bulk build runs entirely under the write lock, so a crash leaves the index consistent up to the last completed operation (a crash mid-build simply leaves it marked for rebuild). Limitation: PID reuse is not detected (very unlikely in practice). 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 interval tree 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::SegmentTree::Shared (range-aggregate over indexed positions), Data::KDTree::Shared (multi-dimensional point index), and the rest of the "Data::*::Shared" family. AUTHOR vividsnow LICENSE This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.