feat: extend ttl

This commit is contained in:
dualfroz committed 2026-09-12 20:58:19 +02:00
1 parent efa7197932
commit 74cf0c7aaa
1 file changed
+135
+135
View File
@@ -156,3 +156,138 @@ fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db {
return Db.open(allocator, dbPath, .{});
}
test "remaining reports noKey, noExpiry, and counted seconds" {
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, "missing", 0));
try db.put("plain", "v");
try std.testing.expectEqual(TtlResult.noExpiry, try remaining(&db, allocator, "plain", 0));
try setExpireSeconds(&db, allocator, "plain", 1000, 30);
const r = try remaining(&db, allocator, "plain", 1000);
try std.testing.expectEqual(@as(u64, 30), r.seconds);
}
test "expireIfDue removes value and marker only once due" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try db.put("session:1", "payload");
try setExpireSeconds(&db, allocator, "session:1", 0, 5);
try std.testing.expect(!try expireIfDue(&db, allocator, "session:1", 4 * secondNanos));
const stillThere = (try db.get(allocator, "session:1")).?;
allocator.free(stillThere);
try std.testing.expect(try expireIfDue(&db, allocator, "session:1", 5 * secondNanos));
try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "session:1"));
try std.testing.expectEqual(@as(?i128, null), try getExpiresAt(&db, allocator, "session:1"));
}
test "getLive lazily expires exactly at the deadline" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try putWithTtl(&db, allocator, "cache:x", "hot", 0, 2);
const before = (try getLive(&db, allocator, "cache:x", 1 * secondNanos)).?;
defer allocator.free(before);
try std.testing.expectEqualStrings("hot", before);
try std.testing.expectEqual(@as(?[]u8, null), try getLive(&db, allocator, "cache:x", 2 * secondNanos));
// A plain db.get after the lazy expiry proves the delete was real, not
// getLive-only bookkeeping - the tombstone is now the source of truth.
try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "cache:x"));
}
test "clearExpire makes a key 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 putWithTtl(&db, allocator, "note", "keep me", 0, 1);
try clearExpire(&db, allocator, "note");
try std.testing.expectEqual(TtlResult.noExpiry, try remaining(&db, allocator, "note", 100 * secondNanos));
const v = (try getLive(&db, allocator, "note", 100 * secondNanos)).?;
defer allocator.free(v);
try std.testing.expectEqualStrings("keep me", v);
}
test "sweepExpired reaps every due key without touching live or unrelated keys" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try putWithTtl(&db, allocator, "a", "1", 0, 1);
try putWithTtl(&db, allocator, "b", "2", 0, 100);
try putWithTtl(&db, allocator, "c", "3", 0, 1);
try db.put("d", "forever");
const reaped = try sweepExpired(&db, allocator, 2 * secondNanos);
try std.testing.expectEqual(@as(usize, 2), reaped);
try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "a"));
try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "c"));
const b = (try db.get(allocator, "b")).?;
defer allocator.free(b);
try std.testing.expectEqualStrings("2", b);
const d = (try db.get(allocator, "d")).?;
defer allocator.free(d);
try std.testing.expectEqualStrings("forever", d);
// Idempotent: a second sweep at the same instant finds nothing left.
try std.testing.expectEqual(@as(usize, 0), try sweepExpired(&db, allocator, 2 * secondNanos));
}
test "sweep survives a wal reopen: markers are durable, not in-memory bookkeeping" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
{
var db = try openTemp(&tmp, allocator);
defer db.close();
try putWithTtl(&db, allocator, "durable-ttl", "v", 0, 5);
}
var db = try openTemp(&tmp, allocator);
defer db.close();
const r = try remaining(&db, allocator, "durable-ttl", 1 * secondNanos);
try std.testing.expectEqual(@as(u64, 4), r.seconds);
try std.testing.expectEqual(@as(usize, 1), try sweepExpired(&db, allocator, 10 * secondNanos));
try std.testing.expectEqual(@as(?[]u8, null), try db.get(allocator, "durable-ttl"));
}
test "setExpireAt on a nonexistent key is a real marker, not silently rejected" {
// Documents the PEXPIREAT-like looseness of setExpireAt directly (the
// CLI layer is what adds the Redis EXPIRE existence check on top).
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try setExpireSeconds(&db, allocator, "ghost", 0, 10);
try std.testing.expectEqual(TtlResult.noKey, try remaining(&db, allocator, "ghost", 0));
const expiresAt = (try getExpiresAt(&db, allocator, "ghost")).?;
try std.testing.expectEqual(@as(i128, 10 * secondNanos), expiresAt);
}