{"record":{"id":"7ae9ef12d4dbf8db","repo":"Billionmail/BillionMail","slug":"failed-to-update-abnormal-recipient-w","errorCode":null,"errorMessage":"Failed to update abnormal recipient: %w","messagePattern":"Failed to update abnormal recipient: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/abnormal_recipient/abnormal_recipient.go","lineNumber":115,"sourceCode":"\tvar existList []entity.AbnormalRecipient\n\terr := g.DB().Model(\"abnormal_recipient\").WhereIn(\"recipient\", recipients).Scan(&existList)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to query existing abnormal recipients: %w\", err)\n\t}\n\texistMap := make(map[string]*entity.AbnormalRecipient)\n\tfor _, r := range existList {\n\t\texistMap[r.Recipient] = &r\n\t}\n\t// 1. Update the existing one count+1\n\tfor _, r := range existList {\n\t\t_, err := g.DB().Model(\"abnormal_recipient\").Where(\"id\", r.Id).Data(g.Map{\n\t\t\t\"count\":       r.Count + 1,\n\t\t\t\"description\": description,\n\t\t\t\"add_type\":    addType,\n\t\t}).Update()\n\t\tif err != nil {\n\n\t\t\treturn fmt.Errorf(\"Failed to update abnormal recipient: %w\", err)\n\t\t}\n\n\t}\n\t// 2. Inserting something that doesn't exist\n\tvar insertList []g.Map\n\tfor _, recipient := range recipients {\n\t\tif _, ok := existMap[recipient]; !ok {\n\t\t\tinsertList = append(insertList, g.Map{\n\t\t\t\t\"recipient\":   recipient,\n\t\t\t\t\"count\":       1,\n\t\t\t\t\"add_type\":    addType,\n\t\t\t\t\"description\": description,\n\t\t\t\t\"create_time\": now,\n\t\t\t})\n\t\t}\n\t}\n\tif len(insertList) > 0 {\n","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/abnormal_recipient/abnormal_recipient.go#L97-L133","documentation":"Within BatchUpsertAbnormalRecipients, existing recipients are updated with count+1/description/add_type. If that UPDATE statement fails (deadlock, lock timeout, connection reset, constraint violation), this wrapped error aborts the remaining batch.","triggerScenarios":"Concurrent batch upserts updating the same rows (deadlock/serialization failure); DB connection dropped mid-loop; column mismatch after schema change.","commonSituations":"Two cron jobs (e.g. AbnormalRecipientAutoStat and a manual import) running simultaneously; long transactions holding row locks; postgres restart during a large import.","solutions":["Retry the whole batch on serialization/deadlock errors (SQLSTATE 40P01, 40001).","Serialize concurrent writers or use a lock (advisory lock / SELECT FOR UPDATE).","Verify schema columns count/description/add_type match the entity.","Check connection pool settings for idle timeouts."],"exampleFix":"// before\n_ = abnormal_recipient.BatchUpsertAbnormalRecipients(ctx, recipients, addType, desc)\n// after\nerr := abnormal_recipient.BatchUpsertAbnormalRecipients(ctx, recipients, addType, desc)\nif err != nil && isRetryableSQLState(err) {\n\terr = abnormal_recipient.BatchUpsertAbnormalRecipients(ctx, recipients, addType, desc)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"err := abnormal_recipient.BatchUpsertAbnormalRecipients(ctx, recs, addType, desc)\nif err != nil {\n\tvar pqErr *pq.Error\n\tif errors.As(err, &pqErr) && (pqErr.Code == \"40P01\" || pqErr.Code == \"40001\") {\n\t\t// deadlock/serialization: retry the whole batch once\n\t}\n}","preventionTips":["Avoid concurrent batch upserts on the same rows (use a job lock)","Keep transactions short","Set a sane deadlock_timeout and monitor deadlocks"],"tags":["database","goframe","update-failure","concurrency"],"backgroundTag":"db-deadlock-retry","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}