Cavil-Matcher
view release on metacpan or search on metacpan
lib/Cavil/Matcher/Index.pm view on Meta::CPAN
# compiles ONE small new segment and appends it to the manifest; removing a pattern only records a
# tombstone. A full recompile ("merge") happens rarely and reads from the authoritative pattern set
# (PostgreSQL stays the source of truth; the compiled index is a derived, disposable cache).
#
# The native engine (Cavil::Matcher::Engine) only walks and resolves; every decision about which
# segments are active and which patterns are tombstoned is made here, in readable Perl.
#
# CONCURRENCY. add_segment/tombstone/merge read the manifest, bump its generation in memory, write
# generation-derived files, then save. Each mutation runs under an exclusive advisory lock (flock on a
# per-index lock file) held around the whole read-modify-save, so concurrent writers on the same host
# serialize instead of clobbering each other's update (which, unlocked, would be last-writer-wins on a
# shared generation-derived filename). The lock is advisory and host-local - which is the right scope,
# since the compiled index is a per-host cache (each host mmaps its own copy). Reads (building a matcher)
# never lock: the manifest swap is atomic (temp+rename), and merge defers deleting the segments it
# retires until the *next* merge (see merge), so a reader that read the old manifest can still mmap the
# files it named. Readers are therefore always safe without a lock.
package Cavil::Matcher::Index;
use strict;
use warnings;
lib/Cavil/Matcher/Index.pm view on Meta::CPAN
}
sub dir ($self) { $self->{dir} }
sub _manifest ($self) { Cavil::Matcher::Manifest->new(dir => $self->{dir}) }
sub generation ($self) { $self->_manifest->generation }
# Run a mutation under an exclusive advisory lock on a per-index lock file, held for the whole
# read-modify-save so concurrent writers on the same host serialize. The lock is released when the
# filehandle is closed as the sub returns - including if $code dies, since the handle is destroyed as the
# stack unwinds.
sub _locked ($self, $code) {
my $path = File::Spec->catfile($self->{dir}, '.lock');
open my $lock, '>', $path or croak "cannot open index lock $path: $!"; # uncoverable branch true (I/O error)
flock $lock, LOCK_EX or croak "cannot lock index $path: $!"; # uncoverable branch true (flock failure)
return $code->();
}
# Checksum a segment file with the engine's own hash (no extra dependency), for manifest-level
# integrity on top of the segment's internal CRC.
sub _checksum ($path) {
open my $fh, '<:raw', $path or return ''; # uncoverable branch true (callers verify -r first)
lib/Cavil/Matcher/Index.pm view on Meta::CPAN
return undef unless $engine->dump($path);
return $basename;
}
# Incrementally add patterns as a new delta segment. Existing segment files are never touched.
# $patterns is an arrayref of [id, pattern_text]. Returns the new generation.
sub add_segment ($self, $patterns) {
return $self->generation unless $patterns && @$patterns;
my $parsed = _parse_patterns($patterns);
return $self->generation unless @$parsed; # every row normalized to empty => nothing compilable to add
return $self->_locked(sub {
my $man = $self->_manifest;
my $gen = $man->bump;
my $file = sprintf('seg-%010d.seg', $gen);
$self->_compile_segment($parsed, $gen, $file) or croak "failed to compile segment $file";
my $path = File::Spec->catfile($self->{dir}, $file);
# Fail closed: we just wrote this segment, so we must be able to checksum it. An empty result means
# the file could not be read back (transient I/O or permissions) - store no entry rather than one
# that silently opts out of the manifest-level integrity check. (An empty checksum in a *read*
# manifest is still honoured for backward compatibility; only fresh writes are strict.)
lib/Cavil/Matcher/Index.pm view on Meta::CPAN
# Validate at the public boundary, as add_pattern does: a tombstone id must be an integer in the
# engine's 32-bit id space. Otherwise we would bump the generation and record a tombstone the native
# engine can never apply (it ignores out-of-range ids to avoid uint32 wraparound), and the manifest
# reader would silently drop it on the next load - a dead write.
for my $id (@ids) {
croak sprintf('Cavil::Matcher::Index::tombstone: id %s out of range (must be 1..4294967295)',
defined $id && !ref $id ? $id : '(invalid)')
unless defined $id && !ref $id && $id =~ /^[0-9]+$/ && $id >= 1 && $id <= 4294967295;
}
return $self->_locked(sub {
my $man = $self->_manifest;
$man->bump;
$man->add_tombstones(@ids);
$man->save;
return $man->generation;
});
}
# Rare compaction: rebuild a single base segment from the authoritative pattern set and retire every
# existing segment and tombstone. This is the "merge" step - it is what keeps segment count and the
# tombstone list bounded over time. Reading the full set from the caller (the DB) keeps the engine
# simple and the source of truth in PostgreSQL. Returns the new generation.
sub merge ($self, $patterns) {
my $parsed = _parse_patterns($patterns // []);
return $self->_locked(sub {
my $man = $self->_manifest;
my $gen = $man->bump;
my $file = sprintf('base-%010d.seg', $gen);
$self->_compile_segment($parsed, $gen, $file) or croak "failed to compile base segment $file";
my $path = File::Spec->catfile($self->{dir}, $file);
# Fail closed on a fresh write, as in add_segment: a base we just wrote but cannot checksum must not
# be recorded with an integrity-check-disabling empty checksum.
my $checksum = _checksum($path);
croak "failed to checksum new base segment $file" unless length $checksum; # uncoverable branch true (I/O race)
( run in 1.188 second using v1.01-cache-2.11-cpan-800906f7e73 )