server: a query that finds every permit taken waits for one instead of failing, the budget is 64, permit and upstream turns wait on a futex with one thread each, the thread pool has no task limit, and transparent huge pages are off for the process
This commit is contained in:
@@ -527,9 +527,21 @@ fn askOn(reader: *std.Io.Reader, writer: *std.Io.Writer) ![]const u8 {
|
||||
return reader.take(len);
|
||||
}
|
||||
|
||||
fn waitForWaiters(permits: *query_budget.QueryBudget, io: std.Io, count: u64) !void {
|
||||
const tick: std.Io.Clock.Duration = .{ .raw = .fromMilliseconds(1), .clock = .awake };
|
||||
while (permits.snapshot().waiting != count) try tick.sleep(io);
|
||||
}
|
||||
|
||||
fn sendQuery(writer: *std.Io.Writer) !void {
|
||||
try writer.writeAll(&transport.framePrefix(@intCast(query_bytes.len)));
|
||||
try writer.writeAll(query_bytes);
|
||||
try writer.flush();
|
||||
}
|
||||
|
||||
/// One answered query, then the connection idles with every permit back in the
|
||||
/// budget; a second query that finds none taken ends the connection unanswered.
|
||||
fn keepAliveThenStarved(io: std.Io, address: net.IpAddress, permits: *query_budget.QueryBudget) anyerror!void {
|
||||
/// budget; a second query that finds every permit taken waits, and is answered
|
||||
/// once one is released.
|
||||
fn keepAliveThenWait(io: std.Io, address: net.IpAddress, permits: *query_budget.QueryBudget) anyerror!void {
|
||||
var stream = try address.connect(io, .{ .mode = .stream });
|
||||
defer stream.close(io);
|
||||
|
||||
@@ -546,17 +558,19 @@ fn keepAliveThenStarved(io: std.Io, address: net.IpAddress, permits: *query_budg
|
||||
while (permits.taken(io) != 0) try tick.sleep(io);
|
||||
|
||||
const held = permits.tryAcquire(io).?;
|
||||
defer permits.release(io, held);
|
||||
var released = false;
|
||||
defer if (!released) permits.release(io, held);
|
||||
|
||||
try writer.interface.writeAll(&transport.framePrefix(@intCast(query_bytes.len)));
|
||||
try writer.interface.writeAll(query_bytes);
|
||||
try writer.interface.flush();
|
||||
try sendQuery(&writer.interface);
|
||||
try waitForWaiters(permits, io, 1);
|
||||
permits.release(io, held);
|
||||
released = true;
|
||||
|
||||
var sink: [64]u8 = undefined;
|
||||
if (try reader.interface.readSliceShort(&sink) != 0) return error.TestUnexpectedBytes;
|
||||
const len = transport.parsePrefix((try reader.interface.takeArray(transport.prefix_len)).*);
|
||||
try expectAnswersQuery(try reader.interface.take(len));
|
||||
}
|
||||
|
||||
test "a keep-alive connection holds no permit between queries, and a query without one ends it" {
|
||||
test "a keep-alive connection holds no permit between queries, and a query without one waits for it" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
const gpa = testing.allocator;
|
||||
@@ -583,11 +597,122 @@ test "a keep-alive connection holds no permit between queries, and a query witho
|
||||
};
|
||||
}
|
||||
|
||||
try bounded(io, keepAliveThenStarved, .{ io, server_address, &permits });
|
||||
try bounded(io, keepAliveThenWait, .{ io, server_address, &permits });
|
||||
|
||||
try testing.expectEqual(@as(u64, 1), server.core.stats.dropped_no_permit.load(.monotonic));
|
||||
try testing.expectEqual(@as(u64, 1), permits.snapshot().waits);
|
||||
try testing.expectEqual(@as(u64, 0), permits.snapshot().wait_timeouts);
|
||||
try testing.expectEqual(@as(u64, 0), server.core.stats.connection_errors.load(.monotonic));
|
||||
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
|
||||
try testing.expectEqual(@as(u64, 2), h.stats.queries.load(.monotonic));
|
||||
}
|
||||
|
||||
/// A query that waits out the whole idle budget for a permit: the server ends
|
||||
/// the connection without an answer.
|
||||
fn waitOutlastsIdle(io: std.Io, address: net.IpAddress) anyerror!void {
|
||||
var stream = try address.connect(io, .{ .mode = .stream });
|
||||
defer stream.close(io);
|
||||
|
||||
var read_buf: [1024]u8 = undefined;
|
||||
var write_buf: [1024]u8 = undefined;
|
||||
var reader = stream.reader(io, &read_buf);
|
||||
var writer = stream.writer(io, &write_buf);
|
||||
|
||||
try sendQuery(&writer.interface);
|
||||
var sink: [64]u8 = undefined;
|
||||
if (try reader.interface.readSliceShort(&sink) != 0) return error.TestUnexpectedBytes;
|
||||
}
|
||||
|
||||
test "a stream query whose permit wait outlasts the idle budget ends its connection, counted" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
const gpa = testing.allocator;
|
||||
var threaded: std.Io.Threaded = .init(gpa, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
var fake: FakeUpstream = .{ .reply = response_bytes };
|
||||
var h_owner: upstream_owner.Borrowed = .{};
|
||||
var h = bareHandler(h_owner.client(fake.client()));
|
||||
|
||||
const listen_address: net.IpAddress = try .parse("127.0.0.1", 0);
|
||||
var permits: query_budget.QueryBudget = try .init(gpa, 1);
|
||||
defer permits.deinit();
|
||||
var server = try tcp_server.TcpServer.listen(gpa, io, listen_address, &h, &permits, .{
|
||||
.max_connections = 2,
|
||||
.idle_timeout = short_idle,
|
||||
});
|
||||
const server_address = server.boundAddress();
|
||||
|
||||
var group: std.Io.Group = .init;
|
||||
try group.concurrent(io, tcp_server.TcpServer.serve, .{ &server, io });
|
||||
defer {
|
||||
server.deinit(io);
|
||||
group.await(io) catch |err| switch (err) {
|
||||
error.Canceled => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
const held = permits.tryAcquire(io).?;
|
||||
defer permits.release(io, held);
|
||||
|
||||
try bounded(io, waitOutlastsIdle, .{ io, server_address });
|
||||
|
||||
try testing.expectEqual(@as(u64, 1), permits.snapshot().wait_timeouts);
|
||||
try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
|
||||
}
|
||||
|
||||
/// Leaves a query waiting for a permit and returns once the server sees it
|
||||
/// waiting.
|
||||
fn queryThenWait(io: std.Io, address: net.IpAddress, permits: *query_budget.QueryBudget, stream: *net.Stream) anyerror!void {
|
||||
stream.* = try address.connect(io, .{ .mode = .stream });
|
||||
var write_buf: [1024]u8 = undefined;
|
||||
var writer = stream.writer(io, &write_buf);
|
||||
try sendQuery(&writer.interface);
|
||||
try waitForWaiters(permits, io, 1);
|
||||
}
|
||||
|
||||
test "closing a tcp listener ends a connection that waits for a permit" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
const gpa = testing.allocator;
|
||||
var threaded: std.Io.Threaded = .init(gpa, .{});
|
||||
defer threaded.deinit();
|
||||
const io = threaded.io();
|
||||
|
||||
var fake: FakeUpstream = .{ .reply = response_bytes };
|
||||
var h_owner: upstream_owner.Borrowed = .{};
|
||||
var h = bareHandler(h_owner.client(fake.client()));
|
||||
|
||||
const listen_address: net.IpAddress = try .parse("127.0.0.1", 0);
|
||||
var permits: query_budget.QueryBudget = try .init(gpa, 1);
|
||||
defer permits.deinit();
|
||||
// An idle budget far past `bounded`'s, so only the close can end the wait.
|
||||
const long_idle: std.Io.Clock.Duration = .{ .raw = .fromSeconds(60), .clock = .awake };
|
||||
var server = try tcp_server.TcpServer.listen(gpa, io, listen_address, &h, &permits, .{
|
||||
.max_connections = 2,
|
||||
.idle_timeout = long_idle,
|
||||
});
|
||||
const server_address = server.boundAddress();
|
||||
|
||||
var group: std.Io.Group = .init;
|
||||
try group.concurrent(io, tcp_server.TcpServer.serve, .{ &server, io });
|
||||
|
||||
const held = permits.tryAcquire(io).?;
|
||||
defer permits.release(io, held);
|
||||
|
||||
var stream: net.Stream = undefined;
|
||||
try bounded(io, queryThenWait, .{ io, server_address, &permits, &stream });
|
||||
defer stream.close(io);
|
||||
|
||||
try bounded(io, closeAndJoin, .{ &server, io, &group });
|
||||
try testing.expectEqual(@as(u64, 0), permits.snapshot().waiting);
|
||||
try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
|
||||
}
|
||||
|
||||
fn closeAndJoin(server: *tcp_server.TcpServer, io: std.Io, group: *std.Io.Group) anyerror!void {
|
||||
server.deinit(io);
|
||||
group.await(io) catch |err| switch (err) {
|
||||
error.Canceled => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Holds the only slot with an answered connection, then opens a second one,
|
||||
|
||||
Reference in New Issue
Block a user