fix(lang-var-migration): use JDBC savepoint to isolate unique_fields failures#35565
fix(lang-var-migration): use JDBC savepoint to isolate unique_fields failures#35565
Conversation
…failures When StartupTasksExecutor wraps the migration in a single PostgreSQL transaction, a duplicate-key violation in unique_fields aborts the entire connection, causing all subsequent variables to fail with "current transaction is aborted". Using a JDBC savepoint before each publish() resets the connection state after a failure so only the conflicting variable is skipped. Adds a regression test that reproduces the production cascade failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
❌ Issue Linking RequiredThis PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes. How to fix this:Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Why is this required?Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.--- This comment was automatically generated by the issue linking workflow |
|
Claude finished @fabrizzio-dotCMS's task in 2m 45s —— View job PR Review: fix(lang-var-migration): JDBC savepoint isolationOverall: The fix is conceptually correct and well-targeted. The savepoint approach is appropriate for this problem. Several implementation issues worth addressing. Issues1. Duplicate catch blocks — use
Replace with a private Contentlet publishWithSavepoint(final Contentlet contentlet) throws DotSecurityException, DotDataException {
final Connection conn = Try.of(DbConnectionFactory::getConnection).getOrNull();
final Savepoint sp = conn != null ? Try.of(() -> conn.setSavepoint()).getOrNull() : null;
boolean success = false;
try {
final Contentlet result = this.publish(contentlet);
success = true;
return result;
} finally {
if (sp != null) {
if (success) {
Try.run(() -> conn.releaseSavepoint(sp))
.onFailure(ex -> Logger.warn(this, "Could not release savepoint: " + ex.getMessage()));
} else {
Try.run(() -> conn.rollback(sp))
.onFailure(ex -> Logger.warn(this, "Could not rollback savepoint: " + ex.getMessage()));
}
}
}
}2. Silent degradation when savepoint cannot be set Lines 368–369: final Connection conn = Try.of(DbConnectionFactory::getConnection).getOrNull();
final Savepoint sp = conn != null ? Try.of(() -> conn.setSavepoint()).getOrNull() : null;If 3. Test success assertion doesn't verify
assertFalse("'key_valid' must have been successfully migrated", successes.isEmpty());
Consider adding a check: after migration, look up the contentlet by language and key 4. Minor: Docstring in
Not an issue
|
Problem
Closes #35568
Related to support ticket: https://helpdesk.dotcms.com/a/tickets/35427
When
StartupTasksExecutor.executeDataUpgrades()wraps the entire migration task in a single PostgreSQL transaction viaHibernateUtil.startTransaction(), a duplicate-key violation inunique_fieldsaborts the entire shared connection. Every subsequent variable then fails with:even though only the first variable had a conflict. In production this caused hundreds of Language Variables to fail in cascade after a single collision.
Root Cause of the Orphan
Orphaned
unique_fieldsrows exist becauseDBUniqueFieldValidationStrategy.innerValidate()is annotated@CloseDBIfOpened, which forces theINSERT INTO unique_fieldsto commit on a separate connection independently of the outer contentlet transaction. If that outer transaction rolls back, theunique_fieldsrow persists. Root cause fix is in PR #35567 (issue #35566).Fix
Added
publishWithSavepoint()inLegacyLangVarMigrationHelper. Before eachpublish()call:conn.rollback(savepoint)resets the PostgreSQL connection from aborted back to active, without touching the outer transaction.Only the conflicting variable is skipped; all remaining variables in the file continue normally.
Test
Added
testMigrationVariableFailureDoesNotCascadeToSubsequentVariables()which:StartupTasksExecutorcontext.unique_fieldsrow forkey_conflict.key_conflict,key_valid).key_conflictfails (expected) andkey_validsucceeds (was also failing before fix).Verified: test fails before the fix, passes after.
Scope
Only
LegacyLangVarMigrationHelper.javaand the integration test are modified. No changes tounique_fieldsinfrastructure.Related
@CloseDBIfOpenednon-atomicity)Test plan
Task240306MigrateLegacyLanguageVariablesTest#testMigrationVariableFailureDoesNotCascadeToSubsequentVariables— must passTask240306MigrateLegacyLanguageVariablesTestsuite — no regressionstestExecuteUpgrade,testDataTaskIdempotency,testDropThenRecreateLanguageVariableContentType— must pass🤖 Generated with Claude Code