feat: add openTemp to collttl

This commit is contained in:
dualfroz committed 2026-09-02 17:23:22 +02:00
1 parent 69af18c2f0
commit 11230ca97c
1 file changed
+182
+182
View File
@@ -170,3 +170,185 @@ pub fn sweepExpired(db: *Db, allocator: Allocator, nowNanos: i128) !usize {
return due.items.len; return due.items.len;
} }
fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db {
const dbPath = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(dbPath);
return Db.open(allocator, dbPath, .{});
}
test "remaining reports noKey for a collection that was never created" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try std.testing.expectEqual(TtlResult.noKey, try remaining(&db, allocator, .hash, "missing", 0));
}
test "remaining reports noExpiry then counted seconds for a real hash" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Hash.set(&db, allocator, "profile", "name", "ada");
try std.testing.expectEqual(TtlResult.noExpiry, try remaining(&db, allocator, .hash, "profile", 0));
try setExpireSeconds(&db, allocator, .hash, "profile", 1000, 30);
const r = try remaining(&db, allocator, .hash, "profile", 1000);
try std.testing.expectEqual(@as(u64, 30), r.seconds);
}
test "expireIfDue drops every field of a hash, not just one key" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Hash.set(&db, allocator, "session:1", "user", "ada");
_ = try collmod.Hash.set(&db, allocator, "session:1", "role", "admin");
try setExpireSeconds(&db, allocator, .hash, "session:1", 0, 5);
try std.testing.expect(!try expireIfDue(&db, allocator, .hash, "session:1", 4 * ttlmod.secondNanos));
try std.testing.expectEqual(@as(usize, 2), try collmod.Hash.len(&db, allocator, "session:1"));
try std.testing.expect(try expireIfDue(&db, allocator, .hash, "session:1", 5 * ttlmod.secondNanos));
try std.testing.expectEqual(@as(usize, 0), try collmod.Hash.len(&db, allocator, "session:1"));
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .hash, "session:1"));
try std.testing.expectEqual(@as(?i128, null), try getExpiresAt(&db, allocator, .hash, "session:1"));
}
test "expireIfDueMany reaps only the due names among several, mirroring a Set.intersect call" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Set.add(&db, allocator, "a", "x");
_ = try collmod.Set.add(&db, allocator, "b", "x");
_ = try collmod.Set.add(&db, allocator, "c", "x");
try setExpireSeconds(&db, allocator, .set, "b", 0, 5);
// Mirrors `coll set inter a b c`: the CLI's blanket lazy check only
// ever covered args[0] ("a"); this call is what now covers args[1..].
const reaped = try expireIfDueMany(&db, allocator, .set, &.{ "b", "c" }, 5 * ttlmod.secondNanos);
try std.testing.expectEqual(@as(usize, 1), reaped);
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .set, "b"));
try std.testing.expect(try collmod.collectionExists(&db, allocator, .set, "a"));
try std.testing.expect(try collmod.collectionExists(&db, allocator, .set, "c"));
// Now Set.intersect(a, c) sees a live pair and finds the real member,
// instead of silently intersecting against a phantom "b" that a
// periodic sweep hadn't reaped yet.
const inter = try collmod.Set.intersect(&db, allocator, &.{ "a", "c" });
defer {
for (inter) |m| allocator.free(m);
allocator.free(inter);
}
try std.testing.expectEqual(@as(usize, 1), inter.len);
try std.testing.expectEqualStrings("x", inter[0]);
}
test "expireIfDueMany is a no-op count of zero when nothing is due" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Set.add(&db, allocator, "a", "x");
_ = try collmod.Set.add(&db, allocator, "b", "x");
try std.testing.expectEqual(@as(usize, 0), try expireIfDueMany(&db, allocator, .set, &.{ "a", "b" }, 0));
}
test "clearExpire makes a collection immortal again" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try collmod.List.pushRight(&db, allocator, "queue", "job-1");
try setExpireSeconds(&db, allocator, .list, "queue", 0, 1);
try clearExpire(&db, allocator, .list, "queue");
try std.testing.expectEqual(TtlResult.noExpiry, try remaining(&db, allocator, .list, "queue", 100 * ttlmod.secondNanos));
try std.testing.expect(!try expireIfDue(&db, allocator, .list, "queue", 100 * ttlmod.secondNanos));
try std.testing.expectEqual(@as(usize, 1), try collmod.List.len(&db, allocator, "queue"));
}
test "sweepExpired reaps due collections of every kind without touching live ones" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Counter.incrBy(&db, allocator, "hits", 3);
try setExpireSeconds(&db, allocator, .counter, "hits", 0, 1);
_ = try collmod.Hash.set(&db, allocator, "profile", "name", "ada");
try setExpireSeconds(&db, allocator, .hash, "profile", 0, 100);
try collmod.ZSet.add(&db, allocator, "leaderboard", "alice", 10);
try setExpireSeconds(&db, allocator, .zset, "leaderboard", 0, 1);
_ = try collmod.Set.add(&db, allocator, "tags", "zig");
const reaped = try sweepExpired(&db, allocator, 2 * ttlmod.secondNanos);
try std.testing.expectEqual(@as(usize, 2), reaped);
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .counter, "hits"));
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .zset, "leaderboard"));
try std.testing.expect(try collmod.collectionExists(&db, allocator, .hash, "profile"));
try std.testing.expect(try collmod.collectionExists(&db, allocator, .set, "tags"));
// Idempotent: a second sweep at the same instant finds nothing left.
try std.testing.expectEqual(@as(usize, 0), try sweepExpired(&db, allocator, 2 * ttlmod.secondNanos));
}
test "collection sweep survives a wal reopen: markers are durable" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
{
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Hash.set(&db, allocator, "durable", "k", "v");
try setExpireSeconds(&db, allocator, .hash, "durable", 0, 5);
}
var db = try openTemp(&tmp, allocator);
defer db.close();
const r = try remaining(&db, allocator, .hash, "durable", 1 * ttlmod.secondNanos);
try std.testing.expectEqual(@as(u64, 4), r.seconds);
try std.testing.expectEqual(@as(usize, 1), try sweepExpired(&db, allocator, 10 * ttlmod.secondNanos));
try std.testing.expect(!try collmod.collectionExists(&db, allocator, .hash, "durable"));
}
test "markers for the same name under different kinds never collide" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try collmod.Hash.set(&db, allocator, "shared", "f", "v");
_ = try collmod.Set.add(&db, allocator, "shared", "m");
try setExpireSeconds(&db, allocator, .hash, "shared", 0, 5);
// The set named "shared" must still read as noExpiry - the marker for
// the hash "shared" must not leak across the kind boundary.
try std.testing.expectEqual(TtlResult.noExpiry, try remaining(&db, allocator, .set, "shared", 0));
const hashResult = try remaining(&db, allocator, .hash, "shared", 0);
try std.testing.expectEqual(@as(u64, 5), hashResult.seconds);
}