-
[OPIK-7264] [BE] fix: serialize concurrent dataset version writes to prevent 500s and silent row loss (#7518)
发布于
2026-07-27 10:58:57 +00:00 - [OPIK-7264] [BE] fix: serialize concurrent dataset version writes to prevent 500s and silent row loss
Parallel dataset uploads raced on the mutable 'latest' pointer in the
versioning path: workers sharing a batch group collided during version
creation (HTTP 500, Bug A), and workers with distinct batch groups each
branched off the same base and overwrote each other's 'latest' pointer
(HTTP 200 but silent row loss, Bug B).- Wrap every read-latest -> create-version -> flip-latest sequence in a
per-dataset distributed lock so concurrent writers serialize instead of
racing. - Add a compare-and-swap backstop: flip 'latest' only if it still points
at the base that was diffed from (deleteTagIfVersion); a lost CAS yields
a retryable 409 rather than a silent clobber, guarding against lock-lease
failure. - Make findByBatchGroupId deterministic (ORDER BY id DESC LIMIT 1) so a
shared batch group resolves to a single version. - Cover Bug A, Bug B, and per-dataset lock independence with concurrency
integration tests.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- refactor(datasets): address PR review — extract latest-tag CAS helper, rename lookup, text-block SQL
- Extract the duplicated latest-tag flip/CAS into a single flipLatestTag() helper
shared by createVersionFromDelta and insertVersionAndUpdateTags so the 409 path
can't drift between them. - Rename findByBatchGroupId -> findLatestByBatchGroupId to reflect the deterministic
ORDER BY id DESC LIMIT 1 contract. - Convert deleteTagIfVersion @SqlUpdate to a text block per the DAO convention.
- Strengthen parallelSharedBatchGroup test to assert exactly one 'latest' version
holding all writers' rows, exercising the deterministic batch-group lookup.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- fix(datasets): lock applyDeltaChanges/patch + configurable lease, address review
Address thiagohora's review on the OPIK-7264 concurrency fix:
- applyDeltaChanges: wrap in withDatasetVersionLock and pass enforceLatestCas =
!override, so its is-latest check and the 'latest' flip are atomic. Previously
unlocked + enforceLatestCas=false, it reopened the OPIK-7264 lost-update on the
primary versioning endpoint (TOCTOU between the isLatest check and the flip). - patch(): wrap the version-creating path in the lock so it shares one
mutual-exclusion domain with uploads and can't spuriously 409 a locked writer. - Make the lock lease configurable via datasetVersioning.lockLease (default 60s)
instead of a hardcoded constant. - Make saveItemsWithVersion and the delete-by-datasetId path self-deferring so no
work runs before the lock is acquired. - delete-by-ids: reuse the datasetId already resolved for the lock instead of
resolving from ClickHouse a second time. - flipLatestTag: log.warn with workspaceId/datasetId/casBase/newVersionId before
the 409; javadoc for enforceLatestCas and the latest-only batch lookup. - Tests: add concurrent applyDeltaChanges no-clobber test (one 2xx, rest 409),
assert row identity (per-writer tags) not just counts, drop a redundant status
assertion, and extract a shared item-builder helper.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- refactor(datasets): address review — cache lock lease, fail-fast, log level, test barrier
- Cache the lock lease Duration via @Getter(lazy=true) instead of recomputing
toJavaDuration() on every lock acquisition. - saveItemsWithVersion: run the dataset-existence check + lazy migration before
span/trace validation so requests for a missing dataset fail fast. - Demote the per-save "Saving items with version" log from INFO to DEBUG (callers
already emit an INFO lifecycle line). - Tests: gate concurrent writers behind a CyclicBarrier so they start together and
genuinely overlap on a loaded CI node.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- fix(datasets): lock the restore path + drop duplicate dataset lookup
- restoreVersion: wrap under the per-dataset lock (shared DATASET_VERSION_LOCK)
so it serializes with the other version-creating writers — a concurrent item
write can no longer move 'latest' mid-restore and race the flip (OPIK-7264).
This makes restore the last version-creating path brought under the lock. - saveItemsWithVersion: remove the redundant dataset-existence findById; every
caller (createFromTraces/Spans, save, saveBatch) already guarantees existence.
The one caller without a prior guard, saveBatch(UUID, List), now performs the
check itself under the lock.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- refactor(datasets): make all lock actions self-deferring + inline lease
Address thiagohora's re-review:
- Make every method passed as an action to withDatasetVersionLock self-deferring
at its source (batchUpdateByIdsWithVersioning, batchUpdateByFiltersWithVersioning,
deleteItemsWithVersion, handleGroupedDeletion), matching saveItemsWithVersion. The
call sites passed bare method calls whose laziness wasn't visible at the boundary;
wrapping the bodies in Mono.defer guarantees no work runs until the lock is held
and keeps the pattern uniform + future-proof. - Replace the @Getter(lazy=true) lease field with an inline
config.getDatasetVersioning().lockLease().toJavaDuration() in withDatasetVersionLock,
avoiding the lazy-init synchronization overhead; matches the codebase norm. - flipLatestTag caller: derive casBase via Optional.ofNullable(...).map(id).orElse(null).
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- refactor(datasets): dedup batch-update scaffold, skip copy pool on select-all, document 409
Address review:
- Extract the shared lazy-migration/defer/latest-version scaffold of the two
batchUpdate*WithVersioning paths into runVersionedBatchUpdate(...); each method now
supplies only its path-specific body. Keeps the defer-under-lock guarantee in one place. - batchUpdateByFiltersWithVersioning: skip the copyUuids pool allocation on select-all
(empty filters), which copies no unchanged rows. - Document the retryable 409 (concurrent latest-version move) on restoreVersion's
OpenAPI @Operation.responses so the spec/SDKs reflect it.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
- chore(datasets): make batch-update logs grep-friendly (action phrase first)
Reorder the "Batch updating/updated" log messages in both versioned batch-update
paths so the action phrase stays intact and the variable values move to the end
(count/baseVersion), making the logs easier to grep by operation.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Co-authored-by: Claude Opus 4.8 (1M context) noreply@anthropic.com
下载附件