diff --git a/frontend-vue/src/shared/task-progress-config.ts b/frontend-vue/src/shared/task-progress-config.ts index c8ddb83d..f2ec1f11 100644 --- a/frontend-vue/src/shared/task-progress-config.ts +++ b/frontend-vue/src/shared/task-progress-config.ts @@ -18,7 +18,8 @@ export const TASK_POLL_HIDDEN_INTERVAL_MS = 60000; export const TASK_PROGRESS_VISIBLE_CACHE_MILLIS = 5000; export const TASK_PROGRESS_HIDDEN_CACHE_MILLIS = 30000; export const TASK_POLL_BACKOFF_BASE_MS = 500; -export const TASK_POLL_BACKOFF_MAX_MS = 5000; +export const TASK_POLL_BACKOFF_MAX_MS = 10000; +export const TASK_POLL_BACKOFF_FACTOR = 2; export const TASK_PROGRESS_TIMEOUT_MS = 10000; export interface TaskPollingConfig { @@ -26,6 +27,8 @@ export interface TaskPollingConfig { backoffBaseMs?: number; /** 退避上限(毫秒),必须为正数 */ backoffMaxMs?: number; + /** 退避指数因子,<1 时钳制为 1 */ + backoffFactor?: number; /** 页面切回前台时是否立即拉取一轮,默认 true */ foregroundRefreshEnabled?: boolean; /** 前台恢复延迟(毫秒),默认 0 */ @@ -37,6 +40,7 @@ export interface TaskPollingConfig { const DEFAULTS: Required = { backoffBaseMs: TASK_POLL_BACKOFF_BASE_MS, backoffMaxMs: TASK_POLL_BACKOFF_MAX_MS, + backoffFactor: TASK_POLL_BACKOFF_FACTOR, foregroundRefreshEnabled: true, foregroundRefreshDelayMs: 0, progressTimeoutMs: TASK_PROGRESS_TIMEOUT_MS, @@ -62,11 +66,21 @@ export function getTaskProgressCacheTtlMs() { return TASK_PROGRESS_VISIBLE_CACHE_MILLIS; } -/** 第 attempt 次退避延迟:base × 2^attempt,封顶 max;attempt 非法时按首次(0)处理 */ +/** 第 attempt 次退避延迟:base × factor^attempt,封顶 max;attempt 非法时按首次(0)处理 */ export function getTaskPollBackoffMs(attempt: number) { const safe = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0; - const doubled = config.backoffBaseMs * Math.pow(2, safe); - return doubled > config.backoffMaxMs ? config.backoffMaxMs : doubled; + const factor = getTaskPollBackoffFactor(); + const scaled = config.backoffBaseMs * Math.pow(factor, safe); + return scaled > config.backoffMaxMs ? config.backoffMaxMs : scaled; +} + +export function getTaskPollBackoffMaxMs() { + return config.backoffMaxMs; +} + +/** 退避指数因子,<1 时钳制为 1 */ +export function getTaskPollBackoffFactor() { + return config.backoffFactor < 1 ? 1 : config.backoffFactor; } export function getTaskForegroundRefreshEnabled() { diff --git a/frontend-vue/tests/task-progress-backoff-config.test.ts b/frontend-vue/tests/task-progress-backoff-config.test.ts new file mode 100644 index 00000000..3f793a97 --- /dev/null +++ b/frontend-vue/tests/task-progress-backoff-config.test.ts @@ -0,0 +1,72 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + TASK_POLL_BACKOFF_MAX_MS, + TASK_POLL_BACKOFF_FACTOR, + getTaskPollBackoffMaxMs, + getTaskPollBackoffFactor, + configureTaskPolling, + resetTaskPollingConfig, +} from '../src/shared/task-progress-config.ts' + +test('test_backoff_max_default', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMaxMs(), 10000) + assert.equal(getTaskPollBackoffMaxMs(), TASK_POLL_BACKOFF_MAX_MS) +}) + +test('test_backoff_factor_default', () => { + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffFactor(), 2) + assert.equal(getTaskPollBackoffFactor(), TASK_POLL_BACKOFF_FACTOR) +}) + +test('test_backoff_max_override', () => { + resetTaskPollingConfig() + configureTaskPolling({ backoffMaxMs: 8000 }) + assert.equal(getTaskPollBackoffMaxMs(), 8000) + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMaxMs(), 10000, 'reset 后恢复默认') +}) + +test('test_backoff_max_positive', () => { + resetTaskPollingConfig() + assert.throws(() => configureTaskPolling({ backoffMaxMs: 0 }), /退避上限必须为正数/) + assert.throws(() => configureTaskPolling({ backoffMaxMs: -100 }), /退避上限必须为正数/) + // 拒绝后 getter 仍返回正值,非正配置不可观察 + assert.ok(getTaskPollBackoffMaxMs() > 0) + assert.equal(getTaskPollBackoffMaxMs(), TASK_POLL_BACKOFF_MAX_MS) +}) + +test('test_backoff_factor_min_one', () => { + resetTaskPollingConfig() + // factor <1 钳制为 1(不抛错) + configureTaskPolling({ backoffFactor: 0.5 }) + assert.equal(getTaskPollBackoffFactor(), 1) + configureTaskPolling({ backoffFactor: 0 }) + assert.equal(getTaskPollBackoffFactor(), 1) + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffFactor(), 2) +}) + +test('test_backoff_config_isolated', () => { + resetTaskPollingConfig() + configureTaskPolling({ backoffMaxMs: 7000, backoffFactor: 3 }) + assert.equal(getTaskPollBackoffMaxMs(), 7000) + assert.equal(getTaskPollBackoffFactor(), 3) + resetTaskPollingConfig() + assert.equal(getTaskPollBackoffMaxMs(), 10000, '不串到其他用例') + assert.equal(getTaskPollBackoffFactor(), 2) +}) + +test('test_backoff_max_freeze', () => { + resetTaskPollingConfig() + assert.equal(TASK_POLL_BACKOFF_MAX_MS, 10000, '默认值快照') + assert.equal(getTaskPollBackoffMaxMs(), TASK_POLL_BACKOFF_MAX_MS) +}) + +test('test_backoff_factor_freeze', () => { + resetTaskPollingConfig() + assert.equal(TASK_POLL_BACKOFF_FACTOR, 2, 'factor 快照') + assert.equal(getTaskPollBackoffFactor(), TASK_POLL_BACKOFF_FACTOR) +}) diff --git a/frontend-vue/tests/task-progress-config.test.ts b/frontend-vue/tests/task-progress-config.test.ts index 766570c0..b211be30 100644 --- a/frontend-vue/tests/task-progress-config.test.ts +++ b/frontend-vue/tests/task-progress-config.test.ts @@ -105,9 +105,10 @@ test('test_task_086_polling_boundary_limit_and_overflow', () => { assert.equal(getTaskPollBackoffMs(1), 1000) assert.equal(getTaskPollBackoffMs(2), 2000) assert.equal(getTaskPollBackoffMs(3), 4000) - assert.equal(getTaskPollBackoffMs(4), 5000) - assert.equal(getTaskPollBackoffMs(10), 5000) - assert.equal(getTaskPollBackoffMs(100), 5000) + assert.equal(getTaskPollBackoffMs(4), 8000) + assert.equal(getTaskPollBackoffMs(5), 10000) + assert.equal(getTaskPollBackoffMs(10), 10000) + assert.equal(getTaskPollBackoffMs(100), 10000) // attempt 为 0 或负数按首次退避处理 assert.equal(getTaskPollBackoffMs(-1), 500) assert.equal(getTaskPollBackoffMs(NaN), 500)