db-mode config changes apply live in-process
Gates / frontend (push) Successful in 1m43s
Gates / test (push) Successful in 2m14s
Gates / test-aarch64 (push) Successful in 8m3s
Gates / package (push) Successful in 5m42s
Gates / container (push) Successful in 54s
CI / gates (push) Successful in 50m24s
Gates / frontend (push) Successful in 1m43s
Gates / test (push) Successful in 2m14s
Gates / test-aarch64 (push) Successful in 8m3s
Gates / package (push) Successful in 5m42s
Gates / container (push) Successful in 54s
CI / gates (push) Successful in 50m24s
settings and upstream writes now follow a prepare, commit, publish, retire contract: candidates are built and validated before the database transaction, published as infallible pointer swaps, and old generations retire after their readers drain. per-query policy values snapshot once per query; upstream pool, cache, rate limiter, sessions, api limiter, log sink, blocklist scheduler and the query-log queue each gained one named live operation. restart_required shrinks from every scalar key to the bind keys and web.enabled; the admin ui drops its restart notices for everything else. file mode is unchanged.
This commit is contained in:
@@ -252,6 +252,200 @@ fn installWithMaxBytes(io: std.Io, cfg: model.Logging, max_bytes: u64) void {
|
||||
if (state.output == .file) openFileLocked();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hot apply (milestone-34 S3.5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Which of the three disjoint shapes a `logging` apply takes. The case is
|
||||
/// decided from the FINAL MERGED config against the live sink, and it depends
|
||||
/// only on `output` and `file_path` — fields nothing but an apply writes.
|
||||
/// Rotation and write-failure recovery move `file`, `file_pos` and
|
||||
/// `rotate_pending`, never these two, so a case decided in one lock hold is
|
||||
/// still the right case in the next.
|
||||
pub const ApplyCase = enum {
|
||||
/// The merged config wants a file, and it is not the file that is open:
|
||||
/// the path differs, or output is switching TO file.
|
||||
target_changed,
|
||||
/// Output is switching away from file. Nothing to open.
|
||||
target_removed,
|
||||
/// Everything else — output stays stderr/syslog, or output stays file on
|
||||
/// the SAME path. Only config fields move; the handle and its position and
|
||||
/// rotation state stay with the rotation machinery that owns them.
|
||||
target_unchanged,
|
||||
};
|
||||
|
||||
/// The complete new target state for a `target_changed` apply. The handle
|
||||
/// couples to both other fields: inheriting the old `file_pos` would write
|
||||
/// past the new file's end, and inheriting a pending rotation would rotate the
|
||||
/// new target on its first line.
|
||||
pub const PreparedSink = struct {
|
||||
file: std.Io.File,
|
||||
file_pos: u64,
|
||||
rotate_pending: bool = false,
|
||||
};
|
||||
|
||||
pub const PrepareError = error{
|
||||
/// `file_path` does not fit in the sink's path buffer, so the sink could
|
||||
/// not name the file it was told to write.
|
||||
PathTooLong,
|
||||
/// The new target could not be opened, created, or measured.
|
||||
TargetUnopenable,
|
||||
};
|
||||
|
||||
/// A validated `logging` apply, owning everything publish needs. Publish takes
|
||||
/// no borrow from the request arena, so this outlives the request that built
|
||||
/// it.
|
||||
pub const PreparedApply = struct {
|
||||
case: ApplyCase,
|
||||
threshold: std.log.Level,
|
||||
output: model.LogOutput,
|
||||
path_buf: [std.Io.Dir.max_path_bytes]u8,
|
||||
path_len: usize,
|
||||
max_files: u8,
|
||||
max_bytes: u64,
|
||||
sink: ?PreparedSink,
|
||||
|
||||
pub fn path(self: *const PreparedApply) []const u8 {
|
||||
return self.path_buf[0..self.path_len];
|
||||
}
|
||||
};
|
||||
|
||||
/// Prepare: everything fallible happens here, and nothing is published. A
|
||||
/// `target_changed` apply opens the NEW file and MEASURES it — the open is the
|
||||
/// step that can fail, and doing it here closes the close-then-reopen window
|
||||
/// `installWithMaxBytes` has, where a bad new path leaves no sink at all.
|
||||
pub fn prepareApply(io: std.Io, cfg: model.Logging) PrepareError!PreparedApply {
|
||||
return prepareApplyWithMaxBytes(io, cfg, model.maxLogBytes(cfg));
|
||||
}
|
||||
|
||||
/// Test-only entry point, mirroring `installForTest`.
|
||||
pub fn prepareApplyForTest(io: std.Io, cfg: model.Logging, max_bytes: u64) PrepareError!PreparedApply {
|
||||
return prepareApplyWithMaxBytes(io, cfg, max_bytes);
|
||||
}
|
||||
|
||||
fn prepareApplyWithMaxBytes(io: std.Io, cfg: model.Logging, max_bytes: u64) PrepareError!PreparedApply {
|
||||
if (cfg.file_path.len > std.Io.Dir.max_path_bytes) return error.PathTooLong;
|
||||
|
||||
var prepared: PreparedApply = .{
|
||||
.case = undefined,
|
||||
.threshold = toStdLevel(cfg.level),
|
||||
.output = cfg.output,
|
||||
.path_buf = undefined,
|
||||
.path_len = cfg.file_path.len,
|
||||
.max_files = cfg.max_files,
|
||||
.max_bytes = max_bytes,
|
||||
.sink = null,
|
||||
};
|
||||
@memcpy(prepared.path_buf[0..prepared.path_len], cfg.file_path);
|
||||
prepared.case = classifyApply(cfg);
|
||||
|
||||
if (prepared.case == .target_changed) {
|
||||
prepared.sink = try openTarget(io, prepared.path());
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
fn classifyApply(cfg: model.Logging) ApplyCase {
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
|
||||
const currently_file = state.output == .file;
|
||||
if (cfg.output != .file) return if (currently_file) .target_removed else .target_unchanged;
|
||||
if (!currently_file) return .target_changed;
|
||||
return if (std.mem.eql(u8, state.path(), cfg.file_path)) .target_unchanged else .target_changed;
|
||||
}
|
||||
|
||||
/// Opens `p` and measures it, without touching the live sink.
|
||||
fn openTarget(io: std.Io, p: []const u8) PrepareError!PreparedSink {
|
||||
if (p.len == 0) return error.TargetUnopenable;
|
||||
|
||||
const prev = io.swapCancelProtection(.blocked);
|
||||
defer _ = io.swapCancelProtection(prev);
|
||||
|
||||
const dir: std.Io.Dir = .cwd();
|
||||
const file = dir.openFile(io, p, .{ .mode = .write_only }) catch |open_err| switch (open_err) {
|
||||
error.FileNotFound => dir.createFile(io, p, .{ .truncate = false }) catch
|
||||
return error.TargetUnopenable,
|
||||
else => return error.TargetUnopenable,
|
||||
};
|
||||
errdefer file.close(io);
|
||||
|
||||
// A pre-existing nonempty target is appended to, so the new position is
|
||||
// its measured length rather than zero.
|
||||
const length = file.length(io) catch return error.TargetUnopenable;
|
||||
return .{ .file = file, .file_pos = length, .rotate_pending = false };
|
||||
}
|
||||
|
||||
/// Publish: infallible and I/O-free, one hold of the sink lock. Returns the
|
||||
/// DETACHED old handle, which `retireApply` closes — closing a file is retire
|
||||
/// work, and doing it here would put a syscall inside the publish.
|
||||
pub fn publishApply(prepared: PreparedApply) ?std.Io.File {
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
|
||||
state.threshold = prepared.threshold;
|
||||
state.output = prepared.output;
|
||||
state.path_len = prepared.path_len;
|
||||
@memcpy(state.path_buf[0..state.path_len], prepared.path());
|
||||
state.max_files = prepared.max_files;
|
||||
state.max_bytes = prepared.max_bytes;
|
||||
|
||||
switch (prepared.case) {
|
||||
// The handle and its position and rotation state are not this apply's
|
||||
// to move; a broken handle is repaired by the existing per-write
|
||||
// recovery, not by a config change.
|
||||
.target_unchanged => return null,
|
||||
.target_changed => {
|
||||
const detached = state.file;
|
||||
const sink = prepared.sink.?;
|
||||
state.file = sink.file;
|
||||
state.file_pos = sink.file_pos;
|
||||
state.rotate_pending = sink.rotate_pending;
|
||||
return detached;
|
||||
},
|
||||
.target_removed => {
|
||||
const detached = state.file;
|
||||
state.file = null;
|
||||
state.file_pos = 0;
|
||||
state.rotate_pending = false;
|
||||
return detached;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Retire: closes the handle `publishApply` detached, after no writer can
|
||||
/// reach it — the swap happened under the sink lock, so any writer that held
|
||||
/// it has already returned.
|
||||
pub fn retireApply(io: std.Io, detached: ?std.Io.File) void {
|
||||
const file = detached orelse return;
|
||||
const prev = io.swapCancelProtection(.blocked);
|
||||
defer _ = io.swapCancelProtection(prev);
|
||||
file.close(io);
|
||||
}
|
||||
|
||||
/// Discards a prepared apply that will not be published, because its commit
|
||||
/// failed or a sibling owner's prepare did.
|
||||
pub fn abortApply(io: std.Io, prepared: PreparedApply) void {
|
||||
const sink = prepared.sink orelse return;
|
||||
const prev = io.swapCancelProtection(.blocked);
|
||||
defer _ = io.swapCancelProtection(prev);
|
||||
sink.file.close(io);
|
||||
}
|
||||
|
||||
/// The directory the disk monitor should measure for `cfg`: the log file's
|
||||
/// directory when output is `file`, and null otherwise — with output on
|
||||
/// stderr or syslog there is no log file to run out of room for.
|
||||
///
|
||||
/// A path with no directory component measures the working directory, which is
|
||||
/// where a bare filename lands.
|
||||
pub fn logDirname(cfg: model.Logging) ?[]const u8 {
|
||||
if (cfg.output != .file) return null;
|
||||
if (cfg.file_path.len == 0) return null;
|
||||
return std.fs.path.dirname(cfg.file_path) orelse ".";
|
||||
}
|
||||
|
||||
/// Flushes and closes the file, and restores pass-through stderr formatting.
|
||||
pub fn deinstall() void {
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
@@ -1099,3 +1293,365 @@ test "a message over the buffer is marked and counted" {
|
||||
const colon = std.mem.indexOf(u8, written, ": ").?;
|
||||
try testing.expectEqual(max_message_bytes, written[colon + 2 ..].len - 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hot apply (milestone-34 S3.5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The sink is one process-wide `state` behind the stderr lock, so an apply
|
||||
/// test has to save it, drive the apply, and put it back. Nothing here holds
|
||||
/// the lock across an apply call: `classifyApply` and `publishApply` take it
|
||||
/// themselves.
|
||||
const ApplyFixture = struct {
|
||||
threaded: std.Io.Threaded,
|
||||
tmp: testing.TmpDir,
|
||||
saved: State,
|
||||
|
||||
fn init(self: *ApplyFixture) void {
|
||||
self.threaded = .init(testing.allocator, .{});
|
||||
self.tmp = testing.tmpDir(.{});
|
||||
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
self.saved = state;
|
||||
state = .{};
|
||||
state.io = self.threaded.io();
|
||||
}
|
||||
|
||||
fn deinit(self: *ApplyFixture) void {
|
||||
{
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
if (state.file) |f| f.close(self.threaded.io());
|
||||
state = self.saved;
|
||||
}
|
||||
self.tmp.cleanup();
|
||||
self.threaded.deinit();
|
||||
}
|
||||
|
||||
fn io(self: *ApplyFixture) std.Io {
|
||||
return self.threaded.io();
|
||||
}
|
||||
|
||||
fn path(self: *ApplyFixture, buf: []u8, name: []const u8) []const u8 {
|
||||
return std.fmt.bufPrint(buf, ".zig-cache/tmp/{s}/{s}", .{ self.tmp.sub_path, name }) catch unreachable;
|
||||
}
|
||||
|
||||
/// Puts the sink on `p` with a real open handle, the way a running server
|
||||
/// with `output = file` sits.
|
||||
fn openOn(self: *ApplyFixture, p: []const u8) void {
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
|
||||
state.installed = true;
|
||||
state.output = .file;
|
||||
state.path_len = p.len;
|
||||
@memcpy(state.path_buf[0..p.len], p);
|
||||
state.max_bytes = 1 << 20;
|
||||
state.max_files = 3;
|
||||
openFileLocked();
|
||||
_ = self;
|
||||
}
|
||||
|
||||
fn snapshot(self: *ApplyFixture) State {
|
||||
_ = self;
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
return state;
|
||||
}
|
||||
|
||||
fn setHandleState(self: *ApplyFixture, file: ?std.Io.File, file_pos: u64, rotate_pending: bool) void {
|
||||
_ = self;
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
state.file = file;
|
||||
state.file_pos = file_pos;
|
||||
state.rotate_pending = rotate_pending;
|
||||
}
|
||||
|
||||
/// Writes one record through the live handle, as `emitFileLocked` does.
|
||||
fn writeThroughSink(self: *ApplyFixture, text: []const u8) !void {
|
||||
_ = self;
|
||||
var stderr_buf: [64]u8 = undefined;
|
||||
_ = std.debug.lockStderr(&stderr_buf);
|
||||
defer std.debug.unlockStderr();
|
||||
try writeLineLocked(state.file.?, text);
|
||||
}
|
||||
|
||||
fn read(self: *ApplyFixture, buf: []u8, name: []const u8) ![]u8 {
|
||||
return self.tmp.dir.readFileAlloc(self.io(), name, testing.allocator, .limited(buf.len)) catch |err| return err;
|
||||
}
|
||||
};
|
||||
|
||||
fn fileCfg(p: []const u8) model.Logging {
|
||||
return .{ .output = .file, .file_path = p, .level = .info, .max_files = 3, .max_size_mb = 1 };
|
||||
}
|
||||
|
||||
test "a bad target path is refused at prepare and the live sink is untouched" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var buf: [160]u8 = undefined;
|
||||
const live = fx.path(&buf, "nxdns.log");
|
||||
fx.openOn(live);
|
||||
const before = fx.snapshot();
|
||||
try testing.expect(before.file != null);
|
||||
|
||||
// A directory that does not exist: the open and the create both fail.
|
||||
var bad_buf: [200]u8 = undefined;
|
||||
const bad = fx.path(&bad_buf, "no-such-dir/nxdns.log");
|
||||
try testing.expectError(
|
||||
error.TargetUnopenable,
|
||||
prepareApplyForTest(fx.io(), fileCfg(bad), 1 << 20),
|
||||
);
|
||||
|
||||
const after = fx.snapshot();
|
||||
try testing.expectEqual(before.file.?.handle, after.file.?.handle);
|
||||
try testing.expectEqualStrings(live, after.path());
|
||||
}
|
||||
|
||||
test "a target change publishes without closing, and retire closes the old handle" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var first_buf: [160]u8 = undefined;
|
||||
var second_buf: [160]u8 = undefined;
|
||||
const first = fx.path(&first_buf, "first.log");
|
||||
const second = fx.path(&second_buf, "second.log");
|
||||
fx.openOn(first);
|
||||
const before = fx.snapshot();
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), fileCfg(second), 1 << 20);
|
||||
try testing.expectEqual(ApplyCase.target_changed, prepared.case);
|
||||
|
||||
const detached = publishApply(prepared);
|
||||
const after = fx.snapshot();
|
||||
|
||||
// Publish swapped the handle and left the old one OPEN: the old descriptor
|
||||
// still writes, which it could not if publish had closed it.
|
||||
try testing.expectEqual(before.file.?.handle, detached.?.handle);
|
||||
try testing.expect(after.file.?.handle != detached.?.handle);
|
||||
try testing.expectEqualStrings(second, after.path());
|
||||
try testing.expectEqual(@as(u64, 0), after.file_pos);
|
||||
try testing.expect(!after.rotate_pending);
|
||||
|
||||
var old_writer_buf: [64]u8 = undefined;
|
||||
var ow = detached.?.writer(fx.io(), &old_writer_buf);
|
||||
try ow.interface.writeAll("still open\n");
|
||||
try ow.interface.flush();
|
||||
|
||||
retireApply(fx.io(), detached);
|
||||
|
||||
// The new target receives lines.
|
||||
try fx.writeThroughSink("1 info: on the new target\n");
|
||||
var read_buf: [256]u8 = undefined;
|
||||
const contents = try fx.read(&read_buf, "second.log");
|
||||
defer testing.allocator.free(contents);
|
||||
try testing.expectEqualStrings("1 info: on the new target\n", contents);
|
||||
}
|
||||
|
||||
test "switching to a pre-existing nonempty file starts at its measured length" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
const existing = "already here\n";
|
||||
try fx.tmp.dir.writeFile(fx.io(), .{ .sub_path = "kept.log", .data = existing });
|
||||
|
||||
var first_buf: [160]u8 = undefined;
|
||||
var kept_buf: [160]u8 = undefined;
|
||||
fx.openOn(fx.path(&first_buf, "first.log"));
|
||||
const kept = fx.path(&kept_buf, "kept.log");
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), fileCfg(kept), 1 << 20);
|
||||
try testing.expectEqual(@as(u64, existing.len), prepared.sink.?.file_pos);
|
||||
retireApply(fx.io(), publishApply(prepared));
|
||||
|
||||
try testing.expectEqual(@as(u64, existing.len), fx.snapshot().file_pos);
|
||||
|
||||
// Inheriting the old position would have overwritten the existing bytes.
|
||||
try fx.writeThroughSink("appended\n");
|
||||
var read_buf: [256]u8 = undefined;
|
||||
const contents = try fx.read(&read_buf, "kept.log");
|
||||
defer testing.allocator.free(contents);
|
||||
try testing.expectEqualStrings(existing ++ "appended\n", contents);
|
||||
}
|
||||
|
||||
test "a target change does not inherit a pending rotation" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var first_buf: [160]u8 = undefined;
|
||||
var second_buf: [160]u8 = undefined;
|
||||
fx.openOn(fx.path(&first_buf, "first.log"));
|
||||
const second = fx.path(&second_buf, "second.log");
|
||||
|
||||
// A rotation the old target owed and never completed: the handle is closed
|
||||
// and the rotation is still pending.
|
||||
const stale = fx.snapshot().file.?;
|
||||
stale.close(fx.io());
|
||||
fx.setHandleState(null, 0, true);
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), fileCfg(second), 1 << 20);
|
||||
try testing.expect(!prepared.sink.?.rotate_pending);
|
||||
retireApply(fx.io(), publishApply(prepared));
|
||||
|
||||
const after = fx.snapshot();
|
||||
try testing.expect(!after.rotate_pending);
|
||||
try testing.expect(after.file != null);
|
||||
try testing.expectEqual(@as(u64, 0), after.file_pos);
|
||||
}
|
||||
|
||||
test "a file to stderr apply detaches the handle and clears position and rotation" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var buf: [160]u8 = undefined;
|
||||
const live = fx.path(&buf, "nxdns.log");
|
||||
fx.openOn(live);
|
||||
fx.setHandleState(fx.snapshot().file, 4_096, true);
|
||||
const before = fx.snapshot();
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), .{
|
||||
.output = .stderr,
|
||||
.file_path = live,
|
||||
.level = .warn,
|
||||
}, 1 << 20);
|
||||
try testing.expectEqual(ApplyCase.target_removed, prepared.case);
|
||||
|
||||
const detached = publishApply(prepared);
|
||||
const after = fx.snapshot();
|
||||
|
||||
try testing.expectEqual(before.file.?.handle, detached.?.handle);
|
||||
try testing.expectEqual(@as(?std.Io.File, null), after.file);
|
||||
try testing.expectEqual(@as(u64, 0), after.file_pos);
|
||||
try testing.expect(!after.rotate_pending);
|
||||
try testing.expectEqual(model.LogOutput.stderr, after.output);
|
||||
// The path still moves, so a later switch back to file opens the right one.
|
||||
try testing.expectEqualStrings(live, after.path());
|
||||
|
||||
retireApply(fx.io(), detached);
|
||||
}
|
||||
|
||||
test "a same-path apply changes only config fields, whatever the handle is doing" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var buf: [160]u8 = undefined;
|
||||
const live = fx.path(&buf, "nxdns.log");
|
||||
fx.openOn(live);
|
||||
|
||||
// Publish takes the same lock a rotation and a write-failure closure hold,
|
||||
// so the only reachable interleavings are "before publish" and "after".
|
||||
// Both leave the handle state the apply must not touch; these are the two
|
||||
// states each of them leaves behind.
|
||||
const handle_states = [_]struct { file: bool, pos: u64, pending: bool }{
|
||||
// Mid-rotation: handle closed, rotation owed.
|
||||
.{ .file = false, .pos = 0, .pending = true },
|
||||
// Healthy and part-written.
|
||||
.{ .file = true, .pos = 8_192, .pending = false },
|
||||
};
|
||||
|
||||
const open_handle = fx.snapshot().file.?;
|
||||
for (handle_states) |want| {
|
||||
fx.setHandleState(if (want.file) open_handle else null, want.pos, want.pending);
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), .{
|
||||
.output = .file,
|
||||
.file_path = live,
|
||||
.level = .debug,
|
||||
.max_files = 9,
|
||||
.max_size_mb = 7,
|
||||
}, 4_242);
|
||||
try testing.expectEqual(ApplyCase.target_unchanged, prepared.case);
|
||||
|
||||
// Nothing detached, so retire has nothing to close.
|
||||
try testing.expectEqual(@as(?std.Io.File, null), publishApply(prepared));
|
||||
|
||||
const after = fx.snapshot();
|
||||
try testing.expectEqual(want.pos, after.file_pos);
|
||||
try testing.expectEqual(want.pending, after.rotate_pending);
|
||||
try testing.expectEqual(want.file, after.file != null);
|
||||
// The config fields did move.
|
||||
try testing.expectEqual(std.log.Level.debug, after.threshold);
|
||||
try testing.expectEqual(@as(u8, 9), after.max_files);
|
||||
try testing.expectEqual(@as(u64, 4_242), after.max_bytes);
|
||||
}
|
||||
|
||||
fx.setHandleState(open_handle, 0, false);
|
||||
}
|
||||
|
||||
test "a file_path change while output is stderr updates the config and touches no handle" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var buf: [160]u8 = undefined;
|
||||
const later = fx.path(&buf, "later.log");
|
||||
|
||||
const prepared = try prepareApplyForTest(fx.io(), .{
|
||||
.output = .syslog,
|
||||
.file_path = later,
|
||||
.level = .info,
|
||||
}, 1 << 20);
|
||||
try testing.expectEqual(ApplyCase.target_unchanged, prepared.case);
|
||||
try testing.expectEqual(@as(?std.Io.File, null), publishApply(prepared));
|
||||
|
||||
const after = fx.snapshot();
|
||||
try testing.expectEqual(@as(?std.Io.File, null), after.file);
|
||||
try testing.expectEqualStrings(later, after.path());
|
||||
|
||||
// The later switch to file opens exactly that path.
|
||||
const to_file = try prepareApplyForTest(fx.io(), fileCfg(later), 1 << 20);
|
||||
try testing.expectEqual(ApplyCase.target_changed, to_file.case);
|
||||
retireApply(fx.io(), publishApply(to_file));
|
||||
try testing.expect(fx.snapshot().file != null);
|
||||
}
|
||||
|
||||
test "an aborted apply closes the target it opened" {
|
||||
var fx: ApplyFixture = undefined;
|
||||
fx.init();
|
||||
defer fx.deinit();
|
||||
|
||||
var buf: [160]u8 = undefined;
|
||||
const target = fx.path(&buf, "never.log");
|
||||
|
||||
// The commit failed, so the prepared target must not leak its descriptor.
|
||||
const prepared = try prepareApplyForTest(fx.io(), fileCfg(target), 1 << 20);
|
||||
abortApply(fx.io(), prepared);
|
||||
|
||||
const after = fx.snapshot();
|
||||
try testing.expectEqual(@as(?std.Io.File, null), after.file);
|
||||
try testing.expectEqual(model.LogOutput.stderr, after.output);
|
||||
}
|
||||
|
||||
test "logDirname follows output and file_path in both directions" {
|
||||
try testing.expectEqualStrings("/var/log/nxdns", logDirname(.{
|
||||
.output = .file,
|
||||
.file_path = "/var/log/nxdns/nxdns.log",
|
||||
}).?);
|
||||
// A bare filename lands in the working directory.
|
||||
try testing.expectEqualStrings(".", logDirname(.{
|
||||
.output = .file,
|
||||
.file_path = "nxdns.log",
|
||||
}).?);
|
||||
// Output away from file stops the measurement whatever the path says.
|
||||
try testing.expectEqual(@as(?[]const u8, null), logDirname(.{
|
||||
.output = .stderr,
|
||||
.file_path = "/var/log/nxdns/nxdns.log",
|
||||
}));
|
||||
try testing.expectEqual(@as(?[]const u8, null), logDirname(.{
|
||||
.output = .syslog,
|
||||
.file_path = "/var/log/nxdns/nxdns.log",
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user