| ... | @@ -16,6 +16,9 @@ const testing = std.testing; | ... | @@ -16,6 +16,9 @@ const testing = std.testing; |
| 16 | pub const Request = @import("Client/Request.zig"); | 16 | pub const Request = @import("Client/Request.zig"); |
| 17 | pub const Response = @import("Client/Response.zig"); | 17 | pub const Response = @import("Client/Response.zig"); |
| 18 | | 18 | |
| | 19 | pub const default_connection_pool_size = 32; |
| | 20 | const connection_pool_size = std.options.http_connection_pool_size; |
| | 21 | |
| 19 | /// Used for tcpConnectToHost and storing HTTP headers when an externally | 22 | /// Used for tcpConnectToHost and storing HTTP headers when an externally |
| 20 | /// managed buffer is not provided. | 23 | /// managed buffer is not provided. |
| 21 | allocator: Allocator, | 24 | allocator: 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. |
| 25 | next_https_rescan_certs: bool = true, | 28 | next_https_rescan_certs: bool = true, |
| 26 | | 29 | |
| 27 | connection_mutex: std.Thread.Mutex = .{}, | | |
| 28 | connection_pool: ConnectionPool = .{}, | 30 | connection_pool: ConnectionPool = .{}, |
| 29 | connection_used: ConnectionPool = .{}, | | |
| 30 | | 31 | |
| 31 | pub const ConnectionPool = std.TailQueue(Connection); | 32 | pub const ConnectionPool = struct { |
| 32 | pub const ConnectionNode = ConnectionPool.Node; | 33 | pub const Criteria = struct { |
| | 34 | host: []const u8, |
| | 35 | port: u16, |
| | 36 | is_tls: bool, |
| | 37 | }; |
| 33 | | 38 | |
| 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; |
| 36 | pub 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 | } |
| 39 | | 63 | |
| 40 | client.connection_pool.remove(node); | 64 | return null; |
| 41 | client.connection_used.append(node); | 65 | } |
| 42 | } | | |
| 43 | | 66 | |
| 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 { |
| 46 | pub 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(); | | |
| 49 | | 71 | |
| 50 | client.connection_used.remove(node); | 72 | pool.used.append(node); |
| | 73 | } |
| 51 | | 74 | |
| 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(); |
| 54 | | 79 | |
| 55 | return client.allocator.destroy(node); | 80 | return pool.acquireUnsafe(node); |
| 56 | } | 81 | } |
| 57 | | 82 | |
| 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 | }; |
| 60 | | 139 | |
| 61 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw); | 140 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw); |
| 62 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw); | 141 | pub 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 | }; |
| 143 | | 222 | |
| 144 | pub fn deinit(client: *Client) void { | 223 | pub 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 | } | | |
| 164 | | 225 | |
| 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 { |
| 168 | | 229 | |
| 169 | pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); | 230 | pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); |
| 170 | | 231 | |
| 171 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionNode { | 232 | pub 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, |
| 175 | | 236 | .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 | } | | |
| 190 | | 239 | |
| 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 }; |
| 193 | | 243 | |
| 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 | }; |
| 201 | | 251 | |
| 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 | } |
| 212 | | 262 | |
| 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 | } | | |
| 219 | | 264 | |
| 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; |
| 248 | | 293 | |
| 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(); |
| 252 | | 297 | |
| 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); |