mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-02 17:31:05 -07:00
feat(core/ui): enhance retry mechanism and UX (#16489)
This commit is contained in:
@@ -97,6 +97,17 @@ export interface HookEndPayload extends HookPayload {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'retry-attempt' event.
|
||||
*/
|
||||
export interface RetryAttemptPayload {
|
||||
attempt: number;
|
||||
maxAttempts: number;
|
||||
delayMs: number;
|
||||
error?: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export enum CoreEvent {
|
||||
UserFeedback = 'user-feedback',
|
||||
ModelChanged = 'model-changed',
|
||||
@@ -108,6 +119,7 @@ export enum CoreEvent {
|
||||
HookStart = 'hook-start',
|
||||
HookEnd = 'hook-end',
|
||||
AgentsRefreshed = 'agents-refreshed',
|
||||
RetryAttempt = 'retry-attempt',
|
||||
}
|
||||
|
||||
export interface CoreEvents {
|
||||
@@ -121,6 +133,7 @@ export interface CoreEvents {
|
||||
[CoreEvent.HookStart]: [HookStartPayload];
|
||||
[CoreEvent.HookEnd]: [HookEndPayload];
|
||||
[CoreEvent.AgentsRefreshed]: never[];
|
||||
[CoreEvent.RetryAttempt]: [RetryAttemptPayload];
|
||||
}
|
||||
|
||||
type EventBacklogItem = {
|
||||
@@ -229,6 +242,13 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
this.emit(CoreEvent.AgentsRefreshed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies subscribers that a retry attempt is happening.
|
||||
*/
|
||||
emitRetryAttempt(payload: RetryAttemptPayload): void {
|
||||
this.emit(CoreEvent.RetryAttempt, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes buffered messages. Call this immediately after primary UI listener
|
||||
* subscribes.
|
||||
|
||||
@@ -101,33 +101,33 @@ describe('retryWithBackoff', () => {
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should default to 3 maxAttempts if no options are provided', async () => {
|
||||
// This function will fail more than 3 times to ensure all retries are used.
|
||||
const mockFn = createFailingFunction(10);
|
||||
it('should default to 10 maxAttempts if no options are provided', async () => {
|
||||
// This function will fail more than 10 times to ensure all retries are used.
|
||||
const mockFn = createFailingFunction(15);
|
||||
|
||||
const promise = retryWithBackoff(mockFn);
|
||||
|
||||
await Promise.all([
|
||||
expect(promise).rejects.toThrow('Simulated error attempt 3'),
|
||||
expect(promise).rejects.toThrow('Simulated error attempt 10'),
|
||||
vi.runAllTimersAsync(),
|
||||
]);
|
||||
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
expect(mockFn).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it('should default to 3 maxAttempts if options.maxAttempts is undefined', async () => {
|
||||
// This function will fail more than 3 times to ensure all retries are used.
|
||||
const mockFn = createFailingFunction(10);
|
||||
it('should default to 10 maxAttempts if options.maxAttempts is undefined', async () => {
|
||||
// This function will fail more than 10 times to ensure all retries are used.
|
||||
const mockFn = createFailingFunction(15);
|
||||
|
||||
const promise = retryWithBackoff(mockFn, { maxAttempts: undefined });
|
||||
|
||||
// Expect it to fail with the error from the 5th attempt.
|
||||
// Expect it to fail with the error from the 10th attempt.
|
||||
await Promise.all([
|
||||
expect(promise).rejects.toThrow('Simulated error attempt 3'),
|
||||
expect(promise).rejects.toThrow('Simulated error attempt 10'),
|
||||
vi.runAllTimersAsync(),
|
||||
]);
|
||||
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
expect(mockFn).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it('should not retry if shouldRetry returns false', async () => {
|
||||
|
||||
@@ -32,10 +32,11 @@ export interface RetryOptions {
|
||||
retryFetchErrors?: boolean;
|
||||
signal?: AbortSignal;
|
||||
getAvailabilityContext?: () => RetryAvailabilityContext | undefined;
|
||||
onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
||||
maxAttempts: 3,
|
||||
maxAttempts: 10,
|
||||
initialDelayMs: 5000,
|
||||
maxDelayMs: 30000, // 30 seconds
|
||||
shouldRetryOnError: isRetryableError,
|
||||
@@ -149,6 +150,7 @@ export async function retryWithBackoff<T>(
|
||||
retryFetchErrors,
|
||||
signal,
|
||||
getAvailabilityContext,
|
||||
onRetry,
|
||||
} = {
|
||||
...DEFAULT_RETRY_OPTIONS,
|
||||
shouldRetryOnError: isRetryableError,
|
||||
@@ -172,6 +174,9 @@ export async function retryWithBackoff<T>(
|
||||
) {
|
||||
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
|
||||
const delayWithJitter = Math.max(0, currentDelay + jitter);
|
||||
if (onRetry) {
|
||||
onRetry(attempt, new Error('Invalid content'), delayWithJitter);
|
||||
}
|
||||
await delay(delayWithJitter, signal);
|
||||
currentDelay = Math.min(maxDelayMs, currentDelay * 2);
|
||||
continue;
|
||||
@@ -252,6 +257,9 @@ export async function retryWithBackoff<T>(
|
||||
debugLogger.warn(
|
||||
`Attempt ${attempt} failed: ${classifiedError.message}. Retrying after ${classifiedError.retryDelayMs}ms...`,
|
||||
);
|
||||
if (onRetry) {
|
||||
onRetry(attempt, error, classifiedError.retryDelayMs);
|
||||
}
|
||||
await delay(classifiedError.retryDelayMs, signal);
|
||||
continue;
|
||||
} else {
|
||||
@@ -261,6 +269,9 @@ export async function retryWithBackoff<T>(
|
||||
// Exponential backoff with jitter for non-quota errors
|
||||
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
|
||||
const delayWithJitter = Math.max(0, currentDelay + jitter);
|
||||
if (onRetry) {
|
||||
onRetry(attempt, error, delayWithJitter);
|
||||
}
|
||||
await delay(delayWithJitter, signal);
|
||||
currentDelay = Math.min(maxDelayMs, currentDelay * 2);
|
||||
continue;
|
||||
@@ -281,6 +292,9 @@ export async function retryWithBackoff<T>(
|
||||
// Exponential backoff with jitter for non-quota errors
|
||||
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
|
||||
const delayWithJitter = Math.max(0, currentDelay + jitter);
|
||||
if (onRetry) {
|
||||
onRetry(attempt, error, delayWithJitter);
|
||||
}
|
||||
await delay(delayWithJitter, signal);
|
||||
currentDelay = Math.min(maxDelayMs, currentDelay * 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user