authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-08 11:27:13-06:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-09 14:55:31-06:00
log524e0cd987a52a60ce1014aa27cd73f99a3b9958
tree10bb83b6939762b3e37ece48e3d5f466da1c3525
parent634e7155048aeaf15553d866783930f3d22b375c
signaturelock-open Commit is signed but in an unrecognized format.

std.http: rework connection pool into its own type


4 files changed, 134 insertions(+), 87 deletions(-)

lib/std/http/Client.zig+117-72
...@@ -16,6 +16,9 @@ const testing = std.testing;...@@ -16,6 +16,9 @@ const testing = std.testing;
16pub const Request = @import("Client/Request.zig");16pub const Request = @import("Client/Request.zig");
17pub const Response = @import("Client/Response.zig");17pub const Response = @import("Client/Response.zig");
1818
19pub const default_connection_pool_size = 32;
20const connection_pool_size = std.options.http_connection_pool_size;
21
19/// Used for tcpConnectToHost and storing HTTP headers when an externally22/// Used for tcpConnectToHost and storing HTTP headers when an externally
20/// managed buffer is not provided.23/// managed buffer is not provided.
21allocator: Allocator,24allocator: Allocator,
...@@ -24,39 +27,115 @@ ca_bundle: std.crypto.Certificate.Bundle = .{},...@@ -24,39 +27,115 @@ ca_bundle: std.crypto.Certificate.Bundle = .{},
24/// it will first rescan the system for root certificates.27/// it will first rescan the system for root certificates.
25next_https_rescan_certs: bool = true,28next_https_rescan_certs: bool = true,
2629
27connection_mutex: std.Thread.Mutex = .{},
28connection_pool: ConnectionPool = .{},30connection_pool: ConnectionPool = .{},
29connection_used: ConnectionPool = .{},
3031
31pub const ConnectionPool = std.TailQueue(Connection);32pub const ConnectionPool = struct {
32pub const ConnectionNode = ConnectionPool.Node;33 pub const Criteria = struct {
34 host: []const u8,
35 port: u16,
36 is_tls: bool,
37 };
3338
34/// Acquires an existing connection from the connection pool. This function is threadsafe.39 const Queue = std.TailQueue(Connection);
35/// If the caller already holds the connection mutex, it should pass `true` for `held`.40 pub const Node = Queue.Node;
36pub fn acquire(client: *Client, node: *ConnectionNode, held: bool) void {41
37 if (!held) client.connection_mutex.lock();42 mutex: std.Thread.Mutex = .{},
38 defer if (!held) client.connection_mutex.unlock();43 used: Queue = .{},
44 free: Queue = .{},
45 free_len: usize = 0,
46 free_size: usize = default_connection_pool_size,
47
48 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.
49 /// If no connection is found, null is returned.
50 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Node {
51 pool.mutex.lock();
52 defer pool.mutex.unlock();
53
54 var next = pool.free.last;
55 while (next) |node| : (next = node.prev) {
56 if ((node.data.protocol == .tls) != criteria.is_tls) continue;
57 if (node.data.port != criteria.port) continue;
58 if (std.mem.eql(u8, node.data.host, criteria.host)) continue;
59
60 pool.acquireUnsafe(node);
61 return node;
62 }
3963
40 client.connection_pool.remove(node);64 return null;
41 client.connection_used.append(node);65 }
42}
4366
44/// Tries to release a connection back to the connection pool. This function is threadsafe.67 /// Acquires an existing connection from the connection pool. This function is not threadsafe.
45/// If the connection is marked as closing, it will be closed instead.68 pub fn acquireUnsafe(pool: *ConnectionPool, node: *Node) void {
46pub fn release(client: *Client, node: *ConnectionNode) void {69 pool.free.remove(node);
47 client.connection_mutex.lock();70 pool.free_len -= 1;
48 defer client.connection_mutex.unlock();
4971
50 client.connection_used.remove(node);72 pool.used.append(node);
73 }
5174
52 if (node.data.closing) {75 /// Acquires an existing connection from the connection pool. This function is threadsafe.
53 node.data.close(client);76 pub fn acquire(pool: *ConnectionPool, node: *Node) void {
77 pool.mutex.lock();
78 defer pool.mutex.unlock();
5479
55 return client.allocator.destroy(node);80 return pool.acquireUnsafe(node);
56 }81 }
5782
58 client.connection_pool.append(node);83 /// Tries to release a connection back to the connection pool. This function is threadsafe.
59}84 /// If the connection is marked as closing, it will be closed instead.
85 pub fn release(pool: *ConnectionPool, client: *Client, node: *Node) void {
86 pool.mutex.lock();
87 defer pool.mutex.unlock();
88
89 pool.used.remove(node);
90
91 if (node.data.closing) {
92 node.data.close(client);
93
94 return client.allocator.destroy(node);
95 }
96
97 if (pool.free_len + 1 >= pool.free_size) {
98 const popped = pool.free.popFirst() orelse unreachable;
99
100 popped.data.close(client);
101
102 return client.allocator.destroy(popped);
103 }
104
105 pool.free.append(node);
106 pool.free_len += 1;
107 }
108
109 /// Adds a newly created node to the pool of used connections. This function is threadsafe.
110 pub fn addUsed(pool: *ConnectionPool, node: *Node) void {
111 pool.mutex.lock();
112 defer pool.mutex.unlock();
113
114 pool.used.append(node);
115 }
116
117 pub fn deinit(pool: *ConnectionPool, client: *Client) void {
118 pool.mutex.lock();
119
120 var next = pool.free.first;
121 while (next) |node| {
122 defer client.allocator.destroy(node);
123 next = node.next;
124
125 node.data.close(client);
126 }
127
128 next = pool.used.first;
129 while (next) |node| {
130 defer client.allocator.destroy(node);
131 next = node.next;
132
133 node.data.close(client);
134 }
135
136 pool.* = undefined;
137 }
138};
60139
61pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw);140pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw);
62pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw);141pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw);
...@@ -142,25 +221,7 @@ pub const Connection = struct {...@@ -142,25 +221,7 @@ pub const Connection = struct {
142};221};
143222
144pub fn deinit(client: *Client) void {223pub fn deinit(client: *Client) void {
145 client.connection_mutex.lock();224 client.connection_pool.deinit(client);
146
147 var next = client.connection_pool.first;
148 while (next) |node| {
149 next = node.next;
150
151 node.data.close(client);
152
153 client.allocator.destroy(node);
154 }
155
156 next = client.connection_used.first;
157 while (next) |node| {
158 next = node.next;
159
160 node.data.close(client);
161
162 client.allocator.destroy(node);
163 }
164225
165 client.ca_bundle.deinit(client.allocator);226 client.ca_bundle.deinit(client.allocator);
166 client.* = undefined;227 client.* = undefined;
...@@ -168,36 +229,25 @@ pub fn deinit(client: *Client) void {...@@ -168,36 +229,25 @@ pub fn deinit(client: *Client) void {
168229
169pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream);230pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream);
170231
171pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionNode {232pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
172 { // Search through the connection pool for a potential connection.233 if (client.connection_pool.findConnection(.{
173 client.connection_mutex.lock();234 .host = host,
174 defer client.connection_mutex.unlock();235 .port = port,
175236 .is_tls = protocol == .tls,
176 var potential = client.connection_pool.last;237 })) |node|
177 while (potential) |node| {238 return node;
178 const same_host = mem.eql(u8, node.data.host, host);
179 const same_port = node.data.port == port;
180 const same_protocol = node.data.protocol == protocol;
181
182 if (same_host and same_port and same_protocol) {
183 client.acquire(node, true);
184 return node;
185 }
186
187 potential = node.prev;
188 }
189 }
190239
191 const conn = try client.allocator.create(ConnectionNode);240 const conn = try client.allocator.create(ConnectionPool.Node);
192 errdefer client.allocator.destroy(conn);241 errdefer client.allocator.destroy(conn);
242 conn.* = .{ .data = undefined };
193243
194 conn.* = .{ .data = .{244 conn.data = .{
195 .stream = try net.tcpConnectToHost(client.allocator, host, port),245 .stream = try net.tcpConnectToHost(client.allocator, host, port),
196 .tls_client = undefined,246 .tls_client = undefined,
197 .protocol = protocol,247 .protocol = protocol,
198 .host = try client.allocator.dupe(u8, host),248 .host = try client.allocator.dupe(u8, host),
199 .port = port,249 .port = port,
200 } };250 };
201251
202 switch (protocol) {252 switch (protocol) {
203 .plain => {},253 .plain => {},
...@@ -210,12 +260,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -210,12 +260,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
210 },260 },
211 }261 }
212262
213 {263 client.connection_pool.addUsed(conn);
214 client.connection_mutex.lock();
215 defer client.connection_mutex.unlock();
216
217 client.connection_used.append(conn);
218 }
219264
220 return conn;265 return conn;
221}266}
...@@ -247,8 +292,8 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req...@@ -247,8 +292,8 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req
247 const host = uri.host orelse return error.UriMissingHost;292 const host = uri.host orelse return error.UriMissingHost;
248293
249 if (client.next_https_rescan_certs and protocol == .tls) {294 if (client.next_https_rescan_certs and protocol == .tls) {
250 client.connection_mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex.295 client.connection_pool.mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex.
251 defer client.connection_mutex.unlock();296 defer client.connection_pool.mutex.unlock();
252297
253 if (client.next_https_rescan_certs) {298 if (client.next_https_rescan_certs) {
254 try client.ca_bundle.rescan(client.allocator);299 try client.ca_bundle.rescan(client.allocator);
lib/std/http/Client/Request.zig+8-14
...@@ -6,7 +6,7 @@ const assert = std.debug.assert;...@@ -6,7 +6,7 @@ const assert = std.debug.assert;
66
7const Client = @import("../Client.zig");7const Client = @import("../Client.zig");
8const Connection = Client.Connection;8const Connection = Client.Connection;
9const ConnectionNode = Client.ConnectionNode;9const ConnectionNode = Client.ConnectionPool.Node;
10const Response = @import("Response.zig");10const Response = @import("Response.zig");
1111
12const Request = @This();12const Request = @This();
...@@ -85,7 +85,7 @@ pub fn deinit(req: *Request) void {...@@ -85,7 +85,7 @@ pub fn deinit(req: *Request) void {
85 if (!req.response.done) {85 if (!req.response.done) {
86 // If the response wasn't fully read, then we need to close the connection.86 // If the response wasn't fully read, then we need to close the connection.
87 req.connection.data.closing = true;87 req.connection.data.closing = true;
88 req.client.release(req.connection);88 req.client.connection_pool.release(req.client, req.connection);
89 }89 }
9090
91 req.arena.deinit();91 req.arena.deinit();
...@@ -135,7 +135,7 @@ fn checkForCompleteHead(req: *Request, buffer: []u8) !usize {...@@ -135,7 +135,7 @@ fn checkForCompleteHead(req: *Request, buffer: []u8) !usize {
135 if (req.response.state == .finished) {135 if (req.response.state == .finished) {
136 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);136 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);
137137
138 if (req.response.upgrade) |_| {138 if (req.response.headers.upgrade) |_| {
139 req.connection.data.closing = false;139 req.connection.data.closing = false;
140 req.response.done = true;140 req.response.done = true;
141 return i;141 return i;
...@@ -226,7 +226,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {...@@ -226,7 +226,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
226 req.response.next_chunk_length -= can_read;226 req.response.next_chunk_length -= can_read;
227227
228 if (req.response.next_chunk_length == 0) {228 if (req.response.next_chunk_length == 0) {
229 req.client.release(req.connection);229 req.client.connection_pool.release(req.client, req.connection);
230 req.connection = undefined;230 req.connection = undefined;
231 req.response.done = true;231 req.response.done = true;
232 }232 }
...@@ -241,7 +241,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {...@@ -241,7 +241,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
241 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);241 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);
242242
243 if (req.response.next_chunk_length == 0) {243 if (req.response.next_chunk_length == 0) {
244 req.client.release(req.connection);244 req.client.connection_pool.release(req.client, req.connection);
245 req.connection = undefined;245 req.connection = undefined;
246 req.response.done = true;246 req.response.done = true;
247 }247 }
...@@ -293,7 +293,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {...@@ -293,7 +293,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
293 .chunk_data => {293 .chunk_data => {
294 if (req.response.next_chunk_length == 0) {294 if (req.response.next_chunk_length == 0) {
295 req.response.done = true;295 req.response.done = true;
296 req.client.release(req.connection);296 req.client.connection_pool.release(req.client, req.connection);
297 req.connection = undefined;297 req.connection = undefined;
298298
299 return out_index;299 return out_index;
...@@ -317,7 +317,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {...@@ -317,7 +317,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
317 req.response.next_chunk_length -= can_read;317 req.response.next_chunk_length -= can_read;
318318
319 if (req.response.next_chunk_length == 0) {319 if (req.response.next_chunk_length == 0) {
320 req.client.release(req.connection);320 req.client.connection_pool.release(req.client, req.connection);
321 req.connection = undefined;321 req.connection = undefined;
322 req.response.done = true;322 req.response.done = true;
323 continue;323 continue;
...@@ -345,13 +345,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {...@@ -345,13 +345,7 @@ fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
345 }345 }
346}346}
347347
348pub const ReadError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{348pub const ReadError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize, CompressionNotSupported };
349 BadHeader,
350 InvalidCompression,
351 StreamTooLong,
352 InvalidWindowSize,
353 CompressionNotSupported
354};
355349
356pub const Reader = std.io.Reader(*Request, ReadError, read);350pub const Reader = std.io.Reader(*Request, ReadError, read);
357351
lib/std/http/Client/Response.zig+4-1
...@@ -32,6 +32,7 @@ pub const Headers = struct {...@@ -32,6 +32,7 @@ pub const Headers = struct {
32 transfer_encoding: ?http.TransferEncoding = null,32 transfer_encoding: ?http.TransferEncoding = null,
33 transfer_compression: ?http.ContentEncoding = null,33 transfer_compression: ?http.ContentEncoding = null,
34 connection: http.Connection = .close,34 connection: http.Connection = .close,
35 upgrade: ?[]const u8 = null,
3536
36 number_of_headers: usize = 0,37 number_of_headers: usize = 0,
3738
...@@ -93,7 +94,7 @@ pub const Headers = struct {...@@ -93,7 +94,7 @@ pub const Headers = struct {
9394
94 if (iter.next()) |second| {95 if (iter.next()) |second| {
95 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;96 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
96 97
97 const trimmed = std.mem.trim(u8, second, " ");98 const trimmed = std.mem.trim(u8, second, " ");
9899
99 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {100 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
...@@ -122,6 +123,8 @@ pub const Headers = struct {...@@ -122,6 +123,8 @@ pub const Headers = struct {
122 } else {123 } else {
123 return error.HttpConnectionHeaderUnsupported;124 return error.HttpConnectionHeaderUnsupported;
124 }125 }
126 } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
127 headers.upgrade = header_value;
125 }128 }
126 }129 }
127130
lib/std/std.zig+5
...@@ -185,6 +185,11 @@ pub const options = struct {...@@ -185,6 +185,11 @@ pub const options = struct {
185 options_override.keep_sigpipe185 options_override.keep_sigpipe
186 else186 else
187 false;187 false;
188
189 pub const http_connection_pool_size = if (@hasDecl(options_override, "http_connection_pool_size"))
190 options_override.http_connection_pool_size
191 else
192 http.Client.default_connection_pool_size;
188};193};
189194
190// This forces the start.zig file to be imported, and the comptime logic inside that195// This forces the start.zig file to be imported, and the comptime logic inside that