Diagnose why a reassigned contract review (LEGAL_REVIEW manual task) still appears for the previous assignee in task list or contract search while DB/activity log show the new assignee. Trigger on: 'reassigned review still in old user's tasks', 'wrong user sees review task after reassignment', 'Metabase correct but UI wrong', 'Elasticsearch assigned_to_list stale', 'TEMPLATE_{contract_id} LEGAL_REVIEW wrong org user', 're_sync_contract_search_object race', 'Retry 1 for get_pending_tasks_for_manual_task', 'personal task list not updating after reassignment'. Covers ES contracts index lag vs ManualTaskData versioning and mitigation via contract search resync.
Symptom: After a user reassigns a contract review (manual / LEGAL_REVIEW-style task), the task still shows up for the original assignee in the task list or search-driven views, while activity log and Metabase already show the reassignment to the new user.
Pattern: PostgreSQL is authoritative (latest ManualTaskData has the new assignee_org_user_id), but the contracts Elasticsearch index still has the old assignee under assigned_to_list for LEGAL_REVIEW (or equivalent). User-facing lists that query ES therefore disagree with DB truth.
Distinct from: Approval visibility bugs (use approvals-specific skills), pure FE caching (refresh + ES check rules this out), and transient ES lag that self-heals seconds later — this pattern is specifically a resync that ran and committed old state because the new row did not exist yet at resync time (ordering/race).
ManualTaskData| Use this skill | Do not use |
|---|---|
| Task list / search shows old assignee; DB shows new assignee | DB still shows old assignee → data or workflow bug, not ES staleness |
Contract doc id TEMPLATE_{contract_id} has wrong assigned_to_list | Issue is only approvals v5 UI with no search involvement |
Cluster + contract_id + workspace_id known | Pure process / training issue with no technical inconsistency |
slack_read_channel / slack_read_thread — read incident context.slack_search_public — find similar past incidents (#incident-*, reassign, LEGAL_REVIEW, assigned_to_list).SPD-41912 or JQL for manual task + search + elastic (example ticket from incident SPD-41912).Logs (adjust start/end to reassignment window, {cluster} e.g. prod-eu):
ReSync, contract, search, object, {contract_id}.ReSync contract search object to elastic successful for contracts: [{contract_id}].Retry 1 for get_pending_tasks_for_manual_task near the same timestamp as resync (secondary signal for read/retry race; not sufficient alone).Confirmed example (Touch ’n Go, Rootly 2937):
prod-eu, contract 360819, window 2026-02-25T08:50:44Z–2026-02-25T09:10:44Z, filters included ReSync, contract, search, object, 360819.
Run BQ queries one at a time — parallel execute_sql calls often return 500 errors.
Latest manual task data rows for a contract’s review task (adjust table if schema differs):
SELECT
mtd.id,
mtd.created,
mtd.modified,
mtd.assignee_org_user_id,
mtd.parent_manual_task_id,
mtd.is_latest,
mtd.type,
mtd.status,
mtd.previous_data_version_id
FROM `spotdraft-prod.prod_{region}_db.public_contracts_v3_manualtaskdata` mtd
WHERE mtd.contract_id = {contract_id}
AND mtd.created_by_workspace_id = {workspace_id}
ORDER BY mtd.id DESC
LIMIT 20
Replace {region} with eu, usa, india, or mea matching the incident cluster.
Contract workspace sanity check:
SELECT id, created_by_workspace_id, business_user_id, status
FROM `spotdraft-prod.prod_{region}_db.public_contracts_v3_contractv3`
WHERE id = {contract_id}
Prod uses jsonPayload; QA often uses textPayload.
resource.type="k8s_container"
resource.labels.project_id="spotdraft-prod"
resource.labels.cluster_name="prod-{region}"
resource.labels.namespace_name="prod"
jsonPayload.message:"ReSync contract search object"
jsonPayload.message:"{contract_id}"
Narrow with a short time range around reassignment.
contracts (alias may include version suffix in some envs — confirm in cluster docs).TEMPLATE_{contract_id} (historic contracts may use HISTORIC_{contract_id} — see BulkUpdateUseCase delete paths in django-rest-api).assigned_to_list — entries include role/type (e.g. LEGAL_REVIEW vs approval-v5-*). Compare org user ids to latest ManualTaskData.assignee_org_user_id.| Environment | BQ project | Dataset | Table prefix |
|---|---|---|---|
| Prod EU | spotdraft-prod | prod_eu_db | public_ |
| Prod USA | spotdraft-prod | prod_usa_db | public_ |
| Prod India | spotdraft-prod | prod_india_db | public_ |
| Prod MEA | spotdraft-prod | prod_mea_db | (verify) |
| QA | spotdraft-qa | qa_*_public | (varies) |
Capture identifiers: {workspace_id}, {contract_id}, {cluster}, affected {org_user_id} (old and new), {parent_manual_task_id} if known.
Confirm DB truth:
ManualTaskData for the review task: is_latest = true, assignee_org_user_id = new user.created timestamp on the new row (critical for race proof).Confirm approvals are not the cause:
Inspect ES document TEMPLATE_{contract_id}:
assigned_to_list still shows old user for LEGAL_REVIEW while step 2 is correct → stale index.Correlate timing (Groundcover + BQ):
re_sync_contract_search_object / bulk update success log time for {contract_id}.ManualTaskData.created for the new row.created time → worker indexed prior version (e.g. previous ManualTaskData id still pointing at old assignee).Rule out:
created is tens of seconds after resync end; timestamp ordering is the strong proof.| Step | Component |
|---|---|
| Manual task reassignment | New ManualTaskData row; is_latest moves forward on task chain. |
| Contract save / signals | re_sync_contract_search_object.delay(contract_id=...) from contracts_v3.signals on ContractV3 post_save. |
| Build search document | BulkUpdateUseCase.execute → build_search_object_use_case from contract domain model. |
| Pending manual tasks in document | ManualTaskService.get_pending_tasks_for_manual_task (retries logged as Retry N for get_pending_tasks_for_manual_task). |
| Index write | contract_search_v2_repo.bulk_update → Elasticsearch contracts index. |
Key files: contracts_v3/tasks.py (re_sync_contract_search_object), contracts_v3/search_v2/domain/use_cases/bulk_update_use_case.py, contracts_v3/services/manual_task_service.py (get_pending_tasks_for_manual_task), contracts_v3/signals.py, contracts_v3/search_v2/data/elastic_repo.py, contracts_v3/contract_assigned_to/domain/use_cases/compute_contract_assigned_to.py.
| If true | Evidence |
|---|---|
| ES stale after race | Latest ManualTaskData correct; ES assigned_to_list wrong; resync log timestamp before new row created. |
| Not this | ES matches DB; or DB still wrong. |
| Different issue | No resync in window; then look for failed tasks, different contract id, or workspace/cluster mismatch. |
re_sync_contract_search_object for {contract_id} (Celery / runbook used in production).TEMPLATE_{contract_id} — LEGAL_REVIEW assignee matches latest ManualTaskData.Does not require changing DB rows if DB is already correct.
ManualTaskData commit (transaction hook, post-commit task, or debounced coalescing).BulkIndexError or resync failures in logs → infra / ES cluster issue, not this race pattern.https://spotdraft.slack.com/archives/C0AJJEU7G84 (incident channel; login required)Confirmed example values (for regression testing only):
workspace_id=178717, contract_id=360819, parent_manual_task_id=50950, ManualTaskData ids 134526 → 134527, org users 88664 (Akhir) → 88631 (Chen), EU cluster.
expiry-report-missing-contracts — different symptom, but shared theme of Elasticsearch lag vs reporting; mitigation mindset similar (resync / wait for index).incident-lookup — use Slack search to find other #incident-* threads on reassignment + search.fullstory-bulk-delete-metadata-value — mentions Elasticsearch resync via partially_resync_contracts_task for metadata-driven fields; same index freshness family.