package app import ( "context" "time" "github.com/jackc/pgx/v5" ) const ( defaultMonitoringReceivedAlertThreshold = 15 * time.Minute defaultMonitoringReceivedInspectLimit = 100 ) type monitoringStaleReceivedTask struct { ID int64 TenantID int64 BrandID int64 QuestionID int64 AIPlatformID string CollectorType string BusinessDate time.Time CallbackReceivedAt time.Time } type MonitoringStaleReceivedTask = monitoringStaleReceivedTask func markMonitoringStaleReceivedTasks(ctx context.Context, tx pgx.Tx, now time.Time, threshold time.Duration, limit int) ([]monitoringStaleReceivedTask, error) { if limit <= 0 { limit = defaultMonitoringReceivedInspectLimit } rows, err := tx.Query(ctx, ` WITH candidates AS ( SELECT id, tenant_id, brand_id, question_id, ai_platform_id, collector_type, business_date, callback_received_at FROM monitoring_collect_tasks WHERE collector_type = $1 AND ( status = 'received' OR ( COALESCE(execution_owner, 'legacy') = 'desktop_tasks' AND status = 'pending' AND callback_received_at IS NOT NULL ) ) AND callback_received_at IS NOT NULL AND callback_received_at < $2 AND ( request_payload_json IS NULL OR jsonb_typeof(request_payload_json) <> 'object' OR COALESCE(NULLIF(request_payload_json ->> 'received_alerted_at', ''), '') = '' ) ORDER BY callback_received_at ASC, id ASC FOR UPDATE SKIP LOCKED LIMIT $3 ) UPDATE monitoring_collect_tasks t SET request_payload_json = ( CASE WHEN t.request_payload_json IS NULL OR jsonb_typeof(t.request_payload_json) <> 'object' THEN '{}'::jsonb ELSE t.request_payload_json END ) || jsonb_build_object( 'received_alerted_at', $4::text, 'received_alert_count', ( CASE WHEN COALESCE(t.request_payload_json ->> 'received_alert_count', '') ~ '^[0-9]+$' THEN (t.request_payload_json ->> 'received_alert_count')::int ELSE 0 END ) + 1 ), updated_at = NOW() FROM candidates c WHERE t.id = c.id RETURNING c.id, c.tenant_id, c.brand_id, c.question_id, c.ai_platform_id, c.collector_type, c.business_date, c.callback_received_at `, monitoringCollectorType, now.Add(-threshold), limit, now.UTC().Format(time.RFC3339)) if err != nil { return nil, monitoringInternalError(50041, "received_watchdog_query_failed", "failed to inspect stale received monitoring tasks", err) } defer rows.Close() result := make([]monitoringStaleReceivedTask, 0) for rows.Next() { var item monitoringStaleReceivedTask if scanErr := rows.Scan( &item.ID, &item.TenantID, &item.BrandID, &item.QuestionID, &item.AIPlatformID, &item.CollectorType, &item.BusinessDate, &item.CallbackReceivedAt, ); scanErr != nil { return nil, monitoringInternalError(50041, "received_watchdog_scan_failed", "failed to parse stale received monitoring tasks", scanErr) } result = append(result, item) } if err := rows.Err(); err != nil { return nil, monitoringInternalError(50041, "received_watchdog_scan_failed", "failed to iterate stale received monitoring tasks", err) } return result, nil } func MarkMonitoringStaleReceivedTasks(ctx context.Context, tx pgx.Tx, now time.Time, threshold time.Duration, limit int) ([]MonitoringStaleReceivedTask, error) { items, err := markMonitoringStaleReceivedTasks(ctx, tx, now, threshold, limit) if err != nil { return nil, err } return items, nil }