NAME

Test::CPAN::Health::Cache - Persistent SQLite cache for network check results

SYNOPSIS

use Test::CPAN::Health::Cache;

my $cache = Test::CPAN::Health::Cache->new(
    cache_dir => "$ENV{HOME}/.cache/cpan-health",
);

$cache->store('sem_ver:Foo-Bar:1.00', { status => 'pass', score => 100 });
my $data = $cache->get('sem_ver:Foo-Bar:1.00');

DESCRIPTION

A lightweight SQLite-backed key/value store that persists health check results between runs to avoid hammering rate-limited external APIs.

Each entry has a TTL (seconds); get returns undef and behaves as a cache miss for expired entries. TTLs are keyed by check id prefix against the %DEFAULT_TTLS table; unknown checks fall back to 24 hours.

The database is created automatically on first use under cache_dir.

LIMITATIONS

  • No row-level locking; concurrent processes writing the same key may race. SQLite's journal mode provides transaction safety but not write ordering.

  • Expired rows are pruned lazily on get and on a scheduled sweep; disk usage may grow unboundedly if purge is never called.

get

Retrieve a cached value by key. Returns undef on cache miss or if the entry has expired.

API SPECIFICATION

INPUT

key  Scalar  required  opaque cache key (e.g. 'sem_ver:Foo-Bar:1.00')

OUTPUT

Hashref of the stored data, or undef on miss/expiry.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
CAC01 | WARNING  | Cache read error: {msg}            | Check DB permissions

FORMAL SPECIFICATION

-- Z schema (placeholder) --
GetOp
key?    : String
value!  : Hashref | undefined
now     : Timestamp
-------------------------------------------------------
key? /= ""
value! /= undefined <=> exists entry(key?) /\ entry(key?).expires > now

SIDE EFFECTS

May delete expired rows from the SQLite database.

USAGE EXAMPLE

my $data = $cache->get('security_advisories:Foo-Bar:1.00');

store

PURPOSE

Store a value in the cache under the given key. The TTL is determined automatically from the check id embedded in the key (first colon-delimited segment) using the %DEFAULT_TTLS table.

API SPECIFICATION

INPUT

key    Scalar   required
value  Hashref  required
ttl    Scalar   optional  override TTL in seconds

OUTPUT

Returns $self.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
CAC02 | WARNING  | Cache write error: {msg}           | Check DB permissions/disk space

FORMAL SPECIFICATION

-- Z schema (placeholder) --
SetOp
Cache
Cache'
key?   : String
value? : Hashref
ttl?   : N | undefined
now    : Timestamp
-------------------------------------------------------
Cache'.entries = Cache.entries (+) {key? |-> {value: value?, expires: now + ttl?}}

SIDE EFFECTS

Writes to the SQLite database.

USAGE EXAMPLE

$cache->store('sem_ver:Foo-Bar:1.00', { status => 'pass', score => 100 });

record_history

PURPOSE

Record an overall health score for a distribution version. Used by the Runner to build a score-over-time trend.

API SPECIFICATION

INPUT

dist     Scalar  required  Distribution name (e.g. 'LWP-UserAgent')
version  Scalar  required  Version string
score    Scalar  required  Integer score 0..100

OUTPUT

Returns $self.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
CAC04 | WARNING  | History write error: {msg}         | Check DB permissions

FORMAL SPECIFICATION

Post: score_history table gains one row (dist, version, score, time())

SIDE EFFECTS

Writes to the SQLite database.

USAGE EXAMPLE

$cache->record_history('LWP-UserAgent', '6.77', 95);

score_history

PURPOSE

Retrieve recent score history for a distribution in reverse-chronological order.

API SPECIFICATION

INPUT

dist   Scalar   required  Distribution name
limit  Integer  optional  Maximum rows to return (default: 10)

OUTPUT

Arrayref of hashrefs with keys dist, version, score, recorded (Unix timestamp). Most-recent first.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
CAC05 | WARNING  | History read error: {msg}          | Check DB permissions

FORMAL SPECIFICATION

Post: result is an arrayref of at most limit rows for dist, ordered by recorded DESC

SIDE EFFECTS

Reads from the SQLite database.

USAGE EXAMPLE

for my $row (@{ $cache->score_history('LWP-UserAgent') }) {
    printf "%s: %d\n", scalar localtime($row->{recorded}), $row->{score};
}

purge

PURPOSE

Delete all expired entries from the cache database. Intended to be called periodically (e.g. at CLI startup) to reclaim disk space.

API SPECIFICATION

INPUT

None.

OUTPUT

Integer: number of rows deleted.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
CAC03 | WARNING  | Cache purge error: {msg}           | Check DB permissions

FORMAL SPECIFICATION

-- Z schema (placeholder) --
PurgeOp
Cache
Cache'
now     : Timestamp
deleted : N
-------------------------------------------------------
Cache'.entries = {e : Cache.entries | e.expires > now}
deleted = #Cache.entries - #Cache'.entries

SIDE EFFECTS

Deletes rows from the SQLite database.

USAGE EXAMPLE

printf "Purged %d stale cache entries\n", $cache->purge;

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

LICENSE AND COPYRIGHT

Copyright (C) 2026 Nigel Horne.

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.