diff --git a/src/internal/runtime/cli.zig b/src/internal/runtime/cli.zig index 8d33e20..1d882ee 100644 --- a/src/internal/runtime/cli.zig +++ b/src/internal/runtime/cli.zig @@ -207,3 +207,167 @@ pub const ServeOptions = struct { .log_format = .{ .serve = false }, }; }; + +// === value parsers === +// +// Go-duration, boolean and size syntax, lifted out of main.zig. These are the +// shapes upstream's flag library accepts, and they are pure functions of their +// input, so they belong beside the flag derivation rather than inline in a +// 1,400-line entry point. + +const Io = std.Io; +const server_mod = @import("../serve/server.zig"); + +pub fn parseDurationNanoseconds(raw: []const u8) !u64 { + return parseDurationMagnitude(raw, std.math.maxInt(i64)); +} +pub fn parseDurationMagnitude(raw: []const u8, limit: u128) !u64 { + if (raw.len == 0) return error.InvalidDuration; + var i: usize = 0; + if (raw[i] == '+') { + i += 1; + if (i == raw.len) return error.InvalidDuration; + } else if (raw[i] == '-') return error.InvalidDuration; + if (std.mem.eql(u8, raw[i..], "0")) return 0; + + const Unit = struct { name: []const u8, nanoseconds: u64 }; + const units = [_]Unit{ + .{ .name = "ns", .nanoseconds = 1 }, + .{ .name = "us", .nanoseconds = std.time.ns_per_us }, + .{ .name = "µs", .nanoseconds = std.time.ns_per_us }, + .{ .name = "μs", .nanoseconds = std.time.ns_per_us }, + .{ .name = "ms", .nanoseconds = std.time.ns_per_ms }, + .{ .name = "s", .nanoseconds = std.time.ns_per_s }, + .{ .name = "m", .nanoseconds = std.time.ns_per_min }, + .{ .name = "h", .nanoseconds = std.time.ns_per_hour }, + }; + var total: u128 = 0; + var tokens: usize = 0; + while (i < raw.len) : (tokens += 1) { + var whole: u128 = 0; + var digits: usize = 0; + while (i < raw.len and std.ascii.isDigit(raw[i])) : (i += 1) { + const digit = raw[i] - '0'; + if (whole > (limit - digit) / 10) return error.DurationOverflow; + whole = whole * 10 + digit; + digits += 1; + } + + var fraction: u128 = 0; + var fraction_scale: u128 = 1; + if (i < raw.len and raw[i] == '.') { + i += 1; + while (i < raw.len and std.ascii.isDigit(raw[i])) : (i += 1) { + digits += 1; + // Nanosecond resolution never needs more than 18 decimal + // places for the largest supported unit; Go truncates below + // one nanosecond as well. + if (fraction_scale < 1_000_000_000_000_000_000) { + fraction = fraction * 10 + (raw[i] - '0'); + fraction_scale *= 10; + } + } + } + if (digits == 0) return error.InvalidDuration; + + var unit: ?Unit = null; + for (units) |candidate| { + if (std.mem.startsWith(u8, raw[i..], candidate.name)) { + unit = candidate; + i += candidate.name.len; + break; + } + } + const selected = unit orelse return error.InvalidDuration; + if (whole > limit / selected.nanoseconds) return error.DurationOverflow; + var component = whole * selected.nanoseconds; + component += (fraction * selected.nanoseconds) / fraction_scale; + if (component > limit - total) return error.DurationOverflow; + total += component; + } + if (tokens == 0) return error.InvalidDuration; + return @intCast(total); +} +pub fn parseDurationCeilSeconds(raw: []const u8) !u64 { + const nanoseconds = try parseDurationNanoseconds(raw); + return (nanoseconds + std.time.ns_per_s - 1) / std.time.ns_per_s; +} +pub fn parseGoDuration(raw: []const u8) !Io.Duration { + if (raw.len > 0 and raw[0] == '-') { + if (raw.len == 1 or raw[1] == '+' or raw[1] == '-') return error.InvalidDuration; + const negative_limit: u128 = @as(u128, std.math.maxInt(i64)) + 1; + const magnitude = try parseDurationMagnitude(raw[1..], negative_limit); + return .fromNanoseconds(-@as(i96, magnitude)); + } + return .fromNanoseconds(@intCast(try parseDurationNanoseconds(raw))); +} +pub fn effectiveHttpShutdownDuration(configured: Io.Duration) Io.Duration { + return if (configured.nanoseconds <= 0) .fromSeconds(30) else configured; +} +pub fn effectiveSubscribeSlowWindowNanoseconds(raw: []const u8) !u64 { + const default = server_mod.default_slow_window_ns; + if (raw.len > 0 and raw[0] == '-') { + // Go's DurationFlag accepts a negative duration, then subscribe's + // defaults replace every non-positive value. Parse the magnitude so + // malformed negative values still fail instead of becoming defaults. + _ = try parseDurationNanoseconds(raw[1..]); + return default; + } + const configured = try parseDurationNanoseconds(raw); + return if (configured == 0) default else configured; +} +test "subscribe slow window uses the upstream non-positive sentinel" { + const default = server_mod.default_slow_window_ns; + try std.testing.expectEqual(default, try effectiveSubscribeSlowWindowNanoseconds("0")); + try std.testing.expectEqual(default, try effectiveSubscribeSlowWindowNanoseconds("-250ms")); + try std.testing.expectEqual(@as(u64, 250 * std.time.ns_per_ms), try effectiveSubscribeSlowWindowNanoseconds("250ms")); + try std.testing.expectError(error.InvalidDuration, effectiveSubscribeSlowWindowNanoseconds("-garbage")); +} +test "shutdown durations preserve Go's signed CLI and HTTP default semantics" { + try std.testing.expectEqual(@as(i96, 5 * std.time.ns_per_s), (try parseGoDuration("5s")).nanoseconds); + try std.testing.expectEqual(@as(i96, -250 * std.time.ns_per_ms), (try parseGoDuration("-250ms")).nanoseconds); + try std.testing.expectEqual(@as(i96, 0), (try parseGoDuration("-0")).nanoseconds); + try std.testing.expectEqual(@as(i96, std.math.maxInt(i64)), (try parseGoDuration("9223372036854775807ns")).nanoseconds); + try std.testing.expectEqual(@as(i96, std.math.minInt(i64)), (try parseGoDuration("-9223372036854775808ns")).nanoseconds); + try std.testing.expectEqual(@as(i96, 30 * std.time.ns_per_s), effectiveHttpShutdownDuration(.zero).nanoseconds); + try std.testing.expectEqual(@as(i96, 30 * std.time.ns_per_s), effectiveHttpShutdownDuration(.fromSeconds(-1)).nanoseconds); + try std.testing.expectEqual(@as(i96, 3 * std.time.ns_per_s), effectiveHttpShutdownDuration(.fromSeconds(3)).nanoseconds); + try std.testing.expectError(error.InvalidDuration, parseGoDuration("-garbage")); + try std.testing.expectError(error.InvalidDuration, parseGoDuration("-+1s")); + try std.testing.expectError(error.DurationOverflow, parseGoDuration("9223372036854775808ns")); + try std.testing.expectError(error.DurationOverflow, parseGoDuration("-9223372036854775809ns")); +} +test "segment cache duration matches Go duration and ceil policy" { + try testing.expectEqual(@as(u64, 0), try parseDurationCeilSeconds("0")); + try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("500ms")); + try testing.expectEqual(@as(u64, 2), try parseDurationCeilSeconds("1500ms")); + try testing.expectEqual(@as(u64, 3600), try parseDurationCeilSeconds("1h")); + try testing.expectEqual(@as(u64, 5400), try parseDurationCeilSeconds("1h30m")); + try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds(".1s")); + try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("1us")); + try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("1µs")); + try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("-1s")); + try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("1")); + try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("wat")); + try testing.expectEqual(@as(u64, 1_500_000_000), try parseDurationNanoseconds("1.5s")); +} +pub fn parseNonNegativeUsize(raw: []const u8) !usize { + if (raw.len > 0 and raw[0] == '-') return error.BadArgs; + return std.fmt.parseInt(usize, raw, 10); +} +pub fn parseBool(raw: []const u8) !bool { + if (std.mem.eql(u8, raw, "1") or std.mem.eql(u8, raw, "t") or std.mem.eql(u8, raw, "T") or + std.mem.eql(u8, raw, "true") or std.mem.eql(u8, raw, "TRUE") or std.mem.eql(u8, raw, "True")) return true; + if (std.mem.eql(u8, raw, "0") or std.mem.eql(u8, raw, "f") or std.mem.eql(u8, raw, "F") or + std.mem.eql(u8, raw, "false") or std.mem.eql(u8, raw, "FALSE") or std.mem.eql(u8, raw, "False")) return false; + return error.InvalidBoolean; +} +test "boolean flags match Go strconv syntax" { + try std.testing.expect(try parseBool("true")); + try std.testing.expect(try parseBool("T")); + try std.testing.expect(try parseBool("1")); + try std.testing.expect(!(try parseBool("false"))); + try std.testing.expect(!(try parseBool("F"))); + try std.testing.expect(!(try parseBool("0"))); + try std.testing.expectError(error.InvalidBoolean, parseBool("yes")); +} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 9835964..ad7e75b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -31,6 +31,7 @@ const operational = @import("internal/runtime/operational.zig"); const inspect_all = @import("internal/runtime/inspect_all.zig"); const logging = @import("internal/runtime/logging.zig"); const environment = @import("internal/runtime/environment.zig"); +const cli = @import("internal/runtime/cli.zig"); const observability = @import("internal/runtime/observability.zig"); const build_options = @import("build_options"); @@ -178,153 +179,19 @@ test "merge discovery skip is explicit or automatic for either debug selection" /// Parse the non-negative subset of Go's time.ParseDuration used by upstream's /// duration flags. Like Go, fractional values are truncated below 1 ns. -fn parseDurationNanoseconds(raw: []const u8) !u64 { - return parseDurationMagnitude(raw, std.math.maxInt(i64)); -} -fn parseDurationMagnitude(raw: []const u8, limit: u128) !u64 { - if (raw.len == 0) return error.InvalidDuration; - var i: usize = 0; - if (raw[i] == '+') { - i += 1; - if (i == raw.len) return error.InvalidDuration; - } else if (raw[i] == '-') return error.InvalidDuration; - if (std.mem.eql(u8, raw[i..], "0")) return 0; - - const Unit = struct { name: []const u8, nanoseconds: u64 }; - const units = [_]Unit{ - .{ .name = "ns", .nanoseconds = 1 }, - .{ .name = "us", .nanoseconds = std.time.ns_per_us }, - .{ .name = "µs", .nanoseconds = std.time.ns_per_us }, - .{ .name = "μs", .nanoseconds = std.time.ns_per_us }, - .{ .name = "ms", .nanoseconds = std.time.ns_per_ms }, - .{ .name = "s", .nanoseconds = std.time.ns_per_s }, - .{ .name = "m", .nanoseconds = std.time.ns_per_min }, - .{ .name = "h", .nanoseconds = std.time.ns_per_hour }, - }; - var total: u128 = 0; - var tokens: usize = 0; - while (i < raw.len) : (tokens += 1) { - var whole: u128 = 0; - var digits: usize = 0; - while (i < raw.len and std.ascii.isDigit(raw[i])) : (i += 1) { - const digit = raw[i] - '0'; - if (whole > (limit - digit) / 10) return error.DurationOverflow; - whole = whole * 10 + digit; - digits += 1; - } - - var fraction: u128 = 0; - var fraction_scale: u128 = 1; - if (i < raw.len and raw[i] == '.') { - i += 1; - while (i < raw.len and std.ascii.isDigit(raw[i])) : (i += 1) { - digits += 1; - // Nanosecond resolution never needs more than 18 decimal - // places for the largest supported unit; Go truncates below - // one nanosecond as well. - if (fraction_scale < 1_000_000_000_000_000_000) { - fraction = fraction * 10 + (raw[i] - '0'); - fraction_scale *= 10; - } - } - } - if (digits == 0) return error.InvalidDuration; - - var unit: ?Unit = null; - for (units) |candidate| { - if (std.mem.startsWith(u8, raw[i..], candidate.name)) { - unit = candidate; - i += candidate.name.len; - break; - } - } - const selected = unit orelse return error.InvalidDuration; - if (whole > limit / selected.nanoseconds) return error.DurationOverflow; - var component = whole * selected.nanoseconds; - component += (fraction * selected.nanoseconds) / fraction_scale; - if (component > limit - total) return error.DurationOverflow; - total += component; - } - if (tokens == 0) return error.InvalidDuration; - return @intCast(total); -} -fn parseDurationCeilSeconds(raw: []const u8) !u64 { - const nanoseconds = try parseDurationNanoseconds(raw); - return (nanoseconds + std.time.ns_per_s - 1) / std.time.ns_per_s; -} /// Full signed Go duration surface used by urfave/cli DurationFlag. The /// non-negative parser above remains the implementation shared by flags whose /// downstream types cannot represent negative durations. -fn parseGoDuration(raw: []const u8) !Io.Duration { - if (raw.len > 0 and raw[0] == '-') { - if (raw.len == 1 or raw[1] == '+' or raw[1] == '-') return error.InvalidDuration; - const negative_limit: u128 = @as(u128, std.math.maxInt(i64)) + 1; - const magnitude = try parseDurationMagnitude(raw[1..], negative_limit); - return .fromNanoseconds(-@as(i96, magnitude)); - } - return .fromNanoseconds(@intCast(try parseDurationNanoseconds(raw))); -} /// net/http Server.New applies this default when its configured timeout is /// non-positive; the CLI's websocket client-drain duration does not. -fn effectiveHttpShutdownDuration(configured: Io.Duration) Io.Duration { - return if (configured.nanoseconds <= 0) .fromSeconds(30) else configured; -} -fn effectiveSubscribeSlowWindowNanoseconds(raw: []const u8) !u64 { - const default = server_mod.default_slow_window_ns; - if (raw.len > 0 and raw[0] == '-') { - // Go's DurationFlag accepts a negative duration, then subscribe's - // defaults replace every non-positive value. Parse the magnitude so - // malformed negative values still fail instead of becoming defaults. - _ = try parseDurationNanoseconds(raw[1..]); - return default; - } - const configured = try parseDurationNanoseconds(raw); - return if (configured == 0) default else configured; -} -test "subscribe slow window uses the upstream non-positive sentinel" { - const default = server_mod.default_slow_window_ns; - try std.testing.expectEqual(default, try effectiveSubscribeSlowWindowNanoseconds("0")); - try std.testing.expectEqual(default, try effectiveSubscribeSlowWindowNanoseconds("-250ms")); - try std.testing.expectEqual(@as(u64, 250 * std.time.ns_per_ms), try effectiveSubscribeSlowWindowNanoseconds("250ms")); - try std.testing.expectError(error.InvalidDuration, effectiveSubscribeSlowWindowNanoseconds("-garbage")); -} -test "shutdown durations preserve Go's signed CLI and HTTP default semantics" { - try std.testing.expectEqual(@as(i96, 5 * std.time.ns_per_s), (try parseGoDuration("5s")).nanoseconds); - try std.testing.expectEqual(@as(i96, -250 * std.time.ns_per_ms), (try parseGoDuration("-250ms")).nanoseconds); - try std.testing.expectEqual(@as(i96, 0), (try parseGoDuration("-0")).nanoseconds); - try std.testing.expectEqual(@as(i96, std.math.maxInt(i64)), (try parseGoDuration("9223372036854775807ns")).nanoseconds); - try std.testing.expectEqual(@as(i96, std.math.minInt(i64)), (try parseGoDuration("-9223372036854775808ns")).nanoseconds); - try std.testing.expectEqual(@as(i96, 30 * std.time.ns_per_s), effectiveHttpShutdownDuration(.zero).nanoseconds); - try std.testing.expectEqual(@as(i96, 30 * std.time.ns_per_s), effectiveHttpShutdownDuration(.fromSeconds(-1)).nanoseconds); - try std.testing.expectEqual(@as(i96, 3 * std.time.ns_per_s), effectiveHttpShutdownDuration(.fromSeconds(3)).nanoseconds); - try std.testing.expectError(error.InvalidDuration, parseGoDuration("-garbage")); - try std.testing.expectError(error.InvalidDuration, parseGoDuration("-+1s")); - try std.testing.expectError(error.DurationOverflow, parseGoDuration("9223372036854775808ns")); - try std.testing.expectError(error.DurationOverflow, parseGoDuration("-9223372036854775809ns")); -} -test "segment cache duration matches Go duration and ceil policy" { - const testing = std.testing; - try testing.expectEqual(@as(u64, 0), try parseDurationCeilSeconds("0")); - try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("500ms")); - try testing.expectEqual(@as(u64, 2), try parseDurationCeilSeconds("1500ms")); - try testing.expectEqual(@as(u64, 3600), try parseDurationCeilSeconds("1h")); - try testing.expectEqual(@as(u64, 5400), try parseDurationCeilSeconds("1h30m")); - try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds(".1s")); - try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("1us")); - try testing.expectEqual(@as(u64, 1), try parseDurationCeilSeconds("1µs")); - try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("-1s")); - try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("1")); - try testing.expectError(error.InvalidDuration, parseDurationCeilSeconds("wat")); - try testing.expectEqual(@as(u64, 1_500_000_000), try parseDurationNanoseconds("1.5s")); -} fn writeVersion(io: Io, args: []const []const u8) !void { if (args.len != 0) return error.BadArgs; @@ -348,11 +215,11 @@ fn runInspectSegment(allocator: std.mem.Allocator, io: Io, args: []const []const if (i >= args.len) return error.BadArgs; mode = parseBlocksMode(args[i]) orelse return error.BadArgs; } else if (std.mem.startsWith(u8, arg, "--blocks-truncate=")) { - truncate = try parseNonNegativeUsize(arg["--blocks-truncate=".len..]); + truncate = try cli.parseNonNegativeUsize(arg["--blocks-truncate=".len..]); } else if (std.mem.eql(u8, arg, "--blocks-truncate")) { i += 1; if (i >= args.len) return error.BadArgs; - truncate = try parseNonNegativeUsize(args[i]); + truncate = try cli.parseNonNegativeUsize(args[i]); } else if (std.mem.startsWith(u8, arg, "--")) { return error.BadArgs; } else if (path != null) { @@ -385,11 +252,11 @@ fn runInspectAll(allocator: std.mem.Allocator, io: Io, args: []const []const u8) } else if (std.mem.eql(u8, arg, "--skip-unsealed")) { skip_unsealed = true; } else if (std.mem.startsWith(u8, arg, "--collections-truncate=")) { - truncate = try parseNonNegativeUsize(arg["--collections-truncate=".len..]); + truncate = try cli.parseNonNegativeUsize(arg["--collections-truncate=".len..]); } else if (std.mem.eql(u8, arg, "--collections-truncate")) { i += 1; if (i >= args.len) return error.BadArgs; - truncate = try parseNonNegativeUsize(args[i]); + truncate = try cli.parseNonNegativeUsize(args[i]); } else { return error.BadArgs; } @@ -415,28 +282,8 @@ fn parseBlocksMode(raw: []const u8) ?operational.BlocksMode { return null; } -fn parseNonNegativeUsize(raw: []const u8) !usize { - if (raw.len > 0 and raw[0] == '-') return error.BadArgs; - return std.fmt.parseInt(usize, raw, 10); -} -fn parseBool(raw: []const u8) !bool { - if (std.mem.eql(u8, raw, "1") or std.mem.eql(u8, raw, "t") or std.mem.eql(u8, raw, "T") or - std.mem.eql(u8, raw, "true") or std.mem.eql(u8, raw, "TRUE") or std.mem.eql(u8, raw, "True")) return true; - if (std.mem.eql(u8, raw, "0") or std.mem.eql(u8, raw, "f") or std.mem.eql(u8, raw, "F") or - std.mem.eql(u8, raw, "false") or std.mem.eql(u8, raw, "FALSE") or std.mem.eql(u8, raw, "False")) return false; - return error.InvalidBoolean; -} -test "boolean flags match Go strconv syntax" { - try std.testing.expect(try parseBool("true")); - try std.testing.expect(try parseBool("T")); - try std.testing.expect(try parseBool("1")); - try std.testing.expect(!(try parseBool("false"))); - try std.testing.expect(!(try parseBool("F"))); - try std.testing.expect(!(try parseBool("0"))); - try std.testing.expectError(error.InvalidBoolean, parseBool("yes")); -} const BindAddress = struct { address: Io.net.IpAddress, @@ -691,9 +538,9 @@ pub fn main(init: std.process.Init.Minimal) !void { debug_addr_raw = if (configured.len == 0) null else configured; if (public_addr_raw == null) public_addr_raw = ":8080"; } else if (std.mem.startsWith(u8, arg, "--shutdown-timeout=")) { - shutdown_timeout = try parseGoDuration(arg["--shutdown-timeout=".len..]); + shutdown_timeout = try cli.parseGoDuration(arg["--shutdown-timeout=".len..]); } else if (std.mem.startsWith(u8, arg, "--client-drain-timeout=")) { - client_drain_timeout = try parseGoDuration(arg["--client-drain-timeout=".len..]); + client_drain_timeout = try cli.parseGoDuration(arg["--client-drain-timeout=".len..]); } else if (std.mem.startsWith(u8, arg, "--otel-service-name=")) { otel_service_name = arg["--otel-service-name=".len..]; } else if (std.mem.eql(u8, arg, "--otel-service-name")) { @@ -738,7 +585,7 @@ pub fn main(init: std.process.Init.Minimal) !void { } else if (std.mem.startsWith(u8, arg, "--relay-http=")) { relay_http = arg["--relay-http=".len..]; } else if (std.mem.startsWith(u8, arg, "--compaction-interval=")) { - compaction_interval_ns = try parseDurationNanoseconds(arg["--compaction-interval=".len..]); + compaction_interval_ns = try cli.parseDurationNanoseconds(arg["--compaction-interval=".len..]); } else if (std.mem.startsWith(u8, arg, "--compaction-tombstone-cap=")) { compaction_tombstone_cap = try std.fmt.parseInt(usize, arg["--compaction-tombstone-cap=".len..], 10); } else if (std.mem.startsWith(u8, arg, "--compaction-rewrite-workers=")) { @@ -747,7 +594,7 @@ pub fn main(init: std.process.Init.Minimal) !void { const seconds = try std.fmt.parseInt(u64, arg["--retry-interval=".len..], 10); retry_interval_ns = try std.math.mul(u64, seconds, std.time.ns_per_s); } else if (std.mem.startsWith(u8, arg, "--failed-repo-retry-interval=")) { - retry_interval_ns = try parseDurationNanoseconds(arg["--failed-repo-retry-interval=".len..]); + retry_interval_ns = try cli.parseDurationNanoseconds(arg["--failed-repo-retry-interval=".len..]); } else if (std.mem.startsWith(u8, arg, "--failed-repo-retry-workers=")) { const configured = try std.fmt.parseInt(usize, arg["--failed-repo-retry-workers=".len..], 10); retry_workers = if (configured == 0) retry_mod.default_workers else configured; @@ -755,12 +602,12 @@ pub fn main(init: std.process.Init.Minimal) !void { const configured = try std.fmt.parseInt(usize, arg["--failed-repo-retry-host-workers=".len..], 10); retry_host_workers = if (configured == 0) retry_mod.default_host_workers else configured; } else if (std.mem.startsWith(u8, arg, "--failed-repo-retry-max-delay=")) { - const configured = try parseDurationNanoseconds(arg["--failed-repo-retry-max-delay=".len..]); + const configured = try cli.parseDurationNanoseconds(arg["--failed-repo-retry-max-delay=".len..]); retry_max_delay_ns = if (configured == 0) retry_mod.default_max_delay_ns else configured; } else if (std.mem.startsWith(u8, arg, "--segment-cache-max-age=")) { - segment_cache_max_age_s = try parseDurationCeilSeconds(arg["--segment-cache-max-age=".len..]); + segment_cache_max_age_s = try cli.parseDurationCeilSeconds(arg["--segment-cache-max-age=".len..]); } else if (std.mem.startsWith(u8, arg, "--cursor-lookback=")) { - cursor_lookback_ns = try parseDurationNanoseconds(arg["--cursor-lookback=".len..]); + cursor_lookback_ns = try cli.parseDurationNanoseconds(arg["--cursor-lookback=".len..]); } else if (std.mem.startsWith(u8, arg, "--subscribe-read-log-retention-bytes=")) { const configured = try std.fmt.parseInt(i64, arg["--subscribe-read-log-retention-bytes=".len..], 10); subscribe_read_log_retention_bytes = if (configured <= 0) @@ -774,7 +621,7 @@ pub fn main(init: std.process.Init.Minimal) !void { else std.math.cast(usize, configured) orelse return error.Overflow; } else if (std.mem.startsWith(u8, arg, "--subscribe-slow-window=")) { - subscribe_slow_window_ns = try effectiveSubscribeSlowWindowNanoseconds(arg["--subscribe-slow-window=".len..]); + subscribe_slow_window_ns = try cli.effectiveSubscribeSlowWindowNanoseconds(arg["--subscribe-slow-window=".len..]); } else if (std.mem.startsWith(u8, arg, "--subscribe-slow-min-rate=")) { const configured = try std.fmt.parseFloat(f64, arg["--subscribe-slow-min-rate=".len..]); subscribe_slow_min_rate = if (configured <= 0) server_mod.default_slow_min_rate else configured; @@ -834,13 +681,13 @@ pub fn main(init: std.process.Init.Minimal) !void { } else if (std.mem.eql(u8, arg, "--skip-merge-discovery")) { skip_merge_discovery = true; } else if (std.mem.startsWith(u8, arg, "--skip-merge-discovery=")) { - skip_merge_discovery = try parseBool(arg["--skip-merge-discovery=".len..]); + skip_merge_discovery = try cli.parseBool(arg["--skip-merge-discovery=".len..]); } else if (std.mem.eql(u8, arg, "--no-verify")) { do_verify = false; } else if (std.mem.eql(u8, arg, "--disable-repo-action-rate-limits")) { repo_action_rate_limits = false; } else if (std.mem.startsWith(u8, arg, "--disable-repo-action-rate-limits=")) { - repo_action_rate_limits = !(try parseBool(arg["--disable-repo-action-rate-limits=".len..])); + repo_action_rate_limits = !(try cli.parseBool(arg["--disable-repo-action-rate-limits=".len..])); } else if (std.mem.eql(u8, arg, "--stdout")) { to_stdout = true; } else if (std.mem.eql(u8, arg, "serve") and !explicit_serve) { @@ -1060,7 +907,7 @@ pub fn main(init: std.process.Init.Minimal) !void { .max_conn = 4096, .max_message_size = 10_000_000, .websocket_shutdown_grace = client_drain_timeout, - .http_shutdown_grace = effectiveHttpShutdownDuration(shutdown_timeout), + .http_shutdown_grace = cli.effectiveHttpShutdownDuration(shutdown_timeout), // Match upstream v1's coder/websocket defaults exactly. The handler // declines this per connection for v2 and custom-zstd subscribers. .compression = .{ @@ -1089,7 +936,7 @@ pub fn main(init: std.process.Init.Minimal) !void { .max_conn = 256, .max_message_size = 1024, .websocket_shutdown_grace = client_drain_timeout, - .http_shutdown_grace = effectiveHttpShutdownDuration(shutdown_timeout), + .http_shutdown_grace = cli.effectiveHttpShutdownDuration(shutdown_timeout), .compression = null, .handshake = .{ .max_size = 16 * 1024, .max_headers = 64 }, }) @@ -1375,7 +1222,7 @@ fn shutdownTracing(runtime: *observability.Runtime, configured: Io.Duration) !vo } fn shutdownTimeoutMillis(configured: Io.Duration) u64 { - const timeout = effectiveHttpShutdownDuration(configured); + const timeout = cli.effectiveHttpShutdownDuration(configured); return @intCast(@divFloor(timeout.nanoseconds + std.time.ns_per_ms - 1, std.time.ns_per_ms)); }