harden article generation reliability
This commit is contained in:
@@ -32,6 +32,13 @@ type generationTaskRecoveryConfig struct {
|
||||
MaxAttempts int
|
||||
}
|
||||
|
||||
type generationTaskStateCheckConfig struct {
|
||||
Interval time.Duration
|
||||
Timeout time.Duration
|
||||
BatchSize int
|
||||
Lookback time.Duration
|
||||
}
|
||||
|
||||
type claimedGenerationTaskLease struct {
|
||||
task generationTaskRecoveryRecord
|
||||
token string
|
||||
@@ -139,6 +146,17 @@ func normalizeGenerationTaskRecoveryConfig(cfg config.GenerationConfig) generati
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGenerationTaskStateCheckConfig(cfg config.GenerationConfig) generationTaskStateCheckConfig {
|
||||
normalized := cfg
|
||||
config.NormalizeGenerationConfig(&normalized)
|
||||
return generationTaskStateCheckConfig{
|
||||
Interval: normalized.TaskStateCheckInterval,
|
||||
Timeout: normalized.TaskStateCheckTimeout,
|
||||
BatchSize: normalized.TaskStateCheckBatchSize,
|
||||
Lookback: normalized.TaskStateCheckLookback,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ArticleGenerationWorker) claimGenerationTask(ctx context.Context, taskID int64, owner string, leaseTTL time.Duration) (*claimedGenerationTaskLease, bool, error) {
|
||||
if leaseTTL <= 0 {
|
||||
leaseTTL = normalizeGenerationTaskRecoveryConfig(config.GenerationConfig{}).LeaseTTL
|
||||
@@ -362,6 +380,20 @@ func (w *ArticleGenerationWorker) requeueStaleGenerationTasks(ctx context.Contex
|
||||
}
|
||||
continue
|
||||
}
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: item.ID,
|
||||
TenantID: item.TenantID,
|
||||
ArticleID: item.ArticleID,
|
||||
TaskType: item.TaskType,
|
||||
Stage: "recovery_stale_queued",
|
||||
StatusBefore: "queued",
|
||||
StatusAfter: "queued",
|
||||
Result: tenantapp.GenerationTaskResultSuccess,
|
||||
Severity: "warning",
|
||||
AttemptCount: item.AttemptCount,
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventRecoveryRequeue, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation stale queued task republished", logCtx)
|
||||
requeued++
|
||||
}
|
||||
|
||||
@@ -538,17 +570,39 @@ func (w *ArticleGenerationWorker) requeueExpiredRunningGenerationTask(ctx contex
|
||||
return false, err
|
||||
}
|
||||
if !shouldRequeue {
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: item.ID,
|
||||
TenantID: tenantID,
|
||||
ArticleID: articleID,
|
||||
TaskType: taskType,
|
||||
Stage: "recovery_expired_running",
|
||||
StatusBefore: "running",
|
||||
StatusAfter: "failed",
|
||||
Result: tenantapp.GenerationTaskResultSkipped,
|
||||
Severity: "warning",
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventRecoveryFinalize, logCtx)
|
||||
if w.logger != nil {
|
||||
w.logger.Info("article generation expired task finalized because article is already failed",
|
||||
zap.Int64("task_id", item.ID),
|
||||
zap.Int64("article_id", articleID),
|
||||
)
|
||||
tenantapp.LogGenerationTaskInfo(w.logger, "article generation expired task finalized because article is already failed", logCtx)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if err := w.publishGenerationTask(ctx, item.ID, taskType, tenantID, articleID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: item.ID,
|
||||
TenantID: tenantID,
|
||||
ArticleID: articleID,
|
||||
TaskType: taskType,
|
||||
Stage: "recovery_expired_running",
|
||||
StatusBefore: "running",
|
||||
StatusAfter: "queued",
|
||||
Result: tenantapp.GenerationTaskResultSuccess,
|
||||
Severity: "warning",
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventRecoveryRequeue, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation expired task requeued", logCtx)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -672,6 +726,21 @@ func (w *ArticleGenerationWorker) finalizeExpiredGenerationTask(ctx context.Cont
|
||||
if item.ArticleID > 0 {
|
||||
tenantapp.InvalidateArticleCaches(cacheCtx, w.cache, item.TenantID, &item.ArticleID)
|
||||
}
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: item.ID,
|
||||
TenantID: item.TenantID,
|
||||
OperatorID: item.OperatorID,
|
||||
ArticleID: item.ArticleID,
|
||||
ReservationID: item.QuotaReservationID,
|
||||
TaskType: item.TaskType,
|
||||
Stage: "recovery_finalize",
|
||||
StatusAfter: "failed",
|
||||
Result: tenantapp.GenerationTaskResultSuccess,
|
||||
Severity: "critical",
|
||||
AttemptCount: item.AttemptCount,
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventRecoveryFinalize, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation stale task finalized", logCtx)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,20 @@ func TestNormalizeGenerationTaskRecoveryConfigUsesGenerationDefaults(t *testing.
|
||||
if cfg.MaxAttempts != 3 {
|
||||
t.Fatalf("expected max attempts 3, got %d", cfg.MaxAttempts)
|
||||
}
|
||||
|
||||
stateCheck := normalizeGenerationTaskStateCheckConfig(config.GenerationConfig{})
|
||||
if stateCheck.Interval != 5*time.Minute {
|
||||
t.Fatalf("expected state check interval 5m, got %s", stateCheck.Interval)
|
||||
}
|
||||
if stateCheck.Timeout != 10*time.Second {
|
||||
t.Fatalf("expected state check timeout 10s, got %s", stateCheck.Timeout)
|
||||
}
|
||||
if stateCheck.BatchSize != 100 {
|
||||
t.Fatalf("expected state check batch size 100, got %d", stateCheck.BatchSize)
|
||||
}
|
||||
if stateCheck.Lookback != 24*time.Hour {
|
||||
t.Fatalf("expected state check lookback 24h, got %s", stateCheck.Lookback)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGenerationTaskRecoveryConfigKeepsOverrides(t *testing.T) {
|
||||
@@ -52,6 +66,19 @@ func TestNormalizeGenerationTaskRecoveryConfigKeepsOverrides(t *testing.T) {
|
||||
cfg.MaxAttempts != 5 {
|
||||
t.Fatalf("unexpected recovery config: %#v", cfg)
|
||||
}
|
||||
|
||||
stateCheck := normalizeGenerationTaskStateCheckConfig(config.GenerationConfig{
|
||||
TaskStateCheckInterval: 9 * time.Minute,
|
||||
TaskStateCheckTimeout: 11 * time.Second,
|
||||
TaskStateCheckBatchSize: 13,
|
||||
TaskStateCheckLookback: 2 * time.Hour,
|
||||
})
|
||||
if stateCheck.Interval != 9*time.Minute ||
|
||||
stateCheck.Timeout != 11*time.Second ||
|
||||
stateCheck.BatchSize != 13 ||
|
||||
stateCheck.Lookback != 2*time.Hour {
|
||||
t.Fatalf("unexpected state check config: %#v", stateCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerationWorkerOwnerIsBounded(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.uber.org/zap"
|
||||
|
||||
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
)
|
||||
|
||||
const (
|
||||
generationTaskAnomalyArticleFailedTaskActive = "article_failed_task_active"
|
||||
generationTaskAnomalyArticleCompletedTaskActive = "article_completed_task_active"
|
||||
generationTaskAnomalyCompletedTaskArticleMissing = "completed_task_article_not_completed"
|
||||
generationTaskAnomalyCompletedTaskMissingVersion = "completed_task_missing_version"
|
||||
generationTaskAnomalyExpiredRunningLease = "expired_running_lease"
|
||||
)
|
||||
|
||||
type generationTaskStateAnomaly struct {
|
||||
TaskID int64
|
||||
TenantID int64
|
||||
OperatorID int64
|
||||
ArticleID int64
|
||||
ReservationID int64
|
||||
TaskType string
|
||||
TaskStatus string
|
||||
ArticleStatus string
|
||||
AttemptCount int
|
||||
LeaseOwner string
|
||||
AnomalyType string
|
||||
Severity string
|
||||
}
|
||||
|
||||
func CheckGenerationTaskStateConsistency(ctx context.Context, pool generationTaskStateDB, logger *zap.Logger, cfg generationTaskStateCheckConfig) (int, error) {
|
||||
if pool == nil || cfg.BatchSize <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if cfg.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
lookback := time.Now().UTC().Add(-cfg.Lookback)
|
||||
|
||||
rows, err := pool.Query(ctx, `
|
||||
WITH candidates AS (
|
||||
SELECT
|
||||
gt.id,
|
||||
gt.tenant_id,
|
||||
gt.operator_id,
|
||||
gt.article_id,
|
||||
gt.quota_reservation_id,
|
||||
gt.task_type,
|
||||
gt.status AS task_status,
|
||||
a.generate_status AS article_status,
|
||||
gt.attempt_count,
|
||||
gt.lease_owner,
|
||||
CASE
|
||||
WHEN a.generate_status = 'failed' AND gt.status IN ('queued', 'running') THEN 'article_failed_task_active'
|
||||
WHEN a.generate_status = 'completed' AND gt.status IN ('queued', 'running') THEN 'article_completed_task_active'
|
||||
WHEN gt.status = 'completed' AND COALESCE(a.generate_status, '') <> 'completed' THEN 'completed_task_article_not_completed'
|
||||
WHEN gt.status = 'completed' AND (a.current_version_id IS NULL OR av.id IS NULL) THEN 'completed_task_missing_version'
|
||||
WHEN gt.status = 'running' AND (gt.lease_expires_at IS NULL OR gt.lease_expires_at < NOW()) THEN 'expired_running_lease'
|
||||
ELSE ''
|
||||
END AS anomaly_type
|
||||
FROM generation_tasks gt
|
||||
LEFT JOIN articles a
|
||||
ON a.id = gt.article_id
|
||||
AND a.tenant_id = gt.tenant_id
|
||||
AND a.deleted_at IS NULL
|
||||
LEFT JOIN article_versions av
|
||||
ON av.id = a.current_version_id
|
||||
WHERE gt.article_id IS NOT NULL
|
||||
AND gt.created_at >= $1
|
||||
AND (
|
||||
gt.status IN ('queued', 'running')
|
||||
OR gt.updated_at >= $1
|
||||
)
|
||||
AND (
|
||||
(a.generate_status IN ('failed', 'completed') AND gt.status IN ('queued', 'running'))
|
||||
OR (gt.status = 'completed' AND COALESCE(a.generate_status, '') <> 'completed')
|
||||
OR (gt.status = 'completed' AND (a.current_version_id IS NULL OR av.id IS NULL))
|
||||
OR (gt.status = 'running' AND (gt.lease_expires_at IS NULL OR gt.lease_expires_at < NOW()))
|
||||
)
|
||||
ORDER BY gt.updated_at DESC, gt.id DESC
|
||||
LIMIT $2
|
||||
)
|
||||
SELECT id, tenant_id, operator_id, article_id, quota_reservation_id, task_type, task_status, article_status, attempt_count, lease_owner, anomaly_type
|
||||
FROM candidates
|
||||
WHERE anomaly_type <> ''
|
||||
`, lookback, cfg.BatchSize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
item, err := scanGenerationTaskStateAnomaly(rows)
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
if item.AnomalyType == "" {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: item.TaskID,
|
||||
TenantID: item.TenantID,
|
||||
OperatorID: item.OperatorID,
|
||||
ArticleID: item.ArticleID,
|
||||
ReservationID: item.ReservationID,
|
||||
TaskType: item.TaskType,
|
||||
Stage: "state_check",
|
||||
AttemptCount: item.AttemptCount,
|
||||
LeaseOwner: item.LeaseOwner,
|
||||
StatusBefore: item.TaskStatus,
|
||||
StatusAfter: item.ArticleStatus,
|
||||
Result: tenantapp.GenerationTaskResultFailure,
|
||||
AnomalyType: item.AnomalyType,
|
||||
Severity: item.Severity,
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventStateAnomaly, logCtx)
|
||||
tenantapp.LogGenerationTaskError(logger, "article generation state consistency anomaly detected", logCtx,
|
||||
zap.String("task_status", item.TaskStatus),
|
||||
zap.String("article_status", item.ArticleStatus),
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return count, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
type generationTaskStateAnomalyRow interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
type generationTaskStateDB interface {
|
||||
Query(context.Context, string, ...any) (pgx.Rows, error)
|
||||
}
|
||||
|
||||
func scanGenerationTaskStateAnomaly(row generationTaskStateAnomalyRow) (generationTaskStateAnomaly, error) {
|
||||
var (
|
||||
item generationTaskStateAnomaly
|
||||
operatorID sql.NullInt64
|
||||
articleID sql.NullInt64
|
||||
reservationID sql.NullInt64
|
||||
articleStatus sql.NullString
|
||||
leaseOwner sql.NullString
|
||||
)
|
||||
if err := row.Scan(
|
||||
&item.TaskID,
|
||||
&item.TenantID,
|
||||
&operatorID,
|
||||
&articleID,
|
||||
&reservationID,
|
||||
&item.TaskType,
|
||||
&item.TaskStatus,
|
||||
&articleStatus,
|
||||
&item.AttemptCount,
|
||||
&leaseOwner,
|
||||
&item.AnomalyType,
|
||||
); err != nil {
|
||||
return generationTaskStateAnomaly{}, err
|
||||
}
|
||||
item.OperatorID = operatorID.Int64
|
||||
item.ArticleID = articleID.Int64
|
||||
item.ReservationID = reservationID.Int64
|
||||
item.ArticleStatus = articleStatus.String
|
||||
item.LeaseOwner = leaseOwner.String
|
||||
item.Severity = generationTaskAnomalySeverity(item.AnomalyType)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func generationTaskAnomalySeverity(anomalyType string) string {
|
||||
switch anomalyType {
|
||||
case generationTaskAnomalyCompletedTaskArticleMissing,
|
||||
generationTaskAnomalyCompletedTaskMissingVersion,
|
||||
generationTaskAnomalyArticleFailedTaskActive:
|
||||
return "critical"
|
||||
case generationTaskAnomalyArticleCompletedTaskActive,
|
||||
generationTaskAnomalyExpiredRunningLease:
|
||||
return "warning"
|
||||
default:
|
||||
return "warning"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type generationTaskStateFakeRow struct {
|
||||
values []any
|
||||
}
|
||||
|
||||
func (r generationTaskStateFakeRow) Scan(dest ...any) error {
|
||||
for idx := range dest {
|
||||
switch target := dest[idx].(type) {
|
||||
case *int64:
|
||||
*target = r.values[idx].(int64)
|
||||
case *int:
|
||||
*target = r.values[idx].(int)
|
||||
case *string:
|
||||
*target = r.values[idx].(string)
|
||||
case *sql.NullInt64:
|
||||
*target = r.values[idx].(sql.NullInt64)
|
||||
case *sql.NullString:
|
||||
*target = r.values[idx].(sql.NullString)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestScanGenerationTaskStateAnomalyNormalizesNullableFields(t *testing.T) {
|
||||
item, err := scanGenerationTaskStateAnomaly(generationTaskStateFakeRow{values: []any{
|
||||
int64(11),
|
||||
int64(22),
|
||||
sql.NullInt64{Int64: 33, Valid: true},
|
||||
sql.NullInt64{Int64: 44, Valid: true},
|
||||
sql.NullInt64{Int64: 55, Valid: true},
|
||||
"template",
|
||||
"completed",
|
||||
sql.NullString{String: "generating", Valid: true},
|
||||
2,
|
||||
sql.NullString{String: "worker-1", Valid: true},
|
||||
generationTaskAnomalyCompletedTaskArticleMissing,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(11), item.TaskID)
|
||||
assert.Equal(t, int64(22), item.TenantID)
|
||||
assert.Equal(t, int64(33), item.OperatorID)
|
||||
assert.Equal(t, int64(44), item.ArticleID)
|
||||
assert.Equal(t, int64(55), item.ReservationID)
|
||||
assert.Equal(t, "template", item.TaskType)
|
||||
assert.Equal(t, "completed", item.TaskStatus)
|
||||
assert.Equal(t, "generating", item.ArticleStatus)
|
||||
assert.Equal(t, 2, item.AttemptCount)
|
||||
assert.Equal(t, "worker-1", item.LeaseOwner)
|
||||
assert.Equal(t, "critical", item.Severity)
|
||||
}
|
||||
|
||||
func TestGenerationTaskAnomalySeverity(t *testing.T) {
|
||||
assert.Equal(t, "critical", generationTaskAnomalySeverity(generationTaskAnomalyCompletedTaskMissingVersion))
|
||||
assert.Equal(t, "critical", generationTaskAnomalySeverity(generationTaskAnomalyArticleFailedTaskActive))
|
||||
assert.Equal(t, "warning", generationTaskAnomalySeverity(generationTaskAnomalyExpiredRunningLease))
|
||||
assert.Equal(t, "warning", generationTaskAnomalySeverity("unknown"))
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config"
|
||||
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
)
|
||||
|
||||
type GenerationTaskStateCheckWorker struct {
|
||||
pool *pgxpool.Pool
|
||||
logger *zap.Logger
|
||||
cfg config.GenerationConfig
|
||||
provider config.Provider
|
||||
}
|
||||
|
||||
func NewGenerationTaskStateCheckWorker(pool *pgxpool.Pool, logger *zap.Logger, cfg config.GenerationConfig) *GenerationTaskStateCheckWorker {
|
||||
return &GenerationTaskStateCheckWorker{
|
||||
pool: pool,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) WithConfigProvider(provider config.Provider) *GenerationTaskStateCheckWorker {
|
||||
w.provider = provider
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) Start(ctx context.Context) {
|
||||
go w.Run(ctx)
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) Run(ctx context.Context) {
|
||||
if w == nil || w.pool == nil {
|
||||
return
|
||||
}
|
||||
w.run(ctx)
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) run(ctx context.Context) {
|
||||
w.runOnce(context.Background())
|
||||
|
||||
for {
|
||||
cfg := w.runtimeConfig()
|
||||
timer := time.NewTimer(cfg.Interval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
w.runOnce(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) runOnce(parent context.Context) {
|
||||
cfg := w.runtimeConfig()
|
||||
startedAt := time.Now()
|
||||
count, err := CheckGenerationTaskStateConsistency(parent, w.pool, w.logger, cfg)
|
||||
duration := time.Since(startedAt)
|
||||
if err != nil {
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
Stage: "state_check",
|
||||
Result: tenantapp.GenerationTaskResultFailure,
|
||||
Severity: "warning",
|
||||
Duration: duration,
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventStateCheckFailure, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation state consistency check failed", logCtx, zap.Error(err))
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation state consistency anomalies detected", tenantapp.GenerationTaskLogContext{
|
||||
Stage: "state_check",
|
||||
Result: tenantapp.GenerationTaskResultSuccess,
|
||||
Severity: "critical",
|
||||
Duration: duration,
|
||||
}, zap.Int("anomaly_count", count))
|
||||
}
|
||||
|
||||
func (w *GenerationTaskStateCheckWorker) runtimeConfig() generationTaskStateCheckConfig {
|
||||
if w != nil && w.provider != nil {
|
||||
if cfg := w.provider.Current(); cfg != nil {
|
||||
return normalizeGenerationTaskStateCheckConfig(cfg.Generation)
|
||||
}
|
||||
}
|
||||
if w != nil {
|
||||
return normalizeGenerationTaskStateCheckConfig(w.cfg)
|
||||
}
|
||||
return normalizeGenerationTaskStateCheckConfig(config.GenerationConfig{})
|
||||
}
|
||||
@@ -180,6 +180,10 @@ func (w *ArticleGenerationWorker) handleDelivery(parent context.Context, deliver
|
||||
}
|
||||
|
||||
task := lease.task.toClaimedTask()
|
||||
owner := generationWorkerOwner(delivery.ConsumerTag)
|
||||
claimLogCtx := generationWorkerTaskLogContext(task, "worker_claim", "queued", tenantapp.GenerationTaskStatusRunning, tenantapp.GenerationTaskResultSuccess, lease.task.AttemptCount, owner)
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventTransition, claimLogCtx)
|
||||
tenantapp.LogGenerationTaskInfo(w.logger, "article generation task claimed", claimLogCtx)
|
||||
heartbeatCtx, stopHeartbeat := context.WithCancel(parent)
|
||||
go w.runGenerationLeaseHeartbeat(heartbeatCtx, task.ID, lease.token, recoveryCfg.LeaseTTL)
|
||||
defer func() {
|
||||
@@ -245,6 +249,9 @@ func articleGenerationProcessTimeout(cfg config.GenerationConfig) time.Duration
|
||||
|
||||
func (w *ArticleGenerationWorker) handleTaskFailure(ctx context.Context, task tenantapp.ClaimedGenerationTask, stage string, taskErr error) {
|
||||
task.TaskType = strings.TrimSpace(task.TaskType)
|
||||
logCtx := generationWorkerTaskLogContext(task, stage, "", "failed", tenantapp.GenerationTaskResultFailure, 0, "")
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventFailure, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation task failed", logCtx, zap.Error(taskErr))
|
||||
var err error
|
||||
switch {
|
||||
case task.TaskType == kolGenerationTaskType && w.kolWorker != nil:
|
||||
@@ -255,13 +262,8 @@ func (w *ArticleGenerationWorker) handleTaskFailure(ctx context.Context, task te
|
||||
err = tenantapp.HandleClaimedGenerationTaskFailure(ctx, w.pool, w.templateService, w.promptRuleService, task, taskErr)
|
||||
}
|
||||
if err != nil && w.logger != nil {
|
||||
w.logger.Warn("article generation task failure handling failed",
|
||||
zap.Error(err),
|
||||
zap.Int64("task_id", task.ID),
|
||||
zap.Int64("article_id", task.ArticleID),
|
||||
zap.String("task_type", task.TaskType),
|
||||
zap.String("stage", stage),
|
||||
)
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventCleanupFailure, logCtx)
|
||||
tenantapp.LogGenerationTaskError(w.logger, "article generation task failure handling failed", logCtx, zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,15 +282,39 @@ func (w *ArticleGenerationWorker) rejectDelivery(delivery amqp.Delivery, requeue
|
||||
)
|
||||
}
|
||||
if w.logger != nil {
|
||||
w.logger.Warn("article generation processing failed",
|
||||
logCtx := tenantapp.GenerationTaskLogContext{
|
||||
TaskID: taskID,
|
||||
Stage: "delivery_reject",
|
||||
Result: tenantapp.GenerationTaskResultFailure,
|
||||
Severity: "warning",
|
||||
StatusAfter: "rejected",
|
||||
}
|
||||
tenantapp.RecordGenerationTaskEvent(tenantapp.GenerationTaskEventQueueFailure, logCtx)
|
||||
tenantapp.LogGenerationTaskWarn(w.logger, "article generation processing failed", logCtx,
|
||||
zap.Error(err),
|
||||
zap.Bool("requeue", requeue),
|
||||
zap.Bool("redelivered", delivery.Redelivered),
|
||||
zap.Int64("task_id", taskID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func generationWorkerTaskLogContext(task tenantapp.ClaimedGenerationTask, stage, statusBefore, statusAfter, result string, attemptCount int, leaseOwner string) tenantapp.GenerationTaskLogContext {
|
||||
return tenantapp.GenerationTaskLogContext{
|
||||
TaskID: task.ID,
|
||||
TenantID: task.TenantID,
|
||||
OperatorID: task.OperatorID,
|
||||
ArticleID: task.ArticleID,
|
||||
ReservationID: task.QuotaReservationID,
|
||||
TaskType: task.TaskType,
|
||||
Stage: stage,
|
||||
AttemptCount: attemptCount,
|
||||
LeaseOwner: leaseOwner,
|
||||
StatusBefore: statusBefore,
|
||||
StatusAfter: statusAfter,
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSONMap(raw []byte) (map[string]interface{}, error) {
|
||||
if len(raw) == 0 {
|
||||
return map[string]interface{}{}, nil
|
||||
|
||||
Reference in New Issue
Block a user