http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapJsonSerDe.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapJsonSerDe.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapJsonSerDe.java
new file mode 100644
index 0000000..170e7ac
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapJsonSerDe.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.state;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A RedisStateMapSerDe that uses JSON as the underlying representation.
+ */
+public class RedisStateMapJsonSerDe implements RedisStateMapSerDe {
+
+    public static final String FIELD_VERSION = "version";
+    public static final String FIELD_ENCODING = "encodingVersion";
+    public static final String FIELD_STATE_VALUES = "stateValues";
+
+    private final JsonFactory jsonFactory = new JsonFactory(new 
ObjectMapper());
+
+    @Override
+    public byte[] serialize(final RedisStateMap stateMap) throws IOException {
+        if (stateMap == null) {
+            return null;
+        }
+
+        try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            final JsonGenerator jsonGenerator = 
jsonFactory.createGenerator(out);
+            jsonGenerator.writeStartObject();
+            jsonGenerator.writeNumberField(FIELD_VERSION, 
stateMap.getVersion());
+            jsonGenerator.writeNumberField(FIELD_ENCODING, 
stateMap.getEncodingVersion());
+
+            jsonGenerator.writeObjectFieldStart(FIELD_STATE_VALUES);
+            for (Map.Entry<String,String> entry : stateMap.toMap().entrySet()) 
{
+                jsonGenerator.writeStringField(entry.getKey(), 
entry.getValue());
+            }
+            jsonGenerator.writeEndObject();
+
+            jsonGenerator.writeEndObject();
+            jsonGenerator.flush();
+
+            return out.toByteArray();
+        }
+    }
+
+    @Override
+    public RedisStateMap deserialize(final byte[] data) throws IOException {
+        if (data == null || data.length == 0) {
+            return null;
+        }
+
+        final RedisStateMap.Builder builder = new RedisStateMap.Builder();
+
+        try (final JsonParser jsonParser = jsonFactory.createParser(data)) {
+            final JsonNode rootNode = jsonParser.readValueAsTree();
+            builder.version(rootNode.get(FIELD_VERSION).asLong());
+            builder.encodingVersion(rootNode.get(FIELD_ENCODING).asInt());
+
+            final JsonNode stateValuesNode = rootNode.get(FIELD_STATE_VALUES);
+            stateValuesNode.fields().forEachRemaining(e -> 
builder.stateValue(e.getKey(), e.getValue().asText()));
+        }
+
+        return builder.build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapSerDe.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapSerDe.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapSerDe.java
new file mode 100644
index 0000000..ce3067d
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateMapSerDe.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.state;
+
+import java.io.IOException;
+
+/**
+ * Provides serialization/deserialization of a RedisStateMap.
+ */
+public interface RedisStateMapSerDe {
+
+    /**
+     * Serializes the given RedisStateMap.
+     *
+     * @param stateMap the RedisStateMap to serialize
+     * @return the serialized bytes or null if stateMap is null
+     * @throws IOException if an error occurs when serializing
+     */
+    byte[] serialize(RedisStateMap stateMap) throws IOException;
+
+    /**
+     * Deserializes the given bytes to a RedisStateMap.
+     *
+     * @param data bytes previously stored by RedisStateProvider
+     * @return a RedisStateMap or null if data is null or length 0
+     * @throws IOException if an error occurs when deserializing
+     */
+    RedisStateMap deserialize(byte[] data) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..c23bb81
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/state/RedisStateProvider.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.state;
+
+import org.apache.nifi.components.AbstractConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.state.Scope;
+import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.components.state.StateProvider;
+import org.apache.nifi.components.state.StateProviderInitializationContext;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.redis.RedisType;
+import org.apache.nifi.redis.util.RedisAction;
+import org.apache.nifi.redis.util.RedisUtils;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A StateProvider backed by Redis.
+ */
+public class RedisStateProvider extends AbstractConfigurableComponent 
implements StateProvider {
+
+    static final int ENCODING_VERSION = 1;
+
+    public static final PropertyDescriptor KEY_PREFIX = new 
PropertyDescriptor.Builder()
+            .name("Key Prefix")
+            .displayName("Key Prefix")
+            .description("The prefix for each key stored by this state 
provider. When sharing a single Redis across multiple NiFi instances, " +
+                    "setting a unique value for the Key Prefix will make it 
easier to identify which instances the keys came from.")
+            .required(true)
+            .defaultValue("nifi/components/")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .build();
+
+    static final List<PropertyDescriptor> STATE_PROVIDER_PROPERTIES;
+    static {
+        final List<PropertyDescriptor> props = new 
ArrayList<>(RedisUtils.REDIS_CONNECTION_PROPERTY_DESCRIPTORS);
+        props.add(KEY_PREFIX);
+        STATE_PROVIDER_PROPERTIES = Collections.unmodifiableList(props);
+    }
+
+    private String identifier;
+    private String keyPrefix;
+    private ComponentLog logger;
+    private PropertyContext context;
+
+    private volatile boolean enabled;
+    private volatile JedisConnectionFactory connectionFactory;
+
+    private final RedisStateMapSerDe serDe = new RedisStateMapJsonSerDe();
+
+    @Override
+    public final void initialize(final StateProviderInitializationContext 
context) throws IOException {
+        this.context = context;
+        this.identifier = context.getIdentifier();
+        this.logger = context.getLogger();
+
+        String keyPrefix = context.getProperty(KEY_PREFIX).getValue();
+        if (!keyPrefix.endsWith("/")) {
+            keyPrefix = keyPrefix + "/";
+        }
+        this.keyPrefix = keyPrefix;
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return STATE_PROVIDER_PROPERTIES;
+    }
+
+    @Override
+    protected Collection<ValidationResult> customValidate(ValidationContext 
validationContext) {
+        final List<ValidationResult> results = new 
ArrayList<>(RedisUtils.validate(validationContext));
+
+        final RedisType redisType = 
RedisType.fromDisplayName(validationContext.getProperty(RedisUtils.REDIS_MODE).getValue());
+        if (redisType != null && redisType == RedisType.CLUSTER) {
+            results.add(new ValidationResult.Builder()
+                    .subject(RedisUtils.REDIS_MODE.getDisplayName())
+                    .valid(false)
+                    .explanation(RedisUtils.REDIS_MODE.getDisplayName()
+                            + " is configured in clustered mode, and this 
service requires a non-clustered Redis")
+                    .build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public void enable() {
+        enabled = true;
+    }
+
+    @Override
+    public void disable() {
+        enabled = false;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    @Override
+    public void shutdown() {
+        if (connectionFactory != null) {
+            connectionFactory.destroy();
+            connectionFactory = null;
+        }
+    }
+
+    @Override
+    public void setState(final Map<String, String> state, final String 
componentId) throws IOException {
+        verifyEnabled();
+
+        final StateMap currStateMap = getState(componentId);
+
+        int attempted = 0;
+        boolean updated = false;
+
+        while (!updated && attempted < 20) {
+            updated = replace(currStateMap, state, componentId, true);
+            attempted++;
+        }
+
+        if (!updated) {
+            throw new IOException("Unable to update state due to concurrent 
modifications");
+        }
+    }
+
+    @Override
+    public StateMap getState(final String componentId) throws IOException {
+        return withConnection(redisConnection -> {
+            final byte[] key = 
getComponentKey(componentId).getBytes(StandardCharsets.UTF_8);
+            final byte[] value = redisConnection.get(key);
+
+            final RedisStateMap stateMap = serDe.deserialize(value);
+            if (stateMap == null) {
+                return new 
RedisStateMap.Builder().encodingVersion(ENCODING_VERSION).build();
+            } else {
+                return stateMap;
+            }
+        });
+    }
+
+    @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;
+
+            // start a watch on the key and retrieve the current value
+            final byte[] key = 
getComponentKey(componentId).getBytes(StandardCharsets.UTF_8);
+            redisConnection.watch(key);
+
+            final long prevVersion = oldValue == null ? -1L : 
oldValue.getVersion();
+
+            final byte[] currValue = redisConnection.get(key);
+            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();
+
+            // compare-and-set
+            if (prevVersion == currVersion) {
+                // build the new RedisStateMap incrementing the version, using 
latest encoding, and using the passed in values
+                final RedisStateMap newStateMap = new RedisStateMap.Builder()
+                        .version(currVersion + 1)
+                        .encodingVersion(ENCODING_VERSION)
+                        .stateValues(newValue)
+                        .build();
+
+                // if we use set(k, newVal) then the results list will always 
have size == 0 b/c when convertPipelineAndTxResults is set to true,
+                // status responses like "OK" are skipped over, so by using 
getSet we can rely on the results list to know if the transaction succeeded
+                redisConnection.getSet(key, serDe.serialize(newStateMap));
+            }
+
+            // execute the transaction
+            final List<Object> results = redisConnection.exec();
+
+            // if we have a result then the replace succeeded
+            if (results.size() > 0) {
+                replaced = true;
+            }
+
+            return replaced;
+        });
+    }
+
+    @Override
+    public void clear(final String componentId) throws IOException {
+        int attempted = 0;
+        boolean updated = false;
+
+        while (!updated && attempted < 20) {
+            final StateMap currStateMap = getState(componentId);
+            updated = replace(currStateMap, Collections.emptyMap(), 
componentId, true);
+
+            final String result = updated ? "successful" : "unsuccessful";
+            logger.debug("Attempt # {} to clear state for component {} was 
{}", new Object[] { attempted + 1, componentId, result});
+
+            attempted++;
+        }
+
+        if (!updated) {
+            throw new IOException("Unable to update state due to concurrent 
modifications");
+        }
+    }
+
+    @Override
+    public void onComponentRemoved(final String componentId) throws 
IOException {
+        withConnection(redisConnection -> {
+            final byte[] key = 
getComponentKey(componentId).getBytes(StandardCharsets.UTF_8);
+            redisConnection.del(key);
+            return true;
+        });
+    }
+
+    @Override
+    public Scope[] getSupportedScopes() {
+        return new Scope[] {Scope.CLUSTER};
+    }
+
+    private String getComponentKey(final String componentId) {
+        return keyPrefix + componentId;
+    }
+
+    private void verifyEnabled() throws IOException {
+        if (!isEnabled()) {
+            throw new IOException("Cannot update or retrieve cluster state 
because node is no longer connected to a cluster.");
+        }
+    }
+
+    // visible for testing
+    synchronized RedisConnection getRedis() {
+        if (connectionFactory == null) {
+            connectionFactory = RedisUtils.createConnectionFactory(context, 
logger);
+        }
+
+        return connectionFactory.getConnection();
+    }
+
+    private <T> T withConnection(final RedisAction<T> action) throws 
IOException {
+        RedisConnection redisConnection = null;
+        try {
+            redisConnection = getRedis();
+            return action.execute(redisConnection);
+        } finally {
+            if (redisConnection != null) {
+                try {
+                    redisConnection.close();
+                } catch (Exception e) {
+                    logger.warn("Error closing connection: " + e.getMessage(), 
e);
+                }
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisAction.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisAction.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisAction.java
new file mode 100644
index 0000000..ada0c6e
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisAction.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.util;
+
+import org.springframework.data.redis.connection.RedisConnection;
+
+import java.io.IOException;
+
+/**
+ * An action to be executed with a RedisConnection.
+ */
+public interface RedisAction<T> {
+
+    T execute(RedisConnection redisConnection) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java
new file mode 100644
index 0000000..229c438
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/java/org/apache/nifi/redis/util/RedisUtils.java
@@ -0,0 +1,428 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.util;
+
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.context.PropertyContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.redis.RedisType;
+import org.apache.nifi.util.StringUtils;
+import org.springframework.data.redis.connection.RedisClusterConfiguration;
+import org.springframework.data.redis.connection.RedisSentinelConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.JedisShardInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class RedisUtils {
+
+    // These properties are shared between the connection pool controller 
service and the state provider, the name
+    // is purposely set to be more human-readable since that will be 
referenced in state-management.xml
+
+    public static final AllowableValue REDIS_MODE_STANDALONE = new 
AllowableValue(RedisType.STANDALONE.getDisplayName(), 
RedisType.STANDALONE.getDisplayName(), RedisType.STANDALONE.getDescription());
+    public static final AllowableValue REDIS_MODE_SENTINEL = new 
AllowableValue(RedisType.SENTINEL.getDisplayName(), 
RedisType.SENTINEL.getDisplayName(), RedisType.SENTINEL.getDescription());
+    public static final AllowableValue REDIS_MODE_CLUSTER = new 
AllowableValue(RedisType.CLUSTER.getDisplayName(), 
RedisType.CLUSTER.getDisplayName(), RedisType.CLUSTER.getDescription());
+
+    public static final PropertyDescriptor REDIS_MODE = new 
PropertyDescriptor.Builder()
+            .name("Redis Mode")
+            .displayName("Redis Mode")
+            .description("The type of Redis being communicated with - 
standalone, sentinel, or clustered.")
+            .allowableValues(REDIS_MODE_STANDALONE, REDIS_MODE_SENTINEL, 
REDIS_MODE_CLUSTER)
+            .defaultValue(REDIS_MODE_STANDALONE.getValue())
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor CONNECTION_STRING = new 
PropertyDescriptor.Builder()
+            .name("Connection String")
+            .displayName("Connection String")
+            .description("The connection string for Redis. In a standalone 
instance this value will be of the form hostname:port. " +
+                    "In a sentinel instance this value will be the 
comma-separated list of sentinels, such as host1:port1,host2:port2,host3:port3. 
" +
+                    "In a clustered instance this value will be the 
comma-separated list of cluster masters, such as 
host1:port,host2:port,host3:port.")
+            .required(true)
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(true)
+            .build();
+
+    public static final PropertyDescriptor DATABASE = new 
PropertyDescriptor.Builder()
+            .name("Database Index")
+            .displayName("Database Index")
+            .description("The database index to be used by connections created 
from this connection pool. " +
+                    "See the databases property in redis.conf, by default 
databases 0-15 will be available.")
+            .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
+            .defaultValue("0")
+            .expressionLanguageSupported(true)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor COMMUNICATION_TIMEOUT = new 
PropertyDescriptor.Builder()
+            .name("Communication Timeout")
+            .displayName("Communication Timeout")
+            .description("The timeout to use when attempting to communicate 
with Redis.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("10 seconds")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor CLUSTER_MAX_REDIRECTS = new 
PropertyDescriptor.Builder()
+            .name("Cluster Max Redirects")
+            .displayName("Cluster Max Redirects")
+            .description("The maximum number of redirects that can be 
performed when clustered.")
+            .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
+            .defaultValue("5")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor SENTINEL_MASTER = new 
PropertyDescriptor.Builder()
+            .name("Sentinel Master")
+            .displayName("Sentinel Master")
+            .description("The name of the sentinel master, require when Mode 
is set to Sentinel")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(true)
+            .build();
+
+    public static final PropertyDescriptor PASSWORD = new 
PropertyDescriptor.Builder()
+            .name("Password")
+            .displayName("Password")
+            .description("The password used to authenticate to the Redis 
server. See the requirepass property in redis.conf.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(true)
+            .sensitive(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_MAX_TOTAL = new 
PropertyDescriptor.Builder()
+            .name("Pool - Max Total")
+            .displayName("Pool - Max Total")
+            .description("The maximum number of connections that can be 
allocated by the pool (checked out to clients, or idle awaiting checkout). " +
+                    "A negative value indicates that there is no limit.")
+            .addValidator(StandardValidators.INTEGER_VALIDATOR)
+            .defaultValue("8")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_MAX_IDLE = new 
PropertyDescriptor.Builder()
+            .name("Pool - Max Idle")
+            .displayName("Pool - Max Idle")
+            .description("The maximum number of idle connections that can be 
held in the pool, or a negative value if there is no limit.")
+            .addValidator(StandardValidators.INTEGER_VALIDATOR)
+            .defaultValue("8")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_MIN_IDLE = new 
PropertyDescriptor.Builder()
+            .name("Pool - Min Idle")
+            .displayName("Pool - Min Idle")
+            .description("The target for the minimum number of idle 
connections to maintain in the pool. If the configured value of Min Idle is " +
+                    "greater than the configured value for Max Idle, then the 
value of Max Idle will be used instead.")
+            .addValidator(StandardValidators.INTEGER_VALIDATOR)
+            .defaultValue("0")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_BLOCK_WHEN_EXHAUSTED = new 
PropertyDescriptor.Builder()
+            .name("Pool - Block When Exhausted")
+            .displayName("Pool - Block When Exhausted")
+            .description("Whether or not clients should block and wait when 
trying to obtain a connection from the pool when the pool has no available 
connections. " +
+                    "Setting this to false means an error will occur 
immediately when a client requests a connection and none are available.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("true")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_MAX_WAIT_TIME = new 
PropertyDescriptor.Builder()
+            .name("Pool - Max Wait Time")
+            .displayName("Pool - Max Wait Time")
+            .description("The amount of time to wait for an available 
connection when Block When Exhausted is set to true.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("10 seconds")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_MIN_EVICTABLE_IDLE_TIME = new 
PropertyDescriptor.Builder()
+            .name("Pool - Min Evictable Idle Time")
+            .displayName("Pool - Min Evictable Idle Time")
+            .description("The minimum amount of time an object may sit idle in 
the pool before it is eligible for eviction.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("60 seconds")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_TIME_BETWEEN_EVICTION_RUNS = 
new PropertyDescriptor.Builder()
+            .name("Pool - Time Between Eviction Runs")
+            .displayName("Pool - Time Between Eviction Runs")
+            .description("The amount of time between attempting to evict idle 
connections from the pool.")
+            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .defaultValue("30 seconds")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_NUM_TESTS_PER_EVICTION_RUN = 
new PropertyDescriptor.Builder()
+            .name("Pool - Num Tests Per Eviction Run")
+            .displayName("Pool - Num Tests Per Eviction Run")
+            .description("The number of connections to tests per eviction 
attempt. A negative value indicates to test all connections.")
+            .addValidator(StandardValidators.INTEGER_VALIDATOR)
+            .defaultValue("-1")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_TEST_ON_CREATE = new 
PropertyDescriptor.Builder()
+            .name("Pool - Test On Create")
+            .displayName("Pool - Test On Create")
+            .description("Whether or not connections should be tested upon 
creation.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("false")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_TEST_ON_BORROW = new 
PropertyDescriptor.Builder()
+            .name("Pool - Test On Borrow")
+            .displayName("Pool - Test On Borrow")
+            .description("Whether or not connections should be tested upon 
borrowing from the pool.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("false")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_TEST_ON_RETURN = new 
PropertyDescriptor.Builder()
+            .name("Pool - Test On Return")
+            .displayName("Pool - Test On Return")
+            .description("Whether or not connections should be tested upon 
returning to the pool.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("false")
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor POOL_TEST_WHILE_IDLE = new 
PropertyDescriptor.Builder()
+            .name("Pool - Test While Idle")
+            .displayName("Pool - Test While Idle")
+            .description("Whether or not connections should be tested while 
idle.")
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .allowableValues("true", "false")
+            .defaultValue("true")
+            .required(true)
+            .build();
+
+    public static final List<PropertyDescriptor> 
REDIS_CONNECTION_PROPERTY_DESCRIPTORS;
+    static {
+        final List<PropertyDescriptor> props = new ArrayList<>();
+        props.add(RedisUtils.REDIS_MODE);
+        props.add(RedisUtils.CONNECTION_STRING);
+        props.add(RedisUtils.DATABASE);
+        props.add(RedisUtils.COMMUNICATION_TIMEOUT);
+        props.add(RedisUtils.CLUSTER_MAX_REDIRECTS);
+        props.add(RedisUtils.SENTINEL_MASTER);
+        props.add(RedisUtils.PASSWORD);
+        props.add(RedisUtils.POOL_MAX_TOTAL);
+        props.add(RedisUtils.POOL_MAX_IDLE);
+        props.add(RedisUtils.POOL_MIN_IDLE);
+        props.add(RedisUtils.POOL_BLOCK_WHEN_EXHAUSTED);
+        props.add(RedisUtils.POOL_MAX_WAIT_TIME);
+        props.add(RedisUtils.POOL_MIN_EVICTABLE_IDLE_TIME);
+        props.add(RedisUtils.POOL_TIME_BETWEEN_EVICTION_RUNS);
+        props.add(RedisUtils.POOL_NUM_TESTS_PER_EVICTION_RUN);
+        props.add(RedisUtils.POOL_TEST_ON_CREATE);
+        props.add(RedisUtils.POOL_TEST_ON_BORROW);
+        props.add(RedisUtils.POOL_TEST_ON_RETURN);
+        props.add(RedisUtils.POOL_TEST_WHILE_IDLE);
+        REDIS_CONNECTION_PROPERTY_DESCRIPTORS = 
Collections.unmodifiableList(props);
+    }
+
+
+    public static JedisConnectionFactory createConnectionFactory(final 
PropertyContext context, final ComponentLog logger) {
+        final String redisMode = 
context.getProperty(RedisUtils.REDIS_MODE).getValue();
+        final String connectionString = 
context.getProperty(RedisUtils.CONNECTION_STRING).evaluateAttributeExpressions().getValue();
+        final Integer dbIndex = 
context.getProperty(RedisUtils.DATABASE).evaluateAttributeExpressions().asInteger();
+        final String password = 
context.getProperty(RedisUtils.PASSWORD).evaluateAttributeExpressions().getValue();
+        final Integer timeout = 
context.getProperty(RedisUtils.COMMUNICATION_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS).intValue();
+        final JedisPoolConfig poolConfig = createJedisPoolConfig(context);
+
+        JedisConnectionFactory connectionFactory;
+
+        if (RedisUtils.REDIS_MODE_STANDALONE.getValue().equals(redisMode)) {
+            final JedisShardInfo jedisShardInfo = 
createJedisShardInfo(connectionString, timeout, password);
+
+            logger.info("Connecting to Redis in standalone mode at " + 
connectionString);
+            connectionFactory = new JedisConnectionFactory(jedisShardInfo);
+
+        } else if 
(RedisUtils.REDIS_MODE_SENTINEL.getValue().equals(redisMode)) {
+            final String[] sentinels = connectionString.split("[,]");
+            final String sentinelMaster = 
context.getProperty(RedisUtils.SENTINEL_MASTER).evaluateAttributeExpressions().getValue();
+            final RedisSentinelConfiguration sentinelConfiguration = new 
RedisSentinelConfiguration(sentinelMaster, new 
HashSet<>(getTrimmedValues(sentinels)));
+            final JedisShardInfo jedisShardInfo = 
createJedisShardInfo(sentinels[0], timeout, password);
+
+            logger.info("Connecting to Redis in sentinel mode...");
+            logger.info("Redis master = " + sentinelMaster);
+
+            for (final String sentinel : sentinels) {
+                logger.info("Redis sentinel at " + sentinel);
+            }
+
+            connectionFactory = new 
JedisConnectionFactory(sentinelConfiguration, poolConfig);
+            connectionFactory.setShardInfo(jedisShardInfo);
+
+        } else {
+            final String[] clusterNodes = connectionString.split("[,]");
+            final Integer maxRedirects = 
context.getProperty(RedisUtils.CLUSTER_MAX_REDIRECTS).asInteger();
+
+            final RedisClusterConfiguration clusterConfiguration = new 
RedisClusterConfiguration(getTrimmedValues(clusterNodes));
+            clusterConfiguration.setMaxRedirects(maxRedirects);
+
+            logger.info("Connecting to Redis in clustered mode...");
+            for (final String clusterNode : clusterNodes) {
+                logger.info("Redis cluster node at " + clusterNode);
+            }
+
+            connectionFactory = new 
JedisConnectionFactory(clusterConfiguration, poolConfig);
+        }
+
+        connectionFactory.setUsePool(true);
+        connectionFactory.setPoolConfig(poolConfig);
+        connectionFactory.setDatabase(dbIndex);
+        connectionFactory.setTimeout(timeout);
+
+        if (!StringUtils.isBlank(password)) {
+            connectionFactory.setPassword(password);
+        }
+
+        // need to call this to initialize the pool/connections
+        connectionFactory.afterPropertiesSet();
+        return connectionFactory;
+    }
+
+    private static List<String> getTrimmedValues(final String[] values) {
+        final List<String> trimmedValues = new ArrayList<>();
+        for (final String value : values) {
+            trimmedValues.add(value.trim());
+        }
+        return trimmedValues;
+    }
+
+    private static JedisShardInfo createJedisShardInfo(final String 
hostAndPort, final Integer timeout, final String password) {
+        final String[] hostAndPortSplit = hostAndPort.split("[:]");
+        final String host = hostAndPortSplit[0].trim();
+        final Integer port = Integer.parseInt(hostAndPortSplit[1].trim());
+
+        final JedisShardInfo jedisShardInfo = new JedisShardInfo(host, port);
+        jedisShardInfo.setConnectionTimeout(timeout);
+        jedisShardInfo.setSoTimeout(timeout);
+
+        if (!StringUtils.isEmpty(password)) {
+            jedisShardInfo.setPassword(password);
+        }
+
+        return jedisShardInfo;
+    }
+
+    private static JedisPoolConfig createJedisPoolConfig(final PropertyContext 
context) {
+        final JedisPoolConfig poolConfig = new JedisPoolConfig();
+        
poolConfig.setMaxTotal(context.getProperty(RedisUtils.POOL_MAX_TOTAL).asInteger());
+        
poolConfig.setMaxIdle(context.getProperty(RedisUtils.POOL_MAX_IDLE).asInteger());
+        
poolConfig.setMinIdle(context.getProperty(RedisUtils.POOL_MIN_IDLE).asInteger());
+        
poolConfig.setBlockWhenExhausted(context.getProperty(RedisUtils.POOL_BLOCK_WHEN_EXHAUSTED).asBoolean());
+        
poolConfig.setMaxWaitMillis(context.getProperty(RedisUtils.POOL_MAX_WAIT_TIME).asTimePeriod(TimeUnit.MILLISECONDS));
+        
poolConfig.setMinEvictableIdleTimeMillis(context.getProperty(RedisUtils.POOL_MIN_EVICTABLE_IDLE_TIME).asTimePeriod(TimeUnit.MILLISECONDS));
+        
poolConfig.setTimeBetweenEvictionRunsMillis(context.getProperty(RedisUtils.POOL_TIME_BETWEEN_EVICTION_RUNS).asTimePeriod(TimeUnit.MILLISECONDS));
+        
poolConfig.setNumTestsPerEvictionRun(context.getProperty(RedisUtils.POOL_NUM_TESTS_PER_EVICTION_RUN).asInteger());
+        
poolConfig.setTestOnCreate(context.getProperty(RedisUtils.POOL_TEST_ON_CREATE).asBoolean());
+        
poolConfig.setTestOnBorrow(context.getProperty(RedisUtils.POOL_TEST_ON_BORROW).asBoolean());
+        
poolConfig.setTestOnReturn(context.getProperty(RedisUtils.POOL_TEST_ON_RETURN).asBoolean());
+        
poolConfig.setTestWhileIdle(context.getProperty(RedisUtils.POOL_TEST_WHILE_IDLE).asBoolean());
+        return poolConfig;
+    }
+
+    public static List<ValidationResult> validate(ValidationContext 
validationContext) {
+        final List<ValidationResult> results = new ArrayList<>();
+
+        final String redisMode = 
validationContext.getProperty(RedisUtils.REDIS_MODE).getValue();
+        final String connectionString = 
validationContext.getProperty(RedisUtils.CONNECTION_STRING).evaluateAttributeExpressions().getValue();
+        final Integer dbIndex = 
validationContext.getProperty(RedisUtils.DATABASE).evaluateAttributeExpressions().asInteger();
+
+        if (StringUtils.isBlank(connectionString)) {
+            results.add(new ValidationResult.Builder()
+                    .subject(RedisUtils.CONNECTION_STRING.getDisplayName())
+                    .valid(false)
+                    .explanation("Connection String cannot be blank")
+                    .build());
+        } else if 
(RedisUtils.REDIS_MODE_STANDALONE.getValue().equals(redisMode)) {
+            final String[] hostAndPort = connectionString.split("[:]");
+            if (hostAndPort == null || hostAndPort.length != 2 || 
StringUtils.isBlank(hostAndPort[0]) || StringUtils.isBlank(hostAndPort[1]) || 
!isInteger(hostAndPort[1])) {
+                results.add(new ValidationResult.Builder()
+                        .subject(RedisUtils.CONNECTION_STRING.getDisplayName())
+                        .input(connectionString)
+                        .valid(false)
+                        .explanation("Standalone Connection String must be in 
the form host:port")
+                        .build());
+            }
+        } else {
+            for (final String connection : connectionString.split("[,]")) {
+                final String[] hostAndPort = connection.split("[:]");
+                if (hostAndPort == null || hostAndPort.length != 2 || 
StringUtils.isBlank(hostAndPort[0]) || StringUtils.isBlank(hostAndPort[1]) || 
!isInteger(hostAndPort[1])) {
+                    results.add(new ValidationResult.Builder()
+                            
.subject(RedisUtils.CONNECTION_STRING.getDisplayName())
+                            .input(connection)
+                            .valid(false)
+                            .explanation("Connection String must be in the 
form host:port,host:port,host:port,etc.")
+                            .build());
+                }
+            }
+        }
+
+        if (RedisUtils.REDIS_MODE_CLUSTER.getValue().equals(redisMode) && 
dbIndex > 0) {
+            results.add(new ValidationResult.Builder()
+                    .subject(RedisUtils.DATABASE.getDisplayName())
+                    .valid(false)
+                    .explanation("Database Index must be 0 when using 
clustered Redis")
+                    .build());
+        }
+
+        if (RedisUtils.REDIS_MODE_SENTINEL.getValue().equals(redisMode)) {
+            final String sentinelMaster = 
validationContext.getProperty(RedisUtils.SENTINEL_MASTER).evaluateAttributeExpressions().getValue();
+            if (StringUtils.isEmpty(sentinelMaster)) {
+                results.add(new ValidationResult.Builder()
+                        .subject(RedisUtils.SENTINEL_MASTER.getDisplayName())
+                        .valid(false)
+                        .explanation("Sentinel Master must be provided when 
Mode is Sentinel")
+                        .build());
+            }
+        }
+
+        return results;
+    }
+
+    private static boolean isInteger(final String number) {
+        try {
+            Integer.parseInt(number);
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.state.StateProvider
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.state.StateProvider
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.state.StateProvider
new file mode 100644
index 0000000..c7445ab
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.state.StateProvider
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+org.apache.nifi.redis.state.RedisStateProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 0000000..5d4073f
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+org.apache.nifi.redis.service.RedisConnectionPoolService
+org.apache.nifi.redis.service.RedisDistributedMapCacheClientService
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/FakeRedisProcessor.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/FakeRedisProcessor.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/FakeRedisProcessor.java
new file mode 100644
index 0000000..7057c90
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/FakeRedisProcessor.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.service;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.redis.RedisConnectionPool;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Fake processor used for testing RedisConnectionPoolService.
+ */
+public class FakeRedisProcessor extends AbstractProcessor {
+
+    public static final PropertyDescriptor REDIS_SERVICE = new 
PropertyDescriptor.Builder()
+            .name("redis-service")
+            .displayName("Redis Service")
+            .identifiesControllerService(RedisConnectionPool.class)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .required(true)
+            .build();
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return Collections.singletonList(REDIS_SERVICE);
+    }
+
+    @Override
+    public void onTrigger(ProcessContext context, ProcessSession session) 
throws ProcessException {
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/ITRedisDistributedMapCacheClientService.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/ITRedisDistributedMapCacheClientService.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/ITRedisDistributedMapCacheClientService.java
new file mode 100644
index 0000000..9d43e67
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/ITRedisDistributedMapCacheClientService.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.service;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.distributed.cache.client.AtomicCacheEntry;
+import 
org.apache.nifi.distributed.cache.client.AtomicDistributedMapCacheClient;
+import org.apache.nifi.distributed.cache.client.Deserializer;
+import org.apache.nifi.distributed.cache.client.Serializer;
+import 
org.apache.nifi.distributed.cache.client.exception.DeserializationException;
+import 
org.apache.nifi.distributed.cache.client.exception.SerializationException;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.redis.util.RedisUtils;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import redis.embedded.RedisServer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.StandardSocketOptions;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This is an integration test that is meant to be run against a real Redis 
instance.
+ */
+public class ITRedisDistributedMapCacheClientService {
+
+    private TestRedisProcessor proc;
+    private TestRunner testRunner;
+    private RedisServer redisServer;
+    private RedisConnectionPoolService redisConnectionPool;
+    private RedisDistributedMapCacheClientService redisMapCacheClientService;
+    private int redisPort;
+
+    @Before
+    public void setup() throws IOException {
+        this.redisPort = getAvailablePort();
+
+        this.redisServer = new RedisServer(redisPort);
+        redisServer.start();
+
+        proc = new TestRedisProcessor();
+        testRunner = TestRunners.newTestRunner(proc);
+    }
+
+    private int getAvailablePort() throws IOException {
+        try (SocketChannel socket = SocketChannel.open()) {
+            socket.setOption(StandardSocketOptions.SO_REUSEADDR, true);
+            socket.bind(new InetSocketAddress("localhost", 0));
+            return socket.socket().getLocalPort();
+        }
+    }
+
+    @After
+    public void teardown() throws IOException {
+        if (redisServer != null) {
+            redisServer.stop();
+        }
+    }
+
+    @Test
+    public void testStandaloneRedis() throws InitializationException {
+        try {
+            // create, configure, and enable the RedisConnectionPool service
+            redisConnectionPool = new RedisConnectionPoolService();
+            testRunner.addControllerService("redis-connection-pool", 
redisConnectionPool);
+            testRunner.setProperty(redisConnectionPool, 
RedisUtils.CONNECTION_STRING, "localhost:" + redisPort);
+
+            // uncomment this to test using a different database index than 
the default 0
+            //testRunner.setProperty(redisConnectionPool, RedisUtils.DATABASE, 
"1");
+
+            // uncomment this to test using a password to authenticate to redis
+            //testRunner.setProperty(redisConnectionPool, RedisUtils.PASSWORD, 
"foobared");
+
+            testRunner.enableControllerService(redisConnectionPool);
+
+            setupRedisMapCacheClientService();
+            executeProcessor();
+        } finally {
+            if (redisConnectionPool != null) {
+                redisConnectionPool.onDisabled();
+            }
+        }
+    }
+
+    private void setupRedisMapCacheClientService() throws 
InitializationException {
+        // create, configure, and enable the RedisDistributedMapCacheClient 
service
+        redisMapCacheClientService = new 
RedisDistributedMapCacheClientService();
+        testRunner.addControllerService("redis-map-cache-client", 
redisMapCacheClientService);
+        testRunner.setProperty(redisMapCacheClientService, 
RedisDistributedMapCacheClientService.REDIS_CONNECTION_POOL, 
"redis-connection-pool");
+        testRunner.enableControllerService(redisMapCacheClientService);
+        testRunner.setProperty(TestRedisProcessor.REDIS_MAP_CACHE, 
"redis-map-cache-client");
+    }
+
+    private void executeProcessor() {
+        // queue a flow file to trigger the processor and executeProcessor it
+        testRunner.enqueue("trigger");
+        testRunner.run();
+        
testRunner.assertAllFlowFilesTransferred(TestRedisProcessor.REL_SUCCESS, 1);
+    }
+
+    /**
+     * Test processor that exercises RedisDistributedMapCacheClient.
+     */
+    private static class TestRedisProcessor extends AbstractProcessor {
+
+        public static final PropertyDescriptor REDIS_MAP_CACHE = new 
PropertyDescriptor.Builder()
+                .name("redis-map-cache")
+                .displayName("Redis Map Cache")
+                
.identifiesControllerService(AtomicDistributedMapCacheClient.class)
+                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .required(true)
+                .build();
+
+        public static final Relationship REL_SUCCESS = new 
Relationship.Builder().name("success").build();
+        public static final Relationship REL_FAILURE = new 
Relationship.Builder().name("failure").build();
+
+        @Override
+        protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+            return Collections.singletonList(REDIS_MAP_CACHE);
+        }
+
+        @Override
+        public Set<Relationship> getRelationships() {
+            return new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE));
+        }
+
+        @Override
+        public void onTrigger(final ProcessContext context, final 
ProcessSession session) throws ProcessException {
+            final FlowFile flowFile = session.get();
+            if (flowFile == null) {
+                return;
+            }
+
+            final ByteArrayOutputStream out = new ByteArrayOutputStream();
+            final Serializer<String> stringSerializer = new StringSerializer();
+            final Deserializer<String> stringDeserializer = new 
StringDeserializer();
+
+            final AtomicDistributedMapCacheClient cacheClient = 
context.getProperty(REDIS_MAP_CACHE).asControllerService(AtomicDistributedMapCacheClient.class);
+
+            try {
+                final long timestamp = System.currentTimeMillis();
+                final String key = "test-redis-processor-" + timestamp;
+                final String value = "the time is " + timestamp;
+
+                // verify the key doesn't exists, put the key/value, then 
verify it exists
+                Assert.assertFalse(cacheClient.containsKey(key, 
stringSerializer));
+                cacheClient.put(key, value, stringSerializer, 
stringSerializer);
+                Assert.assertTrue(cacheClient.containsKey(key, 
stringSerializer));
+
+                // verify get returns the expected value we set above
+                final String retrievedValue = cacheClient.get(key, 
stringSerializer, stringDeserializer);
+                Assert.assertEquals(value, retrievedValue);
+
+                // verify remove removes the entry and contains key returns 
false after
+                Assert.assertTrue(cacheClient.remove(key, stringSerializer));
+                Assert.assertFalse(cacheClient.containsKey(key, 
stringSerializer));
+
+                // verify putIfAbsent works the first time and returns false 
the second time
+                Assert.assertTrue(cacheClient.putIfAbsent(key, value, 
stringSerializer, stringSerializer));
+                Assert.assertFalse(cacheClient.putIfAbsent(key, "some other 
value", stringSerializer, stringSerializer));
+                Assert.assertEquals(value, cacheClient.get(key, 
stringSerializer, stringDeserializer));
+
+                // verify that getAndPutIfAbsent returns the existing value 
and doesn't modify it in the cache
+                final String getAndPutIfAbsentResult = 
cacheClient.getAndPutIfAbsent(key, value, stringSerializer, stringSerializer, 
stringDeserializer);
+                Assert.assertEquals(value, getAndPutIfAbsentResult);
+                Assert.assertEquals(value, cacheClient.get(key, 
stringSerializer, stringDeserializer));
+
+                // verify that getAndPutIfAbsent on a key that doesn't exist 
returns null
+                final String keyThatDoesntExist = key + "_DOES_NOT_EXIST";
+                Assert.assertFalse(cacheClient.containsKey(keyThatDoesntExist, 
stringSerializer));
+                final String getAndPutIfAbsentResultWhenDoesntExist = 
cacheClient.getAndPutIfAbsent(keyThatDoesntExist, value, stringSerializer, 
stringSerializer, stringDeserializer);
+                Assert.assertEquals(null, 
getAndPutIfAbsentResultWhenDoesntExist);
+                Assert.assertEquals(value, cacheClient.get(keyThatDoesntExist, 
stringSerializer, stringDeserializer));
+
+                // verify atomic fetch returns the correct entry
+                final AtomicCacheEntry<String,String,byte[]> entry = 
cacheClient.fetch(key, stringSerializer, stringDeserializer);
+                Assert.assertEquals(key, entry.getKey());
+                Assert.assertEquals(value, entry.getValue());
+                
Assert.assertTrue(Arrays.equals(value.getBytes(StandardCharsets.UTF_8), 
entry.getRevision().orElse(null)));
+
+                final AtomicCacheEntry<String,String,byte[]> notLatestEntry = 
new AtomicCacheEntry<>(entry.getKey(), entry.getValue(), "not 
previous".getBytes(StandardCharsets.UTF_8));
+
+                // verify atomic replace does not replace when previous value 
is not equal
+                Assert.assertFalse(cacheClient.replace(notLatestEntry, 
stringSerializer, stringSerializer));
+                Assert.assertEquals(value, cacheClient.get(key, 
stringSerializer, stringDeserializer));
+
+                // verify atomic replace does replace when previous value is 
equal
+                final String replacementValue = "this value has been replaced";
+                entry.setValue(replacementValue);
+                Assert.assertTrue(cacheClient.replace(entry, stringSerializer, 
stringSerializer));
+                Assert.assertEquals(replacementValue, cacheClient.get(key, 
stringSerializer, stringDeserializer));
+
+                // verify atomic replace does replace no value previous existed
+                final String replaceKeyDoesntExist = key + 
"_REPLACE_DOES_NOT_EXIST";
+                final AtomicCacheEntry<String,String,byte[]> entryDoesNotExist 
= new AtomicCacheEntry<>(replaceKeyDoesntExist, replacementValue, null);
+                Assert.assertTrue(cacheClient.replace(entryDoesNotExist, 
stringSerializer, stringSerializer));
+                Assert.assertEquals(replacementValue, 
cacheClient.get(replaceKeyDoesntExist, stringSerializer, stringDeserializer));
+
+                final int numToDelete = 2000;
+                for (int i=0; i < numToDelete; i++) {
+                    cacheClient.put(key + "-" + i, value, stringSerializer, 
stringSerializer);
+                }
+
+                
Assert.assertTrue(cacheClient.removeByPattern("test-redis-processor-*") >= 
numToDelete);
+                Assert.assertFalse(cacheClient.containsKey(key, 
stringSerializer));
+
+                session.transfer(flowFile, REL_SUCCESS);
+            } catch (final Exception e) {
+                getLogger().error("Routing to failure due to: " + 
e.getMessage(), e);
+                session.transfer(flowFile, REL_FAILURE);
+            }
+        }
+    }
+
+    private static class StringSerializer implements Serializer<String> {
+        @Override
+        public void serialize(String value, OutputStream output) throws 
SerializationException, IOException {
+            if (value != null) {
+                output.write(value.getBytes(StandardCharsets.UTF_8));
+            }
+        }
+    }
+
+    private static class StringDeserializer implements Deserializer<String> {
+        @Override
+        public String deserialize(byte[] input) throws 
DeserializationException, IOException {
+            return input == null ? null : new String(input, 
StandardCharsets.UTF_8);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java
new file mode 100644
index 0000000..2017c3b
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/service/TestRedisConnectionPoolService.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.service;
+
+import org.apache.nifi.redis.RedisConnectionPool;
+import org.apache.nifi.redis.util.RedisUtils;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestRedisConnectionPoolService {
+
+    private TestRunner testRunner;
+    private FakeRedisProcessor proc;
+    private RedisConnectionPool redisService;
+
+    @Before
+    public void setup() throws InitializationException {
+        proc = new FakeRedisProcessor();
+        testRunner = TestRunners.newTestRunner(proc);
+
+        redisService = new RedisConnectionPoolService();
+        testRunner.addControllerService("redis-service", redisService);
+    }
+
+    @Test
+    public void testValidateConnectionString() {
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, " 
");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"${redis.connection}");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setVariable("redis.connection", "localhost:6379");
+        testRunner.assertValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:a");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:6379");
+        testRunner.assertValid(redisService);
+
+        // standalone can only have one host:port pair
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:6379,localhost:6378");
+        testRunner.assertNotValid(redisService);
+
+        // cluster can have multiple host:port pairs
+        testRunner.setProperty(redisService, RedisUtils.REDIS_MODE, 
RedisUtils.REDIS_MODE_CLUSTER.getValue());
+        testRunner.assertValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:6379,localhost");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"local:host:6379,localhost:6378");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:a,localhost:b");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost  :6379,  localhost  :6378,    localhost:6377");
+        testRunner.assertValid(redisService);
+    }
+
+    @Test
+    public void testValidateSentinelMasterRequiredInSentinelMode() {
+        testRunner.setProperty(redisService, RedisUtils.REDIS_MODE, 
RedisUtils.REDIS_MODE_SENTINEL.getValue());
+        testRunner.setProperty(redisService, RedisUtils.CONNECTION_STRING, 
"localhost:6379,localhost:6378");
+        testRunner.assertNotValid(redisService);
+
+        testRunner.setProperty(redisService, RedisUtils.SENTINEL_MASTER, 
"mymaster");
+        testRunner.assertValid(redisService);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..6a5fb82
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/ITRedisStateProvider.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.state;
+
+import org.apache.nifi.attribute.expression.language.StandardPropertyValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.state.StateMap;
+import org.apache.nifi.components.state.StateProvider;
+import org.apache.nifi.components.state.StateProviderInitializationContext;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.redis.util.RedisUtils;
+import org.apache.nifi.util.MockComponentLog;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.embedded.RedisServer;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.StandardSocketOptions;
+import java.nio.channels.SocketChannel;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * NOTE: These test cases should be kept in-sync with 
AbstractTestStateProvider which is in the framework
+ * and couldn't be extended here.
+ */
+public class ITRedisStateProvider {
+
+    protected final String componentId = 
"111111111-1111-1111-1111-111111111111";
+
+    private RedisServer redisServer;
+    private RedisStateProvider provider;
+
+    @Before
+    public void setup() throws Exception {
+        final int redisPort = getAvailablePort();
+
+        this.redisServer = new RedisServer(redisPort);
+        redisServer.start();
+
+        final Map<PropertyDescriptor, String> properties = new HashMap<>();
+        properties.put(RedisUtils.CONNECTION_STRING, "localhost:" + redisPort);
+        this.provider = createProvider(properties);
+    }
+
+    @After
+    public void teardown() throws IOException {
+        if (provider != null) {
+            try {
+                provider.clear(componentId);
+            } catch (IOException e) {
+            }
+            provider.disable();
+            provider.shutdown();
+        }
+
+        if (redisServer != null) {
+            redisServer.stop();
+        }
+    }
+
+    public StateProvider getProvider() {
+        return provider;
+    }
+
+    @Test
+    public void testSetAndGet() throws IOException {
+        getProvider().setState(Collections.singletonMap("testSetAndGet", 
"value"), componentId);
+        assertEquals("value", 
getProvider().getState(componentId).get("testSetAndGet"));
+    }
+
+    @Test
+    public void testReplaceSuccessful() throws IOException {
+        final String key = "testReplaceSuccessful";
+        final StateProvider provider = getProvider();
+
+        StateMap map = provider.getState(componentId);
+        assertNotNull(map);
+        assertEquals(-1, map.getVersion());
+
+        assertNotNull(map.toMap());
+        assertTrue(map.toMap().isEmpty());
+        provider.setState(Collections.singletonMap(key, "value1"), 
componentId);
+
+        map = provider.getState(componentId);
+        assertNotNull(map);
+        assertEquals(0, map.getVersion());
+        assertEquals("value1", map.get(key));
+        assertEquals("value1", map.toMap().get(key));
+
+        final Map<String, String> newMap = new HashMap<>(map.toMap());
+        newMap.put(key, "value2");
+        assertTrue(provider.replace(map, newMap, componentId));
+
+        map = provider.getState(componentId);
+        assertEquals("value2", map.get(key));
+        assertEquals(1L, map.getVersion());
+    }
+
+    @Test
+    public void testReplaceWithWrongVersion() throws IOException {
+        final String key = "testReplaceWithWrongVersion";
+        final StateProvider provider = getProvider();
+        provider.setState(Collections.singletonMap(key, "value1"), 
componentId);
+
+        StateMap stateMap = provider.getState(componentId);
+        assertNotNull(stateMap);
+        assertEquals("value1", stateMap.get(key));
+        assertEquals(0, stateMap.getVersion());
+
+        provider.setState(Collections.singletonMap(key, "intermediate value"), 
componentId);
+
+        assertFalse(provider.replace(stateMap, Collections.singletonMap(key, 
"value2"), componentId));
+        stateMap = provider.getState(componentId);
+        assertEquals(key, stateMap.toMap().keySet().iterator().next());
+        assertEquals(1, stateMap.toMap().size());
+        assertEquals("intermediate value", stateMap.get(key));
+        assertEquals(1, stateMap.getVersion());
+    }
+
+
+    @Test
+    public void testToMap() throws IOException {
+        final String key = "testKeySet";
+        final StateProvider provider = getProvider();
+        Map<String, String> map = provider.getState(componentId).toMap();
+        assertNotNull(map);
+        assertTrue(map.isEmpty());
+
+        provider.setState(Collections.singletonMap(key, "value"), componentId);
+        map = provider.getState(componentId).toMap();
+        assertNotNull(map);
+        assertEquals(1, map.size());
+        assertEquals("value", map.get(key));
+
+        provider.setState(Collections.<String, String> emptyMap(), 
componentId);
+
+        final StateMap stateMap = provider.getState(componentId);
+        map = stateMap.toMap();
+        assertNotNull(map);
+        assertTrue(map.isEmpty());
+        assertEquals(1, stateMap.getVersion());
+    }
+
+    @Test
+    public void testClear() throws IOException {
+        final StateProvider provider = getProvider();
+        StateMap stateMap = provider.getState(componentId);
+        assertNotNull(stateMap);
+        assertEquals(-1L, stateMap.getVersion());
+        assertTrue(stateMap.toMap().isEmpty());
+
+        provider.setState(Collections.singletonMap("testClear", "value"), 
componentId);
+
+        stateMap = provider.getState(componentId);
+        assertNotNull(stateMap);
+        assertEquals(0, stateMap.getVersion());
+        assertEquals("value", stateMap.get("testClear"));
+
+        provider.clear(componentId);
+
+        stateMap = provider.getState(componentId);
+        assertNotNull(stateMap);
+        assertEquals(1L, stateMap.getVersion());
+        assertTrue(stateMap.toMap().isEmpty());
+    }
+
+    @Test
+    public void testReplaceWithNonExistingValue() throws Exception {
+        final StateProvider provider = getProvider();
+        StateMap stateMap = provider.getState(componentId);
+        assertNotNull(stateMap);
+
+        final Map<String, String> newValue = new HashMap<>();
+        newValue.put("value", "value");
+
+        final boolean replaced = provider.replace(stateMap, newValue, 
componentId);
+        assertFalse(replaced);
+    }
+
+    @Test
+    public void 
testReplaceWithNonExistingValueAndVersionGreaterThanNegativeOne() throws 
Exception {
+        final StateProvider provider = getProvider();
+        final StateMap stateMap = new StateMap() {
+            @Override
+            public long getVersion() {
+                return 4;
+            }
+
+            @Override
+            public String get(String key) {
+                return null;
+            }
+
+            @Override
+            public Map<String, String> toMap() {
+                return Collections.emptyMap();
+            }
+        };
+
+        final Map<String, String> newValue = new HashMap<>();
+        newValue.put("value", "value");
+
+        final boolean replaced = provider.replace(stateMap, newValue, 
componentId);
+        assertFalse(replaced);
+    }
+
+    @Test
+    public void testOnComponentRemoved() throws IOException, 
InterruptedException {
+        final StateProvider provider = getProvider();
+        final Map<String, String> newValue = new HashMap<>();
+        newValue.put("value", "value");
+
+        provider.setState(newValue, componentId);
+        final StateMap stateMap = provider.getState(componentId);
+        assertEquals(0L, stateMap.getVersion());
+
+        provider.onComponentRemoved(componentId);
+
+        // wait for the background process to complete
+        Thread.sleep(1000L);
+
+        final StateMap stateMapAfterRemoval = provider.getState(componentId);
+
+        // version should be -1 because the state has been removed entirely.
+        assertEquals(-1L, stateMapAfterRemoval.getVersion());
+    }
+
+
+    private void initializeProvider(final RedisStateProvider provider, final 
Map<PropertyDescriptor, String> properties) throws IOException {
+        provider.initialize(new StateProviderInitializationContext() {
+            @Override
+            public String getIdentifier() {
+                return "Unit Test Provider Initialization Context";
+            }
+
+            @Override
+            public Map<PropertyDescriptor, PropertyValue> getProperties() {
+                final Map<PropertyDescriptor, PropertyValue> propValueMap = 
new HashMap<>();
+                for (final Map.Entry<PropertyDescriptor, String> entry : 
properties.entrySet()) {
+                    propValueMap.put(entry.getKey(), new 
StandardPropertyValue(entry.getValue(), null));
+                }
+                return propValueMap;
+            }
+
+            @Override
+            public Map<String,String> getAllProperties() {
+                final Map<String,String> propValueMap = new LinkedHashMap<>();
+                for (final Map.Entry<PropertyDescriptor, String> entry : 
properties.entrySet()) {
+                    propValueMap.put(entry.getKey().getName(), 
entry.getValue());
+                }
+                return propValueMap;
+            }
+
+            @Override
+            public PropertyValue getProperty(final PropertyDescriptor 
property) {
+                String prop = properties.get(property);
+
+                if (prop == null) {
+                    prop = property.getDefaultValue();
+                }
+
+                return new StandardPropertyValue(prop, null);
+            }
+
+            @Override
+            public SSLContext getSSLContext() {
+                return null;
+            }
+
+            @Override
+            public ComponentLog getLogger() {
+                return new MockComponentLog("Unit Test RedisStateProvider", 
provider);
+            }
+        });
+    }
+
+    private RedisStateProvider createProvider(final Map<PropertyDescriptor, 
String> properties) throws Exception {
+        final RedisStateProvider provider = new RedisStateProvider();
+        initializeProvider(provider, properties);
+        provider.enable();
+        return provider;
+    }
+
+    private int getAvailablePort() throws IOException {
+        try (SocketChannel socket = SocketChannel.open()) {
+            socket.setOption(StandardSocketOptions.SO_REUSEADDR, true);
+            socket.bind(new InetSocketAddress("localhost", 0));
+            return socket.socket().getLocalPort();
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/TestRedisStateMapJsonSerDe.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/TestRedisStateMapJsonSerDe.java
 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/TestRedisStateMapJsonSerDe.java
new file mode 100644
index 0000000..f147045
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-extensions/src/test/java/org/apache/nifi/redis/state/TestRedisStateMapJsonSerDe.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.nifi.redis.state;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class TestRedisStateMapJsonSerDe {
+
+    private RedisStateMapSerDe serDe;
+
+    @Before
+    public void setup() {
+        serDe = new RedisStateMapJsonSerDe();
+    }
+
+    @Test
+    public void testSerializeDeserialize() throws IOException {
+        final RedisStateMap stateMap = new RedisStateMap.Builder()
+                .version(2L)
+                .encodingVersion(3)
+                .stateValue("field1", "value1")
+                .stateValue("field2", "value2")
+                .stateValue("field3", "value3")
+                .build();
+
+        final byte[] serialized = serDe.serialize(stateMap);
+        Assert.assertNotNull(serialized);
+
+        final RedisStateMap deserialized = serDe.deserialize(serialized);
+        Assert.assertNotNull(deserialized);
+        Assert.assertEquals(stateMap.getVersion(), deserialized.getVersion());
+        Assert.assertEquals(stateMap.getEncodingVersion(), 
deserialized.getEncodingVersion());
+        Assert.assertEquals(stateMap.toMap(), deserialized.toMap());
+    }
+
+    @Test
+    public void testSerializeWhenNull() throws IOException {
+        Assert.assertNull(serDe.serialize(null));
+    }
+
+    @Test
+    public void testDeserializeWhenNull() throws IOException {
+        Assert.assertNull(serDe.deserialize(null));
+    }
+
+    @Test
+    public void testDefaultSerialization() throws IOException {
+        final RedisStateMap stateMap = new RedisStateMap.Builder().build();
+
+        final byte[] serialized = serDe.serialize(stateMap);
+        Assert.assertNotNull(serialized);
+
+        final RedisStateMap deserialized = serDe.deserialize(serialized);
+        Assert.assertNotNull(deserialized);
+        Assert.assertEquals(RedisStateMap.DEFAULT_VERSION.longValue(), 
stateMap.getVersion());
+        Assert.assertEquals(RedisStateMap.DEFAULT_ENCODING, 
stateMap.getEncodingVersion());
+        Assert.assertNotNull(deserialized.toMap());
+        Assert.assertEquals(0, deserialized.toMap().size());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/aabd4a25/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-nar/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-nar/pom.xml 
b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-nar/pom.xml
new file mode 100644
index 0000000..de1f5d7
--- /dev/null
+++ b/nifi-nar-bundles/nifi-redis-bundle/nifi-redis-nar/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements. See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-redis-bundle</artifactId>
+        <version>1.4.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-redis-nar</artifactId>
+    <version>1.4.0-SNAPSHOT</version>
+    <packaging>nar</packaging>
+    <properties>
+        <maven.javadoc.skip>true</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-redis-extensions</artifactId>
+            <version>1.4.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-redis-service-api-nar</artifactId>
+            <type>nar</type>
+        </dependency>
+    </dependencies>
+
+</project>

Reply via email to