-
Notifications
You must be signed in to change notification settings - Fork 0
feat: avoid using sensitive user limits endpoint #25
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
Changes from 6 commits
8ff4f7f
78e1b46
7eeb08d
2461031
95ef39a
8d1032b
4d3ec96
70d43d6
39bf825
d5ea85c
e80ca53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ import { MAIN_LOOP_COOLDOWN_MS, MAIN_LOOP_INTERVAL_MS } from '../constants.js'; | |
| import { InsufficientActorJobsError, InsufficientMemoryError } from '../errors.js'; | ||
| import { isRunOkStatus } from '../tracker.js'; | ||
| import type { DatasetItem, ExtendedApifyClient, RunRecord } from '../types.js'; | ||
| import { getUserLimits } from '../utils/apify-api.js'; | ||
| import { parseStartRunError } from '../utils/apify-client.js'; | ||
| import type { OrchestratorContext } from '../utils/context.js'; | ||
| import { Queue } from '../utils/queue.js'; | ||
| import type { EnqueuedRequest, ExtActorClientOptions, RunResult } from './actor-client.js'; | ||
|
|
@@ -80,14 +80,14 @@ export class ExtApifyClient extends ApifyClient implements ExtendedApifyClient { | |
| } | ||
| } | ||
|
|
||
| if (this.mainLoopId !== undefined) { | ||
| if (this.mainLoopId === undefined) { | ||
| // Avoid blocking if the orchestrator is not running | ||
| for (const callback of runRequest.startCallbacks) { | ||
| callback({ run: undefined, error: new Error('Orchestrator is not running') }); | ||
| } | ||
| } else { | ||
| this.context.logger.prfxInfo(runRequest.runName, 'Enqueuing Run request'); | ||
| this.runRequestsQueue.enqueue(runRequest); | ||
| } else { | ||
| // Avoid blocking if the orchestrator is not running | ||
| runRequest.startCallbacks.map((callback) => | ||
| callback({ run: undefined, error: new Error('Orchestrator is not running') }), | ||
| ); | ||
| } | ||
|
|
||
| return undefined; | ||
|
|
@@ -178,85 +178,54 @@ export class ExtApifyClient extends ApifyClient implements ExtendedApifyClient { | |
| // Main loop | ||
| this.mainLoopId = setInterval( | ||
| withMainLoopLock(async () => { | ||
| const nextRunRequest = this.runRequestsQueue.peek(); | ||
| const nextRunRequest = this.runRequestsQueue.dequeue(); | ||
| if (!nextRunRequest) { | ||
| return; | ||
| } | ||
|
|
||
| const userLimits = await getUserLimits(this.token); | ||
|
|
||
| const requiredMemoryMBs = | ||
| const getRequiredMemoryMbytes = async () => | ||
| nextRunRequest.options?.memory ?? | ||
| (await nextRunRequest.defaultMemoryMbytes()) ?? | ||
| // If no information about memory is available, set the requirement to zero. | ||
| 0; | ||
| const requiredMemoryGBs = requiredMemoryMBs / 1024; | ||
| const availableMemoryGBs = userLimits.maxMemoryGBs - userLimits.currentMemoryUsageGBs; | ||
| const availableActorJobs = userLimits.maxConcurrentActorJobs - userLimits.activeActorJobCount; | ||
|
|
||
| const hasEnoughMemory = availableMemoryGBs - requiredMemoryGBs > 0; | ||
| const canRunMoreActors = availableActorJobs > 0; | ||
|
|
||
| // Start the next run | ||
| if (hasEnoughMemory && canRunMoreActors) { | ||
| const runRequest = this.runRequestsQueue.dequeue(); | ||
| if (runRequest) { | ||
| const { runName, input, options } = runRequest; | ||
| this.context.logger.prfxInfo( | ||
| runName, | ||
| 'Starting next', | ||
| { queue: this.runRequestsQueue.length, requiredMemoryGBs }, | ||
| { availableMemoryGBs }, | ||
| ); | ||
| try { | ||
| const run = await runRequest.startRun(input, options); | ||
| await this.context.runsTracker.updateRun(runName, run); | ||
| runRequest.startCallbacks.map((callback) => callback({ run, error: undefined })); | ||
| } catch (e) { | ||
| this.context.logger.prfxError(runName, 'Failed to start Run', { | ||
| message: (e as Error)?.message, | ||
| }); | ||
| runRequest.startCallbacks.map((callback) => | ||
| callback({ run: undefined, error: e as Error }), | ||
| ); | ||
| } | ||
| } else { | ||
| this.context.logger.error("Something wrong with the Apify orchestrator's queue!"); | ||
| } | ||
| } else if (!this.retryOnInsufficientResources) { | ||
| const runRequest = this.runRequestsQueue.dequeue(); | ||
| if (!runRequest) { | ||
| throw new Error('Insufficient resources have been retrieved but no runRequest found!'); | ||
|
|
||
| const { runName, input, options } = nextRunRequest; | ||
|
|
||
| this.context.logger.prfxInfo(runName, 'Starting next', { queue: this.runRequestsQueue.length }); | ||
|
|
||
| let result: RunResult; | ||
|
|
||
| try { | ||
| const run = await nextRunRequest.startRun(input, options); | ||
|
Contributor
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. note to self: what is startRun doing since we can just fail in case it threw? I'd have thought we had to check for memory/concurrency errors in this case..
Collaborator
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. Here, the
Contributor
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. Yeah, sorry, I forgot to delete this comment before posting the review - when I have questions I haven't yet quite answered I put them in comments planning to delete them later 😅 |
||
| result = { run, error: undefined }; | ||
| } catch (startError) { | ||
| this.context.logger.prfxError(runName, 'Failed to start Run', { | ||
| message: (startError as Error)?.message, | ||
| }); | ||
| const error = await parseStartRunError(startError, runName, getRequiredMemoryMbytes); | ||
| result = { run: undefined, error }; | ||
| } | ||
|
|
||
| const { run, error } = result; | ||
|
|
||
| if (run) { | ||
| await this.context.runsTracker.updateRun(runName, run); | ||
| for (const callback of nextRunRequest.startCallbacks) { | ||
| callback({ run, error: undefined }); | ||
| } | ||
| const { runName } = runRequest; | ||
|
|
||
| const errorToThrow = (() => { | ||
| if (!hasEnoughMemory) { | ||
| return new InsufficientMemoryError(runName, requiredMemoryGBs, availableMemoryGBs); | ||
| } | ||
| if (!canRunMoreActors) { | ||
| return new InsufficientActorJobsError(runName); | ||
| } | ||
| throw new Error( | ||
| 'Insufficient resources have been retrieved but they did not match any of the checks!', | ||
| ); | ||
| })(); | ||
|
|
||
| this.context.logger.prfxError( | ||
| runName, | ||
| 'Failed to start Run and retryOnInsufficientResources is set to false', | ||
| { | ||
| message: errorToThrow.message, | ||
| }, | ||
| ); | ||
| runRequest.startCallbacks.map((callback) => callback({ run: undefined, error: errorToThrow })); | ||
| } else { | ||
| // Wait for sometime before checking again | ||
| } else if ( | ||
| this.retryOnInsufficientResources && | ||
|
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. I know this isn't related to this PR but was there ever a use-case for not retrying? Feels like an option we preemptively added but now might keep with us but perhaps I'm wrong :)
Collaborator
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. The default behavior used to be to retry, which became a problem in specific cases when the memory would never be enough and the actor would retry indefinitely until the run's timeout, since we don't have an option to specify the maximum number of retries.
Contributor
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. Yeah, we needed it in socials team - turns out users don't appreciate when they have a run running for 20 hours without progress 😅 |
||
| (error instanceof InsufficientMemoryError || error instanceof InsufficientActorJobsError) | ||
| ) { | ||
| this.context.logger.info( | ||
| `Not enough resources: waiting ${MAIN_LOOP_COOLDOWN_MS}ms before trying again`, | ||
| { availableMemoryGBs, availableActorJobs }, | ||
| ); | ||
| this.runRequestsQueue.enqueue(nextRunRequest); | ||
|
||
| this.mainLoopCooldown = MAIN_LOOP_COOLDOWN_MS; | ||
| } else { | ||
| for (const callback of nextRunRequest.startCallbacks) { | ||
| callback({ run: undefined, error }); | ||
| } | ||
| } | ||
| }), | ||
| MAIN_LOOP_INTERVAL_MS, | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { ApifyApiError } from 'apify-client'; | ||
|
|
||
| import { InsufficientActorJobsError, InsufficientMemoryError } from '../errors.js'; | ||
|
|
||
| export const MEMORY_LIMIT_EXCEEDED_ERROR_TYPE = 'actor-memory-limit-exceeded'; | ||
| export const CONCURRENT_RUNS_LIMIT_EXCEEDED_ERROR_TYPE = 'concurrent-runs-limit-exceeded'; | ||
|
|
||
| export async function parseStartRunError( | ||
| error: unknown, | ||
| runName: string, | ||
| getRequiredMemoryMbytes: () => Promise<number>, | ||
| ): Promise<Error> { | ||
| if (error instanceof ApifyApiError) { | ||
| if (error.type === MEMORY_LIMIT_EXCEEDED_ERROR_TYPE) { | ||
| return new InsufficientMemoryError(runName, await getRequiredMemoryMbytes()); | ||
| } | ||
| if (error.type === CONCURRENT_RUNS_LIMIT_EXCEEDED_ERROR_TYPE) { | ||
| return new InsufficientActorJobsError(runName); | ||
| } | ||
| } | ||
| if (error instanceof Error) { | ||
| return error; | ||
| } | ||
| return new Error(`Unknown error occurred while starting the run: ${runName}`); | ||
| } |
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.
nit: I'd probably do an early return here, drop the else and have implicit return.
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.
Done. The interface for this function is terrible: the return value is unintelligible.