upstream: a diagnostics episode follows health, and a peer fault carries its cause
Gates / frontend (push) Successful in 2m5s
Gates / test (push) Successful in 2m43s
Gates / test-aarch64 (push) Successful in 8m19s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 30m43s
Gates / frontend (push) Successful in 2m5s
Gates / test (push) Successful in 2m43s
Gates / test-aarch64 (push) Successful in 8m19s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 30m43s
This commit is contained in:
+90
-86
@@ -186,7 +186,7 @@ pub const DotClient = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(self: *DotClient) transport.Client {
|
||||
pub fn leaf(self: *DotClient) transport.Leaf {
|
||||
return .{ .ptr = self, .exchangeFn = exchangeFn };
|
||||
}
|
||||
|
||||
@@ -199,12 +199,8 @@ pub const DotClient = struct {
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
selected: *?[]const u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
) transport.LeafError!transport.Outcome {
|
||||
const self: *DotClient = @ptrCast(@alignCast(ptr));
|
||||
// The endpoint outlives the client, so the borrow is safe for the whole
|
||||
// query. Set before the attempt: a failure names this resolver too.
|
||||
selected.* = self.endpoint.url;
|
||||
return self.exchange(io, query, response_buf);
|
||||
}
|
||||
|
||||
@@ -222,24 +218,24 @@ pub const DotClient = struct {
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
) transport.LeafError!transport.Outcome {
|
||||
// The length prefix is 16-bit, so a longer query cannot be framed. No
|
||||
// listener in this process can produce one; a caller that does gets a
|
||||
// local error rather than a silently truncated frame.
|
||||
if (query.len > transport.max_message_len) return error.BufferTooSmall;
|
||||
|
||||
const reused = self.session != null;
|
||||
if (!reused) try self.dial(io);
|
||||
if (!reused) if (try self.dial(io)) |dial_fault| return .{ .fault = dial_fault };
|
||||
|
||||
const failure = switch (self.transact(query, response_buf)) {
|
||||
.ok => |reply| return reply,
|
||||
.ok => |reply| return .{ .reply = reply },
|
||||
.failed => |failure| failure,
|
||||
};
|
||||
|
||||
switch (retryDecision(reused, failure.received_any, failure.cause)) {
|
||||
.final => {
|
||||
self.close(io);
|
||||
return transport.mapPhase(failure.cause, failure.phase);
|
||||
return .{ .fault = try transport.faultOrLocal(failure.cause, failure.phase) };
|
||||
},
|
||||
.retry => {},
|
||||
}
|
||||
@@ -249,38 +245,41 @@ pub const DotClient = struct {
|
||||
// `reuse_recoveries` is what makes the churn visible.
|
||||
log.debug("{f}", .{self.diagnose(.{ .stale_session = failure.cause })});
|
||||
self.close(io);
|
||||
try self.dial(io);
|
||||
if (try self.dial(io)) |dial_fault| return .{ .fault = dial_fault };
|
||||
|
||||
switch (self.transact(query, response_buf)) {
|
||||
.ok => |reply| {
|
||||
if (self.reuse_recoveries) |counter| _ = counter.fetchAdd(1, .monotonic);
|
||||
return reply;
|
||||
return .{ .reply = reply };
|
||||
},
|
||||
// The retry's outcome is the exchange's outcome: one redial, never
|
||||
// two.
|
||||
.failed => |retried| {
|
||||
self.close(io);
|
||||
return transport.mapPhase(retried.cause, retried.phase);
|
||||
return .{ .fault = try transport.faultOrLocal(retried.cause, retried.phase) };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a session and leaves it in `self.session`, or leaves `self.session`
|
||||
/// null and returns the classified failure. No partially initialized
|
||||
/// session ever survives this call.
|
||||
fn dial(self: *DotClient, io: std.Io) transport.ExchangeError!void {
|
||||
/// null and returns the classified fault. `null` means a session is open. No
|
||||
/// partially initialized session ever survives this call.
|
||||
fn dial(self: *DotClient, io: std.Io) transport.LeafError!?transport.Fault {
|
||||
std.debug.assert(self.session == null);
|
||||
|
||||
const address = resolveAddress(self.endpoint) catch |err| {
|
||||
// A host that is not an IP literal is a config error, and
|
||||
// `resolveAddress` has already folded the parse failure into the
|
||||
// classification, so the cause it carries is the classification.
|
||||
log.warn("{f}", .{self.diagnose(.not_an_ip_literal)});
|
||||
return err;
|
||||
return .{ .kind = err, .cause = err };
|
||||
};
|
||||
|
||||
try self.ensureBundle(io);
|
||||
if (try self.ensureBundle(io)) |bundle_fault| return bundle_fault;
|
||||
|
||||
const stream = address.connect(io, .{ .mode = .stream }) catch |err| {
|
||||
log.debug("{f}", .{self.diagnose(.{ .connect_failed = err })});
|
||||
return transport.mapPhase(err, error.ConnectFailed);
|
||||
return try transport.faultOrLocal(err, error.ConnectFailed);
|
||||
};
|
||||
|
||||
// Emplaced before the handshake, never built beside it and copied in:
|
||||
@@ -314,8 +313,9 @@ pub const DotClient = struct {
|
||||
.verify_name = self.verify_name,
|
||||
.cause = cause,
|
||||
} })});
|
||||
return transport.mapPhase(cause, error.TlsFailed);
|
||||
return try transport.faultOrLocal(cause, error.TlsFailed);
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
/// One query and one reply on the open session, with enough detail on
|
||||
@@ -380,17 +380,17 @@ pub const DotClient = struct {
|
||||
/// cancellation into `error.CertificateBundleLoadFailure`. That name cannot
|
||||
/// tell an `error.OutOfMemory` from a corrupt PEM file, and the first is a
|
||||
/// local resource failure that must not count against the upstream's
|
||||
/// health. Scanning here keeps the concrete error for `transport.mapPhase`.
|
||||
fn ensureBundle(self: *DotClient, io: std.Io) transport.ExchangeError!void {
|
||||
/// health. Scanning here keeps the concrete error for the fault.
|
||||
fn ensureBundle(self: *DotClient, io: std.Io) transport.LeafError!?transport.Fault {
|
||||
{
|
||||
try self.bundle_lock.lockShared(io);
|
||||
defer self.bundle_lock.unlockShared(io);
|
||||
if (self.bundle.map.count() != 0) return;
|
||||
if (self.bundle.map.count() != 0) return null;
|
||||
}
|
||||
|
||||
try self.bundle_lock.lock(io);
|
||||
defer self.bundle_lock.unlock(io);
|
||||
if (self.bundle.map.count() != 0) return;
|
||||
if (self.bundle.map.count() != 0) return null;
|
||||
|
||||
// A partial scan leaves entries in `map`, which the check above would
|
||||
// read as "already loaded". Reset so the next exchange scans again.
|
||||
@@ -398,8 +398,9 @@ pub const DotClient = struct {
|
||||
self.bundle.deinit(self.gpa);
|
||||
self.bundle.* = .empty;
|
||||
log.warn("{f}", .{self.diagnose(.{ .bundle_load_failed = err })});
|
||||
return transport.mapPhase(err, error.TlsFailed);
|
||||
return try transport.faultOrLocal(err, error.TlsFailed);
|
||||
};
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -642,40 +643,39 @@ fn stubStream(
|
||||
|
||||
test "the handshake unwrap keeps a cancelled read out of the peer fault group" {
|
||||
var stream = stubStream(error.Canceled, null, null);
|
||||
const mapped = transport.mapPhase(concreteHandshake(&stream, error.ReadFailed), error.TlsFailed);
|
||||
try testing.expectEqual(transport.ExchangeError.Canceled, mapped);
|
||||
try testing.expectEqual(transport.Group.cancellation, transport.group(mapped));
|
||||
try testing.expectError(
|
||||
error.Canceled,
|
||||
transport.faultOrLocal(concreteHandshake(&stream, error.ReadFailed), error.TlsFailed),
|
||||
);
|
||||
}
|
||||
|
||||
test "the handshake unwrap keeps a local resource write failure out of the peer fault group" {
|
||||
var stream = stubStream(null, error.SystemResources, null);
|
||||
const mapped = transport.mapPhase(concreteHandshake(&stream, error.WriteFailed), error.TlsFailed);
|
||||
try testing.expectEqual(transport.ExchangeError.SystemResources, mapped);
|
||||
try testing.expectEqual(transport.Group.local_resource, transport.group(mapped));
|
||||
try testing.expectError(
|
||||
error.SystemResources,
|
||||
transport.faultOrLocal(concreteHandshake(&stream, error.WriteFailed), error.TlsFailed),
|
||||
);
|
||||
}
|
||||
|
||||
test "the handshake unwrap reports a peer side cause as a TLS fault" {
|
||||
var reset = stubStream(error.ConnectionResetByPeer, null, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(concreteHandshake(&reset, error.ReadFailed), error.TlsFailed),
|
||||
);
|
||||
const read_failed = try transport.faultOrLocal(concreteHandshake(&reset, error.ReadFailed), error.TlsFailed);
|
||||
try testing.expectEqual(transport.PeerFault.TlsFailed, read_failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionResetByPeer), read_failed.cause);
|
||||
|
||||
var refused = stubStream(null, error.ConnectionRefused, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(concreteHandshake(&refused, error.WriteFailed), error.TlsFailed),
|
||||
);
|
||||
const write_failed = try transport.faultOrLocal(concreteHandshake(&refused, error.WriteFailed), error.TlsFailed);
|
||||
try testing.expectEqual(transport.PeerFault.TlsFailed, write_failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionRefused), write_failed.cause);
|
||||
}
|
||||
|
||||
test "the handshake unwrap reports a TLS fault when no cause was stored" {
|
||||
var stream = stubStream(null, null, null);
|
||||
try testing.expectEqual(error.ReadFailed, concreteHandshake(&stream, error.ReadFailed));
|
||||
try testing.expectEqual(error.WriteFailed, concreteHandshake(&stream, error.WriteFailed));
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(concreteHandshake(&stream, error.ReadFailed), error.TlsFailed),
|
||||
);
|
||||
const failed = try transport.faultOrLocal(concreteHandshake(&stream, error.ReadFailed), error.TlsFailed);
|
||||
try testing.expectEqual(transport.PeerFault.TlsFailed, failed.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ReadFailed), failed.cause);
|
||||
}
|
||||
|
||||
test "the handshake unwrap passes other errors through untouched" {
|
||||
@@ -685,42 +685,52 @@ test "the handshake unwrap passes other errors through untouched" {
|
||||
concreteHandshake(&stream, error.CertificateExpired),
|
||||
);
|
||||
try testing.expectEqual(error.Canceled, concreteHandshake(&stream, error.Canceled));
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(concreteHandshake(&stream, error.CertificateExpired), error.TlsFailed),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.Canceled,
|
||||
transport.mapPhase(concreteHandshake(&stream, error.Canceled), error.TlsFailed),
|
||||
const expired = try transport.faultOrLocal(concreteHandshake(&stream, error.CertificateExpired), error.TlsFailed);
|
||||
try testing.expectEqual(transport.PeerFault.TlsFailed, expired.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.CertificateExpired), expired.cause);
|
||||
try testing.expectError(
|
||||
error.Canceled,
|
||||
transport.faultOrLocal(concreteHandshake(&stream, error.Canceled), error.TlsFailed),
|
||||
);
|
||||
}
|
||||
|
||||
/// What `exchange` would return for a failure `transact` reported.
|
||||
fn mappedFailure(outcome: Transact) transport.ExchangeError {
|
||||
return transport.mapPhase(outcome.failed.cause, outcome.failed.phase);
|
||||
/// The fault `exchange` returns for a failure `transact` reported. A local cause leaves this as an
|
||||
/// error, which is what the tests of those causes assert instead.
|
||||
fn mappedFailure(outcome: Transact) transport.LeafError!transport.Fault {
|
||||
return transport.faultOrLocal(outcome.failed.cause, outcome.failed.phase);
|
||||
}
|
||||
|
||||
test "the send and receive unwraps prefer the stored cause" {
|
||||
var send = stubStream(null, error.Canceled, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.Canceled,
|
||||
mappedFailure(sendFailed(&send, error.WriteFailed)),
|
||||
);
|
||||
try testing.expectError(error.Canceled, mappedFailure(sendFailed(&send, error.WriteFailed)));
|
||||
|
||||
// The TLS client's own error wins over the socket reader's.
|
||||
var receive = stubStream(error.ConnectionResetByPeer, null, error.TlsAlert);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ReceiveFailed,
|
||||
mappedFailure(receiveFailed(&receive, error.ReadFailed, false)),
|
||||
);
|
||||
const received = try mappedFailure(receiveFailed(&receive, error.ReadFailed, false));
|
||||
try testing.expectEqual(transport.PeerFault.ReceiveFailed, received.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.TlsAlert), received.cause);
|
||||
|
||||
var socket = stubStream(error.SystemResources, null, null);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.SystemResources,
|
||||
try testing.expectError(
|
||||
error.SystemResources,
|
||||
mappedFailure(receiveFailed(&socket, error.ReadFailed, true)),
|
||||
);
|
||||
}
|
||||
|
||||
test "a transact failure becomes a fault carrying its concrete cause" {
|
||||
var stream = stubStream(null, error.ConnectionResetByPeer, null);
|
||||
|
||||
const sent = try mappedFailure(sendFailed(&stream, error.WriteFailed));
|
||||
try testing.expectEqual(transport.PeerFault.SendFailed, sent.kind);
|
||||
try testing.expectEqual(@as(anyerror, error.ConnectionResetByPeer), sent.cause);
|
||||
|
||||
var text: [64]u8 = undefined;
|
||||
try testing.expectEqualStrings(
|
||||
"SendFailed (cause ConnectionResetByPeer)",
|
||||
try std.fmt.bufPrint(&text, "{f}", .{sent}),
|
||||
);
|
||||
}
|
||||
|
||||
test "a send failure is always pre-first-byte, and a receive failure reports what it read" {
|
||||
// The retry rule reads `received_any`, so where it comes from is part of the
|
||||
// contract rather than an incidental field: nothing is read before the query
|
||||
@@ -804,14 +814,13 @@ test "a validation failure is final and its phase survives the mapping" {
|
||||
};
|
||||
for (outcomes) |outcome| {
|
||||
try testing.expectEqual(RetryDecision.final, retryDecision(true, true, outcome.cause));
|
||||
try testing.expectEqual(
|
||||
@as(transport.ExchangeError, outcome.phase),
|
||||
mappedFailure(.{ .failed = .{
|
||||
.cause = outcome.cause,
|
||||
.phase = outcome.phase,
|
||||
.received_any = true,
|
||||
} }),
|
||||
);
|
||||
const failed = try mappedFailure(.{ .failed = .{
|
||||
.cause = outcome.cause,
|
||||
.phase = outcome.phase,
|
||||
.received_any = true,
|
||||
} });
|
||||
try testing.expectEqual(outcome.phase, failed.kind);
|
||||
try testing.expectEqual(outcome.cause, failed.cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,26 +839,21 @@ test "a CA bundle scan failure keeps local resource errors out of the peer fault
|
||||
transport.Group.local_resource,
|
||||
transport.group(transport.mapPhase(err, error.TlsFailed)),
|
||||
);
|
||||
try testing.expectError(err, transport.faultOrLocal(err, error.TlsFailed));
|
||||
}
|
||||
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.Canceled,
|
||||
transport.mapPhase(error.Canceled, error.TlsFailed),
|
||||
);
|
||||
try testing.expectError(error.Canceled, transport.faultOrLocal(error.Canceled, error.TlsFailed));
|
||||
|
||||
// A missing or corrupt bundle is not this process running out of anything,
|
||||
// so it stays a TLS fault.
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(error.FileNotFound, error.TlsFailed),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.TlsFailed,
|
||||
transport.mapPhase(error.MissingEndCertificateMarker, error.TlsFailed),
|
||||
);
|
||||
// so it stays a TLS fault, and the cause names which of the two it was.
|
||||
for ([_]anyerror{ error.FileNotFound, error.MissingEndCertificateMarker }) |err| {
|
||||
const failed = try transport.faultOrLocal(err, error.TlsFailed);
|
||||
try testing.expectEqual(transport.PeerFault.TlsFailed, failed.kind);
|
||||
try testing.expectEqual(err, failed.cause);
|
||||
}
|
||||
}
|
||||
|
||||
test "DotClient satisfies the Client interface" {
|
||||
test "DotClient satisfies the Leaf interface" {
|
||||
const gpa = testing.allocator;
|
||||
|
||||
const buffer = try gpa.alloc(u8, 4 * tls.Client.min_buffer_len);
|
||||
@@ -878,7 +882,7 @@ test "DotClient satisfies the Client interface" {
|
||||
dot.close(undefined);
|
||||
try testing.expect(dot.session == null);
|
||||
|
||||
const iface: transport.Client = dot.client();
|
||||
const iface: transport.Leaf = dot.leaf();
|
||||
try testing.expectEqual(@as(*anyopaque, @ptrCast(&dot)), iface.ptr);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user