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 )