Cache-FastMmap

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for Perl extension Cache::FastMmap.

1.62 Sun May 17 2026
  - Fix remaining Perl-side lock leaks when user supplied
    serializer/deserializer code throws while get_and_set()
    or other locked paths are active.
  - Validate get_and_set() callback options before merging
    them into the internal locked set() call.
  - Fix expire() in write-back mode to write back the
    original Perl value, not the serialized/compressed bytes.
  - Track all live empty_on_exit caches independently instead
    of keeping only one weak reference per class.
  - Preserve silent write/delete callback failure behavior while
    keeping locks exception-safe for serializer/deserializer failures.
  - Add regression coverage for expire() with serialized values,
    multiple empty_on_exit caches, and locked exception paths.

1.61 Sat Apr 25 23:18 2026
  - Remove Cache::FastMmap::OnLeave guard object and inline
    fc_lock/fc_unlock at every call site. The per-call cost
    of building and destroying the guard closure was a
    meaningful share of CPU in cache-heavy workloads.
  - Replace the internal skip_lock/skip_unlock guard-passing
    pattern (used by get_and_set / get_and_remove) with a
    simpler _locked => 1 boolean flag.
  - Preserve the documented exception-safety contract for
    get_and_set, plus the equivalent unwritten contract for
    get (read_cb that dies), multi_get, and multi_set: the
    page lock is always released even when user code throws.
  - Add t/24.t covering the lock-release contract on every
    exception path.
  - mmc_write: check for available space before deleting an
    existing slot for the key, so a failed set() preserves
    the previous value instead of silently losing it.
  - _mmc_set_error (unix.c, win32.c): use snprintf into the

Changes  view on Meta::CPAN

  - mmc_lock: replace hardcoded num_slots >= 89 sanity check
    with cache->start_slots, so caches built with custom
    start_slots values lock correctly.
  - mmc_check_fh: check fstat() return value before trusting
    statbuf.st_ino, restoring the safety net the function
    was added for.
  - mmc_iterate_next: end iteration cleanly if mmc_lock fails
    instead of walking stale page state.
  - Win32 mmc_unlock_page: split p_offset into Offset and
    OffsetHigh and add the missing return, so caches larger
    than 4GB unlock at the same offset they locked at.
  - Win32 mmc_lock_page: CloseHandle on the event handle on
    every exit path; previously every successful lock leaked
    one HANDLE.

1.60 Wed Jun 18 9:25 2025
  - Fix very broken 1.59 release that was leaving pages
    locked after initialising them.

1.59 Tue Jun 3 17:25 2025
  - Add exists() method to check if key exists in the
    cache. This avoid the decompress + deserialize cost
  - Initialise pages under lock. This avoids potential
    race conditions where multiple processes try and init
    a cache file
  - Correctly reset p_changed after storing changed details
    back to cache.

Changes  view on Meta::CPAN

     just function calls in Cache::FastMmap
     namespace

1.38 Sun Jul 17 18:30 2011
  - Fix build process that was completely broken
     after moving files around into different
     directories

1.37 Fri Jul 15 16:30 2011
  - Use a lock object with DESTROY method to avoid
     an alarm with a die leaving around a locked
     paged

1.36 Wed Sep 29 13:10 2010
  - Disable wrapping fcntl() lock call in alarm, hurts
     people that use Time::HiRes::alarm() only to try
     and catch buggy deadlock code. Enable with
     catch_deadlocks option

1.35 Fri Feb 19 12:45 2010
  - Fix for returning potential bug that returns old stored

FastMmap.xs  view on Meta::CPAN

    FC_ENTRY

  CODE:
    RETVAL = mmc_unlock(cache);
  POSTCALL:
    if (RETVAL != 0) {
      croak("%s", mmc_error(cache));
    }

int
fc_is_locked(obj)
    SV * obj;
  INIT:
    FC_ENTRY

  CODE:
    /* Write value to cache */
    RETVAL = mmc_is_locked(cache);

  OUTPUT:
    RETVAL


void
fc_read(obj, hash_slot, key)
    SV * obj;
    U32  hash_slot;
    SV * key;

lib/Cache/FastMmap.pm  view on Meta::CPAN

As with empty_on_exit, this will only unlink the file if the
DESTROY occurs in the same PID that the cache was created in
so that any forked children don't unlink the file.

This value defaults to 1 if the share_file specified does
not already exist. If the share_file specified does already
exist, it defaults to 0.

=item * B<catch_deadlocks>

Sets an alarm(10) before each page is locked via fcntl(F_SETLKW) to catch
any deadlock. This used to be the default behaviour, but it's not really
needed in the default case and could clobber sub-second Time::HiRes
alarms setup by other code. Defaults to 0.

=back

=cut
sub new {
  my $Proto = shift;
  my $Class = ref($Proto) || $Proto;

lib/Cache/FastMmap.pm  view on Meta::CPAN


        fc_write($Cache, $HashSlot, $_[1], $Val, -1, 0);
      }
    }
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };

  # On error, always unlock (caller can't clean up after a thrown exception).
  # On success, unlock unless caller asked us to leave the page locked.
  if (defined $Err) {
    fc_unlock($Cache) if $Locked && fc_is_locked($Cache);
    die $Err;
  }
  if (!$SkipUnlock) {
    fc_unlock($Cache);
    $Locked = 0;
  }

  # If not using raw values, use thaw() to turn data back into object
  eval {
    $Val = $Self->{uncompress}($Val) if defined($Val) && $Self->{compress};
    $Val = ${$Self->{deserialize}($Val)} if defined($Val) && $Self->{deserialize};
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };
  if (defined $Err) {
    fc_unlock($Cache) if $Locked && fc_is_locked($Cache);
    die $Err;
  }

  # If explicitly asked to skip unlocking, return a sentinel so callers
  # using the old 3-tuple unpack still work (slot 2 is now a placeholder).
  return ($Val, 1, { $Found ? (expire_on => $ExpireOn) : () }) if $SkipUnlock;

  return $Val;
}

lib/Cache/FastMmap.pm  view on Meta::CPAN

  my $expire_on = defined($Opts) ? (
    defined $Opts->{expire_on} ? $Opts->{expire_on} :
      (defined $Opts->{expire_time} ? parse_expire_time($Opts->{expire_time}, _time()): -1)
  ) : -1;

  # Are we doing writeback's? If so, need to mark as dirty in cache
  my $write_back = $Self->{write_back};

  # Hash value, lock page (unless caller already holds the lock)
  my ($HashPage, $HashSlot) = fc_hash($Cache, $_[1]);
  fc_lock($Cache, $HashPage) unless $Opts && $Opts->{_locked};

  my ($DidStore, $Err);
  eval {
    my $Val = $Self->{serialize} ? $Self->{serialize}(\$_[2]) : $_[2];
    $Val = $Self->{compress}($Val) if $Self->{compress};

    # Get key/value len (we've got 'use bytes'), and do expunge check to
    #  create space if needed
    my $KVLen = length($_[1]) + (defined($Val) ? length($Val) : 0);
    $Self->_expunge_page(2, 1, $KVLen);

    # Now store into cache
    $DidStore = fc_write($Cache, $HashSlot, $_[1], $Val, $expire_on, $write_back ? FC_ISDIRTY : 0);
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };

  # Always unlock — caller passed _locked to transfer ownership
  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  # If we're doing write-through, or write-back and didn't get into cache,
  #  write back to the underlying store
  if ((!$write_back || !$DidStore) && (my $write_cb = $Self->{write_cb})) {
    eval { $write_cb->($Self->{context}, $_[1], $_[2], $expire_on); };
  }

  return $DidStore;
}

=item I<get_and_set($Key, $AtomicSub)>

Atomically retrieve and set the value of a Key.

The page is locked while retrieving the $Key and is unlocked only after
the value is set, thus guaranteeing the value does not change between
the get and set operations.

$AtomicSub is a reference to a subroutine that is called to calculate the
new value to store. $AtomicSub gets $Key, the current value from the
cache, and an options hash as paramaters. Currently the only option
passed is the expire_on of the item.

It should return the new value to set in the cache for the given $Key,
and an optional hash of arguments in the same format as would be passed

lib/Cache/FastMmap.pm  view on Meta::CPAN

=over 4

=item *

Do not perform any get/set operations from the callback sub, as these
operations lock the page and you may end up with a dead lock!

=item *

If your sub does a die/throws an exception, the page will correctly
be unlocked (1.15 onwards)

=back

=cut
sub get_and_set {
  my ($Self, $Cache) = ($_[0], $_[0]->{Cache});

  my ($Value, $DidStore, $Err);
  eval {
    ($Value, undef, my $Opts) = $Self->get($_[1], { skip_unlock => 1 });
    # Page is still locked here.

    my @NewValue = $_[2]->($_[1], $Value, $Opts);

    $DidStore = 0;
    if (@NewValue) {
      ($Value, my $SetOpts) = @NewValue;
      die "get_and_set callback options must be a hash reference"
        if defined($SetOpts) && ref($SetOpts) ne 'HASH';

      # set() with _locked=>1 takes ownership of the lock and unlocks at end.
      $DidStore = $Self->set($_[1], $Value, { _locked => 1, %{$SetOpts || {}} });
    } else {
      fc_unlock($Cache);
    }
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };
  if (defined $Err) {
    fc_unlock($Cache) if fc_is_locked($Cache);
    die $Err;
  }

  return wantarray ? ($Value, $DidStore) : $Value;
}

=item I<exists($Key)>

Search cache for given Key. Returns false if not found or true
if found. This will *not* call the I<read_cb> if not found.

lib/Cache/FastMmap.pm  view on Meta::CPAN

  my ($HashPage, $HashSlot) = fc_hash($Cache, $_[1]);
  fc_lock($Cache, $HashPage);
  my ($Found, $Err);
  eval {
    my ($Val, $Flags, $ExpireOn);
    ($Val, $Flags, $Found, $ExpireOn) = fc_read($Cache, $HashSlot, $_[1]);
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };
  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  return $Found;
}

=item I<remove($Key, [ \%Options ])>

Delete the given key from the cache

I<%Options> is optional, and is used by get_and_remove() to control
the locking behaviour. For now, you should probably ignore it
unless you read the code to understand how it works

=cut
sub remove {
  my ($Self, $Cache) = ($_[0], $_[0]->{Cache});

  # Hash value, lock page (unless caller already holds the lock), delete
  my ($HashPage, $HashSlot) = fc_hash($Cache, $_[1]);
  fc_lock($Cache, $HashPage) unless $_[2] && $_[2]->{_locked};

  my ($DidDel, $Flags, $Err);
  eval {
    ($DidDel, $Flags) = fc_delete($Cache, $HashSlot, $_[1]);
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };
  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  # If we deleted from the cache, and it's not dirty, also delete
  #  from underlying store
  if ((!$DidDel || ($DidDel && !($Flags & FC_ISDIRTY)))
     && (my $delete_cb = $Self->{delete_cb})) {
    eval { $delete_cb->($Self->{context}, $_[1]); };
  }

  return $DidDel;
}

=item I<get_and_remove($Key)>

Atomically retrieve value of a Key while removing it from the cache.

The page is locked while retrieving the $Key and is unlocked only after
the value is removed, thus guaranteeing the value stored by someone else
isn't removed by us.

=cut
sub get_and_remove {
  my ($Self, $Cache) = ($_[0], $_[0]->{Cache});

  my ($Value) = $Self->get($_[1], { skip_unlock => 1 });
  my $DidDel = $Self->remove($_[1], { _locked => 1 });
  return wantarray ? ($Value, $DidDel) : $Value;
}

=item I<expire($Key)>

Explicitly expire the given $Key. For a cache in write-back mode, this
will cause the item to be written back to the underlying store if dirty,
otherwise it's the same as removing the item. 

=cut

lib/Cache/FastMmap.pm  view on Meta::CPAN

    ($Val, $Flags, $Found, $ExpireOn) = fc_read($Cache, $HashSlot, $_[1]);

    # If we found it, remove it
    if ($Found) {
      (undef, $Flags) = fc_delete($Cache, $HashSlot, $_[1]);
    }
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };
  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  # If it's dirty, write it back
  if (($Flags & FC_ISDIRTY) && (my $write_cb = $Self->{write_cb})) {
    if (defined $Val) {
      $Val = $Self->{uncompress}($Val) if $Self->{uncompress};
      $Val = ${$Self->{deserialize}($Val)} if $Self->{deserialize};
    }
    eval { $write_cb->($Self->{context}, $_[1], $Val, $ExpireOn); };
  }

lib/Cache/FastMmap.pm  view on Meta::CPAN

    my $Err;
    eval {
      my ($PNReads, $PNReadHits) = fc_get_page_details($Cache);
      $NReads += $PNReads;
      $NReadHits += $PNReadHits;
      fc_reset_page_details($Cache) if $Clear;
      1;
    } || do {
      $Err = $@ || 'unknown error';
    };
    fc_unlock($Cache) if fc_is_locked($Cache);
    die $Err if defined $Err;
  }
  return ($NReads, $NReadHits);
}

=item I<multi_get($PageKey, [ $Key1, $Key2, ... ])>

The two multi_xxx routines act a bit differently to the
other routines. With the multi_get, you pass a separate
PageKey value and then multiple keys. The PageKey value
is hashed, and that page locked. Then that page is
searched for each key. It returns a hash ref of
Key => Value items found in that page in the cache.

The main advantage of this is just a speed one, if you
happen to need to search for a lot of items on each call.

For instance, say you have users and a bunch of pieces
of separate information for each user. On a particular
run, you need to retrieve a sub-set of that information
for a user. You could do lots of get() calls, or you

lib/Cache/FastMmap.pm  view on Meta::CPAN

      $Val = ${$Self->{deserialize}($Val)} if defined($Val) && $Self->{deserialize};

      # Save to return
      $KVs{$_} = $Val;
    }
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };

  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  return \%KVs;
}

=item I<multi_set($PageKey, { $Key1 => $Value1, $Key2 => $Value2, ... }, [ \%Options ])>

Store specified key/value pair into cache

=cut

lib/Cache/FastMmap.pm  view on Meta::CPAN


      # Now hash key and store into page
      (undef, $HashSlot) = fc_hash($Cache, $FinalKey);
      my $DidStore = fc_write($Cache, $HashSlot, $FinalKey, $Val, $expire_on, 0);
    }
    1;
  } || do {
    $Err = $@ || 'unknown error';
  };

  fc_unlock($Cache) if fc_is_locked($Cache);
  die $Err if defined $Err;

  return 1;
}

=back

=cut

=head1 INTERNAL METHODS

lib/Cache/FastMmap.pm  view on Meta::CPAN

  # Repeat expunge for each page
  for (0 .. $Self->{num_pages}-1) {
    fc_lock($Cache, $_);
    my $Err;
    eval {
      $Self->_expunge_page($Mode, $WB, -1);
      1;
    } || do {
      $Err = $@ || 'unknown error';
    };
    fc_unlock($Cache) if fc_is_locked($Cache);
    die $Err if defined $Err;
  }

}

=item I<_expunge_page($Mode, $WB, $Len)>

Expunge items from the current page to make space for
$Len bytes key/value items

mmap_cache.c  view on Meta::CPAN

 * descriptors. 
 * 
*/
int mmc_close(mmap_cache *cache) {
  int res;

  /* Shouldn't call if not init'ed */
  ASSERT(cache->fh);
  ASSERT(cache->mm_var);

  /* Shouldn't call if page still locked */
  ASSERT(cache->p_cur == NOPAGE);

  /* Unlock any locked page */
  if (cache->p_cur != NOPAGE) {
    mmc_unlock(cache);
  }

  /* Close file */
  if (cache->fh) {
    mmc_close_fh(cache);
  }

  /* Unmap memory */

mmap_cache.c  view on Meta::CPAN

int mmc_lock(mmap_cache * cache, MU32 p_cur) {
  MU64 p_offset;
  void * p_ptr;
  int res = 0;

  /* Argument sanity check. Valid pages are 0 .. c_num_pages-1; NOPAGE is
   * already excluded by the >= comparison since c_num_pages << NOPAGE. */
  if (p_cur >= cache->c_num_pages)
    return _mmc_set_error(cache, 0, "page %u is NOPAGE or larger than number of pages", p_cur);

  /* Check not already locked */
  if (cache->p_cur != NOPAGE)
    return _mmc_set_error(cache, 0, "page %u is already locked, can't lock multiple pages", cache->p_cur);

  /* Setup page details */
  p_offset = (MU64)p_cur * cache->c_page_size;
  p_ptr = PTR_ADD(cache->mm_var, p_offset);

  res = mmc_lock_page(cache, p_offset);
  if (res) return res;

  if (!(P_Magic(p_ptr) == 0x92f7e3b1)) {
    mmc_unlock_page(cache, p_offset);

mmap_cache.c  view on Meta::CPAN

  ASSERT(_mmc_test_page(cache));

  return 0;
}

/*
 * mmc_unlock(
 *   cache_mmap * cache
 * )
 *
 * Unlock any currently locked page
 *
*/
int mmc_unlock(mmap_cache * cache) {

  ASSERT(cache->p_cur != NOPAGE);

  /* If changed, save page header changes back */
  if (cache->p_changed) {
    void * p_ptr = cache->p_base;

mmap_cache.c  view on Meta::CPAN

  ASSERT(_mmc_test_page(cache));

  mmc_unlock_page(cache, cache->p_offset);

  cache->p_cur = NOPAGE;

  return 0;
}

/*
 * mmc_is_locked(
 *   cache_mmap * cache
 * )
 *
 * Return true if page is locked
 *
*/
int mmc_is_locked(mmap_cache * cache) {

  return cache->p_cur != NOPAGE ? 1 : 0;
}

/*
 * int mmc_hash(
 *   cache_mmap * cache,
 *   void *key_ptr, int key_len,
 *   MU32 *hash_page, MU32 *hash_slot
 * )

mmap_cache.c  view on Meta::CPAN

  free(to_expunge);

  ASSERT(_mmc_test_page(cache));

  return 1;
}

/*
 * void mmc_get_page_details(mmap_cache * cache, MU32 * n_reads, MU32 * n_read_hits)
 *
 * Return details about the current locked page. Currently just
 * number of reads and number of reads that hit
 *
*/
void mmc_get_page_details(mmap_cache * cache, MU32 * n_reads, MU32 * n_read_hits) {
  *n_reads = cache->p_n_reads;
  *n_read_hits = cache->p_n_read_hits;
  return;
}

/*

mmap_cache.c  view on Meta::CPAN

  return base_det;
}

/*
 * void mmc_iterate_close(mmap_cache_it * it)
 *
 * Finish and dispose of iterator memory
 *
*/
void mmc_iterate_close(mmap_cache_it * it) {
  /* Unlock page if locked */
  if (it->p_cur != NOPAGE) {
    mmc_unlock(it->cache);
  }

  /* Free memory */
  free(it);
}

/*
 * void mmc_get_details(

mmap_cache.c  view on Meta::CPAN

  if (1 == mode && 0 != first_deleted)
    return first_deleted;
  else
    return slot_ptr;
}

/*
 * void _mmc_init_page(mmap_cache * cache, int page)
 *
 * Initialise the given page as empty. It's expected
 *  that you've already locked the page before doing
 *  this
 *
*/
void _mmc_init_page(mmap_cache * cache, MU32 p_cur) {
  /* Setup page details */
  MU64 p_offset = (MU64)p_cur * cache->c_page_size;
  void * p_ptr = PTR_ADD(cache->mm_var, p_offset);

  /* Initialise to all 0's */
  memset(p_ptr, 0, cache->c_page_size);

mmap_cache.h  view on Meta::CPAN

int mmc_init(mmap_cache *);
int mmc_set_param(mmap_cache *, char *, char *);
int mmc_get_param(mmap_cache *, char *);
int mmc_close(mmap_cache *);
char * mmc_error(mmap_cache *);

/* Functions for find/locking a page */
int mmc_hash(mmap_cache *, void *, int, MU32 *, MU32 *);
int mmc_lock(mmap_cache *, MU32);
int mmc_unlock(mmap_cache *);
int mmc_is_locked(mmap_cache *);

/* Functions for getting/setting/deleting values in current page */
int mmc_read(mmap_cache *, MU32, void *, int, void **, int *, MU32 *, MU32 *);
int mmc_write(mmap_cache *, MU32, void *, int, void *, int, MU32, MU32);
int mmc_delete(mmap_cache *, MU32, void *, int, MU32 *);

/* Functions of expunging values in current page */
int mmc_calc_expunge(mmap_cache *, int, int, MU32 *, MU32 ***);
int mmc_do_expunge(mmap_cache *, int, MU32, MU32 **);

t/11.t  view on Meta::CPAN

);
return $FC;
}

my $FC = get_fc();
ok( defined $FC );

$FC->set("foo1", "bar");
my $V = eval { $FC->get("foo"); };
ok(!$V, "no return");
like($@, qr/already locked/, "recurse fail");

$FC = undef;

$FC = get_fc(allow_recursive => 1);
ok( defined $FC );

$FC->set("foo1", "bar");
$V = eval { $FC->get("foo"); };
ok(!$@, "recurse success 1");
is($V, "bar", "recurse success 2");

t/24.t  view on Meta::CPAN


#########################

# Lock-release contract: every code path that acquires a page lock must
# release it before returning, including paths where user-supplied code
# (sub passed to get_and_set, read_cb, custom serialize/deserialize) dies.
#
# pod for get_and_set explicitly promises: "If your sub does a die/throws
# an exception, the page will correctly be unlocked". The other paths
# aren't documented but were preserved by the refactor that removed
# Cache::FastMmap::OnLeave in favour of explicit fc_lock/fc_unlock.
#
# Detection: each test triggers the failure path, then performs a second
# operation on the same page. If the lock leaked, that second op blocks
# forever — alarm() turns the hang into a test failure.

use Test::More;
use strict;

t/24.t  view on Meta::CPAN

  } else {
    plan tests => 26;
  }
}
BEGIN { use_ok('Cache::FastMmap') };

#########################

sub run_with_deadlock_guard {
  my ($timeout, $code) = @_;
  my $deadlocked = 0;
  my $result = eval {
    local $SIG{ALRM} = sub { $deadlocked = 1; die "deadlock\n"; };
    alarm($timeout);
    my $r = $code->();
    alarm(0);
    $r;
  };
  alarm(0);
  return ($deadlocked, $result, $@);
}

#########################
# get_and_set: user sub dies

{
  my $FC = Cache::FastMmap->new(init_file => 1, serializer => '');
  ok( $FC->set("k", "initial"), "[get_and_set die] set initial" );

  eval { $FC->get_and_set("k", sub { die "boom\n" }); };

t/24.t  view on Meta::CPAN

  );

  eval { $FC->get("missing") };
  like( $@, qr/rcb-rec-boom/, "[get read_cb die recursive] re-throws" );

  my ($dead) = run_with_deadlock_guard(5, sub { $FC->set("missing", "v") });
  is( $dead, 0, "[get read_cb die recursive] no deadlock on follow-up set" );
}

#########################
# multi_get: deserialize dies inside the locked loop body.

{
  my $FC = Cache::FastMmap->new(
    init_file => 1,
    serializer => [
      sub { ${ $_[0] } },                                    # serialize
      sub { die "deser-boom\n" if $_[0] eq 'POISON'; \$_[0] }, # deserialize
    ],
  );
  ok( $FC->multi_set("page", { good => 'ok', bad => 'POISON' }),

t/24.t  view on Meta::CPAN


  eval { $FC->multi_get("page", [ qw(good bad) ]) };
  like( $@, qr/deser-boom/, "[multi_get deser die] re-throws" );

  my ($dead) = run_with_deadlock_guard(5,
    sub { $FC->multi_set("page", { good => 'still-ok' }) });
  is( $dead, 0, "[multi_get deser die] no deadlock on follow-up multi_set" );
}

#########################
# multi_set: serialize dies inside the locked loop body.

{
  my $FC = Cache::FastMmap->new(
    init_file => 1,
    serializer => [
      sub { die "ser-boom\n" if ${ $_[0] } eq 'POISON'; ${ $_[0] } }, # serialize
      sub { \$_[0] },                                                  # deserialize
    ],
  );

t/24.t  view on Meta::CPAN

}

#########################
# get_and_set: deserializer dies while get() is returning with skip_unlock.

{
  my $FC = Cache::FastMmap->new(
    init_file => 1,
    serializer => [
      sub { ${ $_[0] } },
      sub { die "locked-deser-boom\n" if $_[0] eq 'POISON'; \$_[0] },
    ],
  );
  ok( $FC->set("k", "POISON"), "[get_and_set locked deser die] seed poison" );

  eval { $FC->get_and_set("k", sub { return "unused" }) };
  like( $@, qr/locked-deser-boom/, "[get_and_set locked deser die] re-throws" );

  my ($dead) = run_with_deadlock_guard(5, sub { $FC->set("k", "after") });
  is( $dead, 0, "[get_and_set locked deser die] no deadlock on follow-up set" );
}

#########################
# get_and_set: serializer dies inside set() while it owns the existing lock.

{
  my $FC = Cache::FastMmap->new(
    init_file => 1,
    serializer => [
      sub { die "locked-ser-boom\n" if ${ $_[0] } eq 'POISON'; ${ $_[0] } },
      sub { \$_[0] },
    ],
  );
  ok( $FC->set("k", "initial"), "[get_and_set locked ser die] seed initial" );

  eval { $FC->get_and_set("k", sub { return "POISON" }) };
  like( $@, qr/locked-ser-boom/, "[get_and_set locked ser die] re-throws" );

  my ($dead) = run_with_deadlock_guard(5, sub { $FC->set("k", "after") });
  is( $dead, 0, "[get_and_set locked ser die] no deadlock on follow-up set" );
}

#########################
# get_and_set: non-hash set options from callback must not strand the lock.

{
  my $FC = Cache::FastMmap->new(init_file => 1, serializer => '');
  ok( $FC->set("k", "initial"), "[get_and_set bad opts] seed initial" );

  eval { $FC->get_and_set("k", sub { return ("after", "bad-options") }) };

win32.c  view on Meta::CPAN

    /* Always close the event handle once the lock has been acquired,
     * otherwise long-running processes leak one HANDLE per lock. */
    CloseHandle(lock.hEvent);
    return 0;
}

int mmc_unlock_page(mmap_cache* cache, MU64 p_offset) {
    OVERLAPPED lock;
    memset(&lock, 0, sizeof(lock));
    /* Offset is a DWORD so we must split p_offset across Offset/OffsetHigh,
     * otherwise we'd unlock at a different offset than we locked at for
     * any cache file larger than 4GB. */
    lock.Offset = (DWORD)(p_offset & 0xffffffff);
    lock.OffsetHigh = (DWORD)((p_offset >> 32) & 0xffffffff);
    lock.hEvent = 0;

    UnlockFileEx(cache->fh, 0, cache->c_page_size, 0, &lock);
    return 0;
}

/*



( run in 1.165 second using v1.01-cache-2.11-cpan-bbe5e583499 )