milestone 16: behavioral fixes for silent failures, locks, counters and the query log
This commit is contained in:
@@ -147,9 +147,10 @@ pub fn applyDelete(state: *server.WebState, io: std.Io, id: i64) ?Failure {
|
||||
///
|
||||
/// After the reload and never part of the response: the row is gone and the
|
||||
/// snapshot has stopped enforcing the list, so bytes still on disk are not a
|
||||
/// failed delete. `Manager.pruneOrphans` takes the manager's writer lock, which
|
||||
/// the reload above has already taken and released — nothing here holds it, and
|
||||
/// `state.config_lock` was released before either.
|
||||
/// failed delete. `Manager.pruneOrphans` takes the manager's refresh lock and
|
||||
/// then its writer lock; the reload above has already taken and released the
|
||||
/// writer lock — nothing here holds either, and `state.config_lock` was
|
||||
/// released before both.
|
||||
fn pruneFiles(state: *server.WebState, io: std.Io) void {
|
||||
const manager = state.manager orelse return;
|
||||
manager.pruneOrphans(io) catch |err| {
|
||||
|
||||
@@ -112,9 +112,16 @@ pub fn stream(
|
||||
if (hub.overflowed(io, id)) break;
|
||||
|
||||
const wake = hub.wait(io, id, heartbeat_interval) catch return;
|
||||
if (wake == .timeout) {
|
||||
try w.writeAll(heartbeat);
|
||||
try response.flush();
|
||||
switch (wake) {
|
||||
// Ruling 11 of milestone 16: the server is shutting down. Returning
|
||||
// without `end` leaves the response unterminated, which is what a
|
||||
// shutdown is; the client reconnects or gives up on its own.
|
||||
.closed => return,
|
||||
.timeout => {
|
||||
try w.writeAll(heartbeat);
|
||||
try response.flush();
|
||||
},
|
||||
.ready => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
//! the next start runs — before a single row is written, so a settings PUT
|
||||
//! cannot leave a configuration the server would refuse to boot from.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -283,6 +284,25 @@ pub fn applyPut(
|
||||
.fail => |failure| return .{ .fail = failure },
|
||||
};
|
||||
|
||||
// Ruling 18 of milestone 16: argon2id at m=19 MiB is the longest thing this
|
||||
// handler does, and its input is the parsed patch alone — nothing under the
|
||||
// lock. Hashing inside the lock stalled every settings read and every other
|
||||
// mutation for its duration. The login path already hashes unlocked
|
||||
// (auth.zig), and `LiveHash`'s generation check closes the install race.
|
||||
const password = newPassword(patch);
|
||||
var new_hash: []const u8 = "";
|
||||
if (password) |plain| {
|
||||
if (plain.len > auth.max_password_len) {
|
||||
return .{ .fail = .{ .invalid = "web.password is too long" } };
|
||||
}
|
||||
const buf = try arena.alloc(u8, hash_buf_len);
|
||||
new_hash = hashPassword(io, arena, plain, buf) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Canceled => return .{ .fail = .{ .unavailable = "shutting down" } },
|
||||
else => return .{ .fail = .{ .internal = error.Unexpected } },
|
||||
};
|
||||
}
|
||||
|
||||
state.config_lock.lockUncancelable(io);
|
||||
defer state.config_lock.unlock(io);
|
||||
|
||||
@@ -300,21 +320,10 @@ pub fn applyPut(
|
||||
) } };
|
||||
}
|
||||
|
||||
// The password never becomes a row. It is hashed here and the hash is what
|
||||
// the merged configuration — and therefore the settings table — carries.
|
||||
const password = newPassword(patch);
|
||||
// The password never becomes a row: the hash made above is what the merged
|
||||
// configuration — and therefore the settings table — carries.
|
||||
const previous_hash = cfg.web.password_hash;
|
||||
if (password) |plain| {
|
||||
if (plain.len > auth.max_password_len) {
|
||||
return .{ .fail = .{ .invalid = "web.password is too long" } };
|
||||
}
|
||||
const buf = try arena.alloc(u8, hash_buf_len);
|
||||
cfg.web.password_hash = hashPassword(io, arena, plain, buf) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.Canceled => return .{ .fail = .{ .unavailable = "shutting down" } },
|
||||
else => return .{ .fail = .{ .internal = error.Unexpected } },
|
||||
};
|
||||
}
|
||||
if (password != null) cfg.web.password_hash = new_hash;
|
||||
cfg.web.password = "";
|
||||
|
||||
if (try problem(arena, cfg)) |text| return .{ .fail = .{ .invalid = text } };
|
||||
@@ -374,6 +383,7 @@ fn problem(arena: Allocator, cfg: model.Config) error{OutOfMemory}!?[]const u8 {
|
||||
/// password set through the API and one set through a config import produce the
|
||||
/// same kind of hash.
|
||||
fn hashPassword(io: std.Io, gpa: Allocator, password: []const u8, buf: []u8) ![]const u8 {
|
||||
hash_stall.park(io);
|
||||
return std.crypto.pwhash.argon2.strHash(password, .{
|
||||
.allocator = gpa,
|
||||
.params = .owasp_2id,
|
||||
@@ -390,6 +400,48 @@ fn hashPassword(io: std.Io, gpa: Allocator, password: []const u8, buf: []u8) ![]
|
||||
};
|
||||
}
|
||||
|
||||
/// Holds a hash still so a test can prove another request runs beside it. The
|
||||
/// hash finishing on its own would prove nothing: before ruling 18 a settings
|
||||
/// GET also completed, it merely waited out the hash first. The storage exists
|
||||
/// in a test build only, and `park` reduces to nothing everywhere else — the
|
||||
/// rotation seam's shape (logging.zig).
|
||||
const hash_stall = if (builtin.is_test) struct {
|
||||
var armed: bool = false;
|
||||
var parked: std.Io.Event = .unset;
|
||||
var release: std.Io.Event = .unset;
|
||||
|
||||
fn park(io: std.Io) void {
|
||||
if (!armed) return;
|
||||
parked.set(io);
|
||||
release.waitUncancelable(io);
|
||||
}
|
||||
} else struct {
|
||||
fn park(io: std.Io) void {
|
||||
_ = io;
|
||||
}
|
||||
};
|
||||
|
||||
/// The seam's controls, for the test that proves a settings read runs beside a
|
||||
/// hash in flight. Present in a test build only.
|
||||
pub const hash_stall_control = if (builtin.is_test) struct {
|
||||
pub fn arm() void {
|
||||
hash_stall.parked = .unset;
|
||||
hash_stall.release = .unset;
|
||||
hash_stall.armed = true;
|
||||
}
|
||||
|
||||
/// Returns once a hash is parked on the seam.
|
||||
pub fn waitParked(io: std.Io) void {
|
||||
hash_stall.parked.waitUncancelable(io);
|
||||
}
|
||||
|
||||
/// Lets the parked hash finish and disarms the seam for the next test.
|
||||
pub fn release(io: std.Io) void {
|
||||
hash_stall.armed = false;
|
||||
hash_stall.release.set(io);
|
||||
}
|
||||
} else struct {};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user