Repository navigation
feat: queuing of calls to python process #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
845c65e
0e40d9b
b157fd2
1ba4fb5
5991760
46fc368
515b259
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "apollo": patch | ||
| --- | ||
|
|
||
| Queue requests to the python process |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // FIFO queue capping how many service calls bridge.ts runs at once. | ||
| // Calls past the cap wait here instead of running immediately. | ||
|
|
||
| import { captureMessage } from "./sentry"; | ||
|
|
||
| const DEFAULT_CONCURRENCY = 10; | ||
|
|
||
| export const maxConcurrency = (): number => | ||
| Number(process.env.APOLLO_CONCURRENCY) || DEFAULT_CONCURRENCY; | ||
|
|
||
| const DEFAULT_QUEUE_TIMEOUT_MS = 5 * 60 * 1000; | ||
|
|
||
| export const queueTimeoutMs = (): number => | ||
| Number(process.env.APOLLO_QUEUE_TIMEOUT_MS) || DEFAULT_QUEUE_TIMEOUT_MS; | ||
|
|
||
| let active = 0; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is module scoped. Only one queue per process. Is that bad for testing? For re-use? Should there be a class or at least some instance based here? Open question, maybe this is fine. Just sticks out at me a little as a weak architecture |
||
| const pending: Array<() => void> = []; | ||
|
|
||
| export const activeCount = (): number => active; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are either of these actually neccessary? You could just export Looks like they're both just used by unit tests anyway
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. used by the test only. |
||
| export const queueDepth = (): number => pending.length; | ||
|
|
||
| // Distinct from cancel()'s rejection, so a caller can tell "the server gave | ||
| // up" apart from "the client went away". | ||
| export class QueueTimeoutError extends Error { | ||
| constructor() { | ||
| super("timed out waiting in the queue"); | ||
| this.name = "QueueTimeoutError"; | ||
| } | ||
| } | ||
|
|
||
| export type Enqueued<T> = { | ||
| promise: Promise<T>; | ||
| cancel: () => void; | ||
| }; | ||
|
|
||
| // cancel() drops a still-queued task without ever running it; calling it | ||
| // once the task has started is a no-op. | ||
| export const enqueue = <T>( | ||
| scriptName: string, | ||
| task: () => Promise<T> | ||
| ): Enqueued<T> => { | ||
| let removeFromQueue: ((reason: Error) => void) | null = null; | ||
| let timer: ReturnType<typeof setTimeout> | undefined; | ||
|
|
||
| const promise = new Promise<T>((resolve, reject) => { | ||
| const run = () => { | ||
| if (timer) clearTimeout(timer); | ||
| active++; | ||
| task().then( | ||
| (value) => { | ||
| active--; | ||
| pending.shift()?.(); | ||
| resolve(value); | ||
| }, | ||
| (error) => { | ||
| active--; | ||
| pending.shift()?.(); | ||
| reject(error); | ||
| } | ||
| ); | ||
| }; | ||
|
|
||
| if (active < maxConcurrency()) { | ||
| run(); | ||
| return; | ||
| } | ||
|
|
||
| console.log( | ||
| `queueing ${scriptName}: ${active}/${maxConcurrency()} running, ${ | ||
| pending.length + 1 | ||
| } waiting` | ||
| ); | ||
| captureMessage(`service call queued: ${scriptName}`, { | ||
| scriptName, | ||
| active, | ||
| queueDepth: pending.length + 1, | ||
| }); | ||
|
|
||
| pending.push(run); | ||
| timer = setTimeout( | ||
| () => removeFromQueue?.(new QueueTimeoutError()), | ||
| queueTimeoutMs() | ||
| ); | ||
| timer.unref?.(); | ||
|
|
||
| removeFromQueue = (reason: Error) => { | ||
| const i = pending.indexOf(run); | ||
| if (i === -1) return; // already started - too late to cancel | ||
| pending.splice(i, 1); | ||
| if (timer) clearTimeout(timer); | ||
| reject(reason); | ||
| }; | ||
| }); | ||
|
|
||
| return { | ||
| promise, | ||
| cancel: () => removeFromQueue?.(new Error("queue wait cancelled")), | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is nice - much cleaner control logic I'd say?