NAME

App::Syslogd::Cache - A small, fast in-memory cache with expiry and a size limit

VERSION

Version 0.002.0

SYNOPSIS

use App::Syslogd::Cache;

my $cache = App::Syslogd::Cache->new(max_bytes => 65_536);

# Look a value up; compute and remember it for 300 seconds if missing
my $name = $cache->compute('192.0.2.1', 300, sub { slow_lookup('192.0.2.1') });

DESCRIPTION

App::Syslogd uses this to remember reverse-DNS answers. It holds values in a Perl hash, forgets each one after its time to live, and keeps its total size under a limit by dropping the oldest entries first.

Its only method besides new() is compute(), which has the same calling convention as CHI's, so either can be given to App::Syslogd as its cache.

METHODS

new

Purpose: make an empty cache.

Args: max_bytes (optional): the most memory, roughly in bytes, that the entries may use. Default 262144. Each entry counts as the length of its key, plus the length of its value, plus 64.

Returns: the new cache.

Side Effects: none.

Usage:

my $cache = App::Syslogd::Cache->new(max_bytes => 65_536);

EXAMPLE

my $cache = App::Syslogd::Cache->new();	# 256 KB

API SPECIFICATION

INPUT

{
	max_bytes => { type => 'integer', min => 1, optional => 1 },
}

OUTPUT

{ type => 'object', isa => 'App::Syslogd::Cache' }

MESSAGES

+--------------------------------------------+--------------------------+-------------------------+
| Message (dies)                             | Meaning                  | What to do              |
+--------------------------------------------+--------------------------+-------------------------+
| validate_strict: Parameter 'max_bytes' ... | Not a whole number of at | Give 1 or more          |
|                                            | least 1                  |                         |
| validate_strict: Unknown parameter 'x'     | A misspelt option        | Use max_bytes           |
+--------------------------------------------+--------------------------+-------------------------+

compute

Purpose: return the remembered value for a key, or compute, remember and return it.

Args:

1. The key (a string).
2. How many seconds to remember a new value. 0 (or less) means "do not remember": the value is computed every time.
3. A code reference that computes the value.

Returns: the value (it may be undef, which is remembered like any other).

Side Effects: may call the code reference; may forget the oldest entries to stay under max_bytes. A value too big to fit at all is returned but not remembered. If the code dies, the error is passed on and nothing is remembered.

Usage:

my $value = $cache->compute($key, $seconds, sub { ... });

EXAMPLE

my $calls = 0;
my $get = sub { $cache->compute('k', 60, sub { ++$calls }) };
$get->();	# 1: computed
$get->();	# 1: remembered; $calls is still 1

API SPECIFICATION

INPUT

{
	key => { type => 'string', position => 0 },
	ttl => { type => 'number', position => 1 },
	code => { type => 'coderef', position => 2 },
}

The arguments are not validated: this is called for every datagram, and its only caller is App::Syslogd.

OUTPUT

{ type => 'any', optional => 1 }

MESSAGES

None of its own; an error from the code reference is passed on.

PSEUDOCODE

if the key has an entry that has not expired: return its value
value = code()
if ttl <= 0: return value (not remembered)
forget any old entry for the key
size = length(key) + length(value) + 64
if size > max_bytes: return value (too big to remember)
while the entries plus size would exceed max_bytes:
	forget the oldest entry
remember the value until now + ttl
if the age queue has grown well past the entries: rebuild it
return value

LIMITATIONS

The size limit is an estimate: Perl's real memory use per entry depends on its build and the strings stored. Expired entries are only removed when their key is looked up again or when space is needed, not on a timer. Times are whole seconds.

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

LICENSE AND COPYRIGHT

Copyright 2026 Nigel Horne.

This program is released under the GNU General Public License, version 2 (see the LICENSE file). If you use it, please let me know.

FORMAL SPECIFICATION

new

Cache
  entries : KEY ⇸ (VALUE × TIME × ℕ × ℕ)
  max_bytes, bytes : ℕ
  ─────────
  bytes = Σ { k : dom entries • size(entries(k)) } ∧ bytes ≤ max_bytes

New
  Cache'
  max? : ℕ₁
  ─────────
  entries' = ∅ ∧ bytes' = 0 ∧ max_bytes' = max?

compute

Compute
  ΔCache
  k? : KEY ; ttl? : ℤ ; code? : → VALUE ; v! : VALUE
  ─────────
  k? ∈ dom entries ∧ expiry(entries(k?)) > now ⇒
    v! = value(entries(k?)) ∧ θCache' = θCache
  otherwise ⇒
    v! = code?() ∧
    (ttl? ≤ 0 ∨ size(k?, v!) > max_bytes ⇒ k? ¬in; dom entries') ∧
    (ttl? > 0 ∧ size(k?, v!) ≤ max_bytes ⇒
      entries'(k?) = (v!, now + ttl?, size(k?, v!), serial') ∧
      ∀ k : dom entries' \ {k?} • k ∈ dom entries ∧
        (∀ j : dom entries \ dom entries' • serial(entries(j)) <
                                            min(serial ∘ entries' (dom entries')))) ∧
    bytes' ≤ max_bytes

STATE DIAGRAM

                   new()
                     |
                     v
 compute(k) hit  +-------+   compute(k) miss, ttl > 0, fits
+--------------->| CACHE |------------------------------------+
|  [return the   +-------+   [compute; drop oldest entries    |
|   value]           ^        until it fits; remember until   |
+--------------------+        now + ttl]                      |
                     +----------------------------------------+
 compute(k) miss with ttl <= 0, or too big: [compute; return; nothing kept]
 code dies: [error passed on; nothing kept]