NAME

Data::Shared - Path-addressed facade over the Data-*-Shared family

SYNOPSIS

use Data::Shared qw(shared_get shared_set shared_exists shared_incr
                    shared_call shared_push shared_pop
                    shared_compile sharedc_get shareda_get shareda_set);

# Anonymous: shared with processes forked after the mounts.
my $root = Data::Shared->new;
$root->mount('cache', 'hashmap:ii', undef, 100_000);
$root->mount('visitors', 'hyperloglog', undef, 14);
$root->mount('jobs', 'queue:int', undef, 1024);

shared_set($root, '/cache/42', 100);
shared_get($root, '/cache/42');            # 100
shared_incr($root, '/cache/42');           # 101
shared_exists($root, '/cache/42');         # 1

shared_call($root, '/visitors', 'add', 'alice');
shared_call($root, '/visitors', 'count');  # ~1

shared_push($root, '/jobs', 42);
shared_pop($root, '/jobs');                # 42

# File-backed: any process opens it by its directory.
my $app = Data::Shared->new('/tmp/demo.shared');
$app->mount('cache', 'hashmap:ii', undef, 100_000)
    unless $app->mount_info('cache');      # topology is set up once
shared_set($app, '/cache/42', 100);
my $same = Data::Shared->open('/tmp/demo.shared');
shared_get($same, '/cache/42');            # 100, from another handle

# Hot loops: parse the path once.
my $cp = shared_compile('/cache/42');
sharedc_get($root, $cp);                   # 101

# A key containing '/' needs the array form.
$root->mount('names', 'hashmap:ss', undef, 100_000);
shareda_set($root, ['names', 'a/b'], 1);
shareda_get($root, ['names', 'a/b']);      # 1

DESCRIPTION

Data::Shared wraps all 39 Data-*-Shared distributions behind one path-based API inspired by Data::Path::XS. A root object holds named mounts; a slash path resolves to a mount plus an intra-structure operation:

/cache/42        mount "cache", key 42
/lat/0/5         mount "lat", indices 0, 5 (NDArray)
/visitors        mount "visitors" (used with shared_call)

Paths select a mount and an adapter operation. Composite mounts ("DEEP PATHS (COMPOSITES)") add one child address per level. The string, array and compiled tiers mirror Data::Path::XS; requesting its :keywords syntax croaks. See Data::Shared::Cookbook for recipes.

A root is anonymous or file-backed. An anonymous root (new without a directory) is shared with every process forked after its mounts and disappears with the last of them. A file-backed root lives in a directory: any process can open it there, and it persists until unlink_all. Mount from one process and let the others read and write; "SHARING A ROOT" has the rules, and the Cookbook's PROCESSES section shows both shapes.

Linux only, 64-bit Perl, matching every leaf. Handles are never cloned into ithreads (CLONE_SKIP).

Every error this facade raises begins with Data::Shared: , so one pattern catches them all. A message without that prefix was raised by something the facade called -- a leaf's API, a factory you passed, or the operating system -- and is reported as it was written.

The leaf dists are recommended rather than required, so installing this facade does not compile all 39 of them. Install the ones you mount; mounting a type whose dist is absent croaks naming the dist to install. cpanm --with-recommends Data::Shared installs every leaf, and each leaf's minimum version (recorded in Data::Shared::Registry) is enforced when it is first used. One dependency is not implied by what you mount: a composite on an anonymous root locks its children with Data-Sync-Shared, so install that dist too. File-backed composites use the directory lock and do not need it.

ROOT AND MOUNTS

Data::Shared->new([$dir])

Without a directory, creates an anonymous root. With a directory, creates it if needed; if a manifest.json already exists there it is replayed (every mount reopened), otherwise an empty manifest is written. When the root is file-backed and a family takes its backing path as the leading constructor argument, empty arguments or a leading undef or ':file' make the facade supply $dir/<name>.shm, the name sanitized. A family that takes the path in its options (sortedset:strings) or builds it itself (perfecthash) refuses the placeholder and says what it wants instead.

Data::Shared->open($dir)

Requires an existing directory and manifest. The complete manifest schema, mount specs, and mount backing paths are checked before constructing handles. Missing storage is an error; opening never recreates an empty backing file. Composite descendant files are checked when the child is opened lazily. Native backends validate their own file headers.

Data::Shared->open_sealed($dir)

Reopens frozen leaves read-only while allowing lifecycle cleanup with unlink_all: it gives a writable root over sealed leaves, so reads work and the leaves refuse data writes. Of the three openers on a frozen store, open refuses it outright, open_readonly is for readers and refuses lifecycle writes, and this one is for teardown.

Data::Shared->open_readonly($dir, strict => 1)

Freeze the store in the producing process first. The 21 families with new_readonly (the freeze entries in Data::Shared::Registry, including hashmap and bloomfilter) require frozen files; a live file aborts replay with the leaf's error under failed to replay manifest. Other leaves use their normal constructor. The generic mutating verbs (set, delete, incr, push, pop), every root lifecycle write (mount, unmount, freeze_all, unlink_all) and native unlink, destroy and freeze through shared_call croak on any read-only root. A handle taken with handle is the leaf itself and carries no root flag: every native method on it, those three included, keeps whatever permissions that leaf's own file gives it.

strict => 1, off by default, requires new_readonly or immutability (PerfectHash) throughout the declared tree, including lazy composite leaves. Unsupported backends are rejected before any handle is opened. Raw handles expose the backend's API; filesystem permissions govern direct writes.

$root->mount($name, $spec, @ctor_args)

Mounts a structure under $name (which may itself contain /, for nested namespaces; longest-prefix wins). $spec may be:

  • a short spec: 'hashmap', 'hashmap:ii', 'buffer:i64', 'queue:int', 'sync:semaphore', 'sortedset:strings', etc. Short specs accept mixed case and resolve to lowercase. See Data::Shared::Registry for the full inventory.

  • a full class name: 'Data::HashMap::Shared::SS'.

  • a hashref: { spec => 'hashmap:ss', args => [...] } or with an explicit ctor (for new_weighted/new_decayed style constructors, also available as 'reservoir:weighted' / 'topk:decayed').

  • an already-constructed handle (transient; not written to the manifest).

  • a coderef factory called as $factory->($root, $name) (transient).

  • a composite spec (arrayref or hashref with child): see "DEEP PATHS (COMPOSITES)".

Returns the native handle (for a composite mount, a Data::Shared::Node for the mount). $root->handle($path) also returns it, and any native method can always be reached that way or via shared_call.

Configuration arrays and hashes are copied before construction. Cycles are rejected; a constructor argument JSON has no form for at all -- a code reference, or an object that is not a JSON::PP boolean -- makes the mount transient rather than refusing it. Failed mount publication restores the previous namespace and removes backing paths created by that attempt. Existing files are retained. Factories manage their own external side effects. Nested mount mutations inside a factory are rejected, and so is reopening the same root: the factory runs while its caller holds the manifest lock, which no second root in the same process can acquire. Use the $root it is given.

Mount arguments must survive manifest replay, so a value JSON would record as something else is refused at mount time instead: Inf, NaN and lone surrogates, a stringified Inf or NaN number included; a plain string such as 'Inf' is kept. Strings are recorded as bytes; a character string (perl's UTF8 flag on) is encoded to UTF-8 first, uniformly from U+0080 up, so "\xe9" after utf8::upgrade and the plain byte "\xe9" name two different files.

Special cases: perfecthash is an immutable build-then-load structure, so a 'perfecthash' spec takes either a built file path or a { build_int => \@keys, path => $file } / build_str hashref. A build spec creates a missing file once; an existing file is loaded without rebuilding. To supply one some other way, pass the factory or the loaded handle as the spec, as for any other type. sortedset:strings takes mount('names', 'sortedset:strings', { max => 100_000 }); set/keys backing paths are auto-filled for file-backed roots. Explicit backing paths must be absolute. Derived paths follow the root's directory spelling and are relative exactly when that directory is.

Backing paths already owned by another mount are rejected before construction, including aliases produced by filename sanitization (e.g. a/b and a__b) and string SortedSet sidecars. Mount the same native handle under another name to share it intentionally; an unmount that would unlink storage another mount still names is refused, so retire the aliases first.

$root->unmount($name), $root->mounts, $root->mount_info($name)

Lifecycle and introspection for mounts. Names are canonicalized as mount canonicalizes them, so /a/ and a name one mount. unmount returns 1 when it removed a mount and 0 when there was none; mount_info returns undef for an absent name, and otherwise a hashref of name, class, key, dist, path_class, file, transient and composite. mount_names is a synonym for mounts; available_types (class or root method) lists the 39 short spec names and registry returns the Data::Shared::Registry table itself.

unmount retains storage in a manifest ledger; remounting the same name adopts it. To reclaim it while serving other mounts, remount it with the same spec and unmount($name, unlink => 1). unlink_all also reclaims retained storage from any process, but removes the whole store. Each retained store costs a manifest entry; use unlink => 1 when rotating fresh names.

unmount($name, unlink => 1) publishes the removal and retires storage under the same directory lock. Native unlink and destroy through shared_call at a mount do the same. See "SHARING A ROOT" for what other processes observe.

$root->dir, $root->readonly, $root->manifest_path

Accessors: the root's directory, whether it was opened read-only, and the path of its manifest. The directory and the manifest path are both undef for an anonymous root.

sync_all, freeze_all and unlink_all visit supported handles in mount order. Sync and freeze include a composite's parent, child index and every registered child, open or not. freeze_all counts newly sealed handles; 0 means nothing was left to seal.

stats returns a hashref keyed by mount name, containing native stats or { class => ref $handle }. Composite stats include child and open counts and stats for cached children only; evicted children are absent. Stats fields are backend-specific. eg/monitoring.pl walks registered children and compares native capacities and configured child budgets with usage.

frozen reports whether a nonempty store is sealed or immutable throughout, transient mounts aside, since they never ship with it; only 21 of the 39 families can be sealed. perfecthash is immutable and needs no sealing; any of the other 17 (buffer, queue, stack, deque, heap, log, pool, pubsub, reqrep, ringbuffer, sync, timingwheel, hiertimingwheel, reservoir, topk, segmenttree, disjointset) or the sortedset:strings variant prevents frozen, at any level of a composite too. Freezing is one-way, so freeze_all refuses such a store before sealing anything and names the first mount it cannot seal; which opener to reach for afterwards is under "Data::Shared->open_sealed($dir)" above. A transient mount is neither sealed nor counted: the handle stays its owner's.

unlink_all removes mounts, the manifest and all retained storage, including storage retained by other processes. It also unlinks every .shm file directly in the root directory, which reclaims what an interrupted mount left but takes any other .shm kept there with it, and every .manifest- temporary a killed publication abandoned. A flat mount interrupted between construction and publication is reclaimed only if its backing file is one of those: a path you gave outside the root, or a sidecar with another suffix, is left for its caller. Composite storage under .children is removed whole, whatever the suffixes inside it. It leaves .manifest.lock, which another process may still hold. A transient mount is not unlinked through its handle, though the sweeps above still take any of its files they match. freeze_all and unlink_all croak on a read-only root.

SHARING A ROOT

Topology changes serialize under a directory lock and revision. A root opened before another writer's change gets stale manifest revision from mount, unmount, freeze_all and unlink_all, raised before any constructor or factory runs: reopen the root and retry against the current configuration. A store rebuilt at the same directory, or a copy renamed into its place, is refused as this store was replaced (an older copy as a stale revision) -- each store records an identity of its own in the manifest, so the refusal holds even when the rebuild repeats the old topology and revision -- and one another process removed as this store was unlinked; unlink_all returns 0 there instead. Ordinary reads and writes neither check the revision nor refresh the namespace. A write to a mount another process retired with unlink => 1 can succeed into a mapping whose file is gone.

A root keeps its namespace and cached composite children until reopened. After another process rebuilds the store, cached reads, exists and children may still report the old instance until a composite lock triggers an instance check. They never cross into the replacement store's storage. Workers that must notice teardown should reopen.

Configure topology from one coordinator and let the workers open the result; that is the pattern the revision protocol is built for.

PATH FORMAT

Components are separated by /. A leading / is optional, repeated and trailing slashes are tolerated, and the empty path addresses the root (shared_get returns the root, shared_exists is true, mutating the root croaks). The first component(s) select the mount (longest prefix match); the remainder is interpreted by the mount's adapter:

  • Map-like (HashMap, Buffer, RadixTree, Intern, Pool, PerfectHash): one key/index component, and more croaks. RadixTree instead joins the remainder with / so route-style keys work in string paths.

  • Set-like (Bloom family, BitSet, RoaringBitmap, SortedSet, DisjointSet): one member component, and more croaks.

  • Indexed (NDArray, Fenwick, Fenwick2D, SegmentTree, Graph): all remaining components are indices/coordinates.

  • Queue-, sketch- and sync-like: no remainder; use shared_push/shared_pop/shared_call, which take the value as an argument. The keyed sketches countminsketch and topk are the exception: they require exactly one component, the item, and croak without it. ringbuffer's get may read one, a sequence number, and reads the latest entry without it.

With users and users/sales mounted, /users/sales/eu selects users/sales, hiding the sales record or child of users. Array paths join the same way, including components containing /, but a component is never split: ['users', 'sales/eu'] is the key sales/eu in users, not eu in users/sales, so the same characters select a different mount in the two tiers. Avoid nested mount names that spell an address their parent also serves.

No escaping is provided in the string tier: keys containing / or the empty string need the array tier.

GENERIC VERBS

shared_get, shared_set, shared_delete, shared_exists and shared_incr map to native methods per type in Data::Shared::Registry (e.g. HashMap put/get/remove/exists, RadixTree insert/lookup/delete/exists, SortedSet add/score/remove, Intern intern/id_of). Unsupported verbs croak with guidance to use shared_call: plain Bloom filters have no delete, and unkeyed sketches and sync primitives have no keyed get/set.

Each generic verb is also a method on the root, taking the function's arguments without the leading $root: $root->get('/cache/42'), set, delete, exists, incr, push, pop and $root->call($path, $method, @args). $root->compile($path) is shared_compile.

Where it is defined, shared_exists always returns 1 or 0, an index outside the structure included; the families with no honest exists -- the queue-like leaves, the unkeyed sketches, ndarray and fenwick2d among them -- croak like any other absent verb. shared_incr adds one, or the delta when one is given. sortedset, fenwick, fenwick2d and segmenttree score or add rather than count, so there it needs a delta and croaks without one, and a buffer, pool or hashmap variant with no integer value to count in croaks either way.

Backend semantics:

  • shared_set($path, 0) clears for BitSet, but counts as an add for RoaringBitmap, TopK and the Bloom, counting Bloom and Cuckoo filters, whose value argument is ignored. RoaringBitmap and the counting Bloom and Cuckoo filters remove with shared_delete; TopK and a plain Bloom filter cannot. intern ignores it as well, and returns the id it assigned the key.

  • shared_delete on a buffer writes the variant's zero, which for buffer:str is the one-character string "0".

  • A write can fail by returning false instead of croaking: sortedset:strings and intern derive a string arena of 32 bytes per declared entry, so longer members exhaust it early and shared_set returns undef. Pass a larger arena, or a larger third constructor argument for intern, and check the return value. A full queue, deque, stack or heap drops a shared_push the same way, as does a shared_set past the end of a buffer.

  • Keys cross into XS as bytes, so encode wide characters first. A character above U+00FF is accepted by the HashMap family and the integer SortedSet, and fatal in RadixTree, Intern, the filter family, CountMinSketch, TopK, PerfectHash and sortedset:strings (whose every verb dies).

  • An integer-addressed family refuses a non-integer key or index (index must be an integer): the integer hashmap and sortedset variants and a perfecthash built with build_int included, while their string forms take words as keys. shared_exists returns 0 for keys the structure cannot hold, and croaks where the family has no exists of its own.

shared_call($root, $path, $method, @args)

Resolves $path to a mount, prepends remaining components to @args, and calls the native $method, including methods with no generic verb. For composites, the first remaining component selects a child; the rest are passed to its method.

unlink and destroy are accepted only at a mount or composite child, and refused at a record below one. They retire storage with its registration; at a child this is equivalent to shared_delete on that address.

shared_call($root, '/visitors', 'add_many', ['a', 'b']);
shared_call($root, '/fenwick', 'prefix', 500);
shared_call($root, '/sem', 'acquire', 1.5);

shared_push($root, $path, @values) / shared_pop($root, $path, @args)

Queue sugar mapping to the native pair per type (Queue push/pop, Deque push_back/pop_front, Stack push/pop, Heap push/pop with priority+value, Log append (no pop), PubSub publish (no pop), RingBuffer write (no pop)). Unsupported directions croak. Waiting with a timeout is native -- pop_wait on Queue, Stack and Heap, pop_front_wait on Deque: shared_call($root, '/jobs', 'pop_wait', 0.5) returns undef when nothing arrived in time.

ARRAY PATH API

shareda_get/set/delete/exists/incr/call/push/pop take an arrayref of components instead of a slash string. Binary-safe; components may contain / or be empty strings, subject to the resolution rule in "PATH FORMAT".

COMPILED PATH API

shared_compile($path) (string or arrayref) returns a Data::Shared::Compiled object holding parsed components; sharedc_* functions reuse it without parsing. First use against a root binds the resolved mount, verb and key to that root; subsequent calls reuse that binding. One compiled path serves any number of roots, and needs no recompiling after a fork or a mount change: a root rebinds when its own topology changes. The root owns its bindings, so keeping a compiled path does not keep an unmounted native handle alive.

DEEP PATHS (COMPOSITES)

A composite keeps one child structure per stable parent address. Specs are nested arrayrefs or hashrefs with spec/args/child. Constructor arguments exclude the backing path; a leading undef placeholder is tolerated. A trailing arrayref is always a child spec, so constructors taking a trailing arrayref (such as SpatialHash grid wrapping) must use the hashref form:

$root->mount('users', ['hashmap:ss', 10_000, ['bloomfilter', 10_000, 0.01]]);
$root->mount('leaders', ['sortedset:strings', { max => 10_000 }, ['bloomfilter', 10_000, 0.01]]);
$root->mount('atlas', ['hashmap:ss', 10_000, ['kdtree', 2, 100_000, ['hashmap:ss', 10_000]]]);

shared_set($root, '/users/alice/event-42', 1);
shared_exists($root, '/users/alice/event-42');
shared_set($root, '/leaders/alice', 1500);           # SortedSet score
shared_get($root, '/atlas/europe/42/name');

Parents must be addressable: hashmap and sortedset (child per key/member; for SortedSet the path ending at the child still reads and writes its score), plus kdtree and graph as id-parents (child per point/node id; geometry is added through $root->node('/atlas/europe')->call('add', [$x, $y], $id), and a deep write for an id does not require the point to exist). Bloom filters, sketches, queues, stacks, logs, sync and timing mounts are rejected as parents at mount time; use them as child leaves.

Semantics at an intermediate path: shared_get returns a Data::Shared::Node (for SortedSet, the score instead), shared_exists reports whether a child is registered, shared_delete deletes the whole subtree, and shared_set croaks except for the SortedSet score case. $root->node($path) (also exported as shared_node) returns a cursor with relative-path get/set/delete/exists/incr/push/pop/call/call_at/node/children/handle methods; $root->children($path) (shared_children) lists registered child addresses. handle on a child leaf path returns its native handle. Reads, shared_delete and shared_pop at a missing child never create it; on a writable root, shared_set, shared_incr, shared_push and a call other than unlink and destroy do. Probe with shared_exists to avoid allocating children.

Each composite declares max_children (default 10_000) and cache_size open children (default 32, LRU close/reopen) via the hashref spec form or a trailing {max_children => ..., cache_size => ...} in the arrayref form. Budget explicitly for high-cardinality parents. cache_size bounds one composite's own cache; a nested composite is itself a cached entry with its own cache plus its parent and index segments, so a deep tree's open-segment ceiling multiplies per level.

The budget sizes the child index when the composite is created and cannot be raised afterwards: mounting a name over storage an unmount retained with a larger max_children is refused, and a nested level cannot exceed the ceiling its storage was built with. unmount($name, unlink => 1) first, and build the subtree again. Under a hashmap or sortedset parent every child also consumes a parent entry: size the parent for the budget. A sortedset parent of four fills after four children regardless of max_children. Both ceilings assume short addresses: a 256-byte address exhausts the child index near half of max_children, and a string-keyed parent's own arena can bind first.

Anonymous composites retain all opened children until subtree deletion or root destruction, because an anonymous segment cannot be reopened after eviction; cache_size bounds only file-backed child caches, so a long-lived anonymous composite keeps one mapping per address it has touched, up to max_children. Use a file-backed root when processes need to discover children created after fork. File-backed roots store children under .children/ next to the manifest and reopen them on demand. Composites support sync_all, stats, freeze_all and subtree deletion. Compiled paths over composites stay correct across child creation, eviction, and deletion because they re-dispatch to the child per call.

Allocation, construction, publication, score-only registration, and subtree deletion use the composite lifecycle lock. Failed construction rolls back new registrations, counts, and newly created paths; existing scores and reservations are preserved. Allocated child IDs are never reused. Cached leaf operations use the backend's own locking. An operation already in progress during deletion may complete against the old instance; an explicit native handle continues to refer to that instance after recreation.

The lifecycle lock protects the shared child counter. An unfinished mutation marker triggers a rebuild on writable open or the next lifecycle lock. After a holder dies, the kernel releases file locks; anonymous locks recover the dead owner's lock, costing about 20 kB of shared mapping per composite. A child directory with no surviving registration requires explicit recovery: no API reports it, and it goes only when the subtree that owns it is removed: by unmount with unlink, by unlink_all, or by deleting the nested composite that owns it. A composite kept alive for months therefore keeps that storage. A child whose registration survives -- a sortedset score reserves one before construction -- is reclaimed by deleting its address. A preexisting unpublished directory for a reserved child is an error. Recovery covers exceptions and limited interruption, without promising power-loss transactions or automatic orphan cleanup.

PERFORMANCE

On a flat mount of a map or set path class (the path_class in Data::Shared::Registry), get, set, delete, exists and incr run in XS at every tier and call the leaf directly, as does call on any flat mount. Composite paths, push and pop, the other path classes and malformed arguments take the Perl implementation, with the same results; an error or warning a leaf raises on the XS path reports the caller's line. A sample run of bench/overhead.pl on one x86_64 host, reading one integer key, rates taken from measured CPU time:

native handle    0.08 us/op
compiled         0.20 us/op
shared_call      0.28 us/op
string path      0.33 us/op
array path       0.35 us/op

Two consecutive runs on a lightly loaded host agreed within 6%; measure on your hardware.

A string or array path is split and looked up on every call; a compiled path checks root identity and generation and calls the leaf. Use compiled paths or a native handle in hot loops. bench/benchmark.pl, bench/overhead.pl and bench/mounts.pl supply the measurements here. bench/composite.pl measures deep reads with a warm child cache and with repeated cache eviction; a composite compiled path costs more than a flat one for the reason "DEEP PATHS (COMPOSITES)" gives.

Creating flat mounts costs quadratic total time: each validates and republishes the whole manifest. bench/mounts.pl measures this; on the same host the average was 1.6 ms per mount at 25 mounts and 7.2 ms at 200, i.e. 1.4 s to create 200, with the 200th alone costing 13 ms. Keys are not affected: writing 10_000 keys into one flat mount took 33 ms without touching the manifest. Opening such a root is superlinear too, and paid by every process that opens it: 0.07 s to replay 400 mounts on the same host. A namespace with hundreds of mounts belongs in a composite mount ("DEEP PATHS (COMPOSITES)") instead, where children are registered in the parent and the root manifest stays a single record.

bench/scaling.pl records child-creation scaling, working-set cache costs, process RSS, and wall-time latency percentiles for concurrent increments. Its samples are workload measurements, not latency guarantees.

CLASSIFICATION OF THE 39 LEAVES

Map-like: HashMap (10 variants), Buffer (11), RadixTree, Intern, Pool (+I64/I32/F64/Str), PerfectHash (read-only). Set-like: BloomFilter, CountingBloomFilter, CuckooFilter, BitSet, RoaringBitmap, SortedSet (+Strings), DisjointSet. Queue-like: Queue, Deque, Stack, Heap, RingBuffer, Log, PubSub. Indexed: NDArray, Fenwick, Fenwick2D, SegmentTree, KDTree, IntervalTree, SpatialHash, Graph. Sketch/stats: HyperLogLog, CountMinSketch, MinHash, TopK, DDSketch, Histogram, Reservoir. Sync/timing (call-only): Sync (Semaphore, Barrier, RWLock, Condvar, Once), TimingWheel, HierTimingWheel, ReqRep.

SECURITY

A file-backed root is created mode 0700, and the manifest and the .manifest.lock that serialises topology changes are created 0600. Republishing the manifest keeps whatever mode it already had, so a root you widened on purpose stays widened. Backing files come from the leaves, which create them 0600 and exclusively: with O_NOFOLLOW and O_EXCL, or for perfecthash through a mkstemp temporary renamed into place. A directory or lock that already exists keeps its own mode, so sharing a root between users stays an explicit choice; the facade does not offer a mode argument for it, and a group-writable root lets any member of that group unlink the manifest.

The manifest is trusted input. Whatever can write it chooses which mounts this process opens and which backing files it maps, so a writer who also controls those files controls what the process attaches to: a recorded argument list is not re-applied to a file that already exists, and the leaf rejects a file whose header does not match the declared type, but the file itself decides the mapping. An args entry naming an absolute path is honoured, so a manifest can reach a file outside the root.

Put the root where only its trusted writers can create it: under a world-writable directory -- /tmp on a shared host, as the examples here spell it for brevity -- anyone can pre-create it and so choose what the first real process attaches to.

Mount names are sanitised into the root directory, so a name containing .. or / cannot place a backing file outside it. Anything you grant write access to a shared mapping is trusted not to corrupt it while others are attached, as with the leaves.

SEE ALSO

Data::Path::XS (the API model), Data::Shared::Registry (the inventory and verb tables), Data::Shared::Cookbook (per-dist recipes), the runnable scripts in the distribution's eg/ directory (eg/job-workers.pl drains a queue with forked workers), and each Data::*::Shared leaf for its native semantics.

AUTHOR

vividsnow

LICENSE

This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.