Split workflowexecution into hot/cold live+archive; fix duplicate-_id and multi-generation alias bugs for stateful indexes (org_statistics, notifications) - #455
Open
P4sca1 wants to merge 4 commits into
Conversation
Signed-off-by: Pascal Sthamer <pascal+github@sthamer.xyz>
…indices Signed-off-by: Pascal Sthamer <pascal+github@sthamer.xyz>
Member
Signed-off-by: Pascal Sthamer <pascal+github@sthamer.xyz>
Signed-off-by: Pascal Sthamer <pascal+github@sthamer.xyz>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related classes of bugs come from the same root cause: OpenSearch alias writes/reads route to whichever generation currently owns the alias, with no cross-generation
_idawareness. Any index that is (a) rollover-enabled and (b) written to by re-writing the same_idin place (rather than pure append) will eventually split a single logical record across two physical generations, or — worse — leave an alias pointing at more than one index, which OpenSearch flatly refuses to run single-document ops against:workflowexecution,notifications, andorg_statisticswere all rollover + keyed-write indexes and all exposed to this. This PR fixes both the write path (going forward) and retroactively heals any deployment that has already hit this (like the error above), with no manual intervention required.Why
workflowexecutionis Shuffle's highest-volume index.SetWorkflowExecutionre-writes the sameexecution_idrepeatedly as a run progresses. Once the index rolled over, an in-place update after the boundary left a stale copy in the older generation — the same execution split across two documents, with list/search/count results depending on which generation happened to be searched.org_statistics(andnotifications) are stateful keyed stores, not append logs, but were previously left on the rollover list. Once they roll over to a second generation, the alias itself becomes multi-index, and any single-doc read/write against the alias — e.g.GetOrgStatistics,SetOrgStatistics— starts failing outright with theillegal_argument_exceptionabove. This is a real, observed production error, not a hypothetical.What changed
workflowexecution: hot/cold live + archive split (execution_lifecycle.go, new)workflowexecution_live— a new, single (non-rolling), mutable index. All in-flight execution writes go here, so in-place_idupdates are always safe regardless of rollover state.workflowexecution(unchanged name) — becomes the archive: an append-only rollover index. After a 1-hour grace window past terminal status (to tolerate late async decision-fixups), a background sweep moves executions from live to archive. Archive writes are resolve-before-write (findExecutionInArchive), so re-archiving the same id after a rollover updates the existing copy instead of duplicating it. Deletes always target the concrete backing index (deleteExecutionFromArchiveIndex), never alias-by-id.resolveExecutionWriteTarget/ErrExecutionArchived: a write to an already-archived, terminal execution is a duplicate/late re-affirmation; a write to an already-archived, non-terminal execution is a legitimate reopen and unarchives the doc back to live).GetWorkflowExecution,GetAllWorkflowExecutions[V2],GetUnfinishedExecutions,GetWorkflowRunCount,GetWorkflowRunsBySearch— converted to search both indices withAllowNoIndices/IgnoreUnavailableand dedupe byexecution_id.GetWorkflowRunCountuses acardinalityaggregation onexecution_idinstead oftrack_total_hits, so a duplicate_idbriefly spanning live+archive during a crash/sweep-race window is counted once, not twice.workflowexecutionindex move toworkflowexecution_liveon startup; the legacy index keeps rolling as the archive going forward.org_statistics/notifications: stop rolling stateful stores, retroactively collapse existing multi-generation aliases (db-connector.go)GetOpensearchRolloverIndexes()(line 124) now only lists genuinely append-only, high-volume stores.org_statisticsandnotificationsare still inGetOpensearchBaseIndexes()(line 94, still managed/created) but are no longer rollover-enabled — each stays a single backing index, so in-place_idwrites can never split across generations again.collapseSingleIndexAliases(new): for every base index that is not on the rollover list, on everyInitOpensearchIndexes()startup run (unlessSHUFFLE_SKIP_OPENSEARCH_INDEX_INIT=true), this lists all physical generations currently under the alias; if more than one exists (e.g. a customer who already hit theorg_statisticsalias error, or upgraded from a version where these indexes rolled), it reindexes every older generation into the newest (op_type: create, so the newest generation's copy always wins on_idcollision — no data loss, no duplication); removes the alias from the older generation(s) and deletes them; and detaches rollover/ISM from the sole remaining generation so it can't split again.workflowexecution_liveand theworkflowexecutionarchive share their index mapping via a single Go map reference (opensearchCoreMappings), so they can never drift apart.created/edited/started_atmapped aslong, notdate. OpenSearchdatefields store doc values internally as epoch milliseconds regardless of theepoch_secondformat annotation, whilelongstores the raw value as written. Every pre-existing customer index has these fields dynamically inferred aslong(the app always wrote raw epoch-second ints). Mapping themdateon new generations would silently rank cross-generation sorts by magnitude (ms vs. seconds) instead of true time — no query error, just wrong order. Verified live against a real cluster. Usinglongeverywhere avoids this with no reindex needed.-000001still attach ISM/rollover correctly instead of failing on a write-index conflict.notification_retention.go(new)OPENSEARCH_NOTIFICATION_RETENTION_DAYSdays.[WARNING]instead of silently no-op'ing when an older generation'sread/ignored/updated_atfields can't support the retention query.Opt-in / opt-out
All new background behavior can be disabled independently:
SHUFFLE_SKIP_OPENSEARCH_INDEX_INIT— skips index/mapping management entirely (pre-existing flag), including the alias-collapse migration.SHUFFLE_SKIP_EXECUTION_LIVE_MIGRATION— skips the one-time startup migration of non-terminal legacy docs intoworkflowexecution_live.SHUFFLE_SKIP_EXECUTION_ARCHIVAL_SWEEP— skips the recurring live→archive background sweep.OPENSEARCH_NOTIFICATION_RETENTION_DAYS— unset/0keeps notification retention fully off (default).