-
Notifications
You must be signed in to change notification settings - Fork 64
feat(core): add validationOnly mode to prevent change execution #880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
01a2c58
feat(core): add validationOnly mode to prevent change execution
bercianor 5f5c51a
Merge branch 'master' into feat/validation-only-flag
dieppa 8b552e4
fix: remove pending change count and rename EXECUTE_VALIDATE to VALIDATE
bercianor 1bdc33d
fix: extends ValidateOperation from ExecuteOperation
bercianor d4fb6cc
fix: check if not CLI mode to change operation from apply to validate
bercianor 376b063
refactor: default operation responsibility and AbstractPipelineTraver…
bercianor eeb6d7a
fix: remove operationProvided, fix a test and an Optional.Get()
bercianor 18b2194
fix: operation name should be without "operation" sufix
bercianor 2e61fe7
fix: some PR comments
bercianor addaae5
fix: change some protected methods to private again
bercianor 8b90243
fix: change VALIDATE to VALIDATE_APPLY
bercianor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
...mmons/src/main/java/io/flamingock/internal/common/core/error/PendingChangesException.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright 2026 Flamingock (https://www.flamingock.io) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.flamingock.internal.common.core.error; | ||
|
|
||
| /** | ||
| * Exception thrown when Flamingock runs in validation-only mode and detects pending changes. | ||
| */ | ||
| public class PendingChangesException extends FlamingockException { | ||
|
|
||
| private final int pendingCount; | ||
|
|
||
| public PendingChangesException(int pendingCount) { | ||
| super("Flamingock validationOnly=true: %d pending change(s) detected. Apply them before running in validation-only mode.", pendingCount); | ||
| this.pendingCount = pendingCount; | ||
| } | ||
|
|
||
| public int getPendingCount() { | ||
| return pendingCount; | ||
| } | ||
| } |
88 changes: 88 additions & 0 deletions
88
...s/src/test/java/io/flamingock/internal/common/core/error/PendingChangesExceptionTest.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Copyright 2026 Flamingock (https://www.flamingock.io) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.flamingock.internal.common.core.error; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| class PendingChangesExceptionTest { | ||
|
|
||
| @Test | ||
| @DisplayName("Should return the pending count passed to the constructor") | ||
| void shouldReturnPendingCount() { | ||
| // Given | ||
| int pendingCount = 5; | ||
|
|
||
| // When | ||
| PendingChangesException exception = new PendingChangesException(pendingCount); | ||
|
|
||
| // Then | ||
| assertEquals(pendingCount, exception.getPendingCount()); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should include the pending count in the exception message") | ||
| void shouldIncludeCountInMessage() { | ||
| // Given | ||
| int pendingCount = 5; | ||
|
|
||
| // When | ||
| PendingChangesException exception = new PendingChangesException(pendingCount); | ||
|
|
||
| // Then | ||
| assertTrue(exception.getMessage().contains("5"), | ||
| "Message should contain the pending count as a string"); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should be an instance of FlamingockException") | ||
| void shouldExtendFlamingockException() { | ||
| // Given / When | ||
| PendingChangesException exception = new PendingChangesException(3); | ||
|
|
||
| // Then | ||
| assertInstanceOf(FlamingockException.class, exception); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should work correctly with zero pending changes") | ||
| void shouldWorkWithZeroPendingCount() { | ||
| // Given / When | ||
| PendingChangesException exception = new PendingChangesException(0); | ||
|
|
||
| // Then | ||
| assertEquals(0, exception.getPendingCount()); | ||
| assertTrue(exception.getMessage().contains("0")); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("Should work correctly with a large pending count") | ||
| void shouldWorkWithLargePendingCount() { | ||
| // Given | ||
| int largeCount = 999; | ||
|
|
||
| // When | ||
| PendingChangesException exception = new PendingChangesException(largeCount); | ||
|
|
||
| // Then | ||
| assertEquals(largeCount, exception.getPendingCount()); | ||
| assertTrue(exception.getMessage().contains("999")); | ||
| } | ||
| } |
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
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
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
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
150 changes: 150 additions & 0 deletions
150
...k-core/src/main/java/io/flamingock/internal/core/operation/execute/ValidateOperation.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| /* | ||
| * Copyright 2026 Flamingock (https://www.flamingock.io) | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package io.flamingock.internal.core.operation.execute; | ||
|
|
||
| import io.flamingock.internal.common.core.error.FlamingockException; | ||
| import io.flamingock.internal.common.core.error.PendingChangesException; | ||
| import io.flamingock.internal.common.core.response.data.ExecuteResponseData; | ||
| import io.flamingock.internal.core.event.EventPublisher; | ||
| import io.flamingock.internal.core.event.model.impl.PipelineCompletedEvent; | ||
| import io.flamingock.internal.core.event.model.impl.PipelineFailedEvent; | ||
| import io.flamingock.internal.core.event.model.impl.PipelineStartedEvent; | ||
| import io.flamingock.internal.core.operation.Operation; | ||
| import io.flamingock.internal.core.operation.result.ExecutionResultBuilder; | ||
| import io.flamingock.internal.core.pipeline.execution.ExecutableStage; | ||
| import io.flamingock.internal.core.pipeline.loaded.LoadedPipeline; | ||
| import io.flamingock.internal.core.pipeline.loaded.stage.AbstractLoadedStage; | ||
| import io.flamingock.internal.core.plan.ExecutionPlan; | ||
| import io.flamingock.internal.core.plan.ExecutionPlanner; | ||
| import io.flamingock.internal.core.external.store.lock.LockException; | ||
| import io.flamingock.internal.core.task.executable.ExecutableTask; | ||
| import io.flamingock.internal.util.id.RunnerId; | ||
| import io.flamingock.internal.util.log.FlamingockLoggerFactory; | ||
| import org.slf4j.Logger; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Validates the pipeline without executing any changes. | ||
| * If pending changes exist, throws {@link PendingChangesException}. | ||
| */ | ||
| public class ValidateOperation implements Operation<ExecuteArgs, ExecuteResult> { | ||
|
|
||
| private static final Logger logger = FlamingockLoggerFactory.getLogger("PipelineRunner"); | ||
|
|
||
| private final RunnerId runnerId; | ||
|
|
||
| private final ExecutionPlanner executionPlanner; | ||
|
|
||
| private final EventPublisher eventPublisher; | ||
|
|
||
| private final boolean throwExceptionIfCannotObtainLock; | ||
|
|
||
| private final Runnable finalizer; | ||
|
|
||
| public ValidateOperation(RunnerId runnerId, | ||
| ExecutionPlanner executionPlanner, | ||
| EventPublisher eventPublisher, | ||
| boolean throwExceptionIfCannotObtainLock, | ||
| Runnable finalizer) { | ||
| this.runnerId = runnerId; | ||
| this.executionPlanner = executionPlanner; | ||
| this.eventPublisher = eventPublisher; | ||
| this.throwExceptionIfCannotObtainLock = throwExceptionIfCannotObtainLock; | ||
| this.finalizer = finalizer; | ||
| } | ||
|
|
||
| @Override | ||
| public ExecuteResult execute(ExecuteArgs args) { | ||
| ExecuteResponseData result; | ||
| try { | ||
| result = this.validate(args.getPipeline()); | ||
| } catch (FlamingockException flamingockException) { | ||
| throw flamingockException; | ||
| } catch (Throwable throwable) { | ||
| throw new FlamingockException(throwable); | ||
| } finally { | ||
| finalizer.run(); | ||
| } | ||
| return new ExecuteResult(result); | ||
| } | ||
|
|
||
| private static List<AbstractLoadedStage> validateAndGetExecutableStages(LoadedPipeline pipeline) { | ||
| pipeline.validate(); | ||
| List<AbstractLoadedStage> stages = new ArrayList<>(); | ||
| if (pipeline.getSystemStage().isPresent()) { | ||
| stages.add(pipeline.getSystemStage().get()); | ||
| } | ||
| stages.addAll(pipeline.getStages()); | ||
| return stages; | ||
| } | ||
|
|
||
| private ExecuteResponseData validate(LoadedPipeline pipeline) throws FlamingockException { | ||
| List<AbstractLoadedStage> allStages = validateAndGetExecutableStages(pipeline); | ||
| int stageCount = allStages.size(); | ||
| long changeCount = allStages.stream() | ||
| .mapToLong(stage -> stage.getTasks().size()) | ||
| .sum(); | ||
| logger.info("Flamingock validation started [stages={} changes={}]", stageCount, changeCount); | ||
|
|
||
| eventPublisher.publish(new PipelineStartedEvent()); | ||
| ExecutionResultBuilder resultBuilder = new ExecutionResultBuilder().startTimer(); | ||
|
|
||
| do { | ||
| List<AbstractLoadedStage> stages = validateAndGetExecutableStages(pipeline); | ||
| try (ExecutionPlan execution = executionPlanner.getNextExecution(stages)) { | ||
| execution.validate(); | ||
|
|
||
| if (execution.isExecutionRequired()) { | ||
| int pendingCount = countPendingTasks(execution); | ||
| throw new PendingChangesException(pendingCount); | ||
| } else { | ||
| break; | ||
| } | ||
| } catch (LockException exception) { | ||
| eventPublisher.publish(new PipelineFailedEvent(exception)); | ||
| if (throwExceptionIfCannotObtainLock) { | ||
| logger.debug("Required process lock not acquired - ABORTING VALIDATION", exception); | ||
| throw exception; | ||
| } else { | ||
| logger.warn("Process lock not acquired but throwExceptionIfCannotObtainLock=false - CONTINUING WITHOUT LOCK", exception); | ||
| } | ||
| break; | ||
| } | ||
| } while (true); | ||
|
|
||
| resultBuilder.stopTimer().noChanges(); | ||
| ExecuteResponseData result = resultBuilder.build(); | ||
|
|
||
| logger.info("Flamingock validation completed — no pending changes detected"); | ||
| eventPublisher.publish(new PipelineCompletedEvent()); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| private static int countPendingTasks(ExecutionPlan execution) { | ||
| int count = 0; | ||
| for (ExecutableStage stage : execution.getPipeline().getExecutableStages()) { | ||
| for (ExecutableTask task : stage.getTasks()) { | ||
| if (!task.isAlreadyApplied()) { | ||
| count++; | ||
| } | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
|
bercianor marked this conversation as resolved.
Outdated
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.