Added timeout to queue outbound requests to minimize in-flight requests#1645
Added timeout to queue outbound requests to minimize in-flight requests#1645
Conversation
ref https://linear.app/ghost/issue/BER-3241 - Adds a 10s timeout to all outbound requests. The timeout is more than enough for servers to process the request and triggers a fail-fast mechanism that frees up resources.
WalkthroughThis pull request adds timeout handling for outbound requests in a Google Cloud Pub/Sub push message handler. The error-utils file now treats Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/mq/gcloud-pubsub-push/mq.ts`:
- Around line 23-29: The fetch wrapper currently skips enforcing the timeout
when callers pass init.signal; update the globalThis.fetch override (the wrapper
around originalFetch) to merge the caller's init.signal with the timeout signal
from fetchTimeoutStorage instead of choosing one: create an AbortController, if
init.signal exists attach a listener to abort the controller when the caller
signal aborts, use AbortSignal.timeout(timeout) to set a timeout that aborts the
controller when fired (or race both by wiring both signals to the controller),
pass controller.signal into originalFetch and ensure any listeners are cleaned
up after the request; reference the globalThis.fetch wrapper, originalFetch,
fetchTimeoutStorage.getStore(), and init.signal when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6fcab610-434f-4a31-91a9-fc72ba2346eb
📒 Files selected for processing (2)
src/mq/gcloud-pubsub-push/error-utils.tssrc/mq/gcloud-pubsub-push/mq.ts
| globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => { | ||
| const timeout = fetchTimeoutStorage.getStore(); | ||
| if (timeout && !init?.signal) { | ||
| return originalFetch(input, { | ||
| ...init, | ||
| signal: AbortSignal.timeout(timeout), | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the Fetch API, when fetchis called with aRequestobject plus aninitobject, doesinit.signaloverride the request's existing signal? Also, isAbortSignal.any() available in the Node.js version this project targets?
💡 Result:
-
Yes. When you call
fetch(request, init), the Fetch spec effectively runs theRequestconstructor withinput=requestand yourinit. In that constructor, it starts with the request’s existingsignal, but ifinit.signalis present it replaces (overrides) that signal, and the createdRequestends up using a dependent signal based on the (possibly replaced) one. [1] -
AbortSignal.any()availability in Node.js: It exists in Node.js v18.17.0+ (backported into the Node 18 line) and later. So it’s available only if your project’s target Node version is ≥ 18.17.0. [2]
Sources:
[1] Fetch Standard (WHATWG), new Request(input, init) steps (init["signal"] sets/overrides the signal)
[2] Node.js v18 global docs (AbortSignal.any(signals) “Added in: v18.17.0”)
🏁 Script executed:
#!/bin/bash
# Check Node.js version requirement
if [ -f "package.json" ]; then
echo "=== package.json engines field ==="
cat package.json | jq '.engines' 2>/dev/null || grep -A2 '"engines"' package.json || echo "No engines field"
fi
if [ -f ".nvmrc" ]; then
echo "=== .nvmrc ==="
cat .nvmrc
fi
if [ -f ".node-version" ]; then
echo "=== .node-version ==="
cat .node-version
fiRepository: TryGhost/ActivityPub
Length of output: 126
Preserve caller abort signals while enforcing the 10s timeout.
This wrapper skips the timeout if the caller provides init.signal, which defeats the PR's goal of enforcing timeouts on all outbound requests. Merge the signals instead of choosing one:
Fix
globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const timeout = fetchTimeoutStorage.getStore();
- if (timeout && !init?.signal) {
+ if (timeout) {
+ const callerSignal =
+ init?.signal ??
+ (input instanceof Request ? input.signal : undefined);
+
return originalFetch(input, {
...init,
- signal: AbortSignal.timeout(timeout),
+ signal: callerSignal
+ ? AbortSignal.any([
+ callerSignal,
+ AbortSignal.timeout(timeout),
+ ])
+ : AbortSignal.timeout(timeout),
});
}
return originalFetch(input, init);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => { | |
| const timeout = fetchTimeoutStorage.getStore(); | |
| if (timeout && !init?.signal) { | |
| return originalFetch(input, { | |
| ...init, | |
| signal: AbortSignal.timeout(timeout), | |
| }); | |
| globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => { | |
| const timeout = fetchTimeoutStorage.getStore(); | |
| if (timeout) { | |
| const callerSignal = | |
| init?.signal ?? | |
| (input instanceof Request ? input.signal : undefined); | |
| return originalFetch(input, { | |
| ...init, | |
| signal: callerSignal | |
| ? AbortSignal.any([ | |
| callerSignal, | |
| AbortSignal.timeout(timeout), | |
| ]) | |
| : AbortSignal.timeout(timeout), | |
| }); | |
| } | |
| return originalFetch(input, init); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/mq/gcloud-pubsub-push/mq.ts` around lines 23 - 29, The fetch wrapper
currently skips enforcing the timeout when callers pass init.signal; update the
globalThis.fetch override (the wrapper around originalFetch) to merge the
caller's init.signal with the timeout signal from fetchTimeoutStorage instead of
choosing one: create an AbortController, if init.signal exists attach a listener
to abort the controller when the caller signal aborts, use
AbortSignal.timeout(timeout) to set a timeout that aborts the controller when
fired (or race both by wiring both signals to the controller), pass
controller.signal into originalFetch and ensure any listeners are cleaned up
after the request; reference the globalThis.fetch wrapper, originalFetch,
fetchTimeoutStorage.getStore(), and init.signal when making the change.
|
@rmgpinto Instead of monkey patching Not sure on specific usage, but agent is suggesting something like: This might also require some tweaking in the error utils to catch whatever errors are thrown by Ideally we would be able to pass a customised version of |
ref https://linear.app/ghost/issue/BER-3241