Make review queue jobs resumable and lease-aware#11
Conversation
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35d06d55fd
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35d06d55fd
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing error handling for job maintenance
The runOpportunisticJobMaintenance call is not wrapped in a try-catch block. If this function throws an error, it will reject the promise returned by runWithDb, potentially interrupting the current batch processing or causing the worker to fail.
| Wrap the maintenance call in a try-catch block to ensure that maintenance failures do not break the processing of the current batch. |
| message.ack(); | ||
| continue; | ||
| } | ||
| if (!parseResult.success) { |
There was a problem hiding this comment.
Invalid message schema handling changed to retry
The logic for handling invalid queue messages was changed from message.ack() to message.retry(). While this allows the message to reach a DLQ, it consumes retry credits and CPU time. If the schema is fundamentally invalid, acknowledging the message is the standard pattern to stop the retry loop.
| if (!parseResult.success) { | |
| Revert to `message.ack()` for invalid schemas to prevent unnecessary retries and resource consumption. |
| const text = content | ||
| .map((part) => { | ||
| if (isText(part)) return part; | ||
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; |
There was a problem hiding this comment.
Unsafe type casting with any in extractMessageContent
The function uses '(part as any).text' to access the text property of a message part. This bypasses TypeScript's type checking and can lead to runtime errors if the object structure changes or if 'part' is not shaped as expected.
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; | |
| if (part && typeof part === 'object' && 'text' in part && isText(part.text)) return part.text; |
| return null; | ||
| } | ||
|
|
||
| function extractCloudflareText(result: any, model: string): string { |
There was a problem hiding this comment.
The 'extractCloudflareText' function defines its first parameter 'result' as 'any'. Since Cloudflare Workers AI has a predictable response schema, this should be replaced with a proper interface or a record type to ensure type safety when accessing nested properties like 'choices' or 'result.response'.
| function extractCloudflareText(result: any, model: string): string { | |
| function extractCloudflareText(result: unknown, model: string): string { |
| const startTime = Date.now(); | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`; | ||
| const maxRetries = 2; | ||
| const maxRetries = GOOGLE_MAX_RETRIES; |
There was a problem hiding this comment.
Use of any type for error tracking
The variable lastError is explicitly typed as any on line 20. This bypasses TypeScript's type checking and can lead to runtime errors when accessing properties of lastError later in the code. Using unknown is the modern TypeScript standard for variables that hold potential error objects, as it requires explicit type narrowing before use.
| const maxRetries = GOOGLE_MAX_RETRIES; | |
| let lastError: unknown; |
| const app = new Hono<AppEnv>(); | ||
|
|
||
| app.get('/', async (c) => { | ||
| await runOpportunisticJobMaintenance(c.env); |
There was a problem hiding this comment.
Uncaught exception in opportunistic maintenance
The calls to runOpportunisticJobMaintenance(c.env) are awaited directly within the request handlers. If the maintenance logic fails (e.g., database timeout, lock contention, or internal error), it will throw an exception that results in a 500 Internal Server Error for the user. Since this maintenance is 'opportunistic' and not critical to the immediate request's success, it should be wrapped in a try-catch block to ensure that the API remains available even if maintenance fails.
| await runOpportunisticJobMaintenance(c.env); | |
| try { | |
| await runOpportunisticJobMaintenance(c.env); | |
| } catch (e) { | |
| console.error('Opportunistic job maintenance failed:', e); | |
| } |
| @@ -110,6 +149,7 @@ export class ModelService { | |||
| const modelsToTry = [primary, ...fallbacks]; | |||
There was a problem hiding this comment.
Use of
any type for error variable
The variable lastError is declared as any (line 151). This bypasses TypeScript's type checking, potentially allowing runtime errors if the error object does not have the expected properties.
| const modelsToTry = [primary, ...fallbacks]; | |
| let lastError: unknown; |
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35d06d55fd
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing error handling for job maintenance
The runOpportunisticJobMaintenance call is not wrapped in a try-catch block. If this function throws an error, it will reject the promise returned by runWithDb, potentially interrupting the current batch processing or causing the worker to fail.
| Wrap the maintenance call in a try-catch block to ensure that maintenance failures do not break the processing of the current batch. |
| message.ack(); | ||
| continue; | ||
| } | ||
| if (!parseResult.success) { |
There was a problem hiding this comment.
Invalid message schema handling changed to retry
The logic for handling invalid queue messages was changed from message.ack() to message.retry(). While this allows the message to reach a DLQ, it consumes retry credits and CPU time. If the schema is fundamentally invalid, acknowledging the message is the standard pattern to stop the retry loop.
| if (!parseResult.success) { | |
| Revert to `message.ack()` for invalid schemas to prevent unnecessary retries and resource consumption. |
| const text = content | ||
| .map((part) => { | ||
| if (isText(part)) return part; | ||
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; |
There was a problem hiding this comment.
Unsafe type casting with any in extractMessageContent
The function uses '(part as any).text' to access the text property of a message part. This bypasses TypeScript's type checking and can lead to runtime errors if the object structure changes or if 'part' is not shaped as expected.
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; | |
| if (part && typeof part === 'object' && 'text' in part && isText(part.text)) return part.text; |
| return null; | ||
| } | ||
|
|
||
| function extractCloudflareText(result: any, model: string): string { |
There was a problem hiding this comment.
The 'extractCloudflareText' function defines its first parameter 'result' as 'any'. Since Cloudflare Workers AI has a predictable response schema, this should be replaced with a proper interface or a record type to ensure type safety when accessing nested properties like 'choices' or 'result.response'.
| function extractCloudflareText(result: any, model: string): string { | |
| function extractCloudflareText(result: unknown, model: string): string { |
| const startTime = Date.now(); | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`; | ||
| const maxRetries = 2; | ||
| const maxRetries = GOOGLE_MAX_RETRIES; |
There was a problem hiding this comment.
Use of any type for error tracking
The variable lastError is explicitly typed as any on line 20. This bypasses TypeScript's type checking and can lead to runtime errors when accessing properties of lastError later in the code. Using unknown is the modern TypeScript standard for variables that hold potential error objects, as it requires explicit type narrowing before use.
| const maxRetries = GOOGLE_MAX_RETRIES; | |
| let lastError: unknown; |
| const app = new Hono<AppEnv>(); | ||
|
|
||
| app.get('/', async (c) => { | ||
| await runOpportunisticJobMaintenance(c.env); |
There was a problem hiding this comment.
Uncaught exception in opportunistic maintenance
The calls to runOpportunisticJobMaintenance(c.env) are awaited directly within the request handlers. If the maintenance logic fails (e.g., database timeout, lock contention, or internal error), it will throw an exception that results in a 500 Internal Server Error for the user. Since this maintenance is 'opportunistic' and not critical to the immediate request's success, it should be wrapped in a try-catch block to ensure that the API remains available even if maintenance fails.
| await runOpportunisticJobMaintenance(c.env); | |
| try { | |
| await runOpportunisticJobMaintenance(c.env); | |
| } catch (e) { | |
| console.error('Opportunistic job maintenance failed:', e); | |
| } |
| @@ -110,6 +149,7 @@ export class ModelService { | |||
| const modelsToTry = [primary, ...fallbacks]; | |||
There was a problem hiding this comment.
Use of
any type for error variable
The variable lastError is declared as any (line 151). This bypasses TypeScript's type checking, potentially allowing runtime errors if the error object does not have the expected properties.
| const modelsToTry = [primary, ...fallbacks]; | |
| let lastError: unknown; |
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35d06d55fd
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
| export async function upsertFileReview( | ||
| env: Pick<AppBindings, 'HYPERDRIVE'>, | ||
| jobId: string, | ||
| input: { |
There was a problem hiding this comment.
Implicit 'any' type in object literal
While the function signature defines the shape of input, the variable input inside the function is inferred. In strict TypeScript mode, relying on object literal inference can sometimes lead to issues if the literal doesn't match the interface exactly.
| input: { | |
| Explicitly type the parameter or the variable, e.g., `input: UpsertFileReviewInput` if a dedicated interface is created, or ensure the literal passed in matches the type definition strictly. |
| } catch (err) { | ||
| // Non-fatal: log and continue processing the batch. | ||
| logger.error('Failed to recover stale jobs', err instanceof Error ? err : new Error(String(err))); | ||
| } |
There was a problem hiding this comment.
Stale job recovery moved to end of batch processing
The runOpportunisticJobMaintenance call was moved from the start of the queue function to the end. This is a critical logic error. If a job is stuck in 'running' state due to a previous crash, it will not be recovered until the next batch is processed. If the queue is empty, stale jobs will remain stuck indefinitely, violating the 'resumable' goal.
| } | |
| Move `await runOpportunisticJobMaintenance(env);` back to the beginning of the `queue` function, before processing messages, to ensure stale jobs are recovered immediately. |
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing error handling for job maintenance
The runOpportunisticJobMaintenance call is not wrapped in a try-catch block. If this function throws an error, it will reject the promise returned by runWithDb, potentially interrupting the current batch processing or causing the worker to fail.
| Wrap the maintenance call in a try-catch block to ensure that maintenance failures do not break the processing of the current batch. |
| message.ack(); | ||
| continue; | ||
| } | ||
| if (!parseResult.success) { |
There was a problem hiding this comment.
Invalid message schema handling changed to retry
The logic for handling invalid queue messages was changed from message.ack() to message.retry(). While this allows the message to reach a DLQ, it consumes retry credits and CPU time. If the schema is fundamentally invalid, acknowledging the message is the standard pattern to stop the retry loop.
| if (!parseResult.success) { | |
| Revert to `message.ack()` for invalid schemas to prevent unnecessary retries and resource consumption. |
| const text = content | ||
| .map((part) => { | ||
| if (isText(part)) return part; | ||
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; |
There was a problem hiding this comment.
Unsafe type casting with any in extractMessageContent
The function uses '(part as any).text' to access the text property of a message part. This bypasses TypeScript's type checking and can lead to runtime errors if the object structure changes or if 'part' is not shaped as expected.
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; | |
| if (part && typeof part === 'object' && 'text' in part && isText(part.text)) return part.text; |
| super(message); | ||
| this.name = 'RetryableModelError'; | ||
| if (cause !== undefined) { | ||
| (this as any).cause = cause; |
There was a problem hiding this comment.
Unsafe type cast in
RetryableModelError constructor
The code uses (this as any) to assign the cause property (line 26). This is unnecessary and unsafe. The Error class supports the cause property natively in modern JavaScript/TypeScript.
| (this as any).cause = cause; | |
| Object.defineProperty(this, 'cause', { value: cause, writable: true, configurable: true }); |
| } | ||
|
|
||
| export function isRetryableModelError(error: unknown) { | ||
| return Boolean(error && typeof error === 'object' && (error as any).retryable === true); |
There was a problem hiding this comment.
Unsafe type cast in
isRetryableModelError
The function uses (error as any) to check for the retryable property (line 32). This defeats the purpose of using a type-safe language. It should use instanceof or a type guard.
| return Boolean(error && typeof error === 'object' && (error as any).retryable === true); | |
| return error instanceof RetryableModelError; |
| async send(message: any) { | ||
| this.sent.push(message); | ||
| async send(message: any, options?: { delaySeconds?: number }) { | ||
| this.sent.push({ ...message, options }); |
There was a problem hiding this comment.
Potential data corruption via object spread of message
The code uses the spread operator { ...message, options } to store the sent message. If message is a primitive (e.g., a string or number), the spread operator will not behave as expected (e.g., a string will be spread into indexed characters). Furthermore, if message contains a property named 'options', it will be overwritten by the options argument. It is safer to store the message and options as distinct properties in a wrapper object.
| this.sent.push({ ...message, options }); | |
| this.sent.push({ message, options }); |
| @@ -49,8 +49,8 @@ export class MockAssets { | |||
| export class MockQueue { | |||
| public readonly sent: any[] = []; | |||
There was a problem hiding this comment.
Lack of type safety with 'any' usage
The sent array and the message parameter are typed as any. This bypasses TypeScript's type checking and can lead to runtime errors in tests. Since this is a Mock class, it should ideally use generics to allow the caller to specify the expected message type.
| public readonly sent: any[] = []; | |
| export class MockQueue<T = any> { | |
| public readonly sent: Array<{ message: T; options?: { delaySeconds?: number } }> = []; | |
| async send(message: T, options?: { delaySeconds?: number }) { | |
| this.sent.push({ message, options }); | |
| } | |
| } |
| ], | ||
| usage: { prompt_tokens: 1, completion_tokens: 4096 }, | ||
| }; | ||
| }, |
There was a problem hiding this comment.
Avoid use of 'any' for environment mocks
The use of 'as any' to mock the Cloudflare AI binding (lines 45, 64, 82) bypasses TypeScript's type checking. While common in tests, it's better to use 'Partial' or a specific interface for the binding to ensure the mock remains compatible with the actual API as it evolves.
| }, | |
| // Example: Cast to a partial of the expected AI binding type | |
| AI: { | |
| async run() { | |
| // ... | |
| }, | |
| } as Partial<CloudflareAIBinding> |
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f89d03ec01
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
| }; | ||
|
|
||
| function normalizeGlobalConfig(config: any): ModelRouteConfig { | ||
| export function normalizeGlobalConfig(config: any): ModelRouteConfig { |
There was a problem hiding this comment.
Use of any type for config parameter
The function 'normalizeGlobalConfig' uses 'any' for the 'config' parameter. This bypasses TypeScript's type checking and can lead to runtime errors if the input structure changes. It is better to use 'Partial' or 'unknown' with a type guard to ensure type safety.
| export function normalizeGlobalConfig(config: any): ModelRouteConfig { | |
| export function normalizeGlobalConfig(config: Partial<ModelRouteConfig>): ModelRouteConfig { |
| const inherited = parentReviews.get(file.path); | ||
| if (inherited) { | ||
| if (!canInheritParentFileReview(config, inherited)) { | ||
| logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`); |
There was a problem hiding this comment.
Type safety violation with 'true as any' cast
The code uses 'true as any' when setting the value in 'currentReviews.set(file.path, true as any)' and 'currentReviews.set(file.path, inherited)'. This bypasses TypeScript's type checking and can lead to runtime errors if the Map is expected to contain specific object structures. It's better to use a proper sentinel value or ensure the Map type is correctly defined for the purpose.
| logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`); | |
| Instead of using 'true as any', define a type for the Map values that accommodates both the review object and a marker for 'processed', or use a Set for tracking processed paths separately. |
| } | ||
|
|
||
| let eventName = message.eventName; | ||
| let payload = message.payload as GitHubWebhookPayload | undefined; |
There was a problem hiding this comment.
Potential race condition in job lease management
The job lease is claimed at the beginning of 'runReviewJob' using 'claimJobLease'. However, if the process crashes or fails before 'releaseJobLease' or 'heartbeatJobLease' is called, the lease might remain active until it expires naturally. While the implementation uses a lease-aware approach, the error handling logic for 'isRetryableModelError' calls 'releaseJobLease' but other non-retryable errors might skip it if not carefully structured in every possible execution path (though current try-catch seems to cover it).
| let payload = message.payload as GitHubWebhookPayload | undefined; | |
| Ensure all exit paths in the catch block explicitly handle lease release, or use a 'try...finally' block to guarantee 'releaseJobLease' is called when the job is finished (either successfully, via supersession, or via terminal failure). |
| return { action: 'ack' }; | ||
| } catch (error) { | ||
| const messageText = error instanceof Error ? error.message : 'Unknown review failure'; | ||
| if (messageText === 'JOB_SUPERSEDED') { |
There was a problem hiding this comment.
Incomplete cleanup of job state on supersession
When a job is superseded ('JOB_SUPERSEDED' error), the code calls 'releaseJobLease' and returns 'ack'. However, it does not explicitly mark the job as 'superseded' in the database if it hasn't been already (the supersession is typically handled by the function that creates the new job, but the current job's local state handling could be more robust to ensure no orphaned 'running' statuses persist in sub-steps).
| if (messageText === 'JOB_SUPERSEDED') { | |
| Verify that 'releaseJobLease' or the superseding logic correctly transitions the status of the superseded job to a terminal state to prevent any potential state mismatch. |
| return { | ||
| async query<T>(sqlText: string, params: unknown[] = []) { | ||
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: true })) as T[]; | ||
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: false })) as T[]; |
There was a problem hiding this comment.
Avoid use of 'any' type casting
The use of 'as any[]' when mapping parameters bypasses TypeScript's type checking. Since 'params' is already 'unknown[]', and 'normalizeParam' likely returns a type compatible with the database driver, casting to 'unknown[]' or omitting the cast (if the function return type allows) would be safer and more aligned with TypeScript best practices.
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: false })) as T[]; | |
| return (await sql.unsafe(sqlText, params.map(normalizeParam), { prepare: false })) as T[]; |
| const text = content | ||
| .map((part) => { | ||
| if (isText(part)) return part; | ||
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; |
There was a problem hiding this comment.
Excessive use of any type in response extraction
The functions extractCloudflareText and extractMessageContent use the any type for the result and part parameters. This bypasses TypeScript's type checking and increases the risk of runtime errors if the API response structure changes unexpectedly. Since Cloudflare's AI responses follow specific schemas (e.g., Chat Completion API), these should be defined as interfaces or a union of possible response shapes.
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; | |
| interface CloudflareResponse { | |
| response?: string; | |
| result?: { response?: string }; | |
| choices?: Array<{ | |
| message?: { content?: string | Array<{ text?: string } | string>; reasoning?: string; reasoning_content?: string }; | |
| finish_reason?: string; | |
| stop_reason?: string; | |
| }>; | |
| } | |
| function extractCloudflareText(result: CloudflareResponse | string, model: string): string { ... } |
| const CLOUDFLARE_MAX_RETRIES = 1; | ||
|
|
||
| function isText(value: unknown): value is string { | ||
| return typeof value === 'string' && value.trim().length > 0; |
There was a problem hiding this comment.
Potential logic error in empty string handling
The isText helper returns false if a string is empty or only whitespace (value.trim().length > 0). In extractMessageContent, if the AI returns an array of empty strings, the .join('').trim() result will be an empty string, causing the function to return null (line 26). This subsequently triggers a generic 'empty response' error in extractCloudflareText. While likely desired, it treats 'empty content' as a failure rather than a valid (albeit empty) response.
| const startTime = Date.now(); | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`; | ||
| const maxRetries = 2; | ||
| const maxRetries = GOOGLE_MAX_RETRIES; |
There was a problem hiding this comment.
Avoid use of any type for lastError
The variable 'lastError' is explicitly typed as 'any'. This bypasses TypeScript's type checking and can lead to runtime errors if the error object does not have the expected properties. It is recommended to use 'unknown' or 'Error | unknown' and employ type narrowing (e.g., 'if (lastError instanceof Error)') when accessing properties.
| const maxRetries = GOOGLE_MAX_RETRIES; | |
| let lastError: unknown; |
There was a problem hiding this comment.
Codra Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f89d03ec01
ℹ️ About Codra in GitHub
Your team has set up Codra to review pull requests in this repo. Reviews are triggered when you:
- Open a pull request for review
- Mark a draft as ready
- Comment "@codra-app review"
If Codra has suggestions, it will comment; otherwise it will react with 👍.
Codra can also answer questions or update the PR. Try commenting "@codra-app address that feedback".
| }; | ||
|
|
||
| function normalizeGlobalConfig(config: any): ModelRouteConfig { | ||
| export function normalizeGlobalConfig(config: any): ModelRouteConfig { |
There was a problem hiding this comment.
Use of any type for config parameter
The function 'normalizeGlobalConfig' uses 'any' for the 'config' parameter. This bypasses TypeScript's type checking and can lead to runtime errors if the input structure changes. It is better to use 'Partial' or 'unknown' with a type guard to ensure type safety.
| export function normalizeGlobalConfig(config: any): ModelRouteConfig { | |
| export function normalizeGlobalConfig(config: Partial<ModelRouteConfig>): ModelRouteConfig { |
| const inherited = parentReviews.get(file.path); | ||
| if (inherited) { | ||
| if (!canInheritParentFileReview(config, inherited)) { | ||
| logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`); |
There was a problem hiding this comment.
Type safety violation with 'true as any' cast
The code uses 'true as any' when setting the value in 'currentReviews.set(file.path, true as any)' and 'currentReviews.set(file.path, inherited)'. This bypasses TypeScript's type checking and can lead to runtime errors if the Map is expected to contain specific object structures. It's better to use a proper sentinel value or ensure the Map type is correctly defined for the purpose.
| logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`); | |
| Instead of using 'true as any', define a type for the Map values that accommodates both the review object and a marker for 'processed', or use a Set for tracking processed paths separately. |
| } | ||
|
|
||
| let eventName = message.eventName; | ||
| let payload = message.payload as GitHubWebhookPayload | undefined; |
There was a problem hiding this comment.
Potential race condition in job lease management
The job lease is claimed at the beginning of 'runReviewJob' using 'claimJobLease'. However, if the process crashes or fails before 'releaseJobLease' or 'heartbeatJobLease' is called, the lease might remain active until it expires naturally. While the implementation uses a lease-aware approach, the error handling logic for 'isRetryableModelError' calls 'releaseJobLease' but other non-retryable errors might skip it if not carefully structured in every possible execution path (though current try-catch seems to cover it).
| let payload = message.payload as GitHubWebhookPayload | undefined; | |
| Ensure all exit paths in the catch block explicitly handle lease release, or use a 'try...finally' block to guarantee 'releaseJobLease' is called when the job is finished (either successfully, via supersession, or via terminal failure). |
| return { action: 'ack' }; | ||
| } catch (error) { | ||
| const messageText = error instanceof Error ? error.message : 'Unknown review failure'; | ||
| if (messageText === 'JOB_SUPERSEDED') { |
There was a problem hiding this comment.
Incomplete cleanup of job state on supersession
When a job is superseded ('JOB_SUPERSEDED' error), the code calls 'releaseJobLease' and returns 'ack'. However, it does not explicitly mark the job as 'superseded' in the database if it hasn't been already (the supersession is typically handled by the function that creates the new job, but the current job's local state handling could be more robust to ensure no orphaned 'running' statuses persist in sub-steps).
| if (messageText === 'JOB_SUPERSEDED') { | |
| Verify that 'releaseJobLease' or the superseding logic correctly transitions the status of the superseded job to a terminal state to prevent any potential state mismatch. |
| return { | ||
| async query<T>(sqlText: string, params: unknown[] = []) { | ||
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: true })) as T[]; | ||
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: false })) as T[]; |
There was a problem hiding this comment.
Avoid use of 'any' type casting
The use of 'as any[]' when mapping parameters bypasses TypeScript's type checking. Since 'params' is already 'unknown[]', and 'normalizeParam' likely returns a type compatible with the database driver, casting to 'unknown[]' or omitting the cast (if the function return type allows) would be safer and more aligned with TypeScript best practices.
| return (await sql.unsafe(sqlText, params.map(normalizeParam) as any[], { prepare: false })) as T[]; | |
| return (await sql.unsafe(sqlText, params.map(normalizeParam), { prepare: false })) as T[]; |
| const text = content | ||
| .map((part) => { | ||
| if (isText(part)) return part; | ||
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; |
There was a problem hiding this comment.
Excessive use of any type in response extraction
The functions extractCloudflareText and extractMessageContent use the any type for the result and part parameters. This bypasses TypeScript's type checking and increases the risk of runtime errors if the API response structure changes unexpectedly. Since Cloudflare's AI responses follow specific schemas (e.g., Chat Completion API), these should be defined as interfaces or a union of possible response shapes.
| if (part && typeof part === 'object' && isText((part as any).text)) return (part as any).text; | |
| interface CloudflareResponse { | |
| response?: string; | |
| result?: { response?: string }; | |
| choices?: Array<{ | |
| message?: { content?: string | Array<{ text?: string } | string>; reasoning?: string; reasoning_content?: string }; | |
| finish_reason?: string; | |
| stop_reason?: string; | |
| }>; | |
| } | |
| function extractCloudflareText(result: CloudflareResponse | string, model: string): string { ... } |
| const CLOUDFLARE_MAX_RETRIES = 1; | ||
|
|
||
| function isText(value: unknown): value is string { | ||
| return typeof value === 'string' && value.trim().length > 0; |
There was a problem hiding this comment.
Potential logic error in empty string handling
The isText helper returns false if a string is empty or only whitespace (value.trim().length > 0). In extractMessageContent, if the AI returns an array of empty strings, the .join('').trim() result will be an empty string, causing the function to return null (line 26). This subsequently triggers a generic 'empty response' error in extractCloudflareText. While likely desired, it treats 'empty content' as a failure rather than a valid (albeit empty) response.
| const startTime = Date.now(); | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`; | ||
| const maxRetries = 2; | ||
| const maxRetries = GOOGLE_MAX_RETRIES; |
There was a problem hiding this comment.
Avoid use of any type for lastError
The variable 'lastError' is explicitly typed as 'any'. This bypasses TypeScript's type checking and can lead to runtime errors if the error object does not have the expected properties. It is recommended to use 'unknown' or 'Error | unknown' and employ type narrowing (e.g., 'if (lastError instanceof Error)') when accessing properties.
| const maxRetries = GOOGLE_MAX_RETRIES; | |
| let lastError: unknown; |
| const app = new Hono<AppEnv>(); | ||
|
|
||
| app.get('/', async (c) => { | ||
| await runOpportunisticJobMaintenance(c.env); |
There was a problem hiding this comment.
Uncaught exception in opportunistic maintenance
The calls to runOpportunisticJobMaintenance(c.env) on lines 13 and 23 are awaited without a try-catch block. Since this is described as 'opportunistic maintenance', it likely performs background cleanup tasks. If this function throws an error (e.g., due to a database timeout or connection issue), it will cause the entire API request to fail with a 500 error, preventing users from accessing job lists or details for a non-critical background task.
| await runOpportunisticJobMaintenance(c.env); | |
| try { | |
| await runOpportunisticJobMaintenance(c.env); | |
| } catch (e) { | |
| console.error('Opportunistic maintenance failed:', e); | |
| } |
| @@ -43,7 +54,7 @@ export function createJobsRouter() { | |||
| trigger: 'retry', | |||
| headRef: rawSource.head_ref, | |||
There was a problem hiding this comment.
Potential null pointer dereference for currentConfig
On line 57, the code accesses currentConfig.parsedJson. However, the result of loadRepoConfig (line 39) is not checked for null or undefined. If the configuration fails to load or the repository is not found, currentConfig may be null, leading to a runtime crash. Previously, the code used a nullish coalescing operator with defaultRepoConfig to ensure a fallback value.
| headRef: rawSource.head_ref, | |
| configSnapshot: currentConfig?.parsedJson ?? defaultRepoConfig, |
Description
This PR makes review jobs resilient to worker crashes, duplicate queue deliveries, and transient model/provider failures.
Closes #9
Type of change
How Has This Been Tested?
Checklist: