This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch support/nifi-1.x
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/support/nifi-1.x by this push:
new 878d2e025f NIFI-11595 StateProvider.replace() supports creating the
initial state
878d2e025f is described below
commit 878d2e025f2464dad5ff95e2a095ea22036a0f2e
Author: Peter Turcsanyi <[email protected]>
AuthorDate: Sat May 27 14:24:13 2023 -0500
NIFI-11595 StateProvider.replace() supports creating the initial state
- Extracted common logic from setState() and replace() into modifyState()
- Removed redundant code from createNode() because exceptions are handled
on the caller side
- NodeExistsException and InterruptedException are handled in setState()
and replace()
- Also used KeeperException's subclasses instead of KeeperException.code()
This closes #7324
Signed-off-by: David Handermann <[email protected]>
(cherry picked from commit ac80b9d900221fedb59142e4a4802bf4ab2a17f4)
---
.../local/WriteAheadLocalStateProvider.java | 5 -
.../zookeeper/ZooKeeperStateProvider.java | 188 +++++++++------------
.../state/providers/AbstractTestStateProvider.java | 55 +++++-
.../StandardOidcAuthorizedClientRepository.java | 11 +-
...StandardOidcAuthorizedClientRepositoryTest.java | 4 +-
.../nifi/redis/state/RedisStateProvider.java | 14 +-
.../nifi/redis/state/ITRedisStateProvider.java | 55 +++++-
7 files changed, 196 insertions(+), 136 deletions(-)
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
index e341c4cb47..9e7a365bda 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/local/WriteAheadLocalStateProvider.java
@@ -255,11 +255,6 @@ public class WriteAheadLocalStateProvider extends
AbstractStateProvider {
// see above explanation as to why this method is synchronized.
public synchronized boolean replace(final StateMap oldValue, final
Map<String, String> newValue) throws IOException {
- if (stateMap.getVersion() == -1L) {
- // state has never been set so return false
- return false;
- }
-
if (stateMap != oldValue) {
return false;
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/zookeeper/ZooKeeperStateProvider.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/zookeeper/ZooKeeperStateProvider.java
index e4c3be49a8..747aa56ae6 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/zookeeper/ZooKeeperStateProvider.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/state/providers/zookeeper/ZooKeeperStateProvider.java
@@ -36,8 +36,11 @@ import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.KeeperException.BadVersionException;
import org.apache.zookeeper.KeeperException.Code;
+import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.apache.zookeeper.KeeperException.NoNodeException;
+import org.apache.zookeeper.KeeperException.SessionExpiredException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZKUtil;
@@ -72,6 +75,11 @@ import java.util.stream.Collectors;
* consistency across configuration interactions.
*/
public class ZooKeeperStateProvider extends AbstractStateProvider {
+
+ private static final String COMPONENTS_RELATIVE_PATH = "/components";
+
+ private static final String COMPONENTS_PATH_FORMAT = "%s%s/%s";
+
private static final Logger logger =
LoggerFactory.getLogger(ZooKeeperStateProvider.class);
private NiFiProperties nifiProperties;
@@ -293,11 +301,6 @@ public class ZooKeeperStateProvider extends
AbstractStateProvider {
return new Scope[]{Scope.CLUSTER};
}
- @Override
- public void setState(final Map<String, String> state, final String
componentId) throws IOException {
- setState(state, -1, componentId);
- }
-
private byte[] serialize(final Map<String, String> stateValues) throws
IOException {
try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -348,108 +351,104 @@ public class ZooKeeperStateProvider extends
AbstractStateProvider {
}
}
- private void setState(final Map<String, String> stateValues, final int
version, final String componentId) throws IOException {
- try {
- setState(stateValues, version, componentId, true);
- } catch (final NoNodeException nne) {
- // should never happen because we are passing 'true' for
allowNodeCreation
- throw new IOException("Unable to create Node in ZooKeeper to set
state for component with ID " + componentId, nne);
- }
+ /**
+ * Sets the component state to the given stateValues unconditionally..
+ *
+ * @param stateValues the new values to set
+ * @param componentId the ID of the component whose state is being updated
+ *
+ * @throws IOException if unable to communicate with ZooKeeper
+ * @throws StateTooLargeException if the state to be stored exceeds the
maximum size allowed by ZooKeeper (Based on jute.maxbuffer property, after
serialization)
+ */
+ @Override
+ public void setState(final Map<String, String> stateValues, final String
componentId) throws IOException {
+ final StateModifier stateModifier = (keeper, path, data) -> {
+ try {
+ keeper.setData(path, data, -1);
+ } catch (final NoNodeException nne) {
+ try {
+ createNode(path, data, acl);
+ } catch (NodeExistsException nee) {
+ setState(stateValues, componentId);
+ }
+ }
+ return true;
+ };
+
+ modifyState(stateValues, componentId, stateModifier);
}
/**
- * Sets the component state to the given stateValues if and only if the
version is equal to the version currently
- * tracked by ZooKeeper (or if the version is -1, in which case the state
will be updated regardless of the version).
+ * Sets the component state to the given stateValues if and only if
oldState's version is equal to the version currently
+ * tracked by ZooKeeper or if oldState's version is not present (no old
state) and the Zookeeper node does not exist.
*
+ * @param oldState the old state whose version is used to compare against
* @param stateValues the new values to set
- * @param version the expected version of the ZNode
* @param componentId the ID of the component whose state is being updated
- * @param allowNodeCreation if <code>true</code> and the corresponding
ZNode does not exist in ZooKeeper, it will be created; if <code>false</code>
- * and the corresponding node does not exist in ZooKeeper, a
{@link KeeperException.NoNodeException} will be thrown
*
* @throws IOException if unable to communicate with ZooKeeper
- * @throws NoNodeException if the corresponding ZNode does not exist in
ZooKeeper and allowNodeCreation is set to <code>false</code>
* @throws StateTooLargeException if the state to be stored exceeds the
maximum size allowed by ZooKeeper (Based on jute.maxbuffer property, after
serialization)
*/
- private void setState(final Map<String, String> stateValues, final int
version, final String componentId, final boolean allowNodeCreation) throws
IOException, NoNodeException {
+ @Override
+ public boolean replace(final StateMap oldState, final Map<String, String>
stateValues, final String componentId) throws IOException {
+ final Optional<Integer> version =
oldState.getStateVersion().map(Integer::parseInt);
+
+ final StateModifier stateModifier = (keeper, path, data) -> {
+ if (version.isPresent()) {
+ try {
+ keeper.setData(path, data, version.get());
+ } catch (final BadVersionException | NoNodeException e) {
+ return false;
+ }
+ } else {
+ try {
+ createNode(path, data, acl);
+ } catch (final NodeExistsException e) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ return modifyState(stateValues, componentId, stateModifier);
+ }
+
+ private boolean modifyState(final Map<String, String> stateValues, final
String componentId, final StateModifier stateModifier) throws IOException {
verifyEnabled();
try {
+ final ZooKeeper keeper = getZooKeeper();
final String path = getComponentPath(componentId);
final byte[] data = serialize(stateValues);
- final ZooKeeper keeper = getZooKeeper();
validateDataSize(keeper.getClientConfig(), data, componentId,
stateValues.size());
- try {
- keeper.setData(path, data, version);
- } catch (final NoNodeException nne) {
- if (allowNodeCreation) {
- createNode(path, data, componentId, stateValues, acl);
- return;
- } else {
- throw nne;
- }
- }
- } catch (final InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new IOException("Failed to set cluster-wide state in
ZooKeeper for component with ID " + componentId + " due to interruption", e);
- } catch (final NoNodeException nne) {
- throw nne;
- } catch (final KeeperException ke) {
- if (Code.SESSIONEXPIRED == ke.code()) {
- invalidateClient();
- setState(stateValues, version, componentId, allowNodeCreation);
- return;
- }
- if (Code.NODEEXISTS == ke.code()) {
- setState(stateValues, version, componentId, allowNodeCreation);
- return;
- }
- throw new IOException("Failed to set cluster-wide state in
ZooKeeper for component with ID " + componentId, ke);
+ return stateModifier.apply(keeper, path, data);
+ } catch (final InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Failed to set cluster-wide state in
ZooKeeper for component with ID " + componentId + " due to interruption", ie);
+ } catch (final SessionExpiredException see) {
+ invalidateClient();
+ return modifyState(stateValues, componentId, stateModifier);
} catch (final StateTooLargeException stle) {
throw stle;
- } catch (final IOException ioe) {
- throw new IOException("Failed to set cluster-wide state in
ZooKeeper for component with ID " + componentId, ioe);
+ } catch (final Exception e) {
+ throw new IOException("Failed to set cluster-wide state in
ZooKeeper for component with ID " + componentId, e);
}
}
-
- private void createNode(final String path, final byte[] data, final String
componentId, final Map<String, String> stateValues, final List<ACL> acls)
throws IOException, KeeperException {
+ private void createNode(final String path, final byte[] data, final
List<ACL> acls) throws IOException, KeeperException, InterruptedException {
try {
final ZooKeeper zooKeeper = getZooKeeper();
zooKeeper.create(path, data, acls, CreateMode.PERSISTENT);
- } catch (final InterruptedException ie) {
- throw new IOException("Failed to update cluster-wide state due to
interruption", ie);
- } catch (final KeeperException ke) {
- final Code exceptionCode = ke.code();
- if (Code.NONODE == exceptionCode) {
- final String parentPath =
StringUtils.substringBeforeLast(path, "/");
- createNode(parentPath, null, componentId, stateValues,
Ids.OPEN_ACL_UNSAFE);
- createNode(path, data, componentId, stateValues, acls);
- return;
- }
- if (Code.SESSIONEXPIRED == exceptionCode) {
- invalidateClient();
- createNode(path, data, componentId, stateValues, acls);
- return;
- }
-
- // Node already exists. Node must have been created by "someone
else". Just set the data.
- if (Code.NODEEXISTS == exceptionCode) {
- try {
- getZooKeeper().setData(path, data, -1);
- return;
- } catch (final KeeperException ke1) {
- // Node no longer exists -- it was removed by someone
else. Go recreate the node.
- if (ke1.code() == Code.NONODE) {
- createNode(path, data, componentId, stateValues, acls);
- return;
- }
- } catch (final InterruptedException ie) {
- throw new IOException("Failed to update cluster-wide state
due to interruption", ie);
- }
+ } catch (final NoNodeException nne) {
+ final String parentPath = StringUtils.substringBeforeLast(path,
"/");
+ createNode(parentPath, null, Ids.OPEN_ACL_UNSAFE);
+ createNode(path, data, acls);
+ } catch (final NodeExistsException nee) {
+ // Fail only if it is the data/leaf node (not a parent node)
+ if (data != null) {
+ throw nee;
}
- throw ke;
}
}
@@ -484,29 +483,6 @@ public class ZooKeeperStateProvider extends
AbstractStateProvider {
}
}
- @Override
- public boolean replace(final StateMap oldValue, final Map<String, String>
newValue, final String componentId) throws IOException {
- verifyEnabled();
-
- try {
- setState(newValue, (int) oldValue.getVersion(), componentId,
false);
- return true;
- } catch (final NoNodeException nne) {
- return false;
- } catch (final IOException ioe) {
- final Throwable cause = ioe.getCause();
- if (cause instanceof KeeperException) {
- final KeeperException ke = (KeeperException) cause;
- if (Code.BADVERSION == ke.code()) {
- return false;
- }
- }
-
- throw ioe;
- }
- }
-
-
@Override
public void clear(final String componentId) throws IOException {
verifyEnabled();
@@ -529,4 +505,8 @@ public class ZooKeeperStateProvider extends
AbstractStateProvider {
}
}
+
+ private interface StateModifier {
+ boolean apply(ZooKeeper keeper, String path, byte[] data) throws
InterruptedException, KeeperException, IOException;
+ }
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/providers/AbstractTestStateProvider.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/providers/AbstractTestStateProvider.java
index ccff20597c..2010f8e8cf 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/providers/AbstractTestStateProvider.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/providers/AbstractTestStateProvider.java
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
import org.apache.nifi.components.state.StateMap;
import org.apache.nifi.components.state.StateProvider;
@@ -150,15 +151,21 @@ public abstract class AbstractTestStateProvider {
@Test
public void testReplaceWithNonExistingValue() throws Exception {
+ final String key = "testReplaceWithNonExistingValue";
+ final String value = "value";
final StateProvider provider = getProvider();
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
final Map<String, String> newValue = new HashMap<>();
- newValue.put("value", "value");
+ newValue.put(key, value);
final boolean replaced = provider.replace(stateMap, newValue,
componentId);
- assertFalse(replaced);
+ assertTrue(replaced);
+
+ StateMap map = provider.getState(componentId);
+ assertEquals(value, map.get(key));
+ assertTrue(map.getStateVersion().isPresent());
}
@Test
@@ -188,6 +195,50 @@ public abstract class AbstractTestStateProvider {
assertFalse(replaced);
}
+ @Test
+ void testReplaceConcurrentCreate() throws IOException {
+ final StateProvider provider = getProvider();
+
+ final StateMap stateMap1 = provider.getState(componentId);
+ final StateMap stateMap2 = provider.getState(componentId);
+
+ final boolean replaced1 = provider.replace(stateMap1,
Collections.emptyMap(), componentId);
+
+ assertTrue(replaced1);
+
+ final StateMap replacedStateMap = provider.getState(componentId);
+ final Optional<String> replacedVersion =
replacedStateMap.getStateVersion();
+ assertTrue(replacedVersion.isPresent());
+ assertEquals("0", replacedVersion.get());
+
+ final boolean replaced2 = provider.replace(stateMap2,
Collections.emptyMap(), componentId);
+
+ assertFalse(replaced2);
+ }
+
+ @Test
+ void testReplaceConcurrentUpdate() throws IOException {
+ final StateProvider provider = getProvider();
+
+ provider.setState(Collections.singletonMap("key", "0"), componentId);
+
+ final StateMap stateMap1 = provider.getState(componentId);
+ final StateMap stateMap2 = provider.getState(componentId);
+
+ final boolean replaced1 = provider.replace(stateMap1,
Collections.singletonMap("key", "1"), componentId);
+
+ assertTrue(replaced1);
+
+ final StateMap replacedStateMap = provider.getState(componentId);
+ final Optional<String> replacedVersion =
replacedStateMap.getStateVersion();
+ assertTrue(replacedVersion.isPresent());
+ assertEquals("1", replacedVersion.get());
+
+ final boolean replaced2 = provider.replace(stateMap2,
Collections.singletonMap("key", "2"), componentId);
+
+ assertFalse(replaced2);
+ }
+
@Test
public void testOnComponentRemoved() throws IOException,
InterruptedException {
final StateProvider provider = getProvider();
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepository.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepository.java
index 35bea43be2..29c965025c 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepository.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepository.java
@@ -236,17 +236,10 @@ public class StandardOidcAuthorizedClientRepository
implements OAuth2AuthorizedC
private synchronized void updateState(final String principalId, final
Consumer<Map<String, String>> stateConsumer) {
try {
final StateMap stateMap = getStateMap();
- final Map<String, String> currentStateMap = stateMap.toMap();
- final Map<String, String> updated = new
LinkedHashMap<>(currentStateMap);
+ final Map<String, String> updated = new
LinkedHashMap<>(stateMap.toMap());
stateConsumer.accept(updated);
- final boolean completed;
- if (currentStateMap.isEmpty()) {
- stateManager.setState(updated, SCOPE);
- completed = true;
- } else {
- completed = stateManager.replace(stateMap, updated, SCOPE);
- }
+ final boolean completed = stateManager.replace(stateMap, updated,
SCOPE);
if (completed) {
logger.info("Identity [{}] OIDC Authorized Client update
completed", principalId);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepositoryTest.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepositoryTest.java
index 6d9257ffbe..7e05b64362 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepositoryTest.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/StandardOidcAuthorizedClientRepositoryTest.java
@@ -149,7 +149,7 @@ class StandardOidcAuthorizedClientRepositoryTest {
repository.saveAuthorizedClient(authorizedClient, principal, request,
response);
verify(authorizedClientConverter).getEncoded(isA(OidcAuthorizedClient.class));
- verify(stateManager).setState(stateMapCaptor.capture(), eq(SCOPE));
+ verify(stateManager).replace(eq(stateMap), stateMapCaptor.capture(),
eq(SCOPE));
final Map<String, String> updatedStateMap = stateMapCaptor.getValue();
final String encodedClient = updatedStateMap.get(IDENTITY);
@@ -165,7 +165,7 @@ class StandardOidcAuthorizedClientRepositoryTest {
repository.removeAuthorizedClient(REGISTRATION_ID, principal, request,
response);
- verify(stateManager).setState(stateMapCaptor.capture(), eq(SCOPE));
+ verify(stateManager).replace(eq(stateMap), stateMapCaptor.capture(),
eq(SCOPE));
final Map<String, String> updatedStateMap = stateMapCaptor.getValue();
assertTrue(updatedStateMap.isEmpty());
}
diff --git
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
index 07a36a8557..ba71dc488b 100644
---
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
+++
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
@@ -187,7 +187,7 @@ public class RedisStateProvider extends
AbstractConfigurableComponent implements
boolean updated = false;
while (!updated && attempted < this.maxAttempts) {
- updated = replace(currStateMap, state, componentId, true);
+ updated = replace(currStateMap, state, componentId);
attempted++;
}
@@ -213,10 +213,6 @@ public class RedisStateProvider extends
AbstractConfigurableComponent implements
@Override
public boolean replace(final StateMap oldValue, final Map<String, String>
newValue, final String componentId) throws IOException {
- return replace(oldValue, newValue, componentId, false);
- }
-
- private boolean replace(final StateMap oldValue, final Map<String, String>
newValue, final String componentId, final boolean allowReplaceMissing) throws
IOException {
return withConnection(redisConnection -> {
boolean replaced = false;
@@ -231,12 +227,6 @@ public class RedisStateProvider extends
AbstractConfigurableComponent implements
final RedisStateMap currStateMap = serDe.deserialize(currValue);
final long currVersion = currStateMap == null ? -1L :
currStateMap.getVersion();
- // the replace API expects that you can't call replace on a
non-existing value, so unwatch and return
- if (!allowReplaceMissing && currVersion == -1) {
- redisConnection.unwatch();
- return false;
- }
-
// start a transaction
redisConnection.multi();
@@ -274,7 +264,7 @@ public class RedisStateProvider extends
AbstractConfigurableComponent implements
while (!updated && attempted < this.maxAttempts) {
final StateMap currStateMap = getState(componentId);
- updated = replace(currStateMap, Collections.emptyMap(),
componentId, true);
+ updated = replace(currStateMap, Collections.emptyMap(),
componentId);
final String result = updated ? "successful" : "unsuccessful";
logger.debug("Attempt # {} to clear state for component {} was
{}", new Object[] { attempted + 1, componentId, result});
diff --git
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
index 8b28cd4df6..be660e89ed 100644
---
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
+++
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
@@ -39,6 +39,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -187,15 +188,21 @@ public class ITRedisStateProvider {
@Test
public void testReplaceWithNonExistingValue() throws Exception {
+ final String key = "testReplaceWithNonExistingValue";
+ final String value = "value";
final StateProvider provider = getProvider();
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
final Map<String, String> newValue = new HashMap<>();
- newValue.put("value", "value");
+ newValue.put(key, value);
final boolean replaced = provider.replace(stateMap, newValue,
componentId);
- assertFalse(replaced);
+ assertTrue(replaced);
+
+ StateMap map = provider.getState(componentId);
+ assertEquals(value, map.get(key));
+ assertTrue(map.getStateVersion().isPresent());
}
@Test
@@ -225,6 +232,50 @@ public class ITRedisStateProvider {
assertFalse(replaced);
}
+ @Test
+ void testReplaceConcurrentCreate() throws IOException {
+ final StateProvider provider = getProvider();
+
+ final StateMap stateMap1 = provider.getState(componentId);
+ final StateMap stateMap2 = provider.getState(componentId);
+
+ final boolean replaced1 = provider.replace(stateMap1,
Collections.emptyMap(), componentId);
+
+ assertTrue(replaced1);
+
+ final StateMap replacedStateMap = provider.getState(componentId);
+ final Optional<String> replacedVersion =
replacedStateMap.getStateVersion();
+ assertTrue(replacedVersion.isPresent());
+ assertEquals("0", replacedVersion.get());
+
+ final boolean replaced2 = provider.replace(stateMap2,
Collections.emptyMap(), componentId);
+
+ assertFalse(replaced2);
+ }
+
+ @Test
+ void testReplaceConcurrentUpdate() throws IOException {
+ final StateProvider provider = getProvider();
+
+ provider.setState(Collections.singletonMap("key", "0"), componentId);
+
+ final StateMap stateMap1 = provider.getState(componentId);
+ final StateMap stateMap2 = provider.getState(componentId);
+
+ final boolean replaced1 = provider.replace(stateMap1,
Collections.singletonMap("key", "1"), componentId);
+
+ assertTrue(replaced1);
+
+ final StateMap replacedStateMap = provider.getState(componentId);
+ final Optional<String> replacedVersion =
replacedStateMap.getStateVersion();
+ assertTrue(replacedVersion.isPresent());
+ assertEquals("1", replacedVersion.get());
+
+ final boolean replaced2 = provider.replace(stateMap2,
Collections.singletonMap("key", "2"), componentId);
+
+ assertFalse(replaced2);
+ }
+
@Test
public void testOnComponentRemoved() throws IOException,
InterruptedException {
final StateProvider provider = getProvider();