Fix monitor collect-now queue fairness
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@geo/desktop-client",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
|
||||
"author": {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('electron/main', () => ({
|
||||
app: {
|
||||
getPath: () => join(tmpdir(), 'geo-rankly-monitor-scheduler-test'),
|
||||
},
|
||||
}))
|
||||
|
||||
import {
|
||||
enqueueMonitorLeaseTask,
|
||||
initMonitorScheduler,
|
||||
noteMonitorTaskActivated,
|
||||
noteMonitorTaskCompleted,
|
||||
selectNextMonitorTask,
|
||||
} from './monitor-scheduler'
|
||||
|
||||
describe('monitor scheduler', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-06-22T00:00:00.000Z'))
|
||||
initMonitorScheduler()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
initMonitorScheduler()
|
||||
})
|
||||
|
||||
it('does not let high-priority monitor tasks bypass same-platform cooldown', () => {
|
||||
enqueueMonitorLeaseTask({
|
||||
taskId: 'first',
|
||||
clientId: 'client-1',
|
||||
platform: 'doubao',
|
||||
routing: 'rabbitmq-primary',
|
||||
questionKey: 'q:first',
|
||||
})
|
||||
expect(
|
||||
selectNextMonitorTask({
|
||||
maxConcurrency: 2,
|
||||
activePlatforms: new Set(),
|
||||
activeQuestionKeys: new Set(),
|
||||
activeCount: 0,
|
||||
})?.taskId,
|
||||
).toBe('first')
|
||||
noteMonitorTaskActivated('first')
|
||||
noteMonitorTaskCompleted('first')
|
||||
|
||||
enqueueMonitorLeaseTask({
|
||||
taskId: 'urgent',
|
||||
clientId: 'client-1',
|
||||
platform: 'doubao',
|
||||
routing: 'rabbitmq-primary',
|
||||
priority: 5000,
|
||||
lane: 'high',
|
||||
questionKey: 'q:urgent',
|
||||
})
|
||||
|
||||
expect(
|
||||
selectNextMonitorTask({
|
||||
maxConcurrency: 2,
|
||||
activePlatforms: new Set(),
|
||||
activeQuestionKeys: new Set(),
|
||||
activeCount: 0,
|
||||
}),
|
||||
).toBeNull()
|
||||
|
||||
vi.advanceTimersByTime(5_001)
|
||||
|
||||
expect(
|
||||
selectNextMonitorTask({
|
||||
maxConcurrency: 2,
|
||||
activePlatforms: new Set(),
|
||||
activeQuestionKeys: new Set(),
|
||||
activeCount: 0,
|
||||
})?.taskId,
|
||||
).toBe('urgent')
|
||||
})
|
||||
|
||||
it('does not let high-priority monitor tasks bypass an active same-platform task', () => {
|
||||
enqueueMonitorLeaseTask({
|
||||
taskId: 'urgent-doubao',
|
||||
clientId: 'client-1',
|
||||
platform: 'doubao',
|
||||
routing: 'rabbitmq-primary',
|
||||
priority: 5000,
|
||||
lane: 'high',
|
||||
questionKey: 'q:urgent',
|
||||
})
|
||||
|
||||
expect(
|
||||
selectNextMonitorTask({
|
||||
maxConcurrency: 2,
|
||||
activePlatforms: new Set(['doubao']),
|
||||
activeQuestionKeys: new Set(),
|
||||
activeCount: 1,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('prioritizes collect-now high lane before older normal tasks when platform is available', () => {
|
||||
enqueueMonitorLeaseTask({
|
||||
taskId: 'old-normal',
|
||||
clientId: 'client-1',
|
||||
platform: 'qwen',
|
||||
routing: 'rabbitmq-primary',
|
||||
priority: 100,
|
||||
lane: 'normal',
|
||||
questionKey: 'q:normal',
|
||||
})
|
||||
|
||||
vi.advanceTimersByTime(1_000)
|
||||
|
||||
enqueueMonitorLeaseTask({
|
||||
taskId: 'collect-now',
|
||||
clientId: 'client-1',
|
||||
platform: 'doubao',
|
||||
routing: 'rabbitmq-primary',
|
||||
priority: 5000,
|
||||
lane: 'high',
|
||||
questionKey: 'q:collect-now',
|
||||
})
|
||||
|
||||
expect(
|
||||
selectNextMonitorTask({
|
||||
maxConcurrency: 2,
|
||||
activePlatforms: new Set(),
|
||||
activeQuestionKeys: new Set(),
|
||||
activeCount: 0,
|
||||
})?.taskId,
|
||||
).toBe('collect-now')
|
||||
})
|
||||
})
|
||||
@@ -74,7 +74,7 @@ interface MonitorTaskMetadata {
|
||||
questionText: string | null
|
||||
}
|
||||
|
||||
const schedulerStateVersion = 5
|
||||
const schedulerStateVersion = 6
|
||||
const schedulerQuestionCooldownMs = 45_000
|
||||
const schedulerPlatformCooldownMs = 5_000
|
||||
const schedulerRestartRecoveryDelayMs = 90_000
|
||||
@@ -492,10 +492,10 @@ export function selectNextMonitorTask(
|
||||
continue
|
||||
}
|
||||
|
||||
const bypassCooldowns = candidate.lane === 'high'
|
||||
const bypassQuestionCooldown = candidate.lane === 'high'
|
||||
|
||||
const platformCooldown = draft.platformCooldowns[candidate.platform] ?? 0
|
||||
if (!bypassCooldowns && platformCooldown > now) {
|
||||
if (platformCooldown > now) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ export function selectNextMonitorTask(
|
||||
continue
|
||||
}
|
||||
const questionCooldown = draft.questionCooldowns[candidate.questionKey] ?? 0
|
||||
if (!bypassCooldowns && questionCooldown > now) {
|
||||
if (!bypassQuestionCooldown && questionCooldown > now) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -553,12 +553,12 @@ export function getMonitorSchedulerSnapshot(): MonitorSchedulerSnapshot {
|
||||
|
||||
function leaseMissBackoffMs(leaseMisses: number): number {
|
||||
if (leaseMisses <= 1) {
|
||||
return 20_000
|
||||
return 15_000
|
||||
}
|
||||
if (leaseMisses === 2) {
|
||||
return 60_000
|
||||
return 30_000
|
||||
}
|
||||
return 3 * 60_000
|
||||
return 60_000
|
||||
}
|
||||
|
||||
function metadataFromEvent(event: DesktopTaskEventMessage): MonitorTaskMetadata {
|
||||
|
||||
@@ -63,13 +63,11 @@ import {
|
||||
} from './lease-manager'
|
||||
import {
|
||||
enqueueMonitorLeaseTask,
|
||||
enqueueMonitorTaskFromEvent,
|
||||
getMonitorSchedulerSnapshot,
|
||||
initMonitorScheduler,
|
||||
noteMonitorTaskActivated,
|
||||
noteMonitorTaskCanceled,
|
||||
noteMonitorTaskCompleted,
|
||||
noteMonitorTaskLeaseMiss,
|
||||
noteMonitorTaskLeased,
|
||||
selectNextMonitorTask,
|
||||
} from './monitor-scheduler'
|
||||
@@ -149,6 +147,7 @@ type MonitorExecutionPhase =
|
||||
const heartbeatIntervalMs = 15_000
|
||||
const pullIntervalMs = 60_000
|
||||
const publishPullIntervalMs = 30_000
|
||||
const monitorPullFallbackIntervalMs = 10_000
|
||||
const accountSyncIntervalMs = 30_000
|
||||
const pumpWatchdogIntervalMs = 30_000
|
||||
const leaseExtendIntervalMs = 60_000
|
||||
@@ -256,6 +255,7 @@ interface RuntimeState {
|
||||
activeExecutions: Map<string, ActiveRuntimeExecution>
|
||||
leaseInFlightKinds: Set<'publish' | 'monitor'>
|
||||
publishFallbackBackoffUntil: number
|
||||
monitorFallbackBackoffUntil: number
|
||||
heartbeatTimer: ReturnType<typeof setInterval> | null
|
||||
pullTimer: ReturnType<typeof setInterval> | null
|
||||
accountSyncTimer: ReturnType<typeof setInterval> | null
|
||||
@@ -289,6 +289,7 @@ const state: RuntimeState = {
|
||||
activeExecutions: new Map<string, ActiveRuntimeExecution>(),
|
||||
leaseInFlightKinds: new Set<'publish' | 'monitor'>(),
|
||||
publishFallbackBackoffUntil: 0,
|
||||
monitorFallbackBackoffUntil: 0,
|
||||
heartbeatTimer: null,
|
||||
pullTimer: null,
|
||||
accountSyncTimer: null,
|
||||
@@ -806,6 +807,7 @@ function clearRuntimeState(): void {
|
||||
state.onlineClientCount = null
|
||||
state.lastError = null
|
||||
state.publishFallbackBackoffUntil = 0
|
||||
state.monitorFallbackBackoffUntil = 0
|
||||
setSchedulerQueueDepth(localQueueDepth())
|
||||
}
|
||||
|
||||
@@ -826,11 +828,11 @@ function schedulePullLoop(): void {
|
||||
clearInterval(state.pullTimer)
|
||||
}
|
||||
|
||||
setSchedulerNextPull(Date.now() + publishPullIntervalMs)
|
||||
setSchedulerNextPull(Date.now() + monitorPullFallbackIntervalMs)
|
||||
state.pullTimer = setInterval(() => {
|
||||
setSchedulerNextPull(Date.now() + publishPullIntervalMs)
|
||||
setSchedulerNextPull(Date.now() + monitorPullFallbackIntervalMs)
|
||||
pumpExecutionLoop()
|
||||
}, publishPullIntervalMs)
|
||||
}, monitorPullFallbackIntervalMs)
|
||||
}
|
||||
|
||||
function scheduleAccountSyncLoop(): void {
|
||||
@@ -986,7 +988,7 @@ function handleTaskEvent(event: DesktopTaskEventMessage): void {
|
||||
|
||||
if (event.kind === 'monitor') {
|
||||
if (event.type === 'task_available') {
|
||||
enqueueMonitorTaskFromEvent(event, 'rabbitmq-primary')
|
||||
state.monitorFallbackBackoffUntil = 0
|
||||
}
|
||||
if (
|
||||
event.type === 'task_completed' ||
|
||||
@@ -1027,13 +1029,8 @@ async function handleMonitoringDispatchSignal(event: DesktopTaskEventMessage): P
|
||||
)
|
||||
}
|
||||
|
||||
if (event.task_id.trim()) {
|
||||
enqueueMonitorTaskFromEvent(event, 'rabbitmq-primary')
|
||||
pumpExecutionLoop()
|
||||
return
|
||||
}
|
||||
|
||||
await pullNextTask('monitor')
|
||||
state.monitorFallbackBackoffUntil = 0
|
||||
pumpExecutionLoop()
|
||||
}
|
||||
|
||||
async function handleMonitoringTaskControl(event: DesktopTaskEventMessage): Promise<void> {
|
||||
@@ -1889,11 +1886,21 @@ function pumpExecutionLoop(): void {
|
||||
activeCount: activeMonitorCount(),
|
||||
})
|
||||
if (selected) {
|
||||
void leaseSpecificTask(selected, 'monitor')
|
||||
if (selected.source === 'monitoring_lease') {
|
||||
void leaseSpecificTask(selected, 'monitor')
|
||||
} else {
|
||||
noteMonitorTaskCanceled(selected.taskId)
|
||||
void pullNextTask('monitor')
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPullMonitorFallback()) {
|
||||
void pullNextTask('monitor')
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1917,7 +1924,7 @@ async function leaseSpecificTask(
|
||||
const leased = await leaseDesktopTask({ task_id: request.taskId })
|
||||
if (!leased.task) {
|
||||
if (expectedKind === 'monitor') {
|
||||
noteMonitorTaskLeaseMiss(request.taskId)
|
||||
noteMonitorTaskCanceled(request.taskId)
|
||||
} else {
|
||||
notePublishTaskLeaseMiss(request.taskId)
|
||||
}
|
||||
@@ -1938,7 +1945,7 @@ async function leaseSpecificTask(
|
||||
state.tasks.set(request.taskId, existing)
|
||||
}
|
||||
} else {
|
||||
noteMonitorTaskLeaseMiss(request.taskId)
|
||||
noteMonitorTaskCanceled(request.taskId)
|
||||
}
|
||||
} else {
|
||||
notePublishTaskLeaseMiss(request.taskId)
|
||||
@@ -1973,14 +1980,17 @@ async function pullNextTask(kind: 'publish' | 'monitor'): Promise<void> {
|
||||
noteSchedulerPull()
|
||||
|
||||
if (leased.task) {
|
||||
state.monitorFallbackBackoffUntil = 0
|
||||
state.leaseInFlightKinds.delete('monitor')
|
||||
await executeLeasedTask(leased, 'rabbitmq-primary')
|
||||
return
|
||||
}
|
||||
state.monitorFallbackBackoffUntil = Date.now() + monitorPullFallbackIntervalMs
|
||||
} catch (error) {
|
||||
state.lastPullAt = Date.now()
|
||||
state.lastPullStatus = 'failed'
|
||||
state.lastError = errorMessage(error)
|
||||
state.monitorFallbackBackoffUntil = Date.now() + monitorPullFallbackIntervalMs
|
||||
noteTransportPull(false)
|
||||
setSchedulerError(state.lastError)
|
||||
} finally {
|
||||
@@ -2052,10 +2062,12 @@ async function pullMonitoringTasks(routing: RuntimeTaskRouting): Promise<void> {
|
||||
noteSchedulerPull()
|
||||
|
||||
enqueueLeasedMonitoringTasks(leased.tasks, routing)
|
||||
state.monitorFallbackBackoffUntil = leased.tasks.length > 0 ? 0 : Date.now() + monitorPullFallbackIntervalMs
|
||||
} catch (error) {
|
||||
state.lastPullAt = Date.now()
|
||||
state.lastPullStatus = 'failed'
|
||||
state.lastError = errorMessage(error)
|
||||
state.monitorFallbackBackoffUntil = Date.now() + monitorPullFallbackIntervalMs
|
||||
noteTransportPull(false)
|
||||
setSchedulerError(state.lastError)
|
||||
|
||||
@@ -2176,7 +2188,7 @@ async function executeLeasedMonitoringTask(
|
||||
): Promise<void> {
|
||||
const taskRecord = state.tasks.get(taskId)
|
||||
if (!taskRecord || taskRecord.kind !== 'monitor' || !taskRecord.leaseToken) {
|
||||
noteMonitorTaskLeaseMiss(taskId)
|
||||
noteMonitorTaskCanceled(taskId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3609,6 +3621,14 @@ function shouldPullPublishFallback(): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function shouldPullMonitorFallback(): boolean {
|
||||
return (
|
||||
Date.now() >= state.monitorFallbackBackoffUntil &&
|
||||
!hasPublishBacklog() &&
|
||||
canStartAnotherMonitorExecution()
|
||||
)
|
||||
}
|
||||
|
||||
function canStartAnotherMonitorExecution(): boolean {
|
||||
if (hasPublishBacklog() || isLeaseInFlight('publish') || isLeaseInFlight('monitor')) {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user