Skip to content

Commit 779e3fd

Browse files
committed
refactor: improved logging/report pipeline
1 parent 022bace commit 779e3fd

11 files changed

Lines changed: 129 additions & 30 deletions

File tree

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ChangeResult.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ public boolean isFailed() {
101101
return status == ChangeStatus.FAILED;
102102
}
103103

104+
public boolean isRolledBack() {
105+
return status == ChangeStatus.ROLLED_BACK;
106+
}
107+
104108
public boolean isApplied() {
105109
return status == ChangeStatus.APPLIED;
106110
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/response/data/ExecutionReportFormatter.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ private static void appendStageBlock(StringBuilder sb, StageResult stage) {
138138
List<ChangeResult> changes = stage.getChanges() != null ? stage.getChanges() : Collections.emptyList();
139139
int applied = (int) changes.stream().filter(c -> c != null && c.isApplied()).count();
140140
int skipped = (int) changes.stream().filter(c -> c != null && c.isAlreadyApplied()).count();
141-
int failed = (int) changes.stream().filter(c -> c != null && c.isFailed()).count();
141+
// ROLLED_BACK is widened into the failed bucket — the change did not succeed even though
142+
// the system was left clean. See PipelineRun.toResponse() for the matching aggregate logic.
143+
int failed = (int) changes.stream().filter(c -> c != null && (c.isFailed() || c.isRolledBack())).count();
142144
sb.append(" changes: ")
143145
.append(applied).append(" applied, ")
144146
.append(skipped).append(" skipped, ")
@@ -261,7 +263,7 @@ private static List<String> failedChangeIds(StageResult stage) {
261263
return Collections.emptyList();
262264
}
263265
return changes.stream()
264-
.filter(c -> c != null && c.isFailed() && c.getChangeId() != null)
266+
.filter(c -> c != null && (c.isFailed() || c.isRolledBack()) && c.getChangeId() != null)
265267
.map(ChangeResult::getChangeId)
266268
.collect(Collectors.toList());
267269
}

core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/response/data/ExecutionReportFormatterTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,67 @@ void reportToleratesStageWithNullStateAndNullChanges() {
205205
assertTrue(out.contains("(unnamed)"), out);
206206
}
207207

208+
@Test
209+
void reportCountsRolledBackChangeAsFailedAndListsItsId() {
210+
// Scenario mirrors the real-world MongoDB duplicate-key case: 5 already-applied changes
211+
// plus 1 transactional change that failed and was auto-rolled-back (status ROLLED_BACK).
212+
// The user-facing report must show 1 failed and list the change ID.
213+
StageResult stage = StageResult.builder()
214+
.stageId("database-init").stageName("database-init")
215+
.state(StageState.failed(new ErrorInfo("MongoWriteException",
216+
"E11000 duplicate key", Collections.singletonList("InsertSeedData"),
217+
"database-init")))
218+
.durationMs(126)
219+
.changes(Arrays.asList(
220+
skippedChange("CreateCustomersTable"),
221+
skippedChange("CreateOrdersTable"),
222+
skippedChange("InsertSeedData"),
223+
skippedChange("CreateEmployeesTable"),
224+
skippedChange("CreateEmployeesCollection"),
225+
rolledBackChange("InsertEmployeeSeedData", "MongoWriteException", "E11000 duplicate key")))
226+
.build();
227+
ExecuteResponseData result = ExecuteResponseData.builder()
228+
.status(ExecutionStatus.FAILED)
229+
.totalStages(1).failedStages(1)
230+
.totalChanges(6).appliedChanges(0).skippedChanges(5).failedChanges(1)
231+
.totalDurationMs(221)
232+
.stages(Collections.singletonList(stage))
233+
.build();
234+
235+
String out = ExecutionReportFormatter.report(result);
236+
assertTrue(out.contains("0 applied, 5 skipped, 1 failed"),
237+
"per-stage block must count ROLLED_BACK as failed: " + out);
238+
assertTrue(out.contains("failed change(s): InsertEmployeeSeedData"),
239+
"failed change(s) line must include the rolled-back change ID: " + out);
240+
}
241+
242+
@Test
243+
void summaryCountsRolledBackChangeAsFailed() {
244+
StageResult stage = StageResult.builder()
245+
.stageId("database-init").stageName("database-init")
246+
.state(StageState.failed(new ErrorInfo("MongoWriteException", "boom",
247+
Collections.singletonList("InsertEmployeeSeedData"), "database-init")))
248+
.changes(Collections.singletonList(rolledBackChange("InsertEmployeeSeedData", "MongoWriteException", "boom")))
249+
.build();
250+
ExecuteResponseData result = ExecuteResponseData.builder()
251+
.status(ExecutionStatus.FAILED)
252+
.totalStages(1).failedStages(1)
253+
.totalChanges(1).failedChanges(1)
254+
.stages(Collections.singletonList(stage))
255+
.build();
256+
257+
String out = ExecutionReportFormatter.summary(result);
258+
assertTrue(out.contains("failed=1"), "summary must reflect ROLLED_BACK in failed=N: " + out);
259+
}
260+
208261
private static ChangeResult appliedChange(String id) {
209262
return ChangeResult.builder().changeId(id).status(ChangeStatus.APPLIED).build();
210263
}
211264

265+
private static ChangeResult skippedChange(String id) {
266+
return ChangeResult.builder().changeId(id).status(ChangeStatus.ALREADY_APPLIED).build();
267+
}
268+
212269
private static ChangeResult failedChange(String id, String errorType, String errorMessage) {
213270
return ChangeResult.builder()
214271
.changeId(id)
@@ -217,4 +274,13 @@ private static ChangeResult failedChange(String id, String errorType, String err
217274
.errorMessage(errorMessage)
218275
.build();
219276
}
277+
278+
private static ChangeResult rolledBackChange(String id, String errorType, String errorMessage) {
279+
return ChangeResult.builder()
280+
.changeId(id)
281+
.status(ChangeStatus.ROLLED_BACK)
282+
.errorType(errorType)
283+
.errorMessage(errorMessage)
284+
.build();
285+
}
220286
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/event/listener/DefaultPipelineCompletedReportListener.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
*/
3333
public final class DefaultPipelineCompletedReportListener implements Consumer<IPipelineCompletedEvent> {
3434

35-
public static final String LOGGER_NAME = "FK-Report";
35+
// Bare component name; FlamingockLoggerFactory.getLogger prepends "FK-" → "FK-Report".
36+
public static final String LOGGER_NAME = "Report";
3637

3738
private static final Logger logger = FlamingockLoggerFactory.getLogger(LOGGER_NAME);
3839

core/flamingock-core/src/main/java/io/flamingock/internal/core/event/listener/DefaultPipelineFailedReportListener.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
*/
3535
public final class DefaultPipelineFailedReportListener implements Consumer<IPipelineFailedEvent> {
3636

37-
public static final String LOGGER_NAME = "FK-Report";
37+
// Bare component name; FlamingockLoggerFactory.getLogger prepends "FK-" → "FK-Report".
38+
public static final String LOGGER_NAME = "Report";
3839

3940
private static final Logger logger = FlamingockLoggerFactory.getLogger(LOGGER_NAME);
4041

core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/PipelineExecuteOperationException.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
package io.flamingock.internal.core.operation;
1717

1818
import io.flamingock.internal.common.core.response.data.ExecuteResponseData;
19-
import io.flamingock.internal.common.core.response.data.ExecutionReportFormatter;
2019

2120
/**
2221
* Thrown when a pipeline-wide error breaks iteration (e.g. {@code LockException}, planner abort,
2322
* unexpected throwable). Always carries the response data assembled so far plus the originating
24-
* cause.
23+
* cause. The rich multi-line execution report is emitted by the default execution-report listener
24+
* under the {@code FK-Report} logger; this exception itself stays a clean one-line signal.
2525
*/
2626
public class PipelineExecuteOperationException extends ExecuteOperationException {
2727

@@ -32,10 +32,4 @@ public PipelineExecuteOperationException(Throwable cause, ExecuteResponseData re
3232
public PipelineExecuteOperationException(String message, Throwable cause, ExecuteResponseData result) {
3333
super(message, cause, result);
3434
}
35-
36-
// Rich multi-line report; getMessage() stays as Throwable's default (cause-derived) for log aggregators.
37-
@Override
38-
public String toString() {
39-
return ExecutionReportFormatter.report(getResult());
40-
}
4135
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/operation/StagedExecuteOperationException.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,14 @@
2525
*
2626
* <p>The {@link #getMessage()} is a single-line, log-aggregator-friendly summary (failed stage
2727
* count + names, change counts, run duration, and the IDs of any change requiring manual
28-
* intervention). The full multi-line per-stage report is available via {@link #toString()} —
29-
* see {@code docs/ERROR_REPORTING_PROPOSAL.md} for why the two intentionally differ.
28+
* intervention). The rich multi-line per-stage report is emitted by the default execution-report
29+
* listener under the {@code FK-Report} logger — see {@code docs/ERROR_REPORTING_PROPOSAL.md}.
30+
* {@link #toString()} is intentionally not overridden; printing the exception (e.g. via
31+
* {@code printStackTrace}) yields the same one-line summary, never the multi-line report.
3032
*/
3133
public class StagedExecuteOperationException extends ExecuteOperationException {
3234

3335
public StagedExecuteOperationException(ExecuteResponseData result) {
3436
super(ExecutionReportFormatter.summary(result), result);
3537
}
36-
37-
// Rich multi-line report; getMessage() stays one-line for log aggregators.
38-
@Override
39-
public String toString() {
40-
return ExecutionReportFormatter.report(getResult());
41-
}
4238
}

core/flamingock-core/src/main/java/io/flamingock/internal/core/pipeline/run/PipelineRun.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public void markStageFailed(String stageName, StageExecutionException exception)
176176
StageRun run = lookupOrThrow(stageName);
177177
run.setResult(StageResult.builder(resultFromExecutor)
178178
.state(StageState.failed(errorInfo))
179-
.build());
179+
.build());
180180
}
181181

182182
/**
@@ -248,7 +248,10 @@ public ExecuteResponseData toResponse() {
248248
appliedChanges++;
249249
} else if (change.getStatus() == ChangeStatus.ALREADY_APPLIED) {
250250
skippedChanges++;
251-
} else if (change.getStatus() == ChangeStatus.FAILED) {
251+
} else if (change.getStatus() == ChangeStatus.FAILED
252+
|| change.getStatus() == ChangeStatus.ROLLED_BACK) {
253+
// ROLLED_BACK counts as a failed attempt for the user-facing aggregate.
254+
// The per-change record retains the precise status for downstream consumers.
252255
failedChanges++;
253256
}
254257
}

core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/StagedExecuteOperationExceptionTest.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,18 @@ void getMessageAppendsManualInterventionChangeIds() {
6262
}
6363

6464
@Test
65-
void toStringReturnsMultiLineReport() {
65+
void toStringIsSingleLineSummaryAndCarriesGetMessage() {
6666
ExecuteResponseData result = failureResult();
6767
StagedExecuteOperationException ex = new StagedExecuteOperationException(result);
6868

6969
String rendered = ex.toString();
70-
assertTrue(rendered.contains(System.lineSeparator()),
71-
"toString must be multi-line: " + rendered);
72-
assertTrue(rendered.contains("Flamingock execution report"), rendered);
73-
assertTrue(rendered.contains("[FAILED]"), rendered);
70+
// Default Throwable.toString() = "ClassName: message" — never the multi-line report,
71+
// which lives in the FK-Report listener and would otherwise duplicate via printStackTrace.
72+
assertTrue(rendered.contains(ex.getMessage()), rendered);
73+
assertFalse(rendered.contains("Flamingock execution report"),
74+
"toString must NOT carry the multi-line report banner: " + rendered);
75+
assertFalse(rendered.contains("[FAILED]"),
76+
"toString must NOT carry per-stage report rows: " + rendered);
7477
}
7578

7679
private static ExecuteResponseData failureResult() {

core/flamingock-core/src/test/java/io/flamingock/internal/core/pipeline/run/PipelineRunToResponseTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,34 @@ void notStartedStagesAreReportedInResponseWithNotStartedState() {
159159
assertTrue(response.getStages().get(1).getState().isNotStarted());
160160
}
161161

162+
@Test
163+
void rolledBackChangeIsCountedAsFailedInAggregate() {
164+
// Mirrors the real-world MongoDB duplicate-key case: a transactional change failed and was
165+
// auto-rolled-back. PipelineRun must count it in failedChanges (not silently drop it).
166+
AbstractLoadedStage stage = mockStage("database-init");
167+
PipelineRun pipelineRun = PipelineRun.of(java.util.Collections.singletonList(stage));
168+
169+
StageResult stageResult = StageResult.builder()
170+
.stageId("database-init").stageName("database-init")
171+
.state(StageState.failed(null))
172+
.addChange(ChangeResult.builder().changeId("c1").status(ChangeStatus.ALREADY_APPLIED).build())
173+
.addChange(ChangeResult.builder().changeId("c2").status(ChangeStatus.ALREADY_APPLIED).build())
174+
.addChange(ChangeResult.builder().changeId("c3").status(ChangeStatus.ROLLED_BACK).build())
175+
.build();
176+
177+
pipelineRun.start();
178+
pipelineRun.markStageFailed("database-init", StageExecutionException.fromResult(
179+
new RuntimeException("boom"), stageResult, "c3"));
180+
pipelineRun.stop();
181+
182+
ExecuteResponseData response = pipelineRun.toResponse();
183+
assertEquals(3, response.getTotalChanges());
184+
assertEquals(0, response.getAppliedChanges());
185+
assertEquals(2, response.getSkippedChanges());
186+
assertEquals(1, response.getFailedChanges(),
187+
"ROLLED_BACK must be counted as failed in the user-facing aggregate");
188+
}
189+
162190
private static AbstractLoadedStage mockStage(String name) {
163191
AbstractLoadedStage stage = mock(AbstractLoadedStage.class);
164192
when(stage.getName()).thenReturn(name);

0 commit comments

Comments
 (0)