|
11 | 11 | #include <workerd/io/worker-interface.h> |
12 | 12 | #include <workerd/jsg/exception.h> |
13 | 13 | #include <workerd/jsg/url.h> |
| 14 | +#include <workerd/util/autogate.h> |
14 | 15 |
|
15 | 16 | namespace workerd::api { |
16 | 17 |
|
@@ -90,7 +91,8 @@ jsg::Ref<Socket> setupSocket(jsg::Lock& js, |
90 | 91 | SecureTransportKind secureTransport, |
91 | 92 | kj::Maybe<kj::String> domain, |
92 | 93 | bool isDefaultFetchPort, |
93 | | - kj::Maybe<jsg::PromiseResolverPair<SocketInfo>> maybeOpenedPrPair) { |
| 94 | + kj::Maybe<jsg::PromiseResolverPair<SocketInfo>> maybeOpenedPrPair, |
| 95 | + kj::Maybe<kj::Promise<void>> connectTask) { |
94 | 96 | auto& ioContext = IoContext::current(); |
95 | 97 |
|
96 | 98 | // Disconnection handling is annoyingly complicated: |
@@ -173,7 +175,7 @@ jsg::Ref<Socket> setupSocket(jsg::Lock& js, |
173 | 175 | auto result = js.alloc<Socket>(js, ioContext, kj::mv(refcountedConnection), kj::mv(remoteAddress), |
174 | 176 | kj::mv(readable), kj::mv(writable), kj::mv(closedPrPair), kj::mv(watchForDisconnectTask), |
175 | 177 | kj::mv(options), kj::mv(tlsStarter), secureTransport, kj::mv(domain), isDefaultFetchPort, |
176 | | - kj::mv(openedPrPair)); |
| 178 | + kj::mv(openedPrPair), kj::mv(connectTask)); |
177 | 179 |
|
178 | 180 | KJ_IF_SOME(p, eofPromise) { |
179 | 181 | result->handleReadableEof(js, kj::mv(p)); |
@@ -256,25 +258,99 @@ jsg::Ref<Socket> connectImplNoOutputLock(jsg::Lock& js, |
256 | 258 | } |
257 | 259 | kj::Own<kj::TlsStarterCallback> tlsStarter = kj::heap<kj::TlsStarterCallback>(); |
258 | 260 | httpConnectSettings.tlsStarter = tlsStarter; |
259 | | - auto request = httpClient->connect(addressStr, *headers, httpConnectSettings); |
260 | | - request.connection = request.connection.attach(kj::mv(httpClient)); |
261 | | - |
262 | | - auto result = setupSocket(js, kj::mv(request.connection), kj::mv(addressStr), kj::mv(options), |
263 | | - kj::mv(tlsStarter), secureTransport, kj::mv(domain), isDefaultFetchPort, |
264 | | - kj::none /* maybeOpenedPrPair */); |
265 | | - // `handleProxyStatus` needs an initialized refcount to use `JSG_THIS`, hence it cannot be |
266 | | - // called in Socket's constructor. Also it's only necessary when creating a Socket as a result of |
267 | | - // a `connect`. |
268 | | - result->handleProxyStatus(js, kj::mv(request.status)); |
269 | | - return result; |
| 261 | + |
| 262 | + if (util::Autogate::isEnabled(util::AutogateKey::TCP_SOCKET_CONNECT_OUTPUT_GATE)) { |
| 263 | + // Deferred-connect path: return a Socket backed by a promised stream immediately, deferring |
| 264 | + // the actual httpClient->connect() until after the DO output gate clears. This prevents |
| 265 | + // premature network output while storage writes are still pending. |
| 266 | + // |
| 267 | + // Two promise-fulfiller pairs bridge the deferred connect to the Socket: |
| 268 | + // connStreamPaf — fulfilled with the raw connection stream once connect() returns |
| 269 | + // statusPaf — fulfilled with the proxy status once connect()'s status promise resolves |
| 270 | + // |
| 271 | + // The connect task is stored inside ConnectionData alongside tlsStarter. This ensures |
| 272 | + // that if the Socket is GC'd (destroying ConnectionData), the connect task is cancelled |
| 273 | + // before tlsStarter is destroyed, preventing a use-after-free on the TlsStarterCallback |
| 274 | + // pointer embedded in httpConnectSettings. |
| 275 | + |
| 276 | + // Promise-fulfiller for the raw connection stream. |
| 277 | + auto connStreamPaf = kj::newPromiseAndFulfiller<kj::Own<kj::AsyncIoStream>>(); |
| 278 | + auto& connStreamFulfiller = *connStreamPaf.fulfiller; |
| 279 | + auto deferredCancelStream = kj::defer([fulfiller = kj::mv(connStreamPaf.fulfiller)]() mutable { |
| 280 | + fulfiller->reject(KJ_EXCEPTION(DISCONNECTED, "socket connect cancelled")); |
| 281 | + }); |
| 282 | + |
| 283 | + // Promise-fulfiller for the proxy status. |
| 284 | + auto statusPaf = kj::newPromiseAndFulfiller<kj::HttpClient::ConnectRequest::Status>(); |
| 285 | + auto& statusFulfiller = *statusPaf.fulfiller; |
| 286 | + auto deferredCancelStatus = kj::defer([fulfiller = kj::mv(statusPaf.fulfiller)]() mutable { |
| 287 | + fulfiller->reject(KJ_EXCEPTION(DISCONNECTED, "socket connect cancelled")); |
| 288 | + }); |
| 289 | + |
| 290 | + // The connect task: waits for the output gate, performs the connect, then forwards results. |
| 291 | + static auto constexpr doConnect = |
| 292 | + [](IoContext& ioContext, kj::StringPtr addressStr, kj::HttpHeaders& headers, |
| 293 | + kj::HttpConnectSettings httpConnectSettings, kj::Own<kj::HttpClient> httpClient, |
| 294 | + kj::PromiseFulfiller<kj::Own<kj::AsyncIoStream>>& connFulfiller, |
| 295 | + kj::PromiseFulfiller<kj::HttpClient::ConnectRequest::Status>& statusFulfiller) |
| 296 | + -> kj::Promise<void> { |
| 297 | + try { |
| 298 | + co_await ioContext.waitForOutputLocks(); |
| 299 | + auto request = httpClient->connect(addressStr, headers, httpConnectSettings); |
| 300 | + request.connection = request.connection.attach(kj::mv(httpClient)); |
| 301 | + auto status = kj::mv(request.status); |
| 302 | + connFulfiller.fulfill(kj::mv(request.connection)); |
| 303 | + auto resolvedStatus = co_await kj::mv(status); |
| 304 | + statusFulfiller.fulfill(kj::mv(resolvedStatus)); |
| 305 | + } catch (...) { |
| 306 | + auto e = kj::getCaughtExceptionAsKj(); |
| 307 | + if (!connFulfiller.isWaiting()) { |
| 308 | + // Connection was already fulfilled; only reject status. |
| 309 | + statusFulfiller.reject(kj::mv(e)); |
| 310 | + } else { |
| 311 | + connFulfiller.reject(kj::cp(e)); |
| 312 | + statusFulfiller.reject(kj::mv(e)); |
| 313 | + } |
| 314 | + } |
| 315 | + }; |
| 316 | + |
| 317 | + auto connectTaskPromise = |
| 318 | + doConnect(ioContext, addressStr, *headers, httpConnectSettings, kj::mv(httpClient), |
| 319 | + connStreamFulfiller, statusFulfiller) |
| 320 | + .attach(kj::mv(deferredCancelStream), kj::mv(deferredCancelStatus), kj::mv(headers)); |
| 321 | + |
| 322 | + auto result = setupSocket(js, kj::newPromisedStream(kj::mv(connStreamPaf.promise)), |
| 323 | + kj::mv(addressStr), kj::mv(options), kj::mv(tlsStarter), secureTransport, kj::mv(domain), |
| 324 | + isDefaultFetchPort, kj::none /* maybeOpenedPrPair */, kj::mv(connectTaskPromise)); |
| 325 | + |
| 326 | + // `handleProxyStatus` needs an initialized refcount to use `JSG_THIS`, hence it cannot be |
| 327 | + // called in Socket's constructor. Also it's only necessary when creating a Socket as a result |
| 328 | + // of a `connect`. |
| 329 | + result->handleProxyStatus(js, kj::mv(statusPaf.promise)); |
| 330 | + return result; |
| 331 | + } else { |
| 332 | + // Original synchronous-connect path (no output gate wait). |
| 333 | + auto request = httpClient->connect(addressStr, *headers, httpConnectSettings); |
| 334 | + request.connection = request.connection.attach(kj::mv(httpClient)); |
| 335 | + |
| 336 | + auto result = setupSocket(js, kj::mv(request.connection), kj::mv(addressStr), kj::mv(options), |
| 337 | + kj::mv(tlsStarter), secureTransport, kj::mv(domain), isDefaultFetchPort, |
| 338 | + kj::none /* maybeOpenedPrPair */); |
| 339 | + // `handleProxyStatus` needs an initialized refcount to use `JSG_THIS`, hence it cannot be |
| 340 | + // called in Socket's constructor. Also it's only necessary when creating a Socket as a result |
| 341 | + // of a `connect`. |
| 342 | + result->handleProxyStatus(js, kj::mv(request.status)); |
| 343 | + return result; |
| 344 | + } |
270 | 345 | } |
271 | 346 |
|
272 | 347 | jsg::Ref<Socket> connectImpl(jsg::Lock& js, |
273 | 348 | kj::Maybe<jsg::Ref<Fetcher>> fetcher, |
274 | 349 | AnySocketAddress address, |
275 | 350 | jsg::Optional<SocketOptions> options) { |
276 | | - // TODO(soon): Doesn't this need to check for the presence of an output lock, and if it finds one |
277 | | - // then wait on it, before calling into connectImplNoOutputLock? |
| 351 | + // When the TCP_SOCKET_CONNECT_OUTPUT_GATE autogate is enabled, the output gate wait is |
| 352 | + // handled inside connectImplNoOutputLock via a deferred connect task, so no separate wait |
| 353 | + // is needed here. |
278 | 354 | return connectImplNoOutputLock(js, kj::mv(fetcher), kj::mv(address), kj::mv(options)); |
279 | 355 | } |
280 | 356 |
|
|
0 commit comments