| ... | ... | @@ -16,6 +16,9 @@ const testing = std.testing; |
| 16 | 16 | pub const Request = @import("Client/Request.zig"); |
| 17 | 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 | 22 | /// Used for tcpConnectToHost and storing HTTP headers when an externally |
| 20 | 23 | /// managed buffer is not provided. |
| 21 | 24 | allocator: Allocator, |
| ... | ... | @@ -24,39 +27,115 @@ ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| 24 | 27 | /// it will first rescan the system for root certificates. |
| 25 | 28 | next_https_rescan_certs: bool = true, |
| 26 | 29 | |
| 27 | | connection_mutex: std.Thread.Mutex = .{}, |
| 28 | 30 | connection_pool: ConnectionPool = .{}, |
| 29 | | connection_used: ConnectionPool = .{}, |
| 30 | 31 | |
| 31 | | pub const ConnectionPool = std.TailQueue(Connection); |
| 32 | | pub const ConnectionNode = ConnectionPool.Node; |
| 32 | pub const ConnectionPool = struct { |
| 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. |
| 35 | | /// If the caller already holds the connection mutex, it should pass `true` for `held`. |
| 36 | | pub fn acquire(client: *Client, node: *ConnectionNode, held: bool) void { |
| 37 | | if (!held) client.connection_mutex.lock(); |
| 38 | | defer if (!held) client.connection_mutex.unlock(); |
| 39 | const Queue = std.TailQueue(Connection); |
| 40 | pub const Node = Queue.Node; |
| 41 | |
| 42 | mutex: std.Thread.Mutex = .{}, |
| 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); |
| 41 | | client.connection_used.append(node); |
| 42 | | } |
| 64 | return null; |
| 65 | } |
| 43 | 66 | |
| 44 | | /// Tries to release a connection back to the connection pool. This function is threadsafe. |
| 45 | | /// If the connection is marked as closing, it will be closed instead. |
| 46 | | pub fn release(client: *Client, node: *ConnectionNode) void { |
| 47 | | client.connection_mutex.lock(); |
| 48 | | defer client.connection_mutex.unlock(); |
| 67 | /// Acquires an existing connection from the connection pool. This function is not threadsafe. |
| 68 | pub fn acquireUnsafe(pool: *ConnectionPool, node: *Node) void { |
| 69 | pool.free.remove(node); |
| 70 | pool.free_len -= 1; |
| 49 | 71 | |
| 50 | | client.connection_used.remove(node); |
| 72 | pool.used.append(node); |
| 73 | } |
| 51 | 74 | |
| 52 | | if (node.data.closing) { |
| 53 | | node.data.close(client); |
| 75 | /// Acquires an existing connection from the connection pool. This function is threadsafe. |
| 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); |
| 59 | | } |
| 83 | /// Tries to release a connection back to the connection pool. This function is threadsafe. |
| 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 | 140 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw); |
| 62 | 141 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw); |
| ... | ... | @@ -142,25 +221,7 @@ pub const Connection = struct { |
| 142 | 221 | }; |
| 143 | 222 | |
| 144 | 223 | pub fn deinit(client: *Client) void { |
| 145 | | client.connection_mutex.lock(); |
| 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 | | } |
| 224 | client.connection_pool.deinit(client); |
| 164 | 225 | |
| 165 | 226 | client.ca_bundle.deinit(client.allocator); |
| 166 | 227 | client.* = undefined; |
| ... | ... | @@ -168,36 +229,25 @@ pub fn deinit(client: *Client) void { |
| 168 | 229 | |
| 169 | 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 { |
| 172 | | { // Search through the connection pool for a potential connection. |
| 173 | | client.connection_mutex.lock(); |
| 174 | | defer client.connection_mutex.unlock(); |
| 175 | | |
| 176 | | var potential = client.connection_pool.last; |
| 177 | | while (potential) |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 | | } |
| 232 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 233 | if (client.connection_pool.findConnection(.{ |
| 234 | .host = host, |
| 235 | .port = port, |
| 236 | .is_tls = protocol == .tls, |
| 237 | })) |node| |
| 238 | return node; |
| 190 | 239 | |
| 191 | | const conn = try client.allocator.create(ConnectionNode); |
| 240 | const conn = try client.allocator.create(ConnectionPool.Node); |
| 192 | 241 | errdefer client.allocator.destroy(conn); |
| 242 | conn.* = .{ .data = undefined }; |
| 193 | 243 | |
| 194 | | conn.* = .{ .data = .{ |
| 244 | conn.data = .{ |
| 195 | 245 | .stream = try net.tcpConnectToHost(client.allocator, host, port), |
| 196 | 246 | .tls_client = undefined, |
| 197 | 247 | .protocol = protocol, |
| 198 | 248 | .host = try client.allocator.dupe(u8, host), |
| 199 | 249 | .port = port, |
| 200 | | } }; |
| 250 | }; |
| 201 | 251 | |
| 202 | 252 | switch (protocol) { |
| 203 | 253 | .plain => {}, |
| ... | ... | @@ -210,12 +260,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 210 | 260 | }, |
| 211 | 261 | } |
| 212 | 262 | |
| 213 | | { |
| 214 | | client.connection_mutex.lock(); |
| 215 | | defer client.connection_mutex.unlock(); |
| 216 | | |
| 217 | | client.connection_used.append(conn); |
| 218 | | } |
| 263 | client.connection_pool.addUsed(conn); |
| 219 | 264 | |
| 220 | 265 | return conn; |
| 221 | 266 | } |
| ... | ... | @@ -247,8 +292,8 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 247 | 292 | const host = uri.host orelse return error.UriMissingHost; |
| 248 | 293 | |
| 249 | 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. |
| 251 | | defer client.connection_mutex.unlock(); |
| 295 | client.connection_pool.mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex. |
| 296 | defer client.connection_pool.mutex.unlock(); |
| 252 | 297 | |
| 253 | 298 | if (client.next_https_rescan_certs) { |
| 254 | 299 | try client.ca_bundle.rescan(client.allocator); |