fix: throttle desktop monitoring dispatch
Desktop Client Build / Resolve Build Metadata (push) Successful in 27s
Backend CI / Backend (push) Successful in 15m57s
Desktop Client Build / Build Desktop Client (push) Successful in 22m57s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 27s

This commit is contained in:
2026-06-22 12:21:59 +08:00
parent 54c2f92e02
commit 8c6789dca6
23 changed files with 591 additions and 88 deletions
@@ -398,7 +398,7 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
}
now := time.Now()
tasks := make([]dueScheduleTask, 0, runtimeCfg.BatchSize)
candidates := make([]dueScheduleTask, 0, runtimeCfg.BatchSize)
updates := make([]struct {
cacheUpdate scheduleTaskCacheUpdate
nextRun *time.Time
@@ -469,17 +469,6 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
}
task.PublishAccountIDs = decodeDueSchedulePublishAccountIDs(publishAccountIDsJSON)
task.PublishEnterpriseSiteTargets = tenantapp.DecodeScheduleEnterpriseSiteTargets(publishEnterpriseSiteTargetsJSON)
if task.AutoPublish && len(task.PublishAccountIDs) > 0 {
accountIDs, _, err := tenantapp.LoadExistingSchedulePublishAccountIDsAndPlatformsByIDs(ctx, tx, task.TenantID, task.WorkspaceID, task.PublishAccountIDs)
if err != nil {
rows.Close()
return nil, err
}
task.PublishAccountIDs = accountIDs
}
if task.AutoPublish && len(task.PublishAccountIDs) == 0 && len(task.PublishEnterpriseSiteTargets) == 0 {
task.AutoPublish = false
}
task.CoverRandomFolderID = int64PtrFromInt8(coverRandomFolderID)
if task.CoverMode == "" {
task.CoverMode = tenantapp.ScheduleCoverModeSpecific
@@ -519,7 +508,7 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
})
if nextErr == nil && sharedschedule.IsRunAllowed(task.ScheduledFor, task.StartAt, task.EndAt) {
tasks = append(tasks, task)
candidates = append(candidates, task)
}
}
if err := rows.Err(); err != nil {
@@ -528,6 +517,21 @@ func (w *ScheduleDispatchWorker) claimDueSchedules(ctx context.Context, runtimeC
}
rows.Close()
tasks := make([]dueScheduleTask, 0, len(candidates))
for _, task := range candidates {
if task.AutoPublish && len(task.PublishAccountIDs) > 0 {
accountIDs, _, err := tenantapp.LoadExistingSchedulePublishAccountIDsAndPlatformsByIDs(ctx, tx, task.TenantID, task.WorkspaceID, task.PublishAccountIDs)
if err != nil {
return nil, err
}
task.PublishAccountIDs = accountIDs
}
if task.AutoPublish && len(task.PublishAccountIDs) == 0 && len(task.PublishEnterpriseSiteTargets) == 0 {
task.AutoPublish = false
}
tasks = append(tasks, task)
}
for _, update := range updates {
if update.invalidErr != nil {
message := truncateDispatchError(update.invalidErr)
@@ -514,7 +514,20 @@ func (s *DesktopTaskService) leaseNextQueuedMonitorTask(
}
hasAccountIDs := len(accountIDs) > 0
row := s.pool.QueryRow(ctx, `
row := s.pool.QueryRow(ctx, leaseNextQueuedMonitorTaskSQL(),
client.TenantID,
client.WorkspaceID,
client.ID,
hasAccountIDs,
accountIDs,
params.AttemptID,
params.LeaseTokenHash,
)
return scanRepositoryDesktopTask(row)
}
func leaseNextQueuedMonitorTaskSQL() string {
return `
WITH candidate AS (
SELECT dt.desktop_id
FROM desktop_tasks AS dt
@@ -522,6 +535,29 @@ func (s *DesktopTaskService) leaseNextQueuedMonitorTask(
AND dt.workspace_id = $2
AND dt.kind = 'monitor'
AND dt.status = 'queued'
AND NOT EXISTS (
SELECT 1
FROM desktop_tasks AS active
WHERE active.tenant_id = dt.tenant_id
AND active.workspace_id = dt.workspace_id
AND active.kind = 'monitor'
AND active.target_client_id = $3
AND active.platform_id = dt.platform_id
AND active.status = 'in_progress'
AND active.desktop_id <> dt.desktop_id
)
AND NOT EXISTS (
SELECT 1
FROM desktop_tasks AS recent
WHERE recent.tenant_id = dt.tenant_id
AND recent.workspace_id = dt.workspace_id
AND recent.kind = 'monitor'
AND recent.target_client_id = $3
AND recent.platform_id = dt.platform_id
AND recent.status IN ('succeeded', 'failed', 'unknown', 'aborted')
AND recent.updated_at >= now() - interval '30 seconds'
AND recent.desktop_id <> dt.desktop_id
)
AND (
dt.target_client_id = $3
OR ($4::boolean AND dt.target_account_id = ANY($5::uuid[]))
@@ -544,16 +580,7 @@ func (s *DesktopTaskService) leaseNextQueuedMonitorTask(
updated_at = now()
FROM candidate
WHERE t.desktop_id = candidate.desktop_id
RETURNING `+desktopTaskRepositoryReturningColumns,
client.TenantID,
client.WorkspaceID,
client.ID,
hasAccountIDs,
accountIDs,
params.AttemptID,
params.LeaseTokenHash,
)
return scanRepositoryDesktopTask(row)
RETURNING ` + desktopTaskRepositoryReturningColumns
}
func (s *DesktopTaskService) leaseNextQueuedPublishTask(
@@ -625,7 +652,22 @@ func (s *DesktopTaskService) leaseQueuedDesktopTaskByID(
}
hasAccountIDs := len(accountIDs) > 0
row := s.pool.QueryRow(ctx, `
row := s.pool.QueryRow(ctx, leaseQueuedDesktopTaskByIDSQL(),
desktopID,
client.WorkspaceID,
client.TenantID,
client.ID,
hasAccountIDs,
accountIDs,
params.AttemptID,
params.LeaseTokenHash,
desktopPublishMaxAttempts,
)
return scanRepositoryDesktopTask(row)
}
func leaseQueuedDesktopTaskByIDSQL() string {
return `
WITH candidate AS (
SELECT dt.desktop_id
FROM desktop_tasks AS dt
@@ -645,6 +687,35 @@ func (s *DesktopTaskService) leaseQueuedDesktopTaskByID(
AND pa.delete_requested_at IS NULL
)
)
AND (
dt.kind <> 'monitor'
OR NOT EXISTS (
SELECT 1
FROM desktop_tasks AS active
WHERE active.tenant_id = dt.tenant_id
AND active.workspace_id = dt.workspace_id
AND active.kind = 'monitor'
AND active.target_client_id = $4
AND active.platform_id = dt.platform_id
AND active.status = 'in_progress'
AND active.desktop_id <> dt.desktop_id
)
)
AND (
dt.kind <> 'monitor'
OR NOT EXISTS (
SELECT 1
FROM desktop_tasks AS recent
WHERE recent.tenant_id = dt.tenant_id
AND recent.workspace_id = dt.workspace_id
AND recent.kind = 'monitor'
AND recent.target_client_id = $4
AND recent.platform_id = dt.platform_id
AND recent.status IN ('succeeded', 'failed', 'unknown', 'aborted')
AND recent.updated_at >= now() - interval '30 seconds'
AND recent.desktop_id <> dt.desktop_id
)
)
AND (
(
dt.kind = 'monitor'
@@ -673,18 +744,7 @@ func (s *DesktopTaskService) leaseQueuedDesktopTaskByID(
updated_at = now()
FROM candidate
WHERE t.desktop_id = candidate.desktop_id
RETURNING `+desktopTaskRepositoryReturningColumns,
desktopID,
client.WorkspaceID,
client.TenantID,
client.ID,
hasAccountIDs,
accountIDs,
params.AttemptID,
params.LeaseTokenHash,
desktopPublishMaxAttempts,
)
return scanRepositoryDesktopTask(row)
RETURNING ` + desktopTaskRepositoryReturningColumns
}
func (s *DesktopTaskService) loadMonitorLeaseAccountIDs(ctx context.Context, client *repository.DesktopClient) ([]uuid.UUID, error) {
@@ -203,6 +203,44 @@ func TestBuildCountPublishTasksByStatusesQueryHidesDeletedPublishRecords(t *test
}
}
func TestLeaseNextQueuedMonitorTaskSQLSerializesPerClientPlatform(t *testing.T) {
t.Parallel()
normalized := normalizeSQLForDesktopTaskTest(leaseNextQueuedMonitorTaskSQL())
for _, fragment := range []string{
"NOT EXISTS ( SELECT 1 FROM desktop_tasks AS active",
"active.target_client_id = $3",
"active.platform_id = dt.platform_id",
"active.status = 'in_progress'",
"active.desktop_id <> dt.desktop_id",
"recent.status IN ('succeeded', 'failed', 'unknown', 'aborted')",
"recent.updated_at >= now() - interval '30 seconds'",
} {
if !strings.Contains(normalized, fragment) {
t.Fatalf("monitor lease query missing %q: %s", fragment, normalized)
}
}
}
func TestLeaseQueuedDesktopTaskByIDSQLSerializesMonitorPerClientPlatform(t *testing.T) {
t.Parallel()
normalized := normalizeSQLForDesktopTaskTest(leaseQueuedDesktopTaskByIDSQL())
for _, fragment := range []string{
"dt.kind <> 'monitor' OR NOT EXISTS",
"active.target_client_id = $4",
"active.platform_id = dt.platform_id",
"active.status = 'in_progress'",
"active.desktop_id <> dt.desktop_id",
"recent.status IN ('succeeded', 'failed', 'unknown', 'aborted')",
"recent.updated_at >= now() - interval '30 seconds'",
} {
if !strings.Contains(normalized, fragment) {
t.Fatalf("task-specific lease query missing %q: %s", fragment, normalized)
}
}
}
func recoverDesktopTaskSelectColumns(query string) []string {
re := regexp.MustCompile(`(?is)select\s+(.*?)\s+from\s+desktop_tasks`)
match := re.FindStringSubmatch(query)
@@ -1371,15 +1371,18 @@ func (s *MonitoringCallbackService) loadCollectTask(ctx context.Context, tenantI
skip_reason,
trigger_source,
COALESCE(target_client_id::text, '') AS target_client_id,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots
WHERE tenant_id = t.tenant_id
AND brand_id = t.brand_id
AND question_id = t.question_id
AND question_hash = t.question_hash
AND deleted_at IS NULL
ORDER BY projected_at DESC, id DESC
AND btrim(question_text_snapshot) <> ''
ORDER BY
CASE WHEN deleted_at IS NULL THEN 0 ELSE 1 END,
projected_at DESC,
id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM monitoring_collect_tasks t
@@ -1439,15 +1442,18 @@ func (s *MonitoringCallbackService) loadCollectTaskByID(ctx context.Context, tas
skip_reason,
trigger_source,
COALESCE(target_client_id::text, '') AS target_client_id,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots
WHERE tenant_id = t.tenant_id
AND brand_id = t.brand_id
AND question_id = t.question_id
AND question_hash = t.question_hash
AND deleted_at IS NULL
ORDER BY projected_at DESC, id DESC
AND btrim(question_text_snapshot) <> ''
ORDER BY
CASE WHEN deleted_at IS NULL THEN 0 ELSE 1 END,
projected_at DESC,
id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM monitoring_collect_tasks t
@@ -1538,15 +1544,18 @@ func (s *MonitoringCallbackService) selectPendingLeaseTasks(
t.business_date,
COALESCE(t.dispatch_priority, 100) AS dispatch_priority,
COALESCE(t.dispatch_lane, 'normal') AS dispatch_lane,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots
WHERE tenant_id = t.tenant_id
AND brand_id = t.brand_id
AND question_id = t.question_id
AND question_hash = t.question_hash
AND deleted_at IS NULL
ORDER BY projected_at DESC, id DESC
AND btrim(question_text_snapshot) <> ''
ORDER BY
CASE WHEN deleted_at IS NULL THEN 0 ELSE 1 END,
projected_at DESC,
id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM monitoring_collect_tasks t
@@ -1650,15 +1659,18 @@ func (s *MonitoringCallbackService) selectLeasedTasksForInstallation(
t.business_date,
COALESCE(t.dispatch_priority, 100) AS dispatch_priority,
COALESCE(t.dispatch_lane, 'normal') AS dispatch_lane,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots
WHERE tenant_id = t.tenant_id
AND brand_id = t.brand_id
AND question_id = t.question_id
AND question_hash = t.question_hash
AND deleted_at IS NULL
ORDER BY projected_at DESC, id DESC
AND btrim(question_text_snapshot) <> ''
ORDER BY
CASE WHEN deleted_at IS NULL THEN 0 ELSE 1 END,
projected_at DESC,
id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM monitoring_collect_tasks t
@@ -2070,6 +2082,10 @@ func (s *MonitoringCallbackService) upsertParseResult(ctx context.Context, tx pg
}
func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task *monitoringCollectTask, req MonitoringTaskResultRequest) (*MonitoringTaskResultResponse, error) {
if task == nil || strings.TrimSpace(task.QuestionText) == "" {
return nil, response.ErrInternal(50041, "missing_question_text_snapshot", "monitoring task question text is empty")
}
req = normalizeMonitoringTaskResultRequestValue(req)
runStatus := normalizeRunStatus(req.Status)
@@ -933,23 +933,29 @@ func (s *MonitoringService) ensureDailyMonitoringCollectTasks(
questionIDs := make([]int64, 0, len(candidates))
questionHashes := make([][]byte, 0, len(candidates))
questionTexts := make([]string, 0, len(candidates))
platformIDs := make([]string, 0, len(candidates))
dispatchAfterValues := make([]time.Time, 0, len(candidates))
now := time.Now().UTC()
for _, candidate := range candidates {
questionText, textErr := monitoringQuestionTextSnapshot(candidate.Question)
if textErr != nil {
return 0, textErr
}
dispatchAfter := candidate.DispatchAfter
if dispatchAfter.IsZero() {
dispatchAfter = now
}
questionIDs = append(questionIDs, candidate.Question.ID)
questionHashes = append(questionHashes, candidate.Question.QuestionHash)
questionTexts = append(questionTexts, questionText)
platformIDs = append(platformIDs, candidate.Platform.ID)
dispatchAfterValues = append(dispatchAfterValues, dispatchAfter)
}
tag, err := tx.Exec(ctx, `
INSERT INTO monitoring_collect_tasks (
tenant_id, brand_id, question_id, question_hash, ai_platform_id,
tenant_id, brand_id, question_id, question_hash, question_text_snapshot, ai_platform_id,
collector_type, trigger_source, run_mode, business_date, planned_at, status,
dispatch_priority, dispatch_lane, target_client_id, dispatch_after,
ingest_shard_key, execution_owner
@@ -959,6 +965,7 @@ func (s *MonitoringService) ensureDailyMonitoringCollectTasks(
$2,
input.question_id,
input.question_hash,
input.question_text,
input.ai_platform_id,
$3,
'automatic',
@@ -976,12 +983,17 @@ func (s *MonitoringService) ensureDailyMonitoringCollectTasks(
$7::bigint[],
$8::bytea[],
$9::text[],
$10::timestamptz[]
) AS input(question_id, question_hash, ai_platform_id, dispatch_after)
$10::text[],
$11::timestamptz[]
) AS input(question_id, question_hash, question_text, ai_platform_id, dispatch_after)
ON CONFLICT (
tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date
)
DO NOTHING
DO UPDATE SET
question_hash = EXCLUDED.question_hash,
question_text_snapshot = EXCLUDED.question_text_snapshot,
updated_at = NOW()
WHERE btrim(COALESCE(monitoring_collect_tasks.question_text_snapshot, '')) = ''
`,
plan.TenantID,
brand.BrandID,
@@ -991,6 +1003,7 @@ func (s *MonitoringService) ensureDailyMonitoringCollectTasks(
executionOwner,
questionIDs,
questionHashes,
questionTexts,
platformIDs,
dispatchAfterValues,
)
@@ -1060,15 +1073,18 @@ func (s *MonitoringService) loadDueDailyMonitorDesktopTaskSpecs(
t.dispatch_priority,
t.dispatch_lane,
t.interrupt_generation,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots s
WHERE s.tenant_id = t.tenant_id
AND s.brand_id = t.brand_id
AND s.question_id = t.question_id
AND s.question_hash = t.question_hash
AND s.deleted_at IS NULL
ORDER BY s.projected_at DESC, s.id DESC
AND btrim(s.question_text_snapshot) <> ''
ORDER BY
CASE WHEN s.deleted_at IS NULL THEN 0 ELSE 1 END,
s.projected_at DESC,
s.id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM claimed_tasks t
@@ -1105,6 +1121,10 @@ func (s *MonitoringService) loadDueDailyMonitorDesktopTaskSpecs(
spec.WorkspaceID = workspaceID
spec.BusinessDate = businessDay.Format("2006-01-02")
spec.QuestionHash = encodeQuestionHash(questionHash)
spec.QuestionText = strings.TrimSpace(spec.QuestionText)
if spec.QuestionText == "" {
continue
}
spec.SchedulerGroupKey = monitoringSchedulerGroupKey(spec.QuestionHash, spec.QuestionID, spec.QuestionText)
result = append(result, spec)
}
@@ -129,6 +129,7 @@ func TestBuildMonitoringDailyTaskCandidatesIncludesQuestionsBeyondLibraryLimit(t
for id := int64(1); id <= 30; id++ {
questions = append(questions, monitoringConfiguredQuestion{
ID: id,
QuestionText: "搜索词 " + int64Text(id),
QuestionHash: []byte("q" + int64Text(id)),
})
}
@@ -152,6 +153,80 @@ func TestBuildMonitoringDailyTaskCandidatesIncludesQuestionsBeyondLibraryLimit(t
assert.Contains(t, seen, int64(30))
}
func TestEnsureDailyMonitoringCollectTasksPersistsQuestionTextSnapshot(t *testing.T) {
ctx := context.Background()
tx := &captureDailyMonitorTaskTx{commandTag: pgconn.NewCommandTag("INSERT 0 2")}
businessDay := time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC)
created, err := (&MonitoringService{}).ensureDailyMonitoringCollectTasks(
ctx,
tx,
monitoringDailyPlan{TenantID: 4},
monitoringDailyBrand{BrandID: 8},
businessDay,
[]monitoringDailyTaskCandidate{
{
Question: monitoringConfiguredQuestion{
ID: 145,
QuestionText: " 幽灵门轨道长度定制 ",
QuestionHash: []byte("q145"),
},
Platform: monitoringPlatformMetadata{ID: "qwen"},
},
{
Question: monitoringConfiguredQuestion{
ID: 145,
QuestionText: "幽灵门轨道长度定制",
QuestionHash: []byte("q145"),
},
Platform: monitoringPlatformMetadata{ID: "doubao"},
},
},
"desktop_tasks",
)
require.NoError(t, err)
assert.Equal(t, int64(2), created)
assert.True(t, tx.execCalled)
normalizedSQL := normalizeSQLWhitespace(tx.execSQL)
assert.Contains(t, normalizedSQL, "question_text_snapshot, ai_platform_id")
assert.Contains(t, normalizedSQL, "$10::text[]")
assert.Contains(t, normalizedSQL, "AS input(question_id, question_hash, question_text, ai_platform_id, dispatch_after)")
assert.Contains(t, normalizedSQL, "question_text_snapshot = EXCLUDED.question_text_snapshot")
require.Len(t, tx.execArgs, 11)
assert.Equal(t, []string{"幽灵门轨道长度定制", "幽灵门轨道长度定制"}, tx.execArgs[8])
assert.Equal(t, []string{"qwen", "doubao"}, tx.execArgs[9])
}
func TestEnsureDailyMonitoringCollectTasksRejectsEmptyQuestionText(t *testing.T) {
ctx := context.Background()
tx := &captureDailyMonitorTaskTx{}
created, err := (&MonitoringService{}).ensureDailyMonitoringCollectTasks(
ctx,
tx,
monitoringDailyPlan{TenantID: 4},
monitoringDailyBrand{BrandID: 8},
time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC),
[]monitoringDailyTaskCandidate{
{
Question: monitoringConfiguredQuestion{
ID: 145,
QuestionText: " ",
QuestionHash: []byte("q145"),
},
Platform: monitoringPlatformMetadata{ID: "qwen"},
},
},
"desktop_tasks",
)
require.Error(t, err)
assert.Equal(t, int64(0), created)
assert.False(t, tx.execCalled)
assert.Contains(t, err.Error(), "missing_question_text_snapshot")
}
func TestDecodeDailyMonitoringEnabledPlatformsReportsMalformedJSON(t *testing.T) {
platforms, err := decodeDailyMonitoringEnabledPlatforms([]byte(`["qwen"`))
require.Error(t, err)
@@ -360,6 +435,7 @@ func TestLoadDueDailyMonitorDesktopTaskSpecsFiltersToDispatchablePlatforms(t *te
require.NoError(t, err)
assert.Empty(t, specs)
assert.True(t, queryer.called)
assert.Contains(t, normalizeSQLWhitespace(queryer.sql), "COALESCE(NULLIF(t.question_text_snapshot, ''),")
assert.Contains(t, normalizeSQLWhitespace(queryer.sql), "AND t.ai_platform_id = ANY($7::text[])")
assert.Contains(t, normalizeSQLWhitespace(queryer.sql), "LIMIT $8")
require.Len(t, queryer.args, 8)
@@ -423,3 +499,50 @@ func (r emptyDailyMonitorRows) Scan(...any) error { r
func (r emptyDailyMonitorRows) Values() ([]any, error) { return nil, nil }
func (r emptyDailyMonitorRows) RawValues() [][]byte { return nil }
func (r emptyDailyMonitorRows) Conn() *pgx.Conn { return nil }
type captureDailyMonitorTaskTx struct {
execCalled bool
execSQL string
execArgs []any
commandTag pgconn.CommandTag
}
func (tx *captureDailyMonitorTaskTx) Begin(context.Context) (pgx.Tx, error) { return tx, nil }
func (tx *captureDailyMonitorTaskTx) Commit(context.Context) error { return nil }
func (tx *captureDailyMonitorTaskTx) Rollback(context.Context) error { return nil }
func (tx *captureDailyMonitorTaskTx) CopyFrom(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) (int64, error) {
return 0, nil
}
func (tx *captureDailyMonitorTaskTx) SendBatch(context.Context, *pgx.Batch) pgx.BatchResults {
return nil
}
func (tx *captureDailyMonitorTaskTx) LargeObjects() pgx.LargeObjects { return pgx.LargeObjects{} }
func (tx *captureDailyMonitorTaskTx) Prepare(context.Context, string, string) (*pgconn.StatementDescription, error) {
return nil, nil
}
func (tx *captureDailyMonitorTaskTx) Exec(_ context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) {
tx.execCalled = true
tx.execSQL = sql
tx.execArgs = arguments
return tx.commandTag, nil
}
func (tx *captureDailyMonitorTaskTx) Query(context.Context, string, ...any) (pgx.Rows, error) {
return emptyDailyMonitorRows{}, nil
}
func (tx *captureDailyMonitorTaskTx) QueryRow(context.Context, string, ...any) pgx.Row {
return boolDailyMonitorRow(true)
}
func (tx *captureDailyMonitorTaskTx) Conn() *pgx.Conn { return nil }
var _ pgx.Tx = (*captureDailyMonitorTaskTx)(nil)
type boolDailyMonitorRow bool
func (r boolDailyMonitorRow) Scan(dest ...any) error {
if len(dest) == 1 {
if value, ok := dest[0].(*bool); ok {
*value = bool(r)
}
}
return nil
}
@@ -138,15 +138,18 @@ func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
t.dispatch_priority,
t.dispatch_lane,
t.interrupt_generation,
COALESCE((
COALESCE(NULLIF(t.question_text_snapshot, ''), (
SELECT question_text_snapshot
FROM monitoring_question_config_snapshots s
WHERE s.tenant_id = t.tenant_id
AND s.brand_id = t.brand_id
AND s.question_id = t.question_id
AND s.question_hash = t.question_hash
AND s.deleted_at IS NULL
ORDER BY s.projected_at DESC, s.id DESC
AND btrim(s.question_text_snapshot) <> ''
ORDER BY
CASE WHEN s.deleted_at IS NULL THEN 0 ELSE 1 END,
s.projected_at DESC,
s.id DESC
LIMIT 1
), '') AS question_text_snapshot
FROM monitoring_collect_tasks t
@@ -193,6 +196,10 @@ func (s *MonitoringService) loadMonitorDesktopTaskSpecs(
spec.RequestedByUserID = requestedByUserID
spec.BusinessDate = businessDay.Format("2006-01-02")
spec.QuestionHash = encodeQuestionHash(questionHash)
spec.QuestionText = strings.TrimSpace(spec.QuestionText)
if spec.QuestionText == "" {
continue
}
spec.SchedulerGroupKey = monitoringSchedulerGroupKey(spec.QuestionHash, spec.QuestionID, spec.QuestionText)
result = append(result, spec)
}
@@ -210,9 +217,13 @@ func (s *MonitoringService) dispatchMonitorDesktopTasks(
return nil, nil, nil
}
targets, err := s.loadMonitorDesktopTaskTargets(ctx, specs[0].TenantID, specs[0].WorkspaceID, specs[0].RequestedByUserID, specs)
if err != nil {
return nil, nil, err
var targets map[string]monitorDesktopTaskTarget
if monitorDesktopTaskSpecsNeedTargetLookup(specs) {
var err error
targets, err = s.loadMonitorDesktopTaskTargets(ctx, specs[0].TenantID, specs[0].WorkspaceID, specs[0].RequestedByUserID, specs)
if err != nil {
return nil, nil, err
}
}
tx, err := s.businessPool.Begin(ctx)
@@ -225,13 +236,23 @@ func (s *MonitoringService) dispatchMonitorDesktopTasks(
publishableTasks := make([]*repository.DesktopTask, 0, len(specs))
fallbackLegacyTaskIDs := make([]int64, 0)
for _, spec := range specs {
target, ok := targets[normalizeMonitoringPlatformID(spec.PlatformID)]
if !ok || target.AccountID == uuid.Nil || target.ClientID == uuid.Nil {
fallbackLegacyTaskIDs = append(fallbackLegacyTaskIDs, spec.MonitorTaskID)
if spec.TargetAccountID == uuid.Nil || spec.TargetClientID == uuid.Nil {
target, ok := targets[normalizeMonitoringPlatformID(spec.PlatformID)]
if !ok || target.AccountID == uuid.Nil || target.ClientID == uuid.Nil {
fallbackLegacyTaskIDs = append(fallbackLegacyTaskIDs, spec.MonitorTaskID)
continue
}
spec.TargetAccountID = target.AccountID
spec.TargetClientID = target.ClientID
}
available, throttleErr := acquireMonitorDesktopTaskPlatformSlot(ctx, tx, spec)
if throttleErr != nil {
return nil, nil, throttleErr
}
if !available {
continue
}
spec.TargetAccountID = target.AccountID
spec.TargetClientID = target.ClientID
task, shouldPublish, fallbackLegacy, upsertErr := s.upsertMonitorDesktopTask(ctx, tx, repo, spec)
if upsertErr != nil {
@@ -252,6 +273,54 @@ func (s *MonitoringService) dispatchMonitorDesktopTasks(
return publishableTasks, fallbackLegacyTaskIDs, nil
}
func monitorDesktopTaskSpecsNeedTargetLookup(specs []monitorDesktopTaskSpec) bool {
for _, spec := range specs {
if spec.TargetAccountID == uuid.Nil || spec.TargetClientID == uuid.Nil {
return true
}
}
return false
}
func acquireMonitorDesktopTaskPlatformSlot(ctx context.Context, tx pgx.Tx, spec monitorDesktopTaskSpec) (bool, error) {
if tx == nil || spec.TargetClientID == uuid.Nil || strings.TrimSpace(spec.PlatformID) == "" {
return false, nil
}
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1)`, monitorDesktopTaskPlatformSlotLockKey(spec.TargetClientID, spec.PlatformID)); err != nil {
return false, response.ErrInternal(50123, "desktop_task_throttle_lock_failed", "failed to acquire phase2 monitor desktop platform slot")
}
var busy bool
if err := tx.QueryRow(ctx, monitorDesktopTaskPlatformSlotBusySQL(), spec.TenantID, spec.WorkspaceID, spec.TargetClientID, normalizeMonitoringPlatformID(spec.PlatformID), spec.MonitorTaskID).Scan(&busy); err != nil {
return false, response.ErrInternal(50124, "desktop_task_throttle_lookup_failed", "failed to inspect phase2 monitor desktop platform slot")
}
return !busy, nil
}
func monitorDesktopTaskPlatformSlotBusySQL() string {
return `
SELECT EXISTS (
SELECT 1
FROM desktop_tasks
WHERE tenant_id = $1
AND workspace_id = $2
AND kind = 'monitor'
AND target_client_id = $3
AND platform_id = $4
AND status = 'in_progress'
AND (
$5::bigint <= 0
OR monitor_task_id IS NULL
OR monitor_task_id <> $5
)
)
`
}
func monitorDesktopTaskPlatformSlotLockKey(clientID uuid.UUID, platformID string) int64 {
return int64(monitoringDailyStableHash("monitor_desktop_platform_slot", clientID.String(), normalizeMonitoringPlatformID(platformID)))
}
func (s *MonitoringService) loadMonitorDesktopTaskTargets(
ctx context.Context,
tenantID, workspaceID, userID int64,
@@ -721,6 +790,7 @@ func (s *MonitoringService) requestPhase2MonitorInterrupts(
ctx context.Context,
targetClientID uuid.UUID,
excludedMonitorTaskIDs []int64,
excludedPlatformIDs []string,
interruptGeneration int,
) ([]monitorDesktopInterruptTarget, error) {
if s == nil || s.businessPool == nil {
@@ -728,6 +798,7 @@ func (s *MonitoringService) requestPhase2MonitorInterrupts(
}
queryArgs := []any{targetClientID, interruptGeneration}
nextParam := 3
query := `
UPDATE desktop_tasks
SET control_flags = (
@@ -752,8 +823,14 @@ func (s *MonitoringService) requestPhase2MonitorInterrupts(
AND monitor_task_id IS NOT NULL
`
if len(excludedMonitorTaskIDs) > 0 {
query += ` AND NOT (monitor_task_id = ANY($3::bigint[]))`
query += fmt.Sprintf(` AND NOT (monitor_task_id = ANY($%d::bigint[]))`, nextParam)
queryArgs = append(queryArgs, excludedMonitorTaskIDs)
nextParam++
}
excludedPlatformIDs = reconcileEnabledMonitoringPlatforms(excludedPlatformIDs)
if len(excludedPlatformIDs) > 0 {
query += fmt.Sprintf(` AND NOT (platform_id = ANY($%d::text[]))`, nextParam)
queryArgs = append(queryArgs, excludedPlatformIDs)
}
query += `
RETURNING desktop_id::text, monitor_task_id, target_client_id::text, workspace_id, interrupt_generation
@@ -981,18 +1058,18 @@ func monitoringSchedulerGroupKey(questionHash string, questionID int64, question
}
func marshalMonitorDesktopTaskPayload(spec monitorDesktopTaskSpec) ([]byte, error) {
title := strings.TrimSpace(spec.QuestionText)
platformName := platformDisplayName(spec.PlatformID)
if title != "" {
title = fmt.Sprintf("%s · %s", platformName, title)
} else {
title = fmt.Sprintf("监控任务 · %s", platformName)
questionText := strings.TrimSpace(spec.QuestionText)
if questionText == "" {
return nil, response.ErrInternal(50041, "missing_question_text_snapshot", "monitor desktop task question text is empty")
}
title := questionText
platformName := platformDisplayName(spec.PlatformID)
title = fmt.Sprintf("%s · %s", platformName, title)
payload, err := json.Marshal(map[string]any{
"title": title,
"business_date": spec.BusinessDate,
"scheduler_group_key": spec.SchedulerGroupKey,
"question_text": spec.QuestionText,
"question_text": questionText,
"question_id": spec.QuestionID,
"question_hash": spec.QuestionHash,
"ai_platform_id": spec.PlatformID,
@@ -1,10 +1,13 @@
package app
import (
"encoding/json"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSelectMonitorDesktopTaskTargetsPrefersPresentAccountClient(t *testing.T) {
@@ -147,3 +150,60 @@ func TestSelectMonitorDesktopTaskTargetsPrefersEarlierOnlineClientForSameAccount
ClientID: earlierClientID,
}, targets["doubao"])
}
func TestMarshalMonitorDesktopTaskPayloadRequiresQuestionText(t *testing.T) {
payload, err := marshalMonitorDesktopTaskPayload(monitorDesktopTaskSpec{
PlatformID: "qwen",
QuestionText: " ",
})
assert.Nil(t, payload)
require.Error(t, err)
assert.Contains(t, err.Error(), "missing_question_text_snapshot")
}
func TestMarshalMonitorDesktopTaskPayloadUsesQuestionTextNotFallbackTitle(t *testing.T) {
payload, err := marshalMonitorDesktopTaskPayload(monitorDesktopTaskSpec{
PlatformID: "qwen",
BusinessDate: "2026-06-22",
SchedulerGroupKey: "qid:145",
QuestionID: 145,
QuestionText: " 幽灵门轨道长度定制 ",
QuestionHash: "v1:test",
})
require.NoError(t, err)
var decoded map[string]any
require.NoError(t, json.Unmarshal(payload, &decoded))
assert.Equal(t, "幽灵门轨道长度定制", decoded["question_text"])
assert.Equal(t, "通义千问 · 幽灵门轨道长度定制", decoded["title"])
}
func TestMonitorDesktopTaskPlatformSlotBusySQLThrottlesPerClientPlatformInProgress(t *testing.T) {
query := strings.Join(strings.Fields(monitorDesktopTaskPlatformSlotBusySQL()), " ")
for _, fragment := range []string{
"FROM desktop_tasks",
"kind = 'monitor'",
"target_client_id = $3",
"platform_id = $4",
"status = 'in_progress'",
"monitor_task_id <> $5",
} {
assert.Contains(t, query, fragment)
}
}
func TestRequestPhase2MonitorInterruptsAcceptsExcludedPlatforms(t *testing.T) {
clientID := mustParseDailyMonitoringUUID(t, "00000000-0000-0000-0000-000000000701")
targets, err := (&MonitoringService{}).requestPhase2MonitorInterrupts(
t.Context(),
clientID,
[]int64{101, 102},
[]string{"doubao", "qwen"},
7,
)
require.NoError(t, err)
assert.Empty(t, targets)
}
@@ -361,6 +361,14 @@ type monitoringConfiguredQuestion struct {
QuestionHash []byte
}
func monitoringQuestionTextSnapshot(question monitoringConfiguredQuestion) (string, error) {
questionText := strings.TrimSpace(question.QuestionText)
if questionText == "" {
return "", response.ErrInternal(50041, "missing_question_text_snapshot", "configured monitoring question text is empty")
}
return questionText, nil
}
var defaultMonitoringPlatforms = []monitoringPlatformMetadata{
{ID: "yuanbao", Name: "混元 / 元宝"},
{ID: "kimi", Name: "Kimi"},
@@ -1018,8 +1026,10 @@ func (s *MonitoringService) CollectNow(
phase2DeferredTasks := make([]*repository.DesktopTask, 0)
if options.Preempt && phase2DispatchReady {
excludedMonitorTaskIDs := make([]int64, 0, len(phase2TaskSpecs))
excludedPlatformIDs := make([]string, 0, len(phase2TaskSpecs))
for _, spec := range phase2TaskSpecs {
excludedMonitorTaskIDs = append(excludedMonitorTaskIDs, spec.MonitorTaskID)
excludedPlatformIDs = append(excludedPlatformIDs, spec.PlatformID)
}
phase2TargetClientIDs := desktopTaskClientIDs(phase2PublishedTasks)
if len(phase2TargetClientIDs) == 0 {
@@ -1031,7 +1041,7 @@ func (s *MonitoringService) CollectNow(
return nil, deferErr
}
phase2DeferredTasks = append(phase2DeferredTasks, deferredTasks...)
interruptTargets, interruptErr := s.requestPhase2MonitorInterrupts(ctx, phase2TargetClientID, excludedMonitorTaskIDs, interruptGeneration)
interruptTargets, interruptErr := s.requestPhase2MonitorInterrupts(ctx, phase2TargetClientID, excludedMonitorTaskIDs, excludedPlatformIDs, interruptGeneration)
if interruptErr != nil {
return nil, interruptErr
}
@@ -1534,6 +1544,10 @@ func (s *MonitoringService) loadConfiguredQuestions(ctx context.Context, tenantI
if scanErr := rows.Scan(&item.ID, &item.KeywordID, &item.QuestionText); scanErr != nil {
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse configured monitoring questions")
}
item.QuestionText = strings.TrimSpace(item.QuestionText)
if item.QuestionText == "" {
return nil, response.ErrInternal(50041, "missing_question_text_snapshot", "configured monitoring question text is empty")
}
item.QuestionHash = seededQuestionHash(item.QuestionText)
items = append(items, item)
}
@@ -1631,6 +1645,10 @@ func (s *MonitoringService) syncMonitoringQuestionSnapshots(ctx context.Context,
}
for _, question := range questions {
questionText, textErr := monitoringQuestionTextSnapshot(question)
if textErr != nil {
return textErr
}
if _, err := tx.Exec(ctx, `
UPDATE monitoring_question_config_snapshots
SET superseded_at = COALESCE(superseded_at, NOW()),
@@ -1661,7 +1679,7 @@ func (s *MonitoringService) syncMonitoringQuestionSnapshots(ctx context.Context,
deleted_at = NULL,
projected_at = NOW(),
updated_at = NOW()
`, tenantID, brandID, question.ID, question.KeywordID, question.QuestionHash, question.QuestionText); err != nil {
`, tenantID, brandID, question.ID, question.KeywordID, question.QuestionHash, questionText); err != nil {
return response.ErrInternal(50041, "snapshot_upsert_failed", "failed to persist monitoring question snapshot")
}
}
@@ -1692,6 +1710,10 @@ func (s *MonitoringService) ensureCollectNowTasks(
interruptTaskIDs := make([]int64, 0)
for _, question := range questions {
questionText, textErr := monitoringQuestionTextSnapshot(question)
if textErr != nil {
return 0, 0, 0, 0, nil, textErr
}
for _, platform := range platforms {
platformID := normalizeMonitoringPlatformID(platform.ID)
if platformID == "" {
@@ -1704,6 +1726,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
tag, err := tx.Exec(ctx, `
UPDATE monitoring_collect_tasks
SET question_hash = $7,
question_text_snapshot = $12,
status = 'pending',
planned_at = NOW(),
trigger_source = 'manual',
@@ -1737,7 +1760,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
AND run_mode = $6
AND business_date = $8::date
AND status IN ('pending', 'expired', 'failed', 'completed', 'skipped')
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration, executionOwner)
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration, executionOwner, questionText)
if err != nil {
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to refresh monitoring tasks")
}
@@ -1746,6 +1769,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
rows, err := tx.Query(ctx, `
UPDATE monitoring_collect_tasks
SET question_hash = $7,
question_text_snapshot = $11,
trigger_source = 'manual',
dispatch_priority = 5000,
dispatch_lane = 'high',
@@ -1770,7 +1794,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
AND status = 'leased'
AND COALESCE(execution_owner, 'legacy') = 'legacy'
RETURNING id
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration)
`, tenantID, brandID, question.ID, platformID, monitoringCollectorType, runMode, question.QuestionHash, dateText, platformTargetClientID, interruptGeneration, questionText)
if err != nil {
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "update_failed", "failed to promote leased monitoring tasks")
}
@@ -1790,13 +1814,13 @@ func (s *MonitoringService) ensureCollectNowTasks(
tag, err = tx.Exec(ctx, `
INSERT INTO monitoring_collect_tasks (
tenant_id, brand_id, question_id, question_hash, ai_platform_id,
tenant_id, brand_id, question_id, question_hash, question_text_snapshot, ai_platform_id,
collector_type, trigger_source, run_mode, business_date, planned_at, status,
dispatch_priority, dispatch_lane, target_client_id, dispatch_after,
interrupt_generation, ingest_shard_key, execution_owner
)
VALUES (
$1, $2, $3, $4, $5,
$1, $2, $3, $4, $13, $5,
$6, 'manual', $7, $8::date, NOW(), 'pending',
5000, 'high', $9, NOW(),
$10, $11, $12
@@ -1805,7 +1829,7 @@ func (s *MonitoringService) ensureCollectNowTasks(
tenant_id, brand_id, question_id, ai_platform_id, collector_type, run_mode, business_date
)
DO NOTHING
`, tenantID, brandID, question.ID, question.QuestionHash, platformID, monitoringCollectorType, runMode, dateText, platformTargetClientID, interruptGeneration, fmt.Sprintf("%d:%d:%s", tenantID, brandID, dateText), executionOwner)
`, tenantID, brandID, question.ID, question.QuestionHash, platformID, monitoringCollectorType, runMode, dateText, platformTargetClientID, interruptGeneration, fmt.Sprintf("%d:%d:%s", tenantID, brandID, dateText), executionOwner, questionText)
if err != nil {
return 0, 0, 0, 0, nil, response.ErrInternal(50041, "insert_failed", "failed to create monitoring tasks")
}