markap14 commented on code in PR #11837:
URL: https://github.com/apache/nifi/pull/11837#discussion_r4401948462
##########
nifi-mock/src/main/java/org/apache/nifi/util/MockProcessSession.java:
##########
@@ -1405,21 +1418,64 @@ public ProvenanceReporter getProvenanceReporter() {
@Override
public void setState(final Map<String, String> state, final Scope scope)
throws IOException {
stateManager.setState(state, scope);
+ pendingStatePatches.remove(scope);
}
@Override
public StateMap getState(final Scope scope) throws IOException {
- return stateManager.getState(scope);
+ final StateMap storedState = stateManager.getState(scope);
+ final Map<String, String> patches = pendingStatePatches.get(scope);
+ if (patches == null) {
+ return storedState;
+ }
+
+ final long version =
storedState.getStateVersion().map(Long::parseLong).orElse(-1L);
Review Comment:
[Claude Opus 5.5] Good catch. `MockProcessSession` now counts the patches it
is holding for each scope, and `getState` returns a version equal to the stored
version plus that count, the same way `StandardProcessSession` gives the
session's state a new version on each patch. A `StateMap` obtained before a
patch therefore no longer matches in `replaceState`, even if the patch wrote
the same values.
`TestMockProcessSession.RegardingState.canSetLocalStateWhenDeclaredStateful`
now checks this case.
##########
nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/repository/StandardProcessSessionIT.java:
##########
@@ -2597,12 +2682,14 @@ public void testRollbackAfterCheckpointStoresState()
throws IOException {
session.checkpoint();
session.setState(Collections.singletonMap("abc", "2"), Scope.LOCAL);
+ session.patchState(Map.of("def", "2"), Scope.LOCAL);
Review Comment:
[Claude Opus 5.5] Done. `testRollbackAfterCheckpointStoresState` now patches
`abc`, checkpoints, patches `def`, rolls back, and then commits. It verifies
that `patchState` is called on the state manager with only `{abc=1}`, and that
the stored state is `{abc=1}`, so the patch made after the checkpoint is
discarded and the checkpointed patch is kept.
##########
nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/StateMapSerDe.java:
##########
@@ -28,14 +28,33 @@
import java.util.Map;
import java.util.Optional;
+/**
+ * Serializes component state for the write-ahead log. Full records, which are
used for snapshots, always hold the whole state map.
+ * Starting with version 1, an edit in the journal holds either the whole
state map or only the patches. An edit that holds
+ * only the patches is applied on top of the component's previous record when
the journal is recovered. Version 0 edits always
+ * hold the whole state map, in the same layout as full records.
+ */
public class StateMapSerDe implements SerDe<StateMapUpdate> {
private static final long EMPTY_VERSION = -1;
- private static final int VERSION = 0;
+ private static final int PATCHES_VERSION = 1;
Review Comment:
[Claude Opus 5.5] Agreed that this needs a migration note. I don't have edit
access to the wiki, so here is suggested text for the migration guide. Could
you or someone with access add it?
> Local component state written by this version uses a newer write-ahead log
format (serializer version 1). Older versions of NiFi cannot read it and fail
to start with an error saying the journal was written by a newer serializer
version. To downgrade, restore a backup of the `state/local` directory taken
before the upgrade, or remove that directory, which discards all local
component state.
##########
nifi-mock/src/main/java/org/apache/nifi/util/MockProcessSession.java:
##########
@@ -1405,21 +1418,70 @@ public ProvenanceReporter getProvenanceReporter() {
@Override
public void setState(final Map<String, String> state, final Scope scope)
throws IOException {
stateManager.setState(state, scope);
+ pendingStatePatches.remove(scope);
}
+ /**
+ * Returns the stored state with any patches that have not yet been
committed applied. Each patch gives the returned state a new version,
+ * as the framework does, so that a state obtained before a patch no
longer matches in {@link #replaceState(StateMap, Map, Scope)}.
+ */
@Override
public StateMap getState(final Scope scope) throws IOException {
- return stateManager.getState(scope);
+ final StateMap storedState = stateManager.getState(scope);
+ final PendingStatePatches patches = pendingStatePatches.get(scope);
+ if (patches == null) {
+ return storedState;
+ }
+
+ final long storedVersion =
storedState.getStateVersion().map(Long::parseLong).orElse(-1L);
+ return new MockStateMap(storedState.toMap(patches.values),
storedVersion + patches.count);
}
+ /**
+ * Replaces the state if the given old value matches the session's view of
the state. When the session holds patches that have not yet
+ * been committed, the old value must match the stored state with those
patches applied, and a successful replacement discards the patches.
+ */
@Override
public boolean replaceState(final StateMap oldValue, final Map<String,
String> newValue, final Scope scope) throws IOException {
- return stateManager.replace(oldValue, newValue, scope);
+ if (!pendingStatePatches.containsKey(scope)) {
+ return stateManager.replace(oldValue, newValue, scope);
+ }
+
+ final StateMap currentState = getState(scope);
+ if (oldValue == null ||
!oldValue.getStateVersion().equals(currentState.getStateVersion()) ||
!oldValue.toMap().equals(currentState.toMap())) {
+ return false;
+ }
+
+ final boolean replaced =
stateManager.replace(stateManager.getState(scope), newValue, scope);
Review Comment:
[Claude Opus 5.5] You're right. `replaceState` compared against one read of
the stored state and then passed a second read to `stateManager.replace`, so an
update stored between the two reads would be overwritten. It now reads the
stored state once, applies the pending patches to that copy for the comparison,
and passes the same copy to `stateManager.replace`. If anything else updates
the state in between, the replacement fails. I extended
`canSetLocalStateWhenDeclaredStateful` with a state manager that stores an
update right after the session reads the state. That check failed before this
change and passes now.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]