diff --git a/com.ibm.streamsx.dps/impl/Makefile b/com.ibm.streamsx.dps/impl/Makefile index 28e269a..62ed77a 100644 --- a/com.ibm.streamsx.dps/impl/Makefile +++ b/com.ibm.streamsx.dps/impl/Makefile @@ -22,10 +22,11 @@ AERO_OBJ = AERO_LIB = COUCH_OBJ = CouchbaseDBLayer.o REDIS_CLUSTER_OBJ = RedisClusterDBLayer.o - - +REDIS_CLUSTER_PLUS_PLUS_OBJ = RedisClusterPlusPlusDBLayer.o + REDIS_LIB = $(LIB)/libDPSRedis.so REDIS_CLUSTER_LIB = $(LIB)/libDPSRedisCluster.so +REDIS_CLUSTER_PLUS_PLUS_LIB = $(LIB)/libDPSRedisClusterPlusPlus.so COUCH_LIB = $(LIB)/libDPSCouchbase.so HBASE_LIB = $(LIB)/libDPSHBase.so CASSANDRA_LIB = $(LIB)/libDPSCassandra.so @@ -34,6 +35,7 @@ MONGO_LIB = $(LIB)/libDPSMongo.so MEMCACHED_LIB = $(LIB)/libDPSMemcached.so HELPER_LIBS = $(REDIS_CLUSTER_LIB) +HELPER_LIBS += $(REDIS_CLUSTER_PLUS_PLUS_LIB) HELPER_LIBS += $(COUCH_LIB) HELPER_LIBS += $(REDIS_LIB) HELPER_LIBS += $(HBASE_LIB) @@ -58,7 +60,7 @@ SPL_COMPILE_OPTIONS = $(shell $(SPL_PKGCFG) --cflags $(SPL_PKG)) SPL_LINK_OPTIONS = $(shell $(SPL_PKGCFG) --libs $(SPL_PKG)) CPPFLAGS = -CPPFLAGS += -O3 -Wall -c -fmessage-length=0 -fPIC -D_REENTRANT +CPPFLAGS += -O3 -Wall -c -fmessage-length=0 -fPIC -D_REENTRANT CPPFLAGS += -I include CPPFLAGS += -I nl/include CPPFLAGS += -isystem ext/include @@ -91,7 +93,7 @@ all: check-streams $(DISTRIBUTED_PROCESS_STORE_LIB_STATIC) $(DISTRIBUTED_PROCESS check-streams: @echo "Checking to see if STREAMS_INSTALL is set..." test -d $(STREAMS_INSTALL) || false - + $(DISTRIBUTED_PROCESS_STORE_LIB): $(DISTRIBUTED_PROCESS_STORE_OBJS) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ @@ -108,15 +110,20 @@ $(DISTRIBUTED_PROCESS_STORE_LIB_STATIC): $(DISTRIBUTED_PROCESS_STORE_OBJS) $(COUCH_LIB): $(COUCH_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + $(REDIS_LIB): $(REDIS_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + $(REDIS_CLUSTER_LIB): $(REDIS_CLUSTER_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + +$(REDIS_CLUSTER_PLUS_PLUS_LIB): CPPFLAGS+=-std=c++11 +$(REDIS_CLUSTER_PLUS_PLUS_LIB): $(REDIS_CLUSTER_PLUS_PLUS_OBJ) + mkdir -p $(LIB) + $(CXX) $(LDFLAGS) -o $@ $^ + $(HBASE_LIB): $(HBASE_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ @@ -124,11 +131,11 @@ $(HBASE_LIB): $(HBASE_OBJ) $(CASSANDRA_LIB): $(CASSANDRA_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + $(CLOUDANT_LIB): $(CLOUDANT_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + $(MONGO_LIB): $(MONGO_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ @@ -136,12 +143,13 @@ $(MONGO_LIB): $(MONGO_OBJ) $(MEMCACHED_LIB): $(MEMCACHED_OBJ) mkdir -p $(LIB) $(CXX) $(LDFLAGS) -o $@ $^ - + clean: rm -f $(DISTRIBUTED_PROCESS_STORE_OBJS) rm -f $(DISTRIBUTED_PROCESS_STORE_LIB) rm -f $(REDIS_LIB) $(REDIS_OBJ) rm -f $(REDIS_CLUSTER_LIB) $(REDIS_CLUSTER_OBJ) + rm -f $(REDIS_CLUSTER_PLUS_PLUS_LIB) $(REDIS_CLUSTER_PLUS_PLUS_OBJ) rm -f $(MEMCACHED_LIB) $(MEMCACHED_OBJ) rm -f $(CLOUDANT_LIB) $(CLOUDANT_OBJ) rm -f $(CASSANDRA_LIB) $(CASSANDRA_OBJ) diff --git a/com.ibm.streamsx.dps/impl/include/DpsConstants.h b/com.ibm.streamsx.dps/impl/include/DpsConstants.h index 7bdfa67..1117dfe 100644 --- a/com.ibm.streamsx.dps/impl/include/DpsConstants.h +++ b/com.ibm.streamsx.dps/impl/include/DpsConstants.h @@ -1,6 +1,6 @@ /* # Licensed Materials - Property of IBM -# Copyright IBM Corp. 2011, 2016 +# Copyright IBM Corp. 2011, 2020 # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. @@ -93,6 +93,7 @@ interface with many different back-end in-memory stores. #define COUCHBASE_NO_SQL_DB_NAME "couchbase" #define AEROSPIKE_NO_SQL_DB_NAME "aerospike" #define REDIS_CLUSTER_NO_SQL_DB_NAME "redis-cluster" +#define REDIS_CLUSTER_PLUS_PLUS_NO_SQL_DB_NAME "redis-cluster-plus-plus" #define HTTP_GET "GET" #define HTTP_PUT "PUT" #define HTTP_POST "POST" @@ -228,5 +229,9 @@ interface with many different back-end in-memory stores. #define DL_LOCK_NOT_FOUND_ERROR 514 #define DL_LOCK_REMOVAL_ERROR 515 #define AEROSPIKE_GET_STORE_ID_ERROR 516 +#define REDIS_PLUS_PLUS_NO_ERROR 0 +#define REDIS_PLUS_PLUS_CONNECTION_ERROR 1 +#define REDIS_PLUS_PLUS_REPLY_ERROR 2 +#define REDIS_PLUS_PLUS_OTHER_ERROR 3 #endif /* DPS_CONSTANTS_H_ */ diff --git a/com.ibm.streamsx.dps/impl/include/RedisClusterPlusPlusDBLayer.h b/com.ibm.streamsx.dps/impl/include/RedisClusterPlusPlusDBLayer.h new file mode 100644 index 0000000..ee1bf1f --- /dev/null +++ b/com.ibm.streamsx.dps/impl/include/RedisClusterPlusPlusDBLayer.h @@ -0,0 +1,170 @@ +/* +# Licensed Materials - Property of IBM +# Copyright IBM Corp. 2011, 2020 +# US Government Users Restricted Rights - Use, duplication or +# disclosure restricted by GSA ADP Schedule Contract with +# IBM Corp. +*/ +#ifndef REDIS_CLUSTER_PLUS_PLUS_DB_LAYER_H_ +#define REDIS_CLUSTER_PLUS_PLUS_DB_LAYER_H_ +/* +===================================================================== +Here is the copyright statement for our use of the hiredis APIs: + +Hiredis was written by Salvatore Sanfilippo (antirez at gmail) and +Pieter Noordhuis (pcnoordhuis at gmail) and is released under the +BSD license. + +Copyright (c) 2009-2011, Salvatore Sanfilippo +Copyright (c) 2010-2011, Pieter Noordhuis + +hiredis-cluster-plus-plus include files provide wrappers on top of the hiredis library. +This wrapper allows us to use the familiar hiredis APIs in the context of +a Redis cluster (Version 6 and higher) to provide HA facility for automatic +fail-over when a Redis instance or the entire machine crashes. +In addition, it provides the TLS/SSL support for the redis-cluster. +Please note that this hiredis-cluster-plus-plus wrapper supercedes the older +hiredis-cluster wrapper that we have in the DPS toolkit. If the Redis server +version is v5 and lower, one may continue to use the older hiredis-cluster in DPS. +If the Redis server version is v6 and higher, it is recommented to use the +hiredis-cluster-plus-plus in order to work with both non-TLS and TLS. + +The redis-plus-plus wrapper carries the Apache 2.0 copyright as shown in the following line. + +A permissive license whose main conditions require preservation of copyright and license notices. +Contributors provide an express grant of patent rights. Licensed works, modifications, and larger +works may be distributed under different terms and without source code. +===================================================================== +*/ +#include "DBLayer.h" + +#include +#include +#include +#include + +using namespace sw::redis; + +namespace com { +namespace ibm { +namespace streamsx { +namespace store { +namespace distributed +{ + class RedisClusterPlusPlusDBLayer; + + /// Class that implements the Iterator for Redis cluster + class RedisClusterPlusPlusDBLayerIterator : public DBLayer::Iterator + { + public: + uint64_t store; + std::string storeName; + std::vector dataItemKeys; + uint32_t sizeOfDataItemKeysVector; + uint32_t currentIndex; + bool hasData; + RedisClusterPlusPlusDBLayer *redisClusterPlusPlusDBLayerPtr; + + RedisClusterPlusPlusDBLayerIterator(); + ~RedisClusterPlusPlusDBLayerIterator(); + bool getNext(uint64_t store, unsigned char * & keyData, uint32_t & keySize, + unsigned char * & valueData, uint32_t & valueSize, PersistenceError & dbError); + }; + + /// Class that implements the DBLayer for Redis Cluster Plus Plus + class RedisClusterPlusPlusDBLayer : public DBLayer + { + private: + bool readStoreInformation(std::string const & storeIdString, PersistenceError & dbError, + uint32_t & dataItemCnt, std::string & storeName, + std::string & keySplTypeName, std::string & valueSplTypeName); + bool acquireStoreLock(std::string const & storeIdString); + void releaseStoreLock(std::string const & storeIdString); + bool readLockInformation(std::string const & storeIdString, PersistenceError & dbError, uint32_t & lockUsageCnt, + int32_t & lockExpirationTime, pid_t & lockOwningPid, std::string & lockName); + bool updateLockInformation(std::string const & lockIdString, PersistenceError & lkError, + uint32_t const & lockUsageCnt, int32_t const & lockExpirationTime, pid_t const & lockOwningPid); + bool lockIdExistsOrNot(std::string lockIdString, PersistenceError & lkError); + bool acquireGeneralPurposeLock(std::string const & entityName); + void releaseGeneralPurposeLock(std::string const & entityName); + int32_t getRedisServerPartitionIndex(std::string const & key); + + public: + RedisCluster *redis_cluster = NULL; + + /// Constructor + RedisClusterPlusPlusDBLayer(); + + /// Destructor + ~RedisClusterPlusPlusDBLayer(); + + // These are inherited from DBLayer, see DBLayer for descriptions + void connectToDatabase(std::set const & dbServers, PersistenceError & dbError); + + uint64_t createStore(std::string const & name, + std::string const & keySplTypeName, + std::string const & valueSplTypeName, + PersistenceError & dbError); + uint64_t createOrGetStore(std::string const & name, + std::string const & keySplTypeName, + std::string const & valueSplTypeName, + PersistenceError & dbError); + uint64_t findStore(std::string const & name, + PersistenceError & dbError); + bool removeStore(uint64_t store, PersistenceError & dbError); + + bool put(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, PersistenceError & dbError); + bool putSafe(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, PersistenceError & dbError); + bool putTTL(char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, uint32_t ttl, PersistenceError & dbError, bool encodeKey=true, bool encodeValue=true); + bool get(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, + PersistenceError & dbError); + bool getSafe(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, + PersistenceError & dbError); + bool getTTL(char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, + PersistenceError & dbError, bool encodeKey=true); + bool remove(uint64_t store, char const * keyData, uint32_t keySize, PersistenceError & dbError); + bool removeTTL(char const * keyData, uint32_t keySize, PersistenceError & dbError, bool encodeKey=true); + bool has(uint64_t store, char const * keyData, uint32_t keySize, PersistenceError & dbError); + bool hasTTL(char const * keyData, uint32_t keySize, PersistenceError & dbError, bool encodeKey=true); + void clear(uint64_t store, PersistenceError & dbError); + uint64_t size(uint64_t store, PersistenceError & dbError); + void base64_encode(std::string const & str, std::string & base64); + void base64_decode(std::string & base64, std::string & result); + bool isConnected(); + bool reconnect(std::set & dbServers, PersistenceError & dbError); + + RedisClusterPlusPlusDBLayerIterator * newIterator(uint64_t store, PersistenceError & dbError); + void deleteIterator(uint64_t store, Iterator * iter, PersistenceError & dbError); + bool storeIdExistsOrNot(std::string storeIdString, PersistenceError & dbError); + bool getDataItemFromStore(std::string const & storeIdString, + std::string const & keyDataString, bool const & checkOnlyForDataItemExistence, + bool const & skipDataItemExistenceCheck, unsigned char * & valueData, + uint32_t & valueSize, PersistenceError & dbError); + std::string getStoreName(uint64_t store, PersistenceError & dbError); + std::string getSplTypeNameForKey(uint64_t store, PersistenceError & dbError); + std::string getSplTypeNameForValue(uint64_t store, PersistenceError & dbError); + std::string getNoSqlDbProductName(void); + void getDetailsAboutThisMachine(std::string & machineName, std::string & osVersion, std::string & cpuArchitecture); + bool runDataStoreCommand(std::string const & cmd, PersistenceError & dbError); + bool runDataStoreCommand(uint32_t const & cmdType, std::string const & httpVerb, + std::string const & baseUrl, std::string const & apiEndpoint, std::string const & queryParams, + std::string const & jsonRequest, std::string & jsonResponse, PersistenceError & dbError); + bool runDataStoreCommand(std::vector const & cmdList, std::string & resultValue, PersistenceError & dbError); + + // Lock related methods. + uint64_t createOrGetLock(std::string const & name, PersistenceError & lkError); + void releaseLock(uint64_t lock, PersistenceError & lkError); + bool acquireLock(uint64_t lock, double leaseTime, double maxWaitTimeToAcquireLock, PersistenceError & lkError); + bool removeLock(uint64_t lock, PersistenceError & lkError); + uint32_t getPidForLock(std::string const & name, PersistenceError & lkError); + void persist(PersistenceError & dbError); + + }; +} } } } } +#endif /* REDIS_CLUSTER_PLUS_PLUS_DB_LAYER_H_ */ diff --git a/com.ibm.streamsx.dps/impl/include/RedisDBLayer.h b/com.ibm.streamsx.dps/impl/include/RedisDBLayer.h index 5fe0615..dd6a327 100644 --- a/com.ibm.streamsx.dps/impl/include/RedisDBLayer.h +++ b/com.ibm.streamsx.dps/impl/include/RedisDBLayer.h @@ -1,6 +1,6 @@ /* # Licensed Materials - Property of IBM -# Copyright IBM Corp. 2011, 2014 +# Copyright IBM Corp. 2011, 2020 # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. @@ -49,6 +49,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "DBLayer.h" #include "hiredis/hiredis.h" +#include "hiredis/hiredis_ssl.h" #include #include #include diff --git a/com.ibm.streamsx.dps/impl/src/DistributedProcessStore.cpp b/com.ibm.streamsx.dps/impl/src/DistributedProcessStore.cpp index cf29192..d951484 100644 --- a/com.ibm.streamsx.dps/impl/src/DistributedProcessStore.cpp +++ b/com.ibm.streamsx.dps/impl/src/DistributedProcessStore.cpp @@ -1,6 +1,6 @@ /* # Licensed Materials - Property of IBM -# Copyright IBM Corp. 2011, 2014 +# Copyright IBM Corp. 2011, 2020 # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. @@ -267,6 +267,7 @@ namespace distributed void* handle1 = NULL; void* handle2 = NULL; void* handle3 = NULL; + void* handle4 = NULL; bool libraryLoadingError = false; std::string kvLibName = ""; std::string toolkitDir = ProcessingElement::pe().getToolkitDirectory("com.ibm.streamsx.dps") + "/impl/ext/lib" ; @@ -278,9 +279,15 @@ namespace distributed } else if (noSqlKvStoreProductName.compare("redis") == 0) { handle1 = load_dependent_lib(toolkitDir, "libuv.so"); handle2 = load_dependent_lib(toolkitDir, "libhiredis.so"); + // Oct/12/2020 to support "TLS for redis". If the user configures the DPS toolkit to + // use "TLS for redis", then this additional .so file will take care of that need. + // On the IBM Streams application Linux machines, it is a must to install opensssl and + // and openssl-devel RPM packages. The following libhiredis_ssl.so file has a dependency on the + // /lib64/libssl.so and /lib64/libcrypto.so libraries that are part of openssl. + handle3 = load_dependent_lib(toolkitDir, "libhiredis_ssl.so"); kvLibName= "libDPSRedis.so"; - if (handle1 == NULL || handle2 == NULL) { + if (handle1 == NULL || handle2 == NULL || handle3 == NULL) { libraryLoadingError = true; } } else if (noSqlKvStoreProductName.compare("cassandra") == 0) { @@ -344,6 +351,21 @@ namespace distributed if (handle1 == NULL || handle2 == NULL) { libraryLoadingError = true; } + } else if (noSqlKvStoreProductName.compare("redis-cluster-plus-plus") == 0) { + handle1 = load_dependent_lib(toolkitDir, "libuv.so"); + handle2 = load_dependent_lib(toolkitDir,"libhiredis.so"); + // Oct/12/2020 to support "TLS for redis". If the user configures the DPS toolkit to + // use "TLS for redis", then this additional .so file will take care of that need. + // On the IBM Streams application Linux machines, it is a must to install opensssl and + // and openssl-devel RPM packages. The following libhiredis_ssl.so file has a dependency on the + // /lib64/libssl.so and /lib64/libcrypto.so libraries that are part of openssl. + handle3 = load_dependent_lib(toolkitDir, "libhiredis_ssl.so"); + handle4 = load_dependent_lib(toolkitDir, "libredis++.so"); + kvLibName= "libDPSRedisClusterPlusPlus.so"; + + if (handle1 == NULL || handle2 == NULL || handle3 == NULL || handle4 == NULL) { + libraryLoadingError = true; + } } else { // Invalid no-sql store product name configured. Abort now. // SPLAPPLOG is causing it to get stuck in RHEL6/CentOS6 (RHEL7/CentOS7 is fine) when the @catch annotation is used in the calling SPL code. @@ -381,6 +403,11 @@ namespace distributed handle3 = NULL; } + if (handle4 != NULL) { + dlclose(handle4); + handle4 = NULL; + } + if (handle != NULL) { dlclose(handle); handle = NULL; @@ -416,6 +443,11 @@ namespace distributed handle3 = NULL; } + if (handle4 != NULL) { + dlclose(handle4); + handle4 = NULL; + } + if (handle != NULL) { dlclose(handle); handle = NULL; @@ -449,6 +481,11 @@ namespace distributed handle3 = NULL; } + if (handle4 != NULL) { + dlclose(handle4); + handle4 = NULL; + } + if (handle != NULL) { dlclose(handle); handle = NULL; diff --git a/com.ibm.streamsx.dps/impl/src/RedisClusterDBLayer.cpp b/com.ibm.streamsx.dps/impl/src/RedisClusterDBLayer.cpp index 61b34c6..85a6c89 100644 --- a/com.ibm.streamsx.dps/impl/src/RedisClusterDBLayer.cpp +++ b/com.ibm.streamsx.dps/impl/src/RedisClusterDBLayer.cpp @@ -1,6 +1,6 @@ /* # Licensed Materials - Property of IBM -# Copyright IBM Corp. 2011, 2017 +# Copyright IBM Corp. 2011, 2020 # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. @@ -79,14 +79,9 @@ It is important to note that a Streams application designer/developer should car of his/her application will access the store simultaneously i.e. who puts what, who gets what and at what frequency from where etc. -This C++ project has a companion SPL project (058_data_sharing_between_non_fused_spl_custom_and_cpp_primitive_operators). -Please refer to the commentary in that SPL project file for learning about the procedure to do an -end-to-end test run involving the SPL code, serialization/deserialization code, -redis-cluster interface code (this file), and your redis-cluster infrastructure. - -As a first step, you should run the ./mk script from the C++ project directory (DistributedProcessStoreLib). -That will take care of building the .so file for the dps and copy it to the SPL project's impl/lib directory. -================================================================================================================== +There are simple and advanced examples included in the DPS toolkit to test all the features described in the previous +paragraph. +======================================================================================================== */ #include "RedisClusterDBLayer.h" diff --git a/com.ibm.streamsx.dps/impl/src/RedisClusterPlusPlusDBLayer.cpp b/com.ibm.streamsx.dps/impl/src/RedisClusterPlusPlusDBLayer.cpp new file mode 100644 index 0000000..b2ec284 --- /dev/null +++ b/com.ibm.streamsx.dps/impl/src/RedisClusterPlusPlusDBLayer.cpp @@ -0,0 +1,5130 @@ +/* +# Licensed Materials - Property of IBM +# Copyright IBM Corp. 2011, 2020 +# US Government Users Restricted Rights - Use, duplication or +# disclosure restricted by GSA ADP Schedule Contract with +# IBM Corp. +*/ +/* +===================================================================== +Here is the copyright statement for our use of the hiredis APIs: + +Hiredis was written by Salvatore Sanfilippo (antirez at gmail) and +Pieter Noordhuis (pcnoordhuis at gmail) and is released under the +BSD license. + +Copyright (c) 2009-2011, Salvatore Sanfilippo +Copyright (c) 2010-2011, Pieter Noordhuis + +redis-plus-plus include files provide wrappers on top of the hiredis library. +This wrapper allows us to use the familiar hiredis APIs in the context of +a Redis cluster (Version 6 and higher) to provide HA facility for automatic +fail-over when a Redis instance or the entire machine crashes. +In addition, it provides the TLS/SSL support for the redis-cluster. +Please note that this redis-plus-plus wrapper supercedes the older +hiredis-cluster wrapper that we also use in the DPS toolkit. If the Redis server +version is v5 and lower, one may continue to use the older hiredis-cluster in DPS. +If the Redis server version is v6 and higher, it is recommended to use the +redis-cluster-plus-plus in order to work with both non-TLS and TLS. + +The redis-plus-plus wrapper carries the Apache 2.0 copyright as shown in the following line. + +A permissive license whose main conditions require preservation of copyright and license notices. +Contributors provide an express grant of patent rights. Licensed works, modifications, and larger +works may be distributed under different terms and without source code. +===================================================================== +*/ + +/* +================================================================================================================== +This CPP file contains the source code for the distributed process store (dps) back-end activities such as +insert, update, read, remove, etc. This dps implementation runs on top of the popular redis-cluster in-memory store. +Redis-Cluster is a simple, but a great open source effort that carries a BSD license. +Thanks to Salvatore Sanfilippo, who created Redis, when he was 30 years old in 2009. What an amazing raw talent!!! +In a (2011) interview, he nicely describes about the reason to start that marvelous project. + +http://www.thestartup.eu/2011/01/an-interview-with-salvatore-sanfilippo-creator-of-redis-cluster-working-out-of-sicily/ + +Redis-Cluster (Version 3 and higher) is a full fledged NoSQL data store with support for complex types such as list, +set, and hash. It also has APIs to perform store commands within a transaction block. Its replication, persistence, +and cluster features are far superior considering that its first release was done only in April/2015. +As of Nov/2020, Redis is at version 6 with many more enhancements including TLS/SSL. To work with Redis v6 and higher +in cluster mode, DPS toolkit introduces a new redis-cluster-plus-plus option which is implemented in this file using +the popular redis-plus-plus C++ library. (DPS toolkit also supports two other options: "redis" for non-cluster +configuration and "redis-cluster" for cluster mode configuration for Redis v5 and lower versions.) + +Any dpsXXXXX native function call made from a SPL composite will go through a serialization layer and then hit +this CPP file. Here, the purpose of such SPL native function calls will be fulfilled using the redis-plus-plus APIs. +After that, the results will be sent to a deserialization layer. From there, results will be transformed using the +correct SPL types and delivered back to the SPL composite. In general, our distributed process store provides +a "global + distributed" in-memory cache for different processes (multiple PEs from one or more Streams applications). +We provide a set of free for all native function APIs to create/read/update/delete data items on one or more stores. +In the worst case, there could be multiple writers and multiple readers for the same store. +It is important to note that a Streams application designer/developer should carefully address how different parts +of his/her application will access the store simultaneously i.e. who puts what, who gets what and at +what frequency from where etc. + +There are simple and advanced examples included in the DPS toolkit to test all the features described in the previous +paragraph. +======================================================================================================== +*/ + +#include "RedisClusterPlusPlusDBLayer.h" +#include "DpsConstants.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// On the IBM Streams application Linux machines, it is a must to install opensssl and +// and openssl-devel RPM packages. The TLS connection logic in the code below will +// require openssl to be available. +// In particular, it will use the /lib64/libssl.so and /lib64/libcrypto.so libraries +// that are part of openssl. + +using namespace std; +using namespace SPL; +using namespace streams_boost::archive::iterators; + +namespace com { +namespace ibm { +namespace streamsx { +namespace store { +namespace distributed +{ + RedisClusterPlusPlusDBLayer::RedisClusterPlusPlusDBLayer() + { + redis_cluster = NULL; + } // End of constructor. + + RedisClusterPlusPlusDBLayer::~RedisClusterPlusPlusDBLayer() + { + // Delete the cluster connection object. + delete redis_cluster; + } // End of destructor. + + void RedisClusterPlusPlusDBLayer::connectToDatabase(std::set const & dbServers, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase", "RedisClusterPlusPlusDBLayer"); + + // Get the name, OS version and CPU type of this machine. + struct utsname machineDetails; + + if(uname(&machineDetails) < 0) { + dbError.set("Unable to get the machine/os/cpu details.", DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed to get the machine/os/cpu details. " << DPS_INITIALIZE_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } else { + nameOfThisMachine = string(machineDetails.nodename); + osVersionOfThisMachine = string(machineDetails.sysname) + string(" ") + string(machineDetails.release); + cpuTypeOfThisMachine = string(machineDetails.machine); + } + + string redisClusterConnectionErrorMsg = "Unable to initialize the redis-cluster-plus-plus connection context."; + // Senthil added this block of code on May/02/2017. + // As part of the Redis configuration in the DPS config file, we now allow the user to specify + // an optional Redis authentication password as shown below. + // server:port:RedisPassword:ConnectionTimeoutValue:use_tls + string targetServerPassword = ""; + string targetServerName = ""; + int targetServerPort = 0; + int connectionTimeout = 0; + int useTls = -1; + string redisClusterCACertFileName = ""; + int connectionAttemptCnt = 0; + + // Get the current thread id that is trying to make a connection to redis cluster. + int threadId = (int)gettid(); + + // We need only one server in the Redis cluster to do the cluster based Redis HA operations. + for (std::set::iterator it=dbServers.begin(); it!=dbServers.end(); ++it) { + std::string serverName = *it; + // If the user has configured to use the unix domain socket, take care of that as well. + if (serverName == "unixsocket") { + // We will consider supporting this at a later time. + redisClusterConnectionErrorMsg += " UnixSocket is not supported when DPS is configured with redis-cluster-plus-plus."; + dbError.set(redisClusterConnectionErrorMsg, DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << + redisClusterConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } else { + // Redis server name can have port number, password, connection timeout value and + // use_tls=1 specified along with it --> MyHost:2345:xyz:5:use_tls + int32_t tokenCnt = 0; + // This technique to get the empty tokens while tokenizing a string is described in this URL: + // https://stackoverflow.com/questions/22331648/boosttokenizer-point-separated-but-also-keeping-empty-fields + typedef streams_boost::tokenizer > tokenizer; + streams_boost::char_separator sep( + ":", // dropped delimiters + "", // kept delimiters + streams_boost::keep_empty_tokens); // empty token policy + + STREAMS_BOOST_FOREACH(std::string token, tokenizer(serverName, sep)) { + tokenCnt++; + + if (tokenCnt == 1) { + // This must be our first token. + if (token != "") { + targetServerName = token; + } + } else if (tokenCnt == 2){ + // This must be our second token. + if (token != "") { + targetServerPort = atoi(token.c_str()); + } + + if (targetServerPort <= 0) { + targetServerPort = REDIS_SERVER_PORT; + } + } else if (tokenCnt == 3) { + // This must be our third token. + if (token != "") { + targetServerPassword = token; + } + } else if (tokenCnt == 4) { + // This must be our fourth token. + if (token != "") { + connectionTimeout = atoi(token.c_str()); + } + + if (connectionTimeout <= 0) { + // Set it to a default of 3 seconds. + connectionTimeout = 3; + } + } else if (tokenCnt == 5) { + // This must be our fifth token. + if (token != "") { + useTls = atoi(token.c_str()); + } + + if (useTls < 0) { + // Set it to a default of 0 i.e. no TLS needed. + useTls = 0; + } + + if (useTls > 0) { + // Set it to 1 i.e. TLS needed. + useTls = 1; + } + } else if (tokenCnt == 6) { + // This must be our sixth token. + if (token != "") { + // This is a fully qualified Redis cluster CA cert filename. + redisClusterCACertFileName = token; + } + } // End of the long if-else block. + } // End of the Boost FOREACH loop. + + if (targetServerName == "") { + // User only specified the server name and no port. + // (This is the case of server name followed by a : character with a missing port number) + targetServerName = serverName; + // In this case, use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } + + if (targetServerPort <= 0) { + // User didn't give a Redis server port. + // Only a server name was given not followed by a : character. + // Use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } + + // Password is already set to empty string at the time of variable declaration. + // Because of that, we are good even if the redis configuration string has this field as blank. + + if (connectionTimeout <= 0) { + // User didn't configure a connectionTimeout field at all. So, set it to 3 seconds. + connectionTimeout = 3; + } + + if (useTls < 0) { + // User didn't configure a useTls field at all, So, set it to 0 i.e. no TLS needed. + useTls = 0; + } + + // Use this line to test out the parsed tokens from the Redis configuration string. + connectionAttemptCnt++; + string clusterPasswordUsage = "a"; + + if (targetServerPassword.length() <= 0) { + clusterPasswordUsage = "no"; + } + + cout << connectionAttemptCnt << ") ThreadId=" << threadId << ". Attempting to connect to the Redis cluster node " << targetServerName << " on port " << targetServerPort << " with " << clusterPasswordUsage << " password. " << "connectionTimeout=" << connectionTimeout << ", use_tls=" << useTls << ", redisClusterCACertFileName=" << redisClusterCACertFileName << "." << endl; + + // Current redis cluster version as of Oct/29/2020 is 6.0.9 + // If the redis cluster node is inactive, the following statement may throw an exception. + // We will catch it, log it and proceed to connect with the next available redis cluster node. + try { + // Initialize the connection options with a master node and its port along with + // other options such as password authentication, TLS/SSL etc. + ConnectionOptions connection_options; + connection_options.host = targetServerName; + connection_options.port = targetServerPort; + + if(targetServerPassword.length() > 0) { + connection_options.password = targetServerPassword; + } + + // Set the TCP socket timeout in msec. + connection_options.socket_timeout = std::chrono::milliseconds(connectionTimeout * 1000); + // Set the connection timeout in msec. + connection_options.connect_timeout = std::chrono::milliseconds(connectionTimeout * 1000); + + // Set the desired TLS/SSL option. + if(useTls == 1) { + connection_options.tls.enabled = true; + connection_options.tls.cacert = redisClusterCACertFileName; + } + + // Get a redis-plus-plus cluster object for the given connection options. + // Automatically get other nodes' information. Then, make a (single) connection to + // every given master node. A single connection here means, we don't want a pool of + // connections to every master node. + redis_cluster = new RedisCluster(connection_options); + } catch (Error &ex) { + cout << "Caught an exception connecting to a redis cluster node at " << targetServerName << ":" << targetServerPort << " (" << ex.what() << "). Skipping it and moving on to a next available redis cluster node." << endl; + // Continue with the next iteration in the for loop. + continue; + } + } // End of if (serverName == "unixsocket") + + if (redis_cluster == NULL) { + redisClusterConnectionErrorMsg += " Connection error in the createCluster API."; + cout << "ThreadId=" << threadId << ". Unable to connect to the Redis cluster node " << targetServerName << " on port " << targetServerPort << endl; + } else { + // Reset the error string. + redisClusterConnectionErrorMsg = ""; + string passwordUsage = "a"; + + if (targetServerPassword.length() <= 0) { + passwordUsage = "no"; + } + + cout << "ThreadId=" << threadId << ". Successfully connected with " << passwordUsage << " password to the Redis cluster node " << targetServerName << " on port " << targetServerPort << endl; + // Break out of the for loop. + break; + } + } // End of the for loop. + + // Check if there was any connection error. + if (redisClusterConnectionErrorMsg != "") { + dbError.set(redisClusterConnectionErrorMsg, DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_ERROR, "Inside connectToDatabase, it failed with an error '" << + redisClusterConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + + // We have now made connection to one server in a redis-cluster. + // Let us check if the global storeId key:value pair is already there in the cache. + string keyString = string(DPS_AND_DL_GUID_KEY); + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside connectToDatabase, it failed with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << " " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to check the existence of the dps GUID key. Error=" + exceptionString, DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "Inside connectToDatabase, it failed. Error=" << + exceptionString << ". rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + + if (exists_result_value == 0) { + // It could be that our global store id is not there now. + // Let us create one with an initial value of 0. + // Redis setnx is an atomic operation. It will succeed only for the very first operator that + // attempts to do this setting after a redis-cluster server is started fresh. If some other operator + // already raced us ahead and created this guid_key, then our attempt below will be safely rejected. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->setnx(keyString, string("0")); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + if(exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + dbError.set(string("Unable to connect to the redis-cluster server(s). ") + + string("Error in REDIS_SETNX_CMD. Exception=") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside connectToDatabase, it failed with an error for REDIS_SETNX_CMD. Exception=" << exceptionString << ". rc=" << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + } // End of if (exists_result_value == 0) + + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase done", "RedisClusterPlusPlusDBLayer"); + } // End of connectToDatabase. + + uint64_t RedisClusterPlusPlusDBLayer::createStore(std::string const & name, + std::string const & keySplTypeName, std::string const & valueSplTypeName, + PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside createStore for store " << name, "RedisClusterPlusPlusDBLayer"); + + string base64_encoded_name; + base64_encode(name, base64_encoded_name); + + // Get a general purpose lock so that only one thread can + // enter inside of this method at any given time with the same store name. + if (acquireGeneralPurposeLock(base64_encoded_name) == false) { + // Unable to acquire the general purpose lock. + dbError.set("Unable to get a generic lock for creating a store with its name as " + name + ".", DPS_GET_GENERIC_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside createStore, it failed for an yet to be created store with its name as " << + name << ". " << DPS_GET_GENERIC_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to create this store. + return 0; + } + + // Let us first see if a store with the given name already exists. + // + // Inside Redis, all our store names will have a mapping type indicator of + // "0" at the beginning followed by the actual store name. "0" + 'store name' + // (See the store layout description documented in the next page.) + // Additionally, in Redis, store names can have space characters in them. + string keyString = string(DPS_STORE_NAME_TYPE) + base64_encoded_name; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to check the existence of a store with a name" + name + + ". Error=" + exceptionString, DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to check for a store existence. Error=" << + exceptionString << ". rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + if (exists_result_value == 1) { + // This store already exists in our cache. + // We can't create another one with the same name now. + dbError.set("A store named " + name + " already exists", DPS_STORE_EXISTS); + SPLAPPTRC(L_DEBUG, "Inside createStore, it failed for store " << name << " that already exists. " << DPS_STORE_EXISTS, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // If we reach here that means, the store doesn't exist at this time. + // We can go ahead and try to create that new store. + // Create a new store. + // At first, let us increment our global dps_guid to reserve a new store id. + keyString = string(DPS_AND_DL_GUID_KEY); + long long incr_result_value = 0; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + uint64_t storeId = 0; + + try { + incr_result_value = redis_cluster->incr(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_INCR_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_INCR_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get a unique store id for a store with a name" + name + + ". Error=" + exceptionString, DPS_GUID_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to create a unique store id for a store with a name " << name << ". Error=" << exceptionString << ". rc=" << DPS_GUID_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + storeId = incr_result_value; + + /* + We secured a guid. We can now create this store. Layout for a distributed process store (dps) looks like this. + 1) Create a root entry called "Store Name": '0' + 'store name' => 'store id' + 2) Create "Store Contents Hash": '1' + 'store id' => 'Redis Hash' + This hash will always have the following three metadata entries: + * a) dps_name_of_this_store ==> 'store name' + * b) dps_spl_type_name_of_key ==> 'spl type name for this store's key' + * c) dps_spl_type_name_of_value ==> 'spl type name for this store's value' + 3) In addition, we will also create and delete custom locks for modifying store contents in (2) above: '4' + 'store id' + 'dps_lock' => 1 + + 4) Create a root entry called "Lock Name": '5' + 'lock name' ==> 'lock id' [This lock is used for performing store commands in a transaction block.] + + 5) Create "Lock Info": '6' + 'lock id' ==> 'lock use count' + '_' + 'lock expiration time expressed as elapsed seconds since the epoch' + '_' + 'lock name' + + 6) In addition, we will also create and delete user-defined locks: '7' + 'lock id' + 'dl_lock' => 1 + 7) We will also allow general purpose locks to be created by any entity for sundry use: '501' + 'entity name' + 'generic_lock' => 1 + */ + + // + // 1) Create the Store Name + // '0' + 'store name' => 'store id' + std::ostringstream value; + value << storeId; + keyString = string(DPS_STORE_NAME_TYPE) + base64_encoded_name; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->set(keyString, value.str()); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_SET_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_SET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to set a K/V pair in a store with a name" + name + + ". Error=" + exceptionString, DPS_STORE_NAME_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to set a K/V pair in a store with a name " << name << ". Error=" << exceptionString << ". rc=" << DPS_STORE_NAME_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + // We are simply leaving an incremented value for the dps_guid key in the cache that will never get used. + // Since it is harmless, there is no need to reduce this number by 1. It is okay that this guid number will remain unassigned to any store. + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // 2) Create the Store Contents Hash + // '1' + 'store id' => 'Redis Hash' + // Every store contents hash will always have these three metadata entries: + // dps_name_of_this_store ==> 'store name' + // dps_spl_type_name_of_key ==> 'spl type name for this store's key' + // dps_spl_type_name_of_value ==> 'spl type name for this store's value' + // + // Every store contents hash will have at least three elements in it carrying the actual store name, key spl type name and value spl type name. + // In addition, inside this store contents hash, we will house the + // actual key:value data items that user wants to keep in this store. + // Such a store contents hash is very useful for data item read, write, deletion, enumeration etc. + // Redis hash has the operational efficiency of O(1) i.e. constant time execution for get, put, and del with any hash size. + // 2a) Let us create a new store contents hash with a mandatory element that will carry the name of this store. + // (This mandatory entry will help us to do the reverse mapping from store id to store name.) + // StoreId becomes the new key now. + keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + value.str(); + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_STORE_ID_TO_STORE_NAME_KEY), + base64_encoded_name); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to create 'Store Contents Hash' in a store with a name" + name + + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA1_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to create 'Store Contents Hash' in a store with a name " << name << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA1_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + + // Delete the previous store name root entry we made. + keyString = string(DPS_STORE_NAME_TYPE) + base64_encoded_name; + + try { + redis_cluster->del(keyString); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // 2b) We are now going to save the SPL type names of the key as part of this + // store's metadata. That will help us in the Java dps API "findStore" to cache the + // key and spl type names inside the Java StoreImpl object by querying from the store's metadata. + // Add the key spl type name metadata. + string base64_encoded_keySplTypeName; + base64_encode(keySplTypeName, base64_encoded_keySplTypeName); + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_SPL_TYPE_NAME_OF_KEY), + base64_encoded_keySplTypeName); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to create 'Store Contents Hash' in a store with a name" + name + + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA2_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to create 'Store Contents Hash' in a store with a name " << name << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA2_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + + // Delete the store contents hash with partial entries we created above. + try { + redis_cluster->del(keyString); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + // We will also delete the store name root entry we made above. + keyString = string(DPS_STORE_NAME_TYPE) + base64_encoded_name; + + try { + redis_cluster->del(keyString); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // 2c) We are now going to save the SPL type names of the value as part of this + // store's metadata. That will help us in the Java dps API "findStore" to cache the + // key and spl type names inside the Java StoreImpl object by querying from the store's metadata. + // Add the value spl type name metadata. + string base64_encoded_valueSplTypeName; + base64_encode(valueSplTypeName, base64_encoded_valueSplTypeName); + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_SPL_TYPE_NAME_OF_VALUE), + base64_encoded_valueSplTypeName); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed for store " << name << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to create 'Store Contents Hash' in a store with a name" + name + + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA3_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Inside createStore, it failed to create 'Store Contents Hash' in a store with a name " << name << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA3_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + + // Delete the store contents hash with partial entries we created above. + try { + redis_cluster->del(keyString); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + // We will also delete the store name root entry we made above. + keyString = string(DPS_STORE_NAME_TYPE) + base64_encoded_name; + + try { + redis_cluster->del(keyString); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // A new store is created now. + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } // End of createStore. + + uint64_t RedisClusterPlusPlusDBLayer::createOrGetStore(std::string const & name, + std::string const & keySplTypeName, std::string const & valueSplTypeName, + PersistenceError & dbError) + { + // We will rely on a method above this and another method below this to accomplish what is needed here. + SPLAPPTRC(L_DEBUG, "Inside createOrGetStore for store " << name, "RedisClusterPlusPlusDBLayer"); + + uint64_t storeId = createStore(name, keySplTypeName, valueSplTypeName, dbError); + + if (storeId > 0) { + // It must be a new store that just got created in the method call we made above. + return(storeId); + } + + // Check if any error code is set from the create store method call we made above.. + if ((dbError.hasError() == true) && (dbError.getErrorCode() != DPS_STORE_EXISTS)) { + // There was an error in creating the store. + return(0); + } + + // In all other cases, we are dealing with an existing store in our cache. + // We can get the storeId by calling the method below and return the result to the caller. + dbError.reset(); + return(findStore(name, dbError)); + } // End of createOrGetStore. + + uint64_t RedisClusterPlusPlusDBLayer::findStore(std::string const & name, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside findStore for store " << name, "RedisClusterPlusPlusDBLayer"); + + string base64_encoded_name; + base64_encode(name, base64_encoded_name); + + // Let us first see if this store already exists. + // Inside Redis, all our store names will have a mapping type indicator of + // "0" at the beginning followed by the actual store name. "0" + 'store name' + std::string storeNameKey = DPS_STORE_NAME_TYPE + base64_encoded_name; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(storeNameKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString + + string(" Application code may call the DPS reconnect API and then retry the failed operation. "), + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside findStore, it failed for store " << name << " with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to check the existence of a store with a name" + name + + ". Error=" + exceptionString, DPS_STORE_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "Inside findStore, it failed to check for a store existence. Error=" << + exceptionString << ". rc=" << DPS_STORE_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + return 0; + } + + if (exists_result_value == 0) { + // This store is not there in our cache. + dbError.set("Store named " + name + " not found.", DPS_STORE_DOES_NOT_EXIST); + SPLAPPTRC(L_DEBUG, "Inside findStore, it couldn't find a store named " << name << ". " << DPS_STORE_DOES_NOT_EXIST, "RedisClusterPlusPlusDBLayer"); + return 0; + } + + // It is an existing store. + // We can get the storeId and return it to the caller. + string get_result_value = "0"; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto val = redis_cluster->get(storeNameKey); + + if(val) { + get_result_value = *val; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_GET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside findStore, unable to get the storeId for the storeName " << name << " with a Redis connection error for REDIS_GET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get the storeId for the storeName " + name + + ". Error=" + exceptionString, DPS_GET_STORE_ID_ERROR); + SPLAPPTRC(L_ERROR, "Inside findStore, unable to get the storeId for the storeName " << name << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + return 0; + } + + if (get_result_value.length() <= 0) { + // Requested data item is not there in the cache. + dbError.set("Unable to get The store id for the store " + name + ".", DPS_DATA_ITEM_READ_ERROR); + SPLAPPTRC(L_DEBUG, "Inside findStore, it couldn't get the store id for store " << name << ". " << DPS_DATA_ITEM_READ_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + uint64_t storeId = streams_boost::lexical_cast(get_result_value); + return(storeId); + } // End of findStore. + + bool RedisClusterPlusPlusDBLayer::removeStore(uint64_t store, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside removeStore for store id " << store, "RedisClusterPlusPlusDBLayer"); + + ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside removeStore, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside removeStore, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // Lock the store first. + if (acquireStoreLock(storeIdString) == false) { + // Unable to acquire the store lock. + dbError.set("Unable to get store lock for the StoreId " + storeIdString + ".", DPS_GET_STORE_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside removeStore, it failed for store id " << storeIdString << ". " << DPS_GET_STORE_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to remove the store. + return(false); + } + + // Get rid of these two entries that are holding the store contents hash and the store name root entry. + // 1) Store Contents Hash + // 2) Store Name root entry + // + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside removeStore, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + // This is alarming. This will put this store in a bad state. Poor user has to deal with it. + return(false); + } + + // Let us delete the Store Contents Hash that contains all the active data items in this store. + // '1' + 'store id' => 'Redis Hash' [It will always have this entry: dps_name_of_this_store ==> 'store name'] + string storeContentsHashKey = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(storeContentsHashKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_DEL_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeStore, it failed for store id " << storeIdString << " with a store name " << storeName << " with a Redis connection error for REDIS_DEL_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to delete a store with an id " + storeIdString + + " with a store name " + storeName + + ". Error=" + exceptionString, DPS_STORE_REMOVAL_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeStore, it failed to delete a store with an id " << storeIdString << " with a store name " << storeName << ". Error=" << exceptionString << ". rc=" << DPS_STORE_REMOVAL_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Finally, delete the StoreName key now. + string storeNameKey = string(DPS_STORE_NAME_TYPE) + storeName; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(storeNameKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_DEL_CMD2: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeStore, it failed for store id " << storeIdString << " with a store name " << storeName << " with a Redis connection error for REDIS_DEL_CMD2. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to delete a store name with an id " + storeIdString + + " with a store name " + storeName + + ". Error=" + exceptionString, DPS_STORE_REMOVAL_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeStore, it failed to delete a store name with an id " << storeIdString << " with a store name " << storeName << ". Error=" << exceptionString << ". rc=" << DPS_STORE_REMOVAL_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Life of this store ended completely with no trace left behind. + releaseStoreLock(storeIdString); + return(true); + } // End of removeStore. + + // This is a lean and mean put operation into a store. + // It doesn't do any safety checks before putting a data item into a store. + // If you want to go through that rigor, please use the putSafe method below. + // This version will perform better since no safety checks are done in this. + bool RedisClusterPlusPlusDBLayer::put(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, + PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside put for store id " << store, "RedisClusterPlusPlusDBLayer"); + + // Let us try to store this item irrespective of whether it is + // new or it is an existing item in the cache. + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // CAUTION: Regular and a faster version of dpsPut will simply create an incorrect store structure when an actual store doesn't exist. + // Because, faster version of dpsPut doesn't do any safety checks to validate the store existence. + // If users call dpsPut (faster version) on a non-existing store, that will surely cause all kinds of issues in the + // back-end data store by creating invalid store structures thereby producing dangling stores. Users should take proper care + // and call the faster version of the dpsPut API only on existing stores. If they ignore this rule, then the back-end data store + // will be in a big mess. + // Ideally, at this point here in this API, we can check for whether the store exists or not. But, that will slow down this + // faster put API. Hence, we are going to trust the users to call this faster API only on existing stores. + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key = string(keyData, keySize); + + // We are ready to either store a new data item or update an existing data item. + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' [It will always have this entry: dps_name_of_this_store ==> 'store name'] + // To support space characters in the data item key, let us base64 encode it. + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string base64_encoded_data_item_key; + base64_encode(data_item_key, base64_encoded_data_item_key); + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + base64_encoded_data_item_key, + string((const char*)valueData, (size_t)valueSize)); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside put, it failed for store id " << storeIdString << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to store a data item in the store id " + storeIdString + + ". Error=" + exceptionString, DPS_DATA_ITEM_WRITE_ERROR); + SPLAPPTRC(L_ERROR, "Inside put, it failed for store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_WRITE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + return(true); + } // End of put. + + // This is a special bullet proof version that does several safety checks before putting a data item into a store. + // Because of these checks, it will be slower. If someone doesn't care about these safety checks, + // then the regular put method can be used. + // This version does all the safety checks and hence will have performance overhead. + bool RedisClusterPlusPlusDBLayer::putSafe(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, + PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside putSafe for store id " << store, "RedisClusterPlusPlusDBLayer"); + + // Let us try to store this item irrespective of whether it is + // new or it is an existing item in the cache. + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside putSafe, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside putSafe, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key = string(keyData, keySize); + + // Lock the store first. + if (acquireStoreLock(storeIdString) == false) { + // Unable to acquire the store lock. + dbError.set("Unable to get store lock for the StoreId " + storeIdString + ".", DPS_GET_STORE_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside putSafe, it failed for store id " << storeIdString << ". " << DPS_GET_STORE_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to put the data item in the store. + return(false); + } + + // We are ready to either store a new data item or update an existing data item. + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' [It will always have this entry: dps_name_of_this_store ==> 'store name'] + // To support space characters in the data item key, let us base64 encode it. + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string base64_encoded_data_item_key; + base64_encode(data_item_key, base64_encoded_data_item_key); + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + base64_encoded_data_item_key, + string((const char*)valueData, (size_t)valueSize)); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside putSafe, it failed for store id " << storeIdString << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to store a data item in the store id " + storeIdString + + ". Error=" + exceptionString, DPS_DATA_ITEM_WRITE_ERROR); + SPLAPPTRC(L_ERROR, "Inside put, it failed for store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_WRITE_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + releaseStoreLock(storeIdString); + return(true); + } // End of putSafe. + + // Put a data item with a TTL (Time To Live in seconds) value into the global area of the Redis DB. + bool RedisClusterPlusPlusDBLayer::putTTL(char const * keyData, uint32_t keySize, + unsigned char const * valueData, uint32_t valueSize, + uint32_t ttl, PersistenceError & dbError, bool encodeKey, bool encodeValue) + { + SPLAPPTRC(L_DEBUG, "Inside putTTL.", "RedisClusterPlusPlusDBLayer"); + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key; + + if (encodeKey == true) { + base64_encode(string(keyData, keySize), data_item_key); + } else { + // Since the key data sent here will always be in the network byte buffer format (NBF), + // we can't simply use it as it is even if the user wants us to use the non-base64 encoded key data. + // In the NBF format, very first byte indicates the length of the key data that follows (if the key data is less than 128 characters). + // In the NBF format, 5 bytes at the beginning indicate the length of the key data that follows (for key data >= 128 characters). + if ((uint8_t)keyData[0] < 0x80) { + // Skip the first length byte. + data_item_key = string(&keyData[1], keySize-1); + } else { + // Skip the five bytes at the beginning that represent the length of the key data. + data_item_key = string(&keyData[5], keySize-5); + } + } + + string data_item_value = ""; + + if (encodeValue == false) { + // Caller wants to store the value as plain string. Do the same thing we did above for the key. + if ((uint8_t)valueData[0] < 0x80) { + data_item_value = string((char const *) &valueData[1], valueSize-1); + } else { + data_item_value = string((char const *) &valueData[5], valueSize-5); + } + } else { + data_item_value = string((char const *)valueData, (size_t)valueSize); + } + + // We are ready to either store a new data item or update an existing data item with a TTL value specified in seconds. + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + if(ttl > 0) { + // User wants to do a put with a TTL value. + redis_cluster->setex(data_item_key, + streams_boost::lexical_cast(ttl), + data_item_value); + } else { + // User doesn't want do a put with a TTL value. + // In this case, we will do a regular Redis set that will keep an unexpired K/V pair. + redis_cluster->set(data_item_key, data_item_value); + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_PUTTTL_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside putTTL, it failed with a Redis connection error for REDIS_PUTTTL_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to store a data item with TTL. Error=" + exceptionString, DPS_DATA_ITEM_WRITE_ERROR); + SPLAPPTRC(L_ERROR, "Inside putTTL, it failed to store a K/V pair" << ". Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_WRITE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + return(true); + } // End of putTTL. + + // This is a lean and mean get operation from a store. + // It doesn't do any safety checks before getting a data item from a store. + // If you want to go through that rigor, please use the getSafe method below. + // This version will perform better since no safety checks are done in this. + bool RedisClusterPlusPlusDBLayer::get(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside get for store id " << store, "RedisClusterPlusPlusDBLayer"); + + // Let us get this data item from the cache as it is. + // Since there could be multiple data writers, we are going to get whatever is there now. + // It is always possible that the value for the requested item can change right after + // you read it due to the data write made by some other thread. Such is life in a global distributed in-memory store. + // Every data item's key must be prefixed with its store id before being used for CRUD operation in the Redis store. + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // In our Redis dps implementation, data item keys can have space characters. + string base64_encoded_data_item_key; + base64_encode(string(keyData, keySize), base64_encoded_data_item_key); + + bool result = getDataItemFromStore(storeIdString, base64_encoded_data_item_key, + false, true, valueData, valueSize, dbError); + + if ((result == false) || (dbError.hasError() == true)) { + // Some error has occurred in reading the data item value. + SPLAPPTRC(L_DEBUG, "Inside get, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } + + return(result); + } // End of get. + + // This is a special bullet proof version that does several safety checks before getting a data item from a store. + // Because of these checks, it will be slower. If someone doesn't care about these safety checks, + // then the regular get method can be used. + // This version does all the safety checks and hence will have performance overhead. + bool RedisClusterPlusPlusDBLayer::getSafe(uint64_t store, char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside getSafe for store id " << store, "RedisClusterPlusPlusDBLayer"); + + // Let us get this data item from the cache as it is. + // Since there could be multiple data writers, we are going to get whatever is there now. + // It is always possible that the value for the requested item can change right after + // you read it due to the data write made by some other thread. Such is life in a global distributed in-memory store. + // Every data item's key must be prefixed with its store id before being used for CRUD operation in the Redis store. + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside getSafe, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getSafe, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // In our Redis dps implementation, data item keys can have space characters. + string base64_encoded_data_item_key; + base64_encode(string(keyData, keySize), base64_encoded_data_item_key); + + bool result = getDataItemFromStore(storeIdString, base64_encoded_data_item_key, + false, false, valueData, valueSize, dbError); + + if ((result == false) || (dbError.hasError() == true)) { + // Some error has occurred in reading the data item value. + SPLAPPTRC(L_DEBUG, "Inside getSafe, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } + + return(result); + } // End of getSafe. + + // Get a TTL based data item that is stored in the global area of the Redis DB. + bool RedisClusterPlusPlusDBLayer::getTTL(char const * keyData, uint32_t keySize, + unsigned char * & valueData, uint32_t & valueSize, PersistenceError & dbError, bool encodeKey) + { + SPLAPPTRC(L_DEBUG, "Inside getTTL.", "RedisClusterPlusPlusDBLayer"); + + // Let us get this data item from the cache as it is. + // Since there could be multiple data writers, we are going to get whatever is there now. + // It is always possible that the value for the requested item can change right after + // you read it due to the data write made by some other thread. Such is life in a global distributed in-memory store. + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key; + + if (encodeKey == true) { + base64_encode(string(keyData, keySize), data_item_key); + } else { + // Since the key data sent here will always be in the network byte buffer format (NBF), + // we can't simply use it as it is even if the user wants us to use the non-base64 encoded key data. + // In the NBF format, very first byte indicates the length of the key data that follows (if the key data is less than 128 characters). + // In the NBF format, 5 bytes at the beginning indicate the length of the key data that follows (for key data >= 128 characters). + if ((uint8_t)keyData[0] < 0x80) { + // Skip the first length byte. + data_item_key = string(&keyData[1], keySize-1); + } else { + // Skip the five bytes at the beginning that represent the length of the key data. + data_item_key = string(&keyData[5], keySize-5); + } + } + + // Since this is a data item with TTL, it is stored in the global area of Redis and not inside a user created store (i.e. a Redis hash). + // Hence, we can't use the Redis hash get command. Rather, we will use the plain Redis get command to read this data item. + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + string str_value = ""; + + try { + auto value = redis_cluster->get(data_item_key); + + if(value) { + str_value = *value; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_GETTTL_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside getTTL, it failed with a Redis connection error for REDIS_GETTTL_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get the requested data item with TTL value. Error=" + exceptionString, DPS_DATA_ITEM_READ_ERROR); + SPLAPPTRC(L_ERROR, "Inside getTTL, it failed to get the data item with TTL. It was either never there to begin with or it probably expired due to its TTL value. Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_READ_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (str_value.length() == 0) { + // User stored empty data item value in the cache. + valueData = (unsigned char *)""; + valueSize = 0; + } else { + // We can allocate memory for the exact length of the data item value. + valueSize = str_value.length(); + valueData = (unsigned char *) malloc(valueSize); + + if (valueData == NULL) { + // Unable to allocate memory to transfer the data item value. + dbError.setTTL("Unable to allocate memory to copy the data item value with TTL.", DPS_GET_DATA_ITEM_MALLOC_ERROR); + valueSize = 0; + return(false); + } + + // We expect the caller of this method to free the valueData pointer. + memcpy(valueData, str_value.c_str(), valueSize); + } + + return(true); + } // End of getTTL. + + bool RedisClusterPlusPlusDBLayer::remove(uint64_t store, char const * keyData, uint32_t keySize, + PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside remove for store id " << store, "RedisClusterPlusPlusDBLayer"); + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside remove, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside remove, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key = string(keyData, keySize); + + // Lock the store first. + if (acquireStoreLock(storeIdString) == false) { + // Unable to acquire the store lock. + dbError.set("Unable to get store lock for the StoreId " + storeIdString + ".", DPS_GET_STORE_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside remove, it failed for store id " << storeIdString << ". " << DPS_GET_STORE_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to remove the data item from the store. + return(false); + } + + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' [It will always have this entry: dps_name_of_this_store ==> 'store name'] + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string base64_encoded_data_item_key; + base64_encode(data_item_key, base64_encoded_data_item_key); + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + long long hdel_result_value = 0; + + try { + hdel_result_value = redis_cluster->hdel(keyString, base64_encoded_data_item_key); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HDEL_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside remove, it failed with a Redis connection error for REDIS_HDEL_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to remove the requested data item from the store id " + storeIdString + ". Error=" + exceptionString, DPS_DATA_ITEM_DELETE_ERROR); + SPLAPPTRC(L_ERROR, "Inside remove, it failed while removing the requested data item from the store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_DELETE_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // Let us ensure that it really removed the requested data item. + if (hdel_result_value == 0) { + // Something is not correct here. It didn't remove the data item. Raise an error. + dbError.set("Unable to remove the requested data item from the store id " + storeIdString + ". REDIS_HDEL returned 0 to indicate the absence of the given field.", DPS_DATA_ITEM_DELETE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside remove, it failed to remove the requested data item from the store id " << storeIdString << ". REDIS_HDEL returned 0 to indicate the absence of the given field. rc=" << DPS_DATA_ITEM_DELETE_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return(false); + } + + // All done. An existing data item in the given store has been removed. + releaseStoreLock(storeIdString); + return(true); + } // End of remove. + + // Remove a TTL based data item that is stored in the global area of the Redis DB. + bool RedisClusterPlusPlusDBLayer::removeTTL(char const * keyData, uint32_t keySize, + PersistenceError & dbError, bool encodeKey) + { + SPLAPPTRC(L_DEBUG, "Inside removeTTL.", "RedisClusterPlusPlusDBLayer"); + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key; + + if (encodeKey == true) { + base64_encode(string(keyData, keySize), data_item_key); + } else { + // Since the key data sent here will always be in the network byte buffer format (NBF), + // we can't simply use it as it is even if the user wants us to use the non-base64 encoded key data. + // In the NBF format, very first byte indicates the length of the key data that follows (if the key data is less than 128 characters). + // In the NBF format, 5 bytes at the beginning indicate the length of the key data that follows (for key data >= 128 characters). + if ((uint8_t)keyData[0] < 0x80) { + // Skip the first length byte. + data_item_key = string(&keyData[1], keySize-1); + } else { + // Skip the five bytes at the beginning that represent the length of the key data. + data_item_key = string(&keyData[5], keySize-5); + } + } + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + long long del_result_value = 0; + + try { + del_result_value = redis_cluster->del(data_item_key); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_DEL_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeTTL, it failed with a Redis connection error for REDIS_DEL_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to remove the requested TTL data item from the global store. Error=" + exceptionString, DPS_DATA_ITEM_DELETE_ERROR); + SPLAPPTRC(L_ERROR, "Inside removeTTL, it failed while removing the requested TTL data item from the global store. Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_DELETE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Let us ensure that it really removed the requested data item. + if (del_result_value == 0) { + // Something is not correct here. It didn't remove the data item. Raise an error. + dbError.set("Unable to remove the requested TTL data item from the global store. REDIS_DEL returned 0 to indicate the absence of the given field.", DPS_DATA_ITEM_DELETE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside removeTTL, it failed to remove the requested TTL data item from the global store. REDIS_DEL returned 0 to indicate the absence of the given field. rc=" << DPS_DATA_ITEM_DELETE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // All done. An existing data item with TTL in the global area has been removed. + return(true); + } // End of removeTTL. + + bool RedisClusterPlusPlusDBLayer::has(uint64_t store, char const * keyData, uint32_t keySize, + PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside has for store id " << store, "RedisClusterPlusPlusDBLayer"); + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside has, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside has, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // In our Redis dps implementation, data item keys can have space characters. + string base64_encoded_data_item_key; + base64_encode(string(keyData, keySize), base64_encoded_data_item_key); + unsigned char *dummyValueData; + uint32_t dummyValueSize; + + // Let us see if we already have this data item in our cache. + // Check only for the data item existence and don't fetch the data item value. + bool dataItemAlreadyInCache = getDataItemFromStore(storeIdString, base64_encoded_data_item_key, + true, false, dummyValueData, dummyValueSize, dbError); + + if (dbError.getErrorCode() != 0) { + SPLAPPTRC(L_DEBUG, "Inside has, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } + + return(dataItemAlreadyInCache); + } // End of has. + + // Check for the existence of a TTL based data item that is stored in the global area of the Redis DB. + bool RedisClusterPlusPlusDBLayer::hasTTL(char const * keyData, uint32_t keySize, + PersistenceError & dbError, bool encodeKey) + { + SPLAPPTRC(L_DEBUG, "Inside hasTTL.", "RedisClusterPlusPlusDBLayer"); + + // In our Redis dps implementation, data item keys can have space characters. + string data_item_key; + + if (encodeKey == true) { + base64_encode(string(keyData, keySize), data_item_key); + } else { + // Since the key data sent here will always be in the network byte buffer format (NBF), + // we can't simply use it as it is even if the user wants us to use the non-base64 encoded key data. + // In the NBF format, very first byte indicates the length of the key data that follows (if the key data is less than 128 characters). + // In the NBF format, 5 bytes at the beginning indicate the length of the key data that follows (for key data >= 128 characters). + if ((uint8_t)keyData[0] < 0x80) { + // Skip the first length byte. + data_item_key = string(&keyData[1], keySize-1); + } else { + // Skip the five bytes at the beginning that represent the length of the key data. + data_item_key = string(&keyData[5], keySize-5); + } + } + + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(data_item_key); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside hasTTL, it failed with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << " " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to check the existence of a TTL based data item. Error=" + exceptionString, DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "Inside hasTTL, it failed to check the existence of a TTL based data item. Error=" << + exceptionString << ". rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (exists_result_value == 0) { + // TTL data item doesn't exist. + return(false); + } else { + // TTL data item exists. + return(true); + } + } // End of hasTTL. + + void RedisClusterPlusPlusDBLayer::clear(uint64_t store, PersistenceError & dbError) { + SPLAPPTRC(L_DEBUG, "Inside clear for store id " << store, "RedisClusterPlusPlusDBLayer"); + + ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside clear, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside clear, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return; + } + + // Lock the store first. + if (acquireStoreLock(storeIdString) == false) { + // Unable to acquire the store lock. + dbError.set("Unable to get store lock for the StoreId " + storeIdString + ".", DPS_GET_STORE_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside clear, it failed for store id " << storeIdString << ". " << DPS_GET_STORE_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to remove the store. + return; + } + + // Get the store name. + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside clear, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + // This is alarming. This will put this store in a bad state. Poor user has to deal with it. + return; + } + + // A very fast and quick thing to do is to simply delete the Store Contents Hash and + // recreate it rather than removing one element at a time. + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' [It will always have three metadata entries carrying the value of the actual store name, key spl type name, and value spl type name.] + // Delete the entire store contents hash. + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_DEL_CMD: ") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside clear, it failed with a Redis connection error for REDIS_DEL_CMD. Exception: " << exceptionString << " " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to remove the requested data item from the store for the store id " + storeIdString + ". Error=" + exceptionString, DPS_STORE_CLEARING_ERROR); + SPLAPPTRC(L_ERROR, "Inside clear, it failed to remove the requested data item from the store for the store id " << storeIdString << ". Error=" << + exceptionString << ". rc=" << DPS_STORE_CLEARING_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // Let us now recreate a new Store Contents Hash for this store with three meta data entries (store name, key spl type name, value spl type name). + // Then we are done. + // 1) Store name. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_STORE_ID_TO_STORE_NAME_KEY), storeName); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside clear, it failed for store id " << storeIdString << " with a Redis connection error for REDIS_HSET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Fatal error in clear method: Unable to recreate 'Store Contents Hash' metadata1 in the store id " + storeIdString + "Unable to create 'Store Contents Hash' in a store with a name" + storeName + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA1_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Fatal error: Inside clear, it failed for store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA1_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // 2) Key spl type name. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_SPL_TYPE_NAME_OF_KEY), keySplTypeName); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD2: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside clear, it failed for store id " << storeIdString << " with a Redis connection error for REDIS_HSET_CMD2. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Fatal error in clear method: Unable to recreate 'Store Contents Hash' metadata2 in the store id " + storeIdString + "Unable to create 'Store Contents Hash' in a store with a name" + storeName + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA2_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Fatal error: Inside clear, it failed for store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA2_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // 3) Value spl type name. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->hset(keyString, + string(REDIS_SPL_TYPE_NAME_OF_VALUE), valueSplTypeName); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HSET_CMD3: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside clear, it failed for store id " << storeIdString << " with a Redis connection error for REDIS_HSET_CMD3. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Fatal error in clear method: Unable to recreate 'Store Contents Hash' metadata3 in the store id " + storeIdString + "Unable to create 'Store Contents Hash' in a store with a name" + storeName + ". Error=" + exceptionString, DPS_STORE_HASH_METADATA3_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "Fatal error: Inside clear, it failed for store id " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_STORE_HASH_METADATA3_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseStoreLock(storeIdString); + return; + } + + // If there was an error in the store contents hash recreation, then this store is going to be in a weird state. + // It is a fatal error. If that happened, something terribly had gone wrong in clearing the contents. + // User should look at the dbError code and decide about a corrective action. + releaseStoreLock(storeIdString); + } // End of clear. + + uint64_t RedisClusterPlusPlusDBLayer::size(uint64_t store, PersistenceError & dbError) + { + SPLAPPTRC(L_DEBUG, "Inside size for store id " << store, "RedisClusterPlusPlusDBLayer"); + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside size, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside size, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // Store size information is maintained as part of the store information. + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside size, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(0); + } + + return((uint64_t)dataItemCnt); + } // End of size. + + // We allow space characters in the data item keys. + // Hence, it is required to based64 encode them before storing the + // key:value data item in Redis. + // (Use boost functions to do this.) + void RedisClusterPlusPlusDBLayer::base64_encode(std::string const & str, std::string & base64) { + // Insert line breaks for every 64KB characters. + typedef insert_linebreaks >, 64*1024 > it_base64_t; + + unsigned int writePaddChars = (3-str.length()%3)%3; + base64 = string(it_base64_t(str.begin()),it_base64_t(str.end())); + base64.append(writePaddChars,'='); + } // End of base64_encode. + + // As explained above, we based64 encoded the data item keys before adding them to the store. + // If we need to get back the original key name, this function will help us in + // decoding the base64 encoded key. + // (Use boost functions to do this.) + void RedisClusterPlusPlusDBLayer::base64_decode(std::string & base64, std::string & result) { + // IMPORTANT: + // For performance reasons, we are not passing a const string to this method. + // Instead, we are passing a directly modifiable reference. Caller should be aware that + // the string they passed to this method gets altered during the base64 decoding logic below. + // After this method returns back to the caller, it is not advisable to use that modified string. + typedef transform_width< binary_from_base64 >, 8, 6 > it_binary_t; + + unsigned int paddChars = count(base64.begin(), base64.end(), '='); + std::replace(base64.begin(),base64.end(),'=','A'); // replace '=' by base64 encoding of '\0' + result = string(it_binary_t(base64.begin()), it_binary_t(base64.end())); // decode + result.erase(result.end()-paddChars,result.end()); // erase padding '\0' characters + } // End of base64_decode. + + // This method will check if a store exists for a given store id. + bool RedisClusterPlusPlusDBLayer::storeIdExistsOrNot(string storeIdString, PersistenceError & dbError) { + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside storeIdExistsOrNot, it failed with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << " " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("StoreIdExistsOrNot: Unable to get StoreContentsHash from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_CONTENTS_HASH_ERROR); + SPLAPPTRC(L_ERROR, "Inside storeIdExistsOrNot, it failed to get StoreContentsHash from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_CONTENTS_HASH_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (exists_result_value == 0) { + // Store doesn't exist. + return(false); + } else { + // Store exists. + return(true); + } + } // End of storeIdExistsOrNot. + + // This method will acquire a lock for a given store. + bool RedisClusterPlusPlusDBLayer::acquireStoreLock(string const & storeIdString) { + int32_t retryCnt = 0; + + // Try to get a lock for this store. + while (1) { + // '4' + 'store id' + 'dps_lock' => 1 + std::string storeLockKey = string(DPS_STORE_LOCK_TYPE) + storeIdString + DPS_LOCK_TOKEN; + // This is an atomic activity. + // If multiple threads attempt to do it at the same time, only one will succeed. + // Winner will hold the lock until they release it voluntarily or + // until the Redis back-end removes this lock entry after the DPS_AND_DL_GET_LOCK_TTL times out. + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + bool setnx_result_value = false; + + try { + setnx_result_value = redis_cluster->setnx(storeLockKey, string("1")); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter an exception? + if (exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + // Problem in atomic creation of the store lock. + return(false); + } + + if(setnx_result_value == true) { + // We got the lock. + // Set the expiration time for this lock key. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->expire(storeLockKey, 1); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter an exception? + if (exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + // Problem in setting the lock expiry time. + SPLAPPTRC(L_ERROR, "b) Inside acquireStoreLock, it failed with an exception. Error=" << exceptionString, "RedisClusterPlusPlusDBLayer"); + // We already got an exception. There is not much use in the code block below. + // In any case, we will give it a try. + // Delete the erroneous lock data item we created. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(storeLockKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // We couldn't get a lock. + return(false); + } + + // We got the lock. + return(true); + } // End of if(setnx_result_value == true) + + // Someone else is holding on to the lock of this store. Wait for a while before trying again. + retryCnt++; + + if (retryCnt >= DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT) { + return(false); + } + + // Yield control to other threads. Wait here with patience by doing an exponential back-off delay. + usleep(DPS_AND_DL_GET_LOCK_SLEEP_TIME * + (retryCnt%(DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT/DPS_AND_DL_GET_LOCK_BACKOFF_DELAY_MOD_FACTOR))); + } // End of the while loop. + + return(false); + } // End of acquireStoreLock. + + void RedisClusterPlusPlusDBLayer::releaseStoreLock(string const & storeIdString) { + // '4' + 'store id' + 'dps_lock' => 1 + std::string storeLockKey = DPS_STORE_LOCK_TYPE + storeIdString + DPS_LOCK_TOKEN; + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(storeLockKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter an exception? + if (exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + // Problem in atomic creation of the store lock. + SPLAPPTRC(L_ERROR, "Inside releaseStoreLock, it failed with an exception. Error=" << exceptionString, "RedisClusterPlusPlusDBLayer"); + } + } // End of releaseStoreLock. + + bool RedisClusterPlusPlusDBLayer::readStoreInformation(std::string const & storeIdString, + PersistenceError & dbError, uint32_t & dataItemCnt, std::string & storeName, + std::string & keySplTypeName, std::string & valueSplTypeName) { + // Read the store name, this store's key and value SPL type names, and get the store size. + storeName = ""; + keySplTypeName = ""; + valueSplTypeName = ""; + dataItemCnt = 0; + + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' + // It will always have the following three metadata entries: + // dps_name_of_this_store ==> 'store name' + // dps_spl_type_name_of_key ==> 'spl type name for this store's key' + // dps_spl_type_name_of_value ==> 'spl type name for this store's value' + // 1) Get the store name. + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto value = redis_cluster->hget(keyString, string(REDIS_STORE_ID_TO_STORE_NAME_KEY)); + + if(value) { + storeName = *value; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HGET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "a) Inside readStoreInformation, it failed with a Redis connection error for REDIS_HGET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get StoreContentsHash metadata1 from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_CONTENTS_HASH_ERROR); + SPLAPPTRC(L_ERROR, "Inside readStoreInformation, it failed to get StoreContentsHash metadata1 from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_CONTENTS_HASH_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (storeName == "") { + // Unable to get the name of this store. + dbError.set("Unable to get the store name for StoreId " + storeIdString + ".", DPS_GET_STORE_NAME_ERROR); + return(false); + } + + // 2) Let us get the spl type name for this store's key. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto value = redis_cluster->hget(keyString, string(REDIS_SPL_TYPE_NAME_OF_KEY)); + + if(value) { + keySplTypeName = *value; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HGET_CMD2: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "b) Inside readStoreInformation, it failed with a Redis connection error for REDIS_HGET_CMD2. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get StoreContentsHash metadata2 from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_CONTENTS_HASH_ERROR); + SPLAPPTRC(L_ERROR, "Inside readStoreInformation, it failed to get StoreContentsHash metadata2 from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_CONTENTS_HASH_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (keySplTypeName == "") { + // Unable to get the name of this store. + dbError.set("Unable to get the key spl type name for StoreId " + storeIdString + ".", DPS_GET_STORE_NAME_ERROR); + return(false); + } + + // 3) Let us get the spl type name for this store's value. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto value = redis_cluster->hget(keyString, string(REDIS_SPL_TYPE_NAME_OF_VALUE)); + + if(value) { + valueSplTypeName = *value; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HGET_CMD3: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "c) Inside readStoreInformation, it failed with a Redis connection error for REDIS_HGET_CMD3. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get StoreContentsHash metadata3 from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_CONTENTS_HASH_ERROR); + SPLAPPTRC(L_ERROR, "Inside readStoreInformation, it failed to get StoreContentsHash metadata3 from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_CONTENTS_HASH_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if (valueSplTypeName == "") { + // Unable to get the name of this store. + dbError.set("Unable to get the value spl type name for StoreId " + storeIdString + ".", DPS_GET_STORE_NAME_ERROR); + return(false); + } + + // 4) Let us get the size of the store contents hash now. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + long long hlen_result_value = 0; + + try { + hlen_result_value = redis_cluster->hlen(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HLEN_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "d) Inside readStoreInformation, it failed with a Redis connection error for REDIS_HLEN_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get StoreContentsHash size from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_SIZE_ERROR); + SPLAPPTRC(L_ERROR, "Inside readStoreInformation, it failed to get StoreContentsHash size from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_SIZE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Our Store Contents Hash for every store will have a mandatory reserved internal elements (store name, key spl type name, and value spl type name). + // Let us not count those three elements in the actual store contents hash size that the caller wants now. + if (hlen_result_value <= 0) { + // This is not correct. We must have a minimum hash size of 3 because of the reserved elements. + dbError.set("Wrong value (zero) observed as the store size for StoreId " + storeIdString + ".", DPS_GET_STORE_SIZE_ERROR); + return(false); + } + + dataItemCnt = hlen_result_value - 3; + return(true); + } // End of readStoreInformation. + + string RedisClusterPlusPlusDBLayer::getStoreName(uint64_t store, PersistenceError & dbError) { + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside getStoreName, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getStoreName, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(""); + } + + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside getStoreName, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(""); + } + + string base64_decoded_storeName; + base64_decode(storeName, base64_decoded_storeName); + return(base64_decoded_storeName); + } // End of getStoreName. + + string RedisClusterPlusPlusDBLayer::getSplTypeNameForKey(uint64_t store, PersistenceError & dbError) { + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForKey, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForKey, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(""); + } + + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForKey, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(""); + } + + string base64_decoded_keySplTypeName; + base64_decode(keySplTypeName, base64_decoded_keySplTypeName); + return(base64_decoded_keySplTypeName); + } // End of getSplTypeNameForKey. + + string RedisClusterPlusPlusDBLayer::getSplTypeNameForValue(uint64_t store, PersistenceError & dbError) { + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForValue, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForValue, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(""); + } + + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside getSplTypeNameForValue, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(""); + } + + string base64_decoded_valueSplTypeName; + base64_decode(valueSplTypeName, base64_decoded_valueSplTypeName); + return(base64_decoded_valueSplTypeName); + } // End of getSplTypeNameForValue. + + std::string RedisClusterPlusPlusDBLayer::getNoSqlDbProductName(void) { + return(string(REDIS_CLUSTER_PLUS_PLUS_NO_SQL_DB_NAME)); + } // End of getNoSqlDbProductName. + + void RedisClusterPlusPlusDBLayer::getDetailsAboutThisMachine(std::string & machineName, std::string & osVersion, std::string & cpuArchitecture) { + machineName = nameOfThisMachine; + osVersion = osVersionOfThisMachine; + cpuArchitecture = cpuTypeOfThisMachine; + } // End of getDetailsAboutThisMachine. + + bool RedisClusterPlusPlusDBLayer::runDataStoreCommand(std::string const & cmd, PersistenceError & dbError) { + // Redis commands in this case will be one way commands that don't return any data back after + // running that command. e-g: "set foo bar", "del foo". + // We will get this done using the other comprehensive overloaded method below. + typedef streams_boost::tokenizer > tokenizer; + streams_boost::char_separator sep( + " ", // dropped delimiters + "", // kept delimiters + streams_boost::keep_empty_tokens); // empty token policy + std::vector myVector; + + // Collect all the space delimited tokens in the given Redis command. + STREAMS_BOOST_FOREACH(std::string token, tokenizer(cmd, sep)) { + myVector.push_back(token); + } // End of the Boost FOREACH loop. + + string result = ""; + return(runDataStoreCommand(myVector, result, dbError)); + } // End of runDataStoreCommand. (Fire and Forget style). + + bool RedisClusterPlusPlusDBLayer::runDataStoreCommand(uint32_t const & cmdType, std::string const & httpVerb, + std::string const & baseUrl, std::string const & apiEndpoint, std::string const & queryParams, + std::string const & jsonRequest, std::string & jsonResponse, PersistenceError & dbError) { + // This API can only be supported in NoSQL data stores such as Cloudant, HBase etc. + // Redis doesn't have a way to do this. + dbError.set("From Redis data store: This API to run native data store commands is not supported in Redis.", DPS_RUN_DATA_STORE_COMMAND_ERROR); + SPLAPPTRC(L_DEBUG, "From Redis data store: This API to run native data store commands is not supported in Redis. rc=" << DPS_RUN_DATA_STORE_COMMAND_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } // End of runDataStoreCommand (Http style) + + /// If users want to send any valid Redis command to the Redis server made up as individual parts, + /// this API can be used. This will work only with Redis. Users simply have to split their + /// valid Redis command into individual parts that appear between spaces and pass them in + /// exacly in that order via a list. DPS back-end code will put them together + /// correctly before executing the command on a configured Redis server. This API will also + /// return the resulting value from executing any given Redis command as a string. It is upto + /// the caller to interpret the Redis returned value and make sense out of it. + /// In essence, it is a two way Redis command which is very diffferent from the other plain + /// API that is explained above. [NOTE: If you have to deal with storing or fetching + /// non-string complex Streams data types, you can't use this API. Instead, use the other + /// DPS put/get/remove/has DPS APIs.] + bool RedisClusterPlusPlusDBLayer::runDataStoreCommand(std::vector const & cmdList, std::string & resultValue, PersistenceError & dbError) { + resultValue = ""; + + if (cmdList.size() <= 0) { + resultValue = "Error: Empty Redis command list was given by the caller."; + dbError.set(resultValue, DPS_RUN_DATA_STORE_COMMAND_ERROR); + return(false); + } + + // Unlike the hiredis and cpp-hiredis-cluster libraries, redis-plus-plus doesn't support the + // "format string" to combine the command arguments into a command string i.e. RedisCommandArgv. + // Instead, it has its own way of doing a generic command interface. + // + // Please note that there are several commands that will allow multiple keys in a single + // command execution. In Redis Cluster, that will cause a CROSSSLOT error due to keys not + // getting partitioned to the same Redis cluster node. It requires special things such as + // using a hash tag by the client code calling this method. Please search for CROSSSLOT + // in this file and in the advanced/04_XXXXXX example splmm file. + // + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + resultValue = "OK"; + // Following logic looks for the redis commands to be in upper case. + string cmd = streams_boost::to_upper_copy(cmdList.at(0)); + + try { + // Redis plus-plus has a few overloaded APIs for the generic command execution. + // We will use one of those variations here for our needs. + // + // NOTE1: In Redis, there are a few commands that take no arguments. + // Such commands can't be executed via the generic command API. + // + // NOTE2: Certain Redis command support the use of multiple keys. + // Because of that, there is a potential for CROSSSLOT error. + // This code block can handle multiple keys in a single command. + // However, it will give an exception as shown below if there are no + // hash tags used as part of the keys. + // "CROSSSLOT Keys in request don't hash to the same slot" + // https://stackoverflow.com/questions/38042629/redis-cross-slot-error + // https://redis.io/topics/cluster-spec#keys-hash-tags + // + if(cmdList.size() > 1) { + // Execute it using the generic command API. + auto r = redis_cluster->command(cmdList.begin(), cmdList.end()); + + /* + Redis can return one of these replies after executing a command: + + Status Reply: Also known as Simple String Reply. It's a non-binary string reply. + Bulk String Reply: Binary safe string reply. + Integer Reply: Signed integer reply. Large enough to hold long long. + Array Reply: (Nested) Array reply. + Error Reply: Non-binary string reply that gives error info. + */ + if(reply::is_error(*r) == true) { + resultValue = reply::parse(*r); + } else if(reply::is_nil(*r) == true) { + resultValue = "nil"; + } else if(reply::is_string(*r) == true) { + resultValue = reply::parse(*r); + } else if(reply::is_status(*r) == true) { + resultValue = reply::parse(*r); + } else if(reply::is_integer(*r) == true) { + auto num = reply::parse(*r); + resultValue = streams_boost::lexical_cast(num); + } else if(reply::is_array(*r) == true) { + // In an array based result, there may be Redis nil result items if the query didn't yield any valid result. + // So, it is safe to use the OptionalString instead of std::string which can lead to an exception while trying to convert nil to std::string. + std::vector result = reply::parse>(*r); + resultValue = ""; + + // We will flatten the result entries indide the array to a single std::string. + for(uint32_t i = 0; i < result.size(); i++) { + OptionalString s = result.at(i); + + if(resultValue.length() > 0) { + // We will append a new line character except for the very fist result. + resultValue.append("\n"); + } + + if(s) { + // Consider this string result item only if the OptionalString really has a string value. + resultValue.append(*s); + } else { + // This Redis result probably is Nil. We will make it an empty string. + resultValue.append(""); + } + } // End of for loop. + } + // Unprocessed Functions. + } else { + resultValue = "Your Redis command '" + cmd + + "' either has an incorrect syntax or it is not supported at this time in the redis-plus-plus K/V store."; + dbError.set("Redis_Cluster_Reply_Error while executing the user given Redis command. Error=" + resultValue, DPS_RUN_DATA_STORE_COMMAND_ERROR); + SPLAPPTRC(L_DEBUG, "Redis_Cluster_Reply_Error. Inside runDataStoreCommand using Redis cmdList, it failed to execute the user given Redis command list. Error=" << resultValue << ". " << DPS_RUN_DATA_STORE_COMMAND_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + if(exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + // Error in executing the user given Redis command. + string exceptionTypeString = streams_boost::lexical_cast(exceptionType); + resultValue = "[" + exceptionTypeString + "] " + exceptionString; + dbError.set("Redis_Cluster_Reply_Error while executing the user given Redis command. Error=" + resultValue, DPS_RUN_DATA_STORE_COMMAND_ERROR); + SPLAPPTRC(L_DEBUG, "Redis_Cluster_Reply_Error. Inside runDataStoreCommand using Redis cmdList, it failed to execute the user given Redis command list. Error=" << resultValue << ". " << DPS_RUN_DATA_STORE_COMMAND_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } else { + return(true); + } + } // End of runDataStoreCommand. (Generic command style). + + // This method will get the data item from the store for a given key. + // Caller of this method can also ask us just to find if a data item + // exists in the store without the extra work of fetching and returning the data item value. + bool RedisClusterPlusPlusDBLayer::getDataItemFromStore(std::string const & storeIdString, + std::string const & keyDataString, bool const & checkOnlyForDataItemExistence, + bool const & skipDataItemExistenceCheck, unsigned char * & valueData, + uint32_t & valueSize, PersistenceError & dbError) { + // Let us get this data item from the cache as it is. + // Since there could be multiple data writers, we are going to get whatever is there now. + // It is always possible that the value for the requested item can change right after + // you read it due to the data write made by some other thread. Such is life in a global distributed in-memory store. + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + // If the caller doesn't want to perform the data existence check to save time, honor that wish here. + if (skipDataItemExistenceCheck == false) { + // This action is performed on the Store Contents Hash that takes the following format. + // '1' + 'store id' => 'Redis Hash' [It will always have this entry: dps_name_of_this_store ==> 'store name'] + bool hexists_result_value = false; + + try { + hexists_result_value = redis_cluster->hexists(keyString, keyDataString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HEXISTS_CMD: ") + exceptionString, DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "a) Inside getDataItemFromStore, it failed with a Redis connection error for REDIS_HEXISTS_CMD. Exception: " << exceptionString << " " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("getDataItemFromStore: Unable to check for the existence of the data item in the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "a) Inside getDataItemFromStore, unable to check for the existence of the data item in the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // If the caller only wanted us to check for the data item existence, we can exit now. + if (checkOnlyForDataItemExistence == true) { + return(hexists_result_value); + } + + // Caller wants us to fetch and return the data item value. + // If the data item is not there, we can't do much at this point. + if (hexists_result_value == false) { + // This data item doesn't exist. Let us raise an error. + // Requested data item is not there in the cache. + dbError.set("The requested data item doesn't exist in the StoreId " + storeIdString + + ".", DPS_DATA_ITEM_READ_ERROR); + return(false); + } + } // End of if (skipDataItemExistenceCheck == false) + + // Fetch the data item now. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + string data_item_value = ""; + + try { + auto value = redis_cluster->hget(keyString, keyDataString); + + if(value) { + data_item_value = *value; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HGET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "b) Inside getDataItemFromStore, it failed with a Redis connection error for REDIS_HGET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + dbError.set("Unable to get a data item from the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_DATA_ITEM_READ_ERROR); + SPLAPPTRC(L_ERROR, "Inside getDataItemFromStore, it failed to get a data item from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_DATA_ITEM_READ_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Data item value read from the store will be in this format: 'value' + if (data_item_value.length() == 0) { + // User stored empty data item value in the cache. + valueData = (unsigned char *)""; + valueSize = 0; + } else { + // We can allocate memory for the exact length of the data item value. + valueSize = data_item_value.length(); + valueData = (unsigned char *) malloc(valueSize); + + if (valueData == NULL) { + // Unable to allocate memory to transfer the data item value. + dbError.set("Unable to allocate memory to copy the data item value for the StoreId " + + storeIdString + ".", DPS_GET_DATA_ITEM_MALLOC_ERROR); + valueSize = 0; + return(false); + } + + // We expect the caller of this method to free the valueData pointer. + memcpy(valueData, data_item_value.c_str(), valueSize); + } + + return(true); + } // End of getDataItemFromStore. + + RedisClusterPlusPlusDBLayerIterator * RedisClusterPlusPlusDBLayer::newIterator(uint64_t store, PersistenceError & dbError) { + SPLAPPTRC(L_DEBUG, "Inside newIterator for store id " << store, "RedisClusterPlusPlusDBLayer"); + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + // Ensure that a store exists for the given store id. + if (storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside newIterator, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside newIterator, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(NULL); + } + + // Get the general information about this store. + uint32_t dataItemCnt = 0; + string storeName = ""; + string keySplTypeName = ""; + string valueSplTypeName = ""; + + if (readStoreInformation(storeIdString, dbError, dataItemCnt, storeName, keySplTypeName, valueSplTypeName) == false) { + SPLAPPTRC(L_DEBUG, "Inside newIterator, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(NULL); + } + + // It is a valid store. Create a new iterator and return it to the caller. + RedisClusterPlusPlusDBLayerIterator *iter = new RedisClusterPlusPlusDBLayerIterator(); + iter->store = store; + base64_decode(storeName, iter->storeName); + iter->hasData = true; + // Give this iterator access to our RedisClusterPlusPlusDBLayer object. + iter->redisClusterPlusPlusDBLayerPtr = this; + iter->sizeOfDataItemKeysVector = 0; + iter->currentIndex = 0; + + SPLAPPTRC(L_DEBUG, "Inside newIterator: store=" << iter->store << ", storeName=" << iter->storeName << ", hasData=" << iter->hasData << ", sizeOfDataItemKeysVector=" << iter->sizeOfDataItemKeysVector << ", currentIndex=" << iter->currentIndex, "RedisClusterPlusPlusDBLayer"); + + return(iter); + } // End of newIterator. + + void RedisClusterPlusPlusDBLayer::deleteIterator(uint64_t store, Iterator * iter, PersistenceError & dbError) { + SPLAPPTRC(L_DEBUG, "Inside deleteIterator for store id " << store, "RedisClusterPlusPlusDBLayer"); + + if (iter == NULL) { + return; + } + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + + RedisClusterPlusPlusDBLayerIterator *myIter = static_cast(iter); + + // Let us ensure that the user wants to delete an iterator that really belongs to the store passed to us. + // This will handle user's coding errors where a wrong combination of store id and iterator is passed to us for deletion. + if (myIter->store != store) { + // User sent us a wrong combination of a store and an iterator. + dbError.set("A wrong iterator has been sent for deletion. This iterator doesn't belong to the StoreId " + + storeIdString + ".", DPS_STORE_ITERATION_DELETION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside deleteIterator, it failed for store id " << storeIdString << ". " << DPS_STORE_ITERATION_DELETION_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } else { + delete iter; + } + } // End of deleteIterator. + + // This method will acquire a lock for any given generic/arbitrary identifier passed as a string.. + // This is typically used inside the createStore, createOrGetStore, createOrGetLock methods to + // provide thread safety. There are other lock acquisition/release methods once someone has a valid store id or lock id. + bool RedisClusterPlusPlusDBLayer::acquireGeneralPurposeLock(string const & entityName) { + int32_t retryCnt = 0; + + //Try to get a lock for this generic entity. + while (1) { + // '501' + 'entity name' + 'generic_lock' => 1 + std::string genericLockKey = GENERAL_PURPOSE_LOCK_TYPE + entityName + GENERIC_LOCK_TOKEN; + // This is an atomic activity. + // If multiple threads attempt to do it at the same time, only one will succeed. + // Winner will hold the lock until they release it voluntarily or + // until the Redis back-end removes this lock entry after the DPS_AND_DL_GET_LOCK_TTL times out. + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + bool setnx_result_value = false; + + try { + setnx_result_value = redis_cluster->setnx(genericLockKey, string("1")); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + SPLAPPTRC(L_ERROR, "a) Inside acquireGeneralPurposeLock, it failed with a Redis connection error for REDIS_SETNX_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Problem in atomic creation of the general purpose lock. + return(false); + } + + if(setnx_result_value == true) { + // We got the lock. + // Set the expiration time for this lock key. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->expire(genericLockKey, DPS_AND_DL_GET_LOCK_TTL); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter an exception? + if (exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + // Problem in setting the lock expiry time. + SPLAPPTRC(L_ERROR, "b) Inside acquireGeneralPurposeLock, it failed with an exception for REDIS_EXPIRE_CMD. Exception: " << exceptionString << ".", "RedisClusterPlusPlusDBLayer"); + + // We already got an exception. There is not much use in the code block below. + // In any case, we will give it a try. + // Delete the erroneous lock data item we created. + try { + redis_cluster->del(genericLockKey); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + SPLAPPTRC(L_ERROR, "c) Inside acquireGeneralPurposeLock, it failed with an exception for REDIS_DEL_CMD. Exception: " << ex.what() << ".", "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // We got the lock with proper expiry time set. + return(true); + } // End of if(setnx_result_value == true). + + // Someone else is holding on to the lock of this entity. Wait for a while before trying again. + retryCnt++; + + if (retryCnt >= DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT) { + return(false); + } + + // Yield control to other threads. Wait here with patience by doing an exponential back-off delay. + usleep(DPS_AND_DL_GET_LOCK_SLEEP_TIME * + (retryCnt%(DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT/DPS_AND_DL_GET_LOCK_BACKOFF_DELAY_MOD_FACTOR))); + } // End of while loop. + + return(false); + } // End of acquireGeneralPurposeLock. + + void RedisClusterPlusPlusDBLayer::releaseGeneralPurposeLock(string const & entityName) { + // '501' + 'entity name' + 'generic_lock' => 1 + std::string genericLockKey = GENERAL_PURPOSE_LOCK_TYPE + entityName + GENERIC_LOCK_TOKEN; + + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(genericLockKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we get into an exception? + if (exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + SPLAPPTRC(L_ERROR, "Inside releaseGeneralPurposeLock, it failed to delete a lock. Error=" << exceptionString << ". rc=" << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + } + } // End of releaseGeneralPurposeLock. + + RedisClusterPlusPlusDBLayerIterator::RedisClusterPlusPlusDBLayerIterator() { + + } + + RedisClusterPlusPlusDBLayerIterator::~RedisClusterPlusPlusDBLayerIterator() { + + } + + bool RedisClusterPlusPlusDBLayerIterator::getNext(uint64_t store, unsigned char * & keyData, uint32_t & keySize, + unsigned char * & valueData, uint32_t & valueSize, PersistenceError & dbError) { + SPLAPPTRC(L_DEBUG, "Inside getNext for store id " << store, "RedisClusterPlusPlusDBLayerIterator"); + + // If the iteration already ended, do a quick return back to the caller. + // Another possibility we want to detect is whether the caller really passed the + // correct store id that belongs to this iterator object. If either of them + // is not in our favor, bail out right away. + if ((this->hasData == false) || (store != this->store)) { + return(false); + } + + std::ostringstream storeId; + storeId << store; + string storeIdString = storeId.str(); + string data_item_key = ""; + + // Ensure that a store exists for the given store id. + if (this->redisClusterPlusPlusDBLayerPtr->storeIdExistsOrNot(storeIdString, dbError) == false) { + if (dbError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside getNext, it failed to check for the existence of store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayerIterator"); + } else { + dbError.set("No store exists for the StoreId " + storeIdString + ".", DPS_INVALID_STORE_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getNext, it failed for store id " << storeIdString << ". " << DPS_INVALID_STORE_ID_ERROR, "RedisClusterPlusPlusDBLayerIterator"); + } + + return(false); + } + + // Ensure that this store is not empty at this time. + if (this->redisClusterPlusPlusDBLayerPtr->size(store, dbError) <= 0) { + dbError.set("Store is empty for the StoreId " + storeIdString + ".", DPS_STORE_EMPTY_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getNext, it failed for store id " << storeIdString << ". " << DPS_STORE_EMPTY_ERROR, "RedisClusterPlusPlusDBLayerIterator"); + return(false); + } + + if (this->sizeOfDataItemKeysVector <= 0) { + // This is the first time we are coming inside getNext for store iteration. + // Let us get the available data item keys from this store. + this->dataItemKeys.clear(); + string keyString = string(DPS_STORE_CONTENTS_HASH_TYPE) + storeIdString; + std::vector keys; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + this->redisClusterPlusPlusDBLayerPtr->redis_cluster->hkeys(keyString, std::back_inserter(keys)); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + dbError.set(string("getNext: Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_HKEYS_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DPS_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside getNext, it failed with a Redis connection error for REDIS_HKEYS_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + this->hasData = false; + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Unable to get data item keys from the store. + dbError.set("Unable to get data item keys for the StoreId " + storeIdString + ". Error=" + exceptionString, DPS_GET_STORE_DATA_ITEM_KEYS_ERROR); + SPLAPPTRC(L_ERROR, "Inside getNext, it failed to get data item keys from the StoreId " << storeIdString << ". Error=" << exceptionString << ". rc=" << DPS_GET_STORE_DATA_ITEM_KEYS_ERROR, "RedisClusterPlusPlusDBLayer"); + this->hasData = false; + return(false); + } + + // We have the data item keys returned in array now. + // Let us insert them into the iterator object's member variable that will hold the data item keys for this store. + for(unsigned int i=0; i < keys.size(); i++) { + data_item_key = keys[i]; + + // Every dps store will have three mandatory reserved data item keys for internal use. + // Let us not add them to the iteration object's member variable. + if (data_item_key.compare(REDIS_STORE_ID_TO_STORE_NAME_KEY) == 0) { + continue; // Skip this one. + } else if (data_item_key.compare(REDIS_SPL_TYPE_NAME_OF_KEY) == 0) { + continue; // Skip this one. + } else if (data_item_key.compare(REDIS_SPL_TYPE_NAME_OF_VALUE) == 0) { + continue; // Skip this one. + } + + this->dataItemKeys.push_back(data_item_key); + } // End of for loop. + + this->sizeOfDataItemKeysVector = this->dataItemKeys.size(); + this->currentIndex = 0; + + if (this->sizeOfDataItemKeysVector == 0) { + // This is an empty store at this time. + // Let us exit now. + this->hasData = false; + return(false); + } + + SPLAPPTRC(L_DEBUG, "Inside getNext: store=" << this->store << ", storeName=" << this->storeName << ", hasData=" << this->hasData << ", sizeOfDataItemKeysVector=" << this->sizeOfDataItemKeysVector << ", currentIndex=" << this->currentIndex, "RedisClusterPlusPlusDBLayer"); + } // End of if (this->sizeOfDataItemKeysVector <= 0) + + // We have data item keys. + // Let us get the next available data. + SPLAPPTRC(L_DEBUG, "Inside getNext: Just about to get the data item key at index " << this->currentIndex << " from store " << this->storeName << ". Total number of keys=" << this->sizeOfDataItemKeysVector, "RedisClusterPlusPlusDBLayer"); + + data_item_key = this->dataItemKeys.at(this->currentIndex); + // Advance the data item key vector index by 1 for it to be ready for the next iteration. + this->currentIndex += 1; + + if (this->currentIndex >= this->sizeOfDataItemKeysVector) { + // We have served all the available data to the caller who is iterating this store. + // There is no more data to deliver for subsequent iteration requests from the caller. + this->dataItemKeys.clear(); + this->currentIndex = 0; + this->sizeOfDataItemKeysVector = 0; + this->hasData = false; + } + + // Get this data item's value data and value size. + // data_item_key was obtained straight from the store contents hash, where it is + // already in the base64 encoded format. + bool result = this->redisClusterPlusPlusDBLayerPtr->getDataItemFromStore(storeIdString, data_item_key, + false, false, valueData, valueSize, dbError); + + if (result == false) { + // Some error has occurred in reading the data item value. + SPLAPPTRC(L_DEBUG, "Inside getNext, it failed for store id " << storeIdString << ". " << dbError.getErrorCode(), "RedisClusterPlusPlusDBLayerIterator"); + // We will disable any future action for this store using the current iterator. + this->hasData = false; + return(false); + } + + // We are almost done once we take care of arranging to return the key name and key size. + // In order to support spaces in data item keys, we base64 encoded them before storing it in Redis. + // Let us base64 decode it now to get the original data item key. + string base64_decoded_data_item_key; + this->redisClusterPlusPlusDBLayerPtr->base64_decode(data_item_key, base64_decoded_data_item_key); + data_item_key = base64_decoded_data_item_key; + keySize = data_item_key.length(); + // Allocate memory for this key and copy it to that buffer. + keyData = (unsigned char *) malloc(keySize); + + if (keyData == NULL) { + // This error will occur very rarely. + // If it happens, we will handle it. + // We will not return any useful data to the caller. + if (valueSize > 0) { + delete [] valueData; + valueData = NULL; + } + + // We will disable any future action for this store using the current iterator. + this->hasData = false; + keySize = 0; + dbError.set("Unable to allocate memory for the keyData while doing the next data item iteration for the StoreId " + storeIdString + ".", DPS_STORE_ITERATION_MALLOC_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getNext, it failed for store id " << storeIdString << ". " << DPS_STORE_ITERATION_MALLOC_ERROR, "RedisClusterPlusPlusDBLayerIterator"); + return(false); + } + + // Copy the raw key data into the allocated buffer. + memcpy(keyData, data_item_key.data(), keySize); + // We are done. We expect the caller to free the keyData and valueData buffers. + return(true); + } // End of getNext. + +// ======================================================================================================= +// Beyond this point, we have code that deals with the distributed locks that a SPL developer can +// create, remove,acquire, and release. +// ======================================================================================================= + uint64_t RedisClusterPlusPlusDBLayer::createOrGetLock(std::string const & name, PersistenceError & lkError) { + SPLAPPTRC(L_DEBUG, "Inside createOrGetLock with a name " << name, "RedisClusterPlusPlusDBLayer"); + + string base64_encoded_name; + base64_encode(name, base64_encoded_name); + + // Get a general purpose lock so that only one thread can + // enter inside of this method at any given time with the same lock name. + if (acquireGeneralPurposeLock(base64_encoded_name) == false) { + // Unable to acquire the general purpose lock. + lkError.set("Unable to get a generic lock for creating a lock with its name as " + name + ".", DPS_GET_GENERIC_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside createOrGetLock, it failed for an yet to be created lock with its name as " << + name << ". " << DPS_GET_GENERIC_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to create this distributed lock. + return 0; + } + + // Let us first see if a lock with the given name already exists. + // In our Redis dps implementation, data item keys can have space characters. + // Inside Redis, all our lock names will have a mapping type indicator of + // "5" at the beginning followed by the actual lock name. + // '5' + 'lock name' ==> 'lock id' + std::string lockNameKey = DL_LOCK_NAME_TYPE + base64_encoded_name; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(lockNameKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + lkError.set(string("Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_EXISTS_CMD: ") + exceptionString, DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "a) Inside createOrGetLock, it failed with a Redis connection error for REDIS_EXISTS_CMD. Exception: " << exceptionString << " " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to check the existence of the lock. Error=" + exceptionString, DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_ERROR, "b) Inside createOrGetLock, it failed to check for the existence of the lock. Error=" << exceptionString << ". rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + if (exists_result_value == 1) { + // This lock already exists in our cache. + // We can get the lockId and return it to the caller. + string get_result_value = ""; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto val = redis_cluster->get(lockNameKey); + + if(val) { + get_result_value = *val; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + lkError.set(string("createOrGetLock: Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_GET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "c) Inside createOrGetLock, it failed for the lock named " << name << " with a Redis connection error for REDIS_GET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to get the lockId for the lockName " + name + + ". Error=" + exceptionString, DL_GET_LOCK_ID_ERROR); + SPLAPPTRC(L_ERROR, "d) Inside createOrGetLock, unable to get the lockId for the lockName " << name << ". Error=" << exceptionString << ". rc=" << DL_GET_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + if (get_result_value.length() <= 0) { + // Unable to get the lock information. It is an abnormal error. Convey this to the caller. + lkError.set("Redis returned an empty lockId for the lockName " + name + ".", DL_GET_LOCK_ID_ERROR); + SPLAPPTRC(L_DEBUG, "e) Inside createOrGetLock, it failed with an empty lockId for the lockName " << + name << ". " << DL_GET_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } else { + uint64_t lockId = 0; + lockId = streams_boost::lexical_cast(get_result_value); + releaseGeneralPurposeLock(base64_encoded_name); + return(lockId); + } + } // End of if (exists_result_value == 1) + + // There is no existing lock. + // Create a new lock. + // At first, let us increment our global dps_and_dl_guid to reserve a new lock id. + uint64_t lockId = 0; + std::string guid_key = DPS_AND_DL_GUID_KEY; + long long incr_result_value = 0; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + incr_result_value = redis_cluster->incr(guid_key); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + lkError.set(string("createOrGetLock: Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_INCR_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "f) Inside createOrGetLock, it failed for the lock named " << name << " with a Redis connection error for REDIS_INCR_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return 0; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to get a unique lock id for a lock named " + name + + ". Error=" + exceptionString, DL_GUID_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "g) Inside createOrGetLock, unable to get a unique lock id for a lock named " << name << ". Error=" << exceptionString << ". rc=" << DL_GUID_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // Get the newly created lock id. + lockId = incr_result_value; + // We secured a guid. We can now create this lock. + // + // 1) Create the Lock Name + // '5' + 'lock name' ==> 'lock id' + std::ostringstream value; + value << lockId; + std::string value_string = value.str(); + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->set(lockNameKey, value_string); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + lkError.set(string("createOrGetLock: Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_SET_CMD: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "h) Inside createOrGetLock, it failed for the lock named " << name << " with a Redis connection error for REDIS_SET_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to create 'LockName:LockId' in the cache for a lock named " + name + + ". Error=" + exceptionString, DL_LOCK_NAME_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "i) Inside createOrGetLock, it failed to create 'LockName:LockId' in the cache for a lock named " << name << ". Error=" << exceptionString << ". rc=" << DL_LOCK_NAME_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + // We are simply leaving an incremented value for the dps_and_dl_guid key in the cache that will never get used. + // Since it is harmless, there is no need to reduce this number by 1. It is okay that this guid number will remain unassigned to any store. + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // 2) Create the Lock Info + // '6' + 'lock id' ==> 'lock use count' + '_' + 'lock expiration time expressed as elapsed seconds since the epoch' + '_' + 'pid that owns this lock' + "_" + lock name' + std::string lockInfoKey = DL_LOCK_INFO_TYPE + value_string; // LockId becomes the new key now. + value_string = string("0_0_0_") + base64_encoded_name; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->set(lockInfoKey, value_string); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + // This is how we can detect that a wrong redis-cluster server name is configured by the user or + // not even a single redis-cluster server daemon being up and running. + // This is a serious error. + lkError.set(string("createOrGetLock: Unable to connect to the redis-cluster server(s).") + + string(" Got an exception for REDIS_SET_CMD2: ") + exceptionString + + " Application code may call the DPS reconnect API and then retry the failed operation. ", + DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "j) Inside createOrGetLock, it failed for the lock named " << name << " with a Redis connection error for REDIS_SET_CMD2. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to create 'LockId:LockInfo' in the cache for a lock named " + name + + ". Error=" + exceptionString, DL_LOCK_NAME_CREATION_ERROR); + SPLAPPTRC(L_ERROR, "k) Inside createOrGetLock, it failed to create 'LockId:LockInfo' in the cache for a lock named " << name << ". Error=" << exceptionString << ". rc=" << DL_LOCK_NAME_CREATION_ERROR, "RedisClusterPlusPlusDBLayer"); + // We are simply leaving an incremented value for the dps_and_dl_guid key in the cache that will never get used. + // Since it is harmless, there is no need to reduce this number by 1. It is okay that this guid number will remain unassigned to any store. + + // Delete the previous entry we made. + try { + redis_cluster->del(lockNameKey); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + releaseGeneralPurposeLock(base64_encoded_name); + return(0); + } + + // We created the lock. + SPLAPPTRC(L_DEBUG, "Inside createOrGetLock done for a lock named " << name, "RedisClusterPlusPlusDBLayer"); + releaseGeneralPurposeLock(base64_encoded_name); + return (lockId); + } // End of createOrGetLock. + + bool RedisClusterPlusPlusDBLayer::removeLock(uint64_t lock, PersistenceError & lkError) { + SPLAPPTRC(L_DEBUG, "Inside removeLock for lock id " << lock, "RedisClusterPlusPlusDBLayer"); + + ostringstream lockId; + lockId << lock; + string lockIdString = lockId.str(); + + // If the lock doesn't exist, there is nothing to remove. Don't allow this caller inside this method. + if(lockIdExistsOrNot(lockIdString, lkError) == false) { + if (lkError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside removeLock, it failed to check for the existence of lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + lkError.set("No lock exists for the LockId " + lockIdString + ".", DL_INVALID_LOCK_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside removeLock, it failed for lock id " << lockIdString << ". " << DL_INVALID_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // Before removing the lock entirely, ensure that the lock is not currently being used by anyone else. + if (acquireLock(lock, 5, 3, lkError) == false) { + // Unable to acquire the distributed lock. + lkError.set("Unable to get a distributed lock for the LockId " + lockIdString + ".", DL_GET_DISTRIBUTED_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside removeLock, it failed for the lock id " << lockIdString << ". " << DL_GET_DISTRIBUTED_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // User has to retry again to remove the lock. + return(false); + } + + // We ensured that this lock is not being used by anyone at this time. + // We are safe to remove this distributed lock entirely. + // Let us first get the lock name for this lock id. + uint32_t lockUsageCnt = 0; + int32_t lockExpirationTime = 0; + std::string lockName = ""; + pid_t lockOwningPid = 0; + + if (readLockInformation(lockIdString, lkError, lockUsageCnt, lockExpirationTime, lockOwningPid, lockName) == false) { + SPLAPPTRC(L_DEBUG, "Inside removeLock, it failed for lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + releaseLock(lock, lkError); + // This is alarming. This will put this lock in a bad state. Poor user has to deal with it. + return(false); + } + + // Let us first remove the lock info for this distributed lock. + // '6' + 'lock id' ==> 'lock use count' + '_' + 'lock expiration time expressed as elapsed seconds since the epoch' + '_' + 'pid that owns this lock' + "_" + lock name' + std::string lockInfoKey = DL_LOCK_INFO_TYPE + lockIdString; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(lockInfoKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Do we have an exception? + if(exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + SPLAPPTRC(L_ERROR, "a) Inside removeLock, it failed with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + // We can now delete the lock name root entry. + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + string lockNameKey = DL_LOCK_NAME_TYPE + lockName; + + try { + redis_cluster->del(lockNameKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Do we have an exception? + if(exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + SPLAPPTRC(L_ERROR, "b) Inside removeLock, it failed with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + // We can delete the lock item itself now. + releaseLock(lock, lkError); + // Inside the release lock function we called in the previous statement, it makes a + // call to update the lock info. That will obviously fail since we removed here everything about + // this lock. Hence, let us not propagate that error and cause the user to panic. + // Reset any error that may have happened in the previous operation of releasing the lock. + lkError.reset(); + // Life of this lock ended completely with no trace left behind. + return(true); + } // End of removeLock. + + bool RedisClusterPlusPlusDBLayer::acquireLock(uint64_t lock, double leaseTime, double maxWaitTimeToAcquireLock, PersistenceError & lkError) { + SPLAPPTRC(L_DEBUG, "Inside acquireLock for lock id " << lock, "RedisClusterPlusPlusDBLayer"); + + ostringstream lockId; + lockId << lock; + string lockIdString = lockId.str(); + int32_t retryCnt = 0; + + // If the lock doesn't exist, there is nothing to acquire. Don't allow this caller inside this method. + if(lockIdExistsOrNot(lockIdString, lkError) == false) { + if (lkError.hasError() == true) { + SPLAPPTRC(L_DEBUG, "Inside acquireLock, it failed to check for the existence of lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + lkError.set("No lock exists for the LockId " + lockIdString + ".", DL_INVALID_LOCK_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside acquireLock, it failed for lock id " << lockIdString << ". " << DL_INVALID_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + } + + return(false); + } + + // We will first check if we can get this lock. + // '7' + 'lock id' + 'dl_lock' => 1 + std::string distributedLockKey = DL_LOCK_TYPE + lockIdString + DL_LOCK_TOKEN; + time_t startTime, timeNow; + // Get the start time for our lock acquisition attempts. + time(&startTime); + + // Try to get a distributed lock. + while(1) { + // This is an atomic activity. + // If multiple threads attempt to do it at the same time, only one will succeed. + // Winner will hold the lock until they release it voluntarily or + // until the Redis back-end removes this lock entry after the lease time ends. + // We will add the lease time to the current timestamp i.e. seconds elapsed since the epoch. + time_t new_lock_expiry_time = time(0) + (time_t)leaseTime; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + bool setnx_result_value = false; + + try { + setnx_result_value = redis_cluster->setnx(distributedLockKey, string("1")); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + SPLAPPTRC(L_ERROR, "a) Inside acquireLock, it failed with a Redis connection error for REDIS_SETNX_CMD. Exception: " << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Problem in atomic creation of the distributed lock. + SPLAPPTRC(L_ERROR, "b) Inside acquireLock, it failed with an error for REDIS_SETNX_CMD. Exception: " << exceptionString << ".", "RedisClusterPlusPlusDBLayer"); + return(false); + } + + if(setnx_result_value == true) { + // We got the lock. + // Set the expiration time for this lock key. + ostringstream expiryTimeInMillis; + expiryTimeInMillis << (leaseTime*1000.00); + long long ttlInMillis = streams_boost::lexical_cast(expiryTimeInMillis.str()); + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->psetex(distributedLockKey, ttlInMillis, "2"); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Is there an exception? + if(exceptionType != REDIS_PLUS_PLUS_NO_ERROR) { + SPLAPPTRC(L_ERROR, "c) Inside acquireLock, it failed with an exception for REDIS_PSETEX_CMD. Exception: " << exceptionString << ".", "RedisClusterPlusPlusDBLayer"); + // In any case, let us try the following code block which may also fail. + // Delete the erroneous lock data item we created. + try { + redis_cluster->del(distributedLockKey); + } catch (const Error &ex) { + // It is not good if this one fails. We can't do much about that. + } + + return(false); + } + + // We got the lock. + // Let us update the lock information now. + if(updateLockInformation(lockIdString, lkError, 1, new_lock_expiry_time, getpid()) == true) { + return(true); + } else { + // Some error occurred while updating the lock information. + // It will be in an inconsistent state. Let us release the lock. + // After than, we will continue in the while loop. + releaseLock(lock, lkError); + } + } else { + // We didn't get the lock. + // Let us check if the previous owner of this lock simply forgot to release it. + // In that case, we will release this expired lock. + // Read the time at which this lock is expected to expire. + uint32_t _lockUsageCnt = 0; + int32_t _lockExpirationTime = 0; + std::string _lockName = ""; + pid_t _lockOwningPid = 0; + + if (readLockInformation(lockIdString, lkError, _lockUsageCnt, _lockExpirationTime, _lockOwningPid, _lockName) == false) { + SPLAPPTRC(L_DEBUG, "Inside acquireLock, it failed for lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + } else { + // Is current time greater than the lock expiration time? + if ((_lockExpirationTime > 0) && (time(0) > (time_t)_lockExpirationTime)) { + // Time has passed beyond the lease of this lock. + // Lease expired for this lock. Original owner forgot to release the lock and simply left it hanging there without a valid lease. + releaseLock(lock, lkError); + } + } + } // End of if(setnx_result_value == true) + + // Someone else is holding on to this distributed lock. Wait for a while before trying again. + retryCnt++; + + if (retryCnt >= DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT) { + lkError.set("Unable to acquire the lock named " + lockIdString + ".", DL_GET_LOCK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside acquireLock, it failed for a lock named " << lockIdString << ". " << DL_GET_LOCK_ERROR, "RedisClusterPlusPlusDBLayer"); + // Our caller can check the error code and try to acquire the lock again. + return(false); + } + + // Check if we have gone past the maximum wait time the caller was willing to wait in order to acquire this lock. + time(&timeNow); + if (difftime(startTime, timeNow) > maxWaitTimeToAcquireLock) { + lkError.set("Unable to acquire the lock named " + lockIdString + " within the caller specified wait time.", DL_GET_LOCK_TIMEOUT_ERROR); + SPLAPPTRC(L_DEBUG, "Inside acquireLock, it failed to acquire the lock named " << lockIdString << + " within the caller specified wait time." << DL_GET_LOCK_TIMEOUT_ERROR, "RedisClusterPlusPlusDBLayer"); + // Our caller can check the error code and try to acquire the lock again. + return(false); + } + + // Yield control to other threads. Wait here with patience by doing an exponential back-off delay. + usleep(DPS_AND_DL_GET_LOCK_SLEEP_TIME * + (retryCnt%(DPS_AND_DL_GET_LOCK_MAX_RETRY_CNT/DPS_AND_DL_GET_LOCK_BACKOFF_DELAY_MOD_FACTOR))); + } // End of while(1) + } // End of acquireLock. + + void RedisClusterPlusPlusDBLayer::releaseLock(uint64_t lock, PersistenceError & lkError) { + SPLAPPTRC(L_DEBUG, "Inside releaseLock for lock id " << lock, "RedisClusterPlusPlusDBLayer"); + + ostringstream lockId; + lockId << lock; + string lockIdString = lockId.str(); + + // '7' + 'lock id' + 'dl_lock' => 1 + std::string distributedLockKey = DL_LOCK_TYPE + lockIdString + DL_LOCK_TOKEN; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->del(distributedLockKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("Unable to release the distributed lock id " + lockIdString + ". Possible connection error. Error=" + exceptionString, DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside releaseLock, it failed with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + lkError.set("Unable to release the distributed lock id " + lockIdString + ". Error=" + exceptionString, DL_LOCK_RELEASE_ERROR); + + SPLAPPTRC(L_ERROR, "Inside releaseLock, it failed to release a lock using REDIS_DEL_CMD. Error=" << exceptionString << ". rc=" << DL_LOCK_RELEASE_ERROR, "RedisClusterPlusPlusDBLayer"); + return; + } + + updateLockInformation(lockIdString, lkError, 0, 0, 0); + } // End of releaseLock. + + bool RedisClusterPlusPlusDBLayer::updateLockInformation(std::string const & lockIdString, + PersistenceError & lkError, uint32_t const & lockUsageCnt, int32_t const & lockExpirationTime, pid_t const & lockOwningPid) { + // Get the lock name for this lock. + uint32_t _lockUsageCnt = 0; + int32_t _lockExpirationTime = 0; + std::string _lockName = ""; + pid_t _lockOwningPid = 0; + + if (readLockInformation(lockIdString, lkError, _lockUsageCnt, _lockExpirationTime, _lockOwningPid, _lockName) == false) { + SPLAPPTRC(L_DEBUG, "Inside updateLockInformation, it failed for lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Let us update the lock information. + // '6' + 'lock id' ==> 'lock use count' + '_' + 'lock expiration time expressed as elapsed seconds since the epoch' + '_' + 'pid that owns this lock' + "_" + lock name' + std::string lockInfoKey = DL_LOCK_INFO_TYPE + lockIdString; + ostringstream lockInfoValue; + lockInfoValue << lockUsageCnt << "_" << lockExpirationTime << "_" << lockOwningPid << "_" << _lockName; + string lockInfoValueString = lockInfoValue.str(); + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + redis_cluster->set(lockInfoKey, lockInfoValueString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("updateLockInformation: Unable to update 'LockId:LockInfo' for " + _lockName + ". Error=" + exceptionString + ". Possible connection error. Application code may call the DPS reconnect API and then retry the failed operation.", DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside updateLockInformation, it failed with an exception. Error=" << exceptionString << ". rc=" << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Problem in updating the "LockId:LockInfo" entry in the cache. + lkError.set("Unable to update 'LockId:LockInfo' in the cache for a lock named " + _lockName + + ". Error=" + exceptionString, DL_LOCK_INFO_UPDATE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside updateLockInformation, it failed for a lock named " << _lockName << + ". Error=" << exceptionString << ". rc=" << + DL_LOCK_INFO_UPDATE_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + return(true); + } // End of updateLockInformation. + + bool RedisClusterPlusPlusDBLayer::readLockInformation(std::string const & lockIdString, PersistenceError & lkError, uint32_t & lockUsageCnt, int32_t & lockExpirationTime, pid_t & lockOwningPid, std::string & lockName) { + // Read the contents of the lock information. + lockName = ""; + + // Lock Info contains meta data information about a given lock. + // '6' + 'lock id' ==> 'lock use count' + '_' + 'lock expiration time expressed as elapsed seconds since the epoch' + '_' + 'pid that owns this lock' + "_" + lock name' + string lockInfoKey = DL_LOCK_INFO_TYPE + lockIdString; + + string get_result_value = ""; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto val = redis_cluster->get(lockInfoKey); + + if(val) { + get_result_value = *val; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("readLockInformation: Unable to get LockInfo for " + lockIdString + ". Error=" + exceptionString + ". Possible connection error. Application code may call the DPS reconnect API and then retry the failed operation.", DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside readLockInformation, it failed with an exception. Error=" << exceptionString << "rc=" << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Unable to get the LockInfo from our cache. + lkError.set("Unable to get LockInfo using the LockId " + lockIdString + + ". Exception=" + exceptionString, DL_GET_LOCK_INFO_ERROR); + return(false); + } + + std::string lockInfo = get_result_value; + // As shown in the comment line above, lock information is a string that has multiple pieces of + // information each separated by an underscore character. We are interested in all the three tokens (lock usage count, lock expiration time, lock name). + // Let us parse it now. + std::vector words; + streams_boost::split(words, lockInfo, streams_boost::is_any_of("_"), streams_boost::token_compress_on); + int32_t tokenCnt = 0; + lockUsageCnt = 0; + + for (std::vector::iterator it = words.begin(); it != words.end(); ++it) { + string tmpString = *it; + + switch(++tokenCnt) { + case 1: + if (tmpString.empty() == false) { + lockUsageCnt = streams_boost::lexical_cast(tmpString.c_str()); + } + + break; + + case 2: + if (tmpString.empty() == false) { + lockExpirationTime = streams_boost::lexical_cast(tmpString.c_str()); + } + + break; + + case 3: + if (tmpString.empty() == false) { + lockOwningPid = streams_boost::lexical_cast(tmpString.c_str()); + } + + break; + + case 4: + lockName = *it; + break; + + default: + // If we keep getting more than 3 tokens, then it means that the lock name has + // underscore character(s) in it. e-g: Super_Duper_Lock. + lockName += "_" + *it; + } // End of switch + } // End of for loop. + + if (lockName == "") { + // Unable to get the name of this lock. + lkError.set("Unable to get the lock name for lockId " + lockIdString + ".", DL_GET_LOCK_NAME_ERROR); + return(false); + } + + return(true); + } // End of readLockInformation. + + // This method will check if a lock exists for a given lock id. + bool RedisClusterPlusPlusDBLayer::lockIdExistsOrNot(string lockIdString, PersistenceError & lkError) { + string keyString = string(DL_LOCK_INFO_TYPE) + lockIdString; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(keyString); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("lockIdExistsOrNot: LockIdExistsOrNot: Unable to connect to the redis-cluster server(s). Application code may call the DPS reconnect API and then retry the failed operation.", DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "Inside lockIdExistsOrNot, it failed with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(false); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Unable to get the lock info for the given lock id. + lkError.set("LockIdExistsOrNot: Unable to get LockInfo for the lockId " + lockIdString + + ". Error=" + exceptionString, DL_GET_LOCK_INFO_ERROR); + return(false); + } + + if(exists_result_value == 1) { + return(true); + } else { + return(false); + } + } // End of lockIdExistsOrNot. + + // This method will return the process id that currently owns the given lock. + uint32_t RedisClusterPlusPlusDBLayer::getPidForLock(string const & name, PersistenceError & lkError) { + SPLAPPTRC(L_DEBUG, "Inside getPidForLock with a name " << name, "RedisClusterPlusPlusDBLayer"); + + string base64_encoded_name; + base64_encode(name, base64_encoded_name); + uint64_t lock = 0; + + // Let us first see if a lock with the given name already exists. + // In our Redis dps implementation, data item keys can have space characters. + // Inside Redis, all our lock names will have a mapping type indicator of + // "5" at the beginning followed by the actual lock name. + // '5' + 'lock name' ==> 'lock id' + std::string lockNameKey = DL_LOCK_NAME_TYPE + base64_encoded_name; + long long exists_result_value = 0; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + exists_result_value = redis_cluster->exists(lockNameKey); + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("a) getPidForLock: Unable to connect to the redis-cluster server(s). Error=" + exceptionString + ". Application code may call the DPS reconnect API and then retry the failed operation.", DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "a) Inside getPidForLock, it failed for the lock named " << name << " with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Unable to get the lock info for the given lock id. + lkError.set("b) getPidForLock: It failed for the lock name " + name + + " in the REDIS_EXISTS_CMD. Error=" + exceptionString, DL_GET_LOCK_INFO_ERROR); + return(false); + } + + if(exists_result_value == 0) { + // Lock with the given name doesn't exist. + lkError.set("d) Unable to find a lockName " + name + ".", DL_LOCK_NOT_FOUND_ERROR); + SPLAPPTRC(L_DEBUG, "d) Inside getPidForLock, unable to find the lockName " << name << ". " << DL_LOCK_NOT_FOUND_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + // This lock already exists in our cache. + // We can get the lockId and return it to the caller. + string get_result_value = ""; + exceptionString = ""; + exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto val = redis_cluster->get(lockNameKey); + + if(val) { + get_result_value = *val; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + lkError.set("e) getPidForLock: Unable to connect to the redis-cluster server(s). Error=" + exceptionString + ". Application code may call the DPS reconnect API and then retry the failed operation.", DL_CONNECTION_ERROR); + SPLAPPTRC(L_ERROR, "e) Inside getPidForLock, it failed for the lock named " << name << " with an exception. Error=" << exceptionString << ". Application code may call the DPS reconnect API and then retry the failed operation. " << DL_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + // Did we encounter a redis reply error? + if (exceptionType == REDIS_PLUS_PLUS_REPLY_ERROR || + exceptionType == REDIS_PLUS_PLUS_OTHER_ERROR) { + // Unable to get an existing lock id from the cache. + lkError.set("f) Unable to get the lockId for the lockName " + name + ". Error=" + + exceptionString, DL_GET_LOCK_ID_ERROR); + SPLAPPTRC(L_DEBUG, "f) Inside getPidForLock, it failed for the lockName " << name << + ". Error=" << exceptionString << ". rc=" << + DL_GET_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + if(get_result_value.length() <= 0) { + // Unable to get the lock information. It is an abnormal error. Convey this to the caller. + lkError.set("Redis returned an empty lockId for the lockName " + name + ".", DL_GET_LOCK_ID_ERROR); + SPLAPPTRC(L_DEBUG, "Inside getPidForLock, it failed with an empty lockId for the lockName " << name << ". " << DL_GET_LOCK_ID_ERROR, "RedisClusterPlusPlusDBLayer"); + return(0); + } + + lock = streams_boost::lexical_cast(get_result_value); + // Read the lock information. + ostringstream lockId; + lockId << lock; + string lockIdString = lockId.str(); + + uint32_t _lockUsageCnt = 0; + int32_t _lockExpirationTime = 0; + std::string _lockName = ""; + pid_t _lockOwningPid = 0; + + if (readLockInformation(lockIdString, lkError, _lockUsageCnt, _lockExpirationTime, _lockOwningPid, _lockName) == false) { + SPLAPPTRC(L_DEBUG, "Inside getPidForLock, it failed for lock id " << lockIdString << ". " << lkError.getErrorCode(), "RedisClusterPlusPlusDBLayer"); + return(0); + } else { + return(_lockOwningPid); + } + } // End of getPidForLock. + + void RedisClusterPlusPlusDBLayer::persist(PersistenceError & dbError){ + // As of Nov/2020, there is no support for Wait command in the redis-plus-plus library for redis_cluster. + // So, Wwe will use the generic command to execute this. + std::vector vec1; + vec1.push_back(string("WAIT")); + vec1.push_back("1"); + vec1.push_back("0"); + string result = ""; + runDataStoreCommand(vec1, result, dbError); + + if (result.compare(string("1")) != 0) { + // Didn't write to at least one slave + dbError.set("dpsPersist: return value should be at least 1.", DPS_MAKE_DURABLE_ERROR); + SPLAPPTRC(L_ERROR, "dpsPersist: WAIT return value should be 1. But it is not.", "RedisClusterPlusPlusDBLayer"); + } else{ + SPLAPPTRC(L_DEBUG, "dpsPersist, WAIT returned successfully, wrote to " << result << " replica.", "RedisClusterPlusPlusDBLayer"); + } + } // End of dpsPersist. + + // This method will return the status of the connection to the back-end data store. + bool RedisClusterPlusPlusDBLayer::isConnected() { + if (redis_cluster == NULL) { + // There is no active connection. + return(false); + } + + // We will simply do a read API for a dummy key. + // If it results in a connection error, that will tell us the status of the connection. + string get_result_value = ""; + string exceptionString = ""; + int exceptionType = REDIS_PLUS_PLUS_NO_ERROR; + + try { + auto val = redis_cluster->get("my_dummy_key"); + + if(val) { + get_result_value = *val; + } + } catch (const ReplyError &ex) { + // WRONGTYPE Operation against a key holding the wrong kind of value + exceptionString = ex.what(); + // Command execution error. + exceptionType = REDIS_PLUS_PLUS_REPLY_ERROR; + } catch (const TimeoutError &ex) { + // Reading or writing timeout + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const ClosedError &ex) { + // Connection has been closed. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const IoError &ex) { + // I/O error on the connection. + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_CONNECTION_ERROR; + } catch (const Error &ex) { + // Other errors + exceptionString = ex.what(); + // Connectivity related error. + exceptionType = REDIS_PLUS_PLUS_OTHER_ERROR; + } + + // Did we encounter a redis-cluster server connection error? + if (exceptionType == REDIS_PLUS_PLUS_CONNECTION_ERROR) { + SPLAPPTRC(L_ERROR, "Inside isConnected: Unable to connect to the redis-cluster server(s). Failed with an exception. Error=" << exceptionString << ". rc=" << DPS_CONNECTION_ERROR, "RedisClusterPlusPlusDBLayer"); + // Connection error. + return(false); + } else { + // Connection is active. + return(true); + } + } // End of isConnected. + + // This method will reestablish the status of the connection to the back-end data store. + bool RedisClusterPlusPlusDBLayer::reconnect(std::set & dbServers, PersistenceError & dbError) { + if (redis_cluster != NULL) { + // Delete the existing cluster connection object. + delete redis_cluster; + redis_cluster = NULL; + } + + connectToDatabase(dbServers, dbError); + + if(dbError.hasError()) { + // Connection didn't happen. + // Caller can query the error code and error string using two other DPS APIs meant for that purpose. + return(false); + } else { + // All good. + return(true); + } + } // End of reconnect. + +} } } } } + +using namespace com::ibm::streamsx::store; +using namespace com::ibm::streamsx::store::distributed; +extern "C" { + DBLayer * create(){ + return new RedisClusterPlusPlusDBLayer(); + } +} diff --git a/com.ibm.streamsx.dps/impl/src/RedisDBLayer.cpp b/com.ibm.streamsx.dps/impl/src/RedisDBLayer.cpp index ff0a074..2aea7f9 100644 --- a/com.ibm.streamsx.dps/impl/src/RedisDBLayer.cpp +++ b/com.ibm.streamsx.dps/impl/src/RedisDBLayer.cpp @@ -1,6 +1,6 @@ /* # Licensed Materials - Property of IBM -# Copyright IBM Corp. 2011, 2014 +# Copyright IBM Corp. 2011, 2020 # US Government Users Restricted Rights - Use, duplication or # disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. @@ -71,13 +71,8 @@ It is important to note that a Streams application designer/developer should car of his/her application will access the store simultaneously i.e. who puts what, who gets what and at what frequency from where etc. -This C++ project has a companion SPL project (058_data_sharing_between_non_fused_spl_custom_and_cpp_primitive_operators). -Please refer to the commentary in that SPL project file for learning about the procedure to do an -end-to-end test run involving the SPL code, serialization/deserialization code, -redis interface code (this file), and your redis infrastructure. - -As a first step, you should run the ./mk script from the C++ project directory (DistributedProcessStoreLib). -That will take care of building the .so file for the dps and copy it to the SPL project's impl/lib directory. +There are simple and advanced examples included in the DPS toolkit to test all the features described in the previous +paragraph. ================================================================================================================== */ @@ -107,6 +102,15 @@ That will take care of building the .so file for the dps and copy it to the SPL #include #include #include +#include +#include +// On the IBM Streams application Linux machines, it is a must to install opensssl and +// and openssl-devel RPM packages. The following include files and the SSL custom +// initialization logic below in this file will require openssl to be available. +// In particular, we will use the /lib64/libssl.so and /lib64/libcrypto.so libraries +// that are part of openssl. +#include +#include using namespace std; using namespace SPL; @@ -150,325 +154,526 @@ namespace distributed void RedisDBLayer::connectToDatabase(std::set const & dbServers, PersistenceError & dbError) { - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase", "RedisDBLayer"); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase", "RedisDBLayer"); + // Get the name, OS version and CPU type of this machine. + struct utsname machineDetails; + + if(uname(&machineDetails) < 0) { + dbError.set("Unable to get the machine/os/cpu details.", DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed to get the machine/os/cpu details. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); + return; + } else { + nameOfThisMachine = string(machineDetails.nodename); + osVersionOfThisMachine = string(machineDetails.sysname) + string(" ") + string(machineDetails.release); + cpuTypeOfThisMachine = string(machineDetails.machine); + } - // Get the name, OS version and CPU type of this machine. - struct utsname machineDetails; + string redisConnectionErrorMsg = "Unable to initialize the redis connection context."; + // Senthil added this block of code on May/02/2017. + // As part of the Redis configuration in the DPS config file, we now allow the user to specify + // an optional Redis authentication password as shown below. + // server:port:RedisPassword:ConnectionTimeoutValue:use_tls + string targetServerPassword = ""; + string targetServerName = ""; + int targetServerPort = 0; + int connectionTimeout = 0; + int useTls = -1; + int connectionAttemptCnt = 0; + + // Get the current thread id that is trying to make a connection to redis cluster. + int threadId = (int)gettid(); + + // When the Redis cluster releases with support for the hiredis client, then change this logic to + // take advantage of the Redis cluster features. + // + // If the user configured only one redis server, connect to it using unixsocket or TCP. + // If the user configured multiple redis servers, then we are going to do the client side + // partitioning. In that case, we will connect to all of them and get a separate handle + // and store them in an array of structures. + // + if (dbServers.size() == 1) { + // This means, no client side Redis partitioning. In addition, we will optionally + // support "TLS for Redis" only when a single redis server is configured. + redisPartitionCnt = 0; + // We have only one Redis server configured by the user. + for (std::set::iterator it=dbServers.begin(); it!=dbServers.end(); ++it) { + std::string serverName = *it; + // If the user has configured to use the unix domain socket, take care of that as well. + if (serverName == "unixsocket") { + redisPartitions[0].rdsc = redisConnectUnix((char *)"/tmp/redis.sock"); + } else { + // Redis server name can have port number, password, connection timeout value and + // use_tls=1 specified along with it --> MyHost:2345:xyz:5:use_tls + int32_t tokenCnt = 0; + // This technique to get the empty tokens while tokenizing a string is described in this URL: + // https://stackoverflow.com/questions/22331648/boosttokenizer-point-separated-but-also-keeping-empty-fields + typedef streams_boost::tokenizer > tokenizer; + streams_boost::char_separator sep( + ":", // dropped delimiters + "", // kept delimiters + streams_boost::keep_empty_tokens); // empty token policy + + STREAMS_BOOST_FOREACH(std::string token, tokenizer(serverName, sep)) { + tokenCnt++; + + if (tokenCnt == 1) { + // This must be our first token. + if (token != "") { + targetServerName = token; + } + } else if (tokenCnt == 2){ + // This must be our second token. + if (token != "") { + targetServerPort = atoi(token.c_str()); + } + + if (targetServerPort <= 0) { + targetServerPort = REDIS_SERVER_PORT; + } + } else if (tokenCnt == 3) { + // This must be our third token. + if (token != "") { + targetServerPassword = token; + } + } else if (tokenCnt == 4) { + // This must be our fourth token. + if (token != "") { + connectionTimeout = atoi(token.c_str()); + } - if(uname(&machineDetails) < 0) { - dbError.set("Unable to get the machine/os/cpu details.", DPS_INITIALIZE_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed to get the machine/os/cpu details. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); - return; - } else { - nameOfThisMachine = string(machineDetails.nodename); - osVersionOfThisMachine = string(machineDetails.sysname) + string(" ") + string(machineDetails.release); - cpuTypeOfThisMachine = string(machineDetails.machine); - } + if (connectionTimeout <= 0) { + // Set it to a default of 3 seconds. + connectionTimeout = 3; + } + } else if (tokenCnt == 5) { + // This must be our fifth token. + if (token != "") { + useTls = atoi(token.c_str()); + } - string redisConnectionErrorMsg = "Unable to initialize the redis connection context."; - // Senthil added this block of code on May/02/2017. - // As part of the Redis configuration in the DPS config file, we now allow the user to specify - // an optional Redis authentication password as shown below. - // server:port:RedisPassword - string targetServerPassword = ""; - string targetServerName = ""; - int targetServerPort = 0; - - // When the Redis cluster releases with support for the hiredis client, then change this logic to - // take advantage of the Redis cluster features. - // - // If the user configured only one redis server, connect to it using unixsocket or TCP. - // If the user configured multiple redis servers, then we are going to do the client side - // partitioning. In that case, we will connect to all of them and get a separate handle - // and store them in an array of structures. - // - if (dbServers.size() == 1) { - // This means, no client side Redis partitioning. - redisPartitionCnt = 0; - // We have only one Redis server configured by the user. - for (std::set::iterator it=dbServers.begin(); it!=dbServers.end(); ++it) { - std::string serverName = *it; - // If the user has configured to use the unix domain socket, take care of that as well. - if (serverName == "unixsocket") { - redisPartitions[0].rdsc = redisConnectUnix((char *)"/tmp/redis.sock"); - } else { - struct timeval timeout = { 1, 500000 }; // 1.5 seconds {tv_sec, tv_microsecs} - // Redis server name can have port number specified along with it --> MyHost:2345 - char serverNameBuf[300]; - strcpy(serverNameBuf, serverName.c_str()); - char *ptr = strtok(serverNameBuf, ":"); - - while(ptr) { - if (targetServerName == "") { - // This must be our first token. - targetServerName = string(ptr); - ptr = strtok(NULL, ":"); - } else if (targetServerPort == 0){ - // This must be our second token. - targetServerPort = atoi(ptr); - - if (targetServerPort == 0) { - targetServerPort = REDIS_SERVER_PORT; - } - - ptr = strtok(NULL, ":"); - } else if (targetServerPassword == "") { - // This must be our third token. - targetServerPassword = string(ptr); - // We are done. - break; - } - } - - if (targetServerName == "") { - // User only specified the server name and no port. - // (This is the case of server name followed by a : character with a missing port number) - targetServerName = serverName; - // In this case, use the default Redis server port. - targetServerPort = REDIS_SERVER_PORT; - } - - if (targetServerPort == 0) { - // User didn't give a Redis server port. - // Only a server name was given not followed by a : character. - // Use the default Redis server port. - targetServerPort = REDIS_SERVER_PORT; - } - - redisPartitions[0].rdsc = redisConnectWithTimeout(targetServerName.c_str(), targetServerPort, timeout); - } - - if (redisPartitions[0].rdsc == NULL || redisPartitions[0].rdsc->err) { - if (redisPartitions[0].rdsc) { - redisConnectionErrorMsg += " Connection error: " + string(redisPartitions[0].rdsc->errstr); - } + if (useTls < 0) { + // Set it to a default of 0 i.e. no TLS needed. + useTls = 0; + } - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - } else { - // We connected to at least one redis server. That is enough for our needs. - // If the user configured it with a Redis auth password, then we must authenticate now. - // If the authentication is successful, all good. If any error, Redis will send one of the - // following two errors: - // ERR invalid password (OR) ERR Client sent AUTH, but no password is set - if (targetServerPassword.length() > 0) { - std::string cmd = string(REDIS_AUTH_CMD) + targetServerPassword; - redis_reply = (redisReply*)redisCommand(redisPartitions[0].rdsc, cmd.c_str()); - - // If we get a NULL reply, then it indicates a redis server connection error. - if (redis_reply == NULL) { - // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - dbError.set("Unable to authenticate to the redis server(s). Possible connection breakage. " + std::string(redisPartitions[0].rdsc->errstr), DPS_CONNECTION_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication with an error " << string("Possible connection breakage. ") << DPS_CONNECTION_ERROR, "RedisDBLayer"); - return; - } - - if (redis_reply->type == REDIS_REPLY_ERROR) { - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - dbError.set("Unable to authenticate to the Redis server. Error msg=" + std::string(redis_reply->str), DPS_AUTHENTICATION_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication. error=" << redis_reply->str << ", rc=" << DPS_AUTHENTICATION_ERROR, "RedisDBLayer"); - freeReplyObject(redis_reply); - return; - } - - freeReplyObject(redis_reply); - } // End of Redis authentication. - - // Reset the error string. - redisConnectionErrorMsg = ""; - cout << "Successfully connected to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - break; - } - } + if (useTls > 0) { + // Set it to 1 i.e. TLS needed. + useTls = 1; + } + } // End of the long if-else block. + } // End of the Boost FOREACH loop. - // Check if there was any connection error. - if (redisConnectionErrorMsg != "") { - if (redisPartitions[0].rdsc != NULL) { - redisFree(redisPartitions[0].rdsc); - redisPartitions[0].rdsc = NULL; - } + if (targetServerName == "") { + // User only specified the server name and no port. + // (This is the case of server name followed by a : character with a missing port number) + targetServerName = serverName; + // In this case, use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } - dbError.set(redisConnectionErrorMsg, DPS_INITIALIZE_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); - return; - } - } else { - // We have more than one Redis server configured by the user. - // In our dps toolkit, we allow only upto 50 servers. (It is just our own limit). - if (dbServers.size() > 50) { - redisConnectionErrorMsg += " Too many Redis servers configured. DPS toolkit supports only a maximum of 50 Redis servers."; - dbError.set(redisConnectionErrorMsg, DPS_TOO_MANY_REDIS_SERVERS_CONFIGURED); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_TOO_MANY_REDIS_SERVERS_CONFIGURED, "RedisDBLayer"); - return; - } + if (targetServerPort <= 0) { + // User didn't give a Redis server port. + // Only a server name was given not followed by a : character. + // Use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } - redisPartitionCnt = dbServers.size(); - int32_t idx = -1; - // Now stay in a loop and connect to each of them. - for (std::set::iterator it=dbServers.begin(); it!=dbServers.end(); ++it) { - std::string serverName = *it; - struct timeval timeout = { 1, 500000 }; // 1.5 seconds {tv_sec, tv_microsecs} - // In the case of client side Redis server partitioning, we expect the user to configure the ports of - // their Redis servers starting from our REDIS_SERVER_PORT (base port number 6379) + 2 and go up by one for each new Redis server. - idx++; - - // Redis server name can have port number and password specified along with it --> MyHost:2345:MyPassword - targetServerName = ""; - targetServerPort = 0; - targetServerPassword = ""; - char serverNameBuf[300]; - strcpy(serverNameBuf, serverName.c_str()); - char *ptr = strtok(serverNameBuf, ":"); - - while(ptr) { - if (targetServerName == "") { - // This must be our first token. - targetServerName = string(ptr); - ptr = strtok(NULL, ":"); - } else if (targetServerPort == 0){ - // This must be our second token. - targetServerPort = atoi(ptr); - - if (targetServerPort == 0) { - targetServerPort = REDIS_SERVER_PORT; - } + // Password is already set to empty string at the time of variable declaration. + // Because of that, we are good even if the redis configuration string has this field as blank. - ptr = strtok(NULL, ":"); - } else if (targetServerPassword == "") { - // This must be our third token. - targetServerPassword = string(ptr); - // We are done. - break; - } + if (connectionTimeout <= 0) { + // User didn't configure a connectionTimeout field at all. So, set it to 3 seconds. + connectionTimeout = 3; } - if (targetServerName == "") { - // User only specified the server name and no port. - // (This is the case of server name followed by a : character with a missing port number) - targetServerName = serverName; - // In this case, use the default Redis server port. - targetServerPort = REDIS_SERVER_PORT; + if (useTls < 0) { + // User didn't configure a useTls field at all, So, set it to 0 i.e. no TLS needed. + useTls = 0; } - if (targetServerPort == 0) { - // User didn't give a Redis server port. - // Only a server name was given not followed by a : character. - // Use the default Redis server port. - targetServerPort = REDIS_SERVER_PORT; + // Use this line to test out the parsed tokens from the Redis configuration string. + string clusterPasswordUsage = "a"; + + if (targetServerPassword.length() <= 0) { + clusterPasswordUsage = "no"; } - redisPartitions[idx].rdsc = redisConnectWithTimeout(targetServerName.c_str(), targetServerPort, timeout); + cout << connectionAttemptCnt << ") ThreadId=" << threadId << ". Attempting to connect to the Redis server " << targetServerName << " on port " << targetServerPort << " with " << clusterPasswordUsage << " password. " << "connectionTimeout=" << connectionTimeout << ", use_tls=" << useTls << "." << endl; - if (redisPartitions[idx].rdsc == NULL || redisPartitions[idx].rdsc->err) { - if (redisPartitions[idx].rdsc) { - char serverNumber[50]; - sprintf(serverNumber, "%d", idx+1); - redisConnectionErrorMsg += " Connection error for Redis server " + serverName + ". Error=" + string(redisPartitions[idx].rdsc->errstr); - } + // Redis connection timeout structure format: {tv_sec, tv_microsecs} + struct timeval timeout = { connectionTimeout, 0 }; + redisPartitions[0].rdsc = redisConnectWithTimeout(targetServerName.c_str(), targetServerPort, timeout); + } // End of if (serverName == "unixsocket") { - // Since we got a connection error on one of the servers, let us disconnect from the servers that we successfully connected to so far. - // Loop backwards. - for(int32_t cnt=idx; cnt >=0; cnt--) { - if (redisPartitions[cnt].rdsc != NULL) { - redisFree(redisPartitions[cnt].rdsc); - redisPartitions[cnt].rdsc = NULL; - } - } + if (redisPartitions[0].rdsc == NULL || redisPartitions[0].rdsc->err) { + if (redisPartitions[0].rdsc) { + redisConnectionErrorMsg += " Connection error: " + string(redisPartitions[0].rdsc->errstr); + } - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - dbError.set(redisConnectionErrorMsg, DPS_INITIALIZE_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); - return; - } - - // If the user configured it with a Redis auth password, then we must authenticate now. - if (targetServerPassword.length() > 0) { - std::string cmd = string(REDIS_AUTH_CMD) + targetServerPassword; - redis_reply = (redisReply*)redisCommand(redisPartitions[idx].rdsc, cmd.c_str()); - - // If we get a NULL reply, then it indicates a redis server connection error. - if (redis_reply == NULL) { - // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - dbError.set("Unable to authenticate to the redis server(s). Possible connection breakage. " + std::string(redisPartitions[idx].rdsc->errstr), DPS_CONNECTION_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication with an error " << string("Possible connection breakage. ") << DPS_CONNECTION_ERROR, "RedisDBLayer"); - - // Since we got a connection error on one of the servers, let us disconnect from the servers that we successfully connected to so far. - // Loop backwards. - for(int32_t cnt=idx; cnt >=0; cnt--) { - if (redisPartitions[cnt].rdsc != NULL) { - redisFree(redisPartitions[cnt].rdsc); - redisPartitions[cnt].rdsc = NULL; - } - } + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << ". " << redisConnectionErrorMsg << endl; + } else { + // If the user has configured to use TLS, we must now establish the Redis server + // connection we made above to go over TLS. This has to be done before executing + // any Redis command including the password authentication command. + if (useTls == 1) { + // In order NOT to do any client side peer verification, the new SSL feature in + // hiredis library (as of Nov/2019) requires us to do the SSL context and + // SSL (i.e. ssl_st) structure initialization on our own with the appropriate + // SSL peer verification flags. + // hiredis SSL feature will always do the peer verification and hence we must + // do the disabling of the peer verification on our own. In our case, the Streams DPS client + // application always trusts the remote Redis server running either in an on-prem environment or + // in the IBM Cloud Compose Redis or in the AWS Elasticache Redis. After all, the remote Redis service + // instance is always created by the same entity i.e. customer team and it gets used by that same team. + // So, in a trusted setup such as this, we can avoid the overhead involved in the peer verification that + // happens before the SSL connection establishment with the remote Redis service. In spite of disabling the + // peer verification at the DPS (hiredis) client side, data going back and forth + // between the client and the Redis server will always be encrypted which is + // what is more important than the peer verification done at the client side. + // + // The following piece of SSL initialization code is from the following IBM Z URL: + // https://www.ibm.com/support/knowledgecenter/en/SSB23S_1.1.0.13/gtps7/s5sple2.html + // + SSL_CTX *ssl_ctx; + SSL *myssl; + + SSL_library_init(); + SSL_load_error_strings(); + + // Create a new SSL context block + ssl_ctx=SSL_CTX_new(SSLv23_client_method()); + + if (!ssl_ctx) { + cout << "Unable to do SSL connection to the Redis server " << targetServerName << " on port " << targetServerPort << ", Error=SSL_CTX_new failed." << endl; + dbError.set("Unable to do SSL connection to the redis server. Error=SSL_CTX_new failed." , DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during SSL connection with an error " << "'SSL_CTX_new failed'." << ", rc=" << DPS_CONNECTION_ERROR, "RedisDBLayer"); + return; + } - return; - } - - if (redis_reply->type == REDIS_REPLY_ERROR) { - cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - dbError.set("Unable to authenticate to the Redis server. Error msg=" + std::string(redis_reply->str), DPS_AUTHENTICATION_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication. error=" << redis_reply->str << ", rc=" << DPS_AUTHENTICATION_ERROR, "RedisDBLayer"); - - // Since we got an authentication error on one of the servers, let us disconnect from the servers that we successfully connected to so far. - // Loop backwards. - for(int32_t cnt=idx; cnt >=0; cnt--) { - if (redisPartitions[cnt].rdsc != NULL) { - redisFree(redisPartitions[cnt].rdsc); - redisPartitions[cnt].rdsc = NULL; - } - } + // Disable the deprecated SSLv2 and SSLv3 protocols in favor of the TLS protocol that superseded SSL. + SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + // Set for no peer/server verification. + SSL_CTX_set_verify(ssl_ctx,SSL_VERIFY_NONE,NULL); + // Create a new ssl object structure. + myssl=SSL_new(ssl_ctx); - freeReplyObject(redis_reply); - return; - } + if(!myssl) { + cout << "Unable to do SSL connection to the Redis server " << targetServerName << " on port " << targetServerPort << ", Error=SSL_new failed." << endl; + dbError.set("Unable to do SSL connection to the redis server. Error=SSL_new failed." , DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during SSL connection with an error " << "'SSL_new failed'." << ", rc=" << DPS_CONNECTION_ERROR, "RedisDBLayer"); + return; + } + + if (redisInitiateSSL(redisPartitions[0].rdsc, myssl) != REDIS_OK) { + cout << "Unable to do SSL connection to the Redis server " << targetServerName << " on port " << targetServerPort << ", Error=" << std::string(redisPartitions[0].rdsc->errstr) << endl; + dbError.set("Unable to do SSL connection to the redis server. " + std::string(redisPartitions[0].rdsc->errstr), DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during SSL connection with an error " << + std::string(redisPartitions[0].rdsc->errstr) << ", rc=" << DPS_CONNECTION_ERROR, "RedisDBLayer"); + return; + } + } // End of if (useTls == 1) + + // We connected to at least one redis server. That is enough for our needs. + // If the user configured it with a Redis auth password, then we must authenticate now. + // If the authentication is successful, all good. If any error, Redis will send one of the + // following two errors: + // ERR invalid password (OR) ERR Client sent AUTH, but no password is set + if (targetServerPassword.length() > 0) { + std::string cmd = string(REDIS_AUTH_CMD) + targetServerPassword; + redis_reply = (redisReply*)redisCommand(redisPartitions[0].rdsc, cmd.c_str()); + + // If we get a NULL reply, then it indicates a redis server connection error. + if (redis_reply == NULL) { + // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + dbError.set("Unable to authenticate to the redis server(s). Possible connection breakage. " + std::string(redisPartitions[0].rdsc->errstr), DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication with an error " << string("Possible connection breakage. ") << DPS_CONNECTION_ERROR, "RedisDBLayer"); + return; + } - freeReplyObject(redis_reply); - } // End of Redis authentication. + if (redis_reply->type == REDIS_REPLY_ERROR) { + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + dbError.set("Unable to authenticate to the Redis server. Error msg=" + std::string(redis_reply->str), DPS_AUTHENTICATION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication. error=" << redis_reply->str << ", rc=" << DPS_AUTHENTICATION_ERROR, "RedisDBLayer"); + freeReplyObject(redis_reply); + return; + } + + freeReplyObject(redis_reply); + } // End of Redis authentication. + + // Reset the error string. + redisConnectionErrorMsg = ""; + cout << "Successfully connected to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + break; + } // End of else block within which connection to single Redis server is done. + } // Outer for loop that iterates over just a single Redis server. + + // Check if there was any connection error. + if (redisConnectionErrorMsg != "") { + if (redisPartitions[0].rdsc != NULL) { + redisFree(redisPartitions[0].rdsc); + redisPartitions[0].rdsc = NULL; + } - cout << "Successfully connected to the Redis server " << targetServerName << " on port " << targetServerPort << endl; - } // End of for loop. - } + dbError.set(redisConnectionErrorMsg, DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); + return; + } + } else { + // We have more than one Redis server configured by the user. + // In our dps toolkit, we allow only upto 50 servers. (It is just our own limit). + // Please note that TLS is not allowed in a multi-server non-cluster Redis config. + // Because, IBM Cloud, AWS etc. will allow only one server for Redis non-cluster. + // For on-prem (roll your own), DPS toolkit allows grouping multiple + // non-cluster Redis servers to do its own sharding. In that special + // configuration, we will not support TLS. + if (dbServers.size() > 50) { + redisConnectionErrorMsg += " Too many Redis servers configured. DPS toolkit supports only a maximum of 50 Redis servers."; + dbError.set(redisConnectionErrorMsg, DPS_TOO_MANY_REDIS_SERVERS_CONFIGURED); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_TOO_MANY_REDIS_SERVERS_CONFIGURED, "RedisDBLayer"); + return; + } - // We have now made connection to one or more servers in a redis cluster. - // Let us check if the global storeId key:value pair is already there in the cache. - string keyString = string(DPS_AND_DL_GUID_KEY); - int32_t partitionIdx = getRedisServerPartitionIndex(keyString); - std::string cmd = string(REDIS_EXISTS_CMD) + keyString; - redis_reply = (redisReply*)redisCommand(redisPartitions[partitionIdx].rdsc, cmd.c_str()); + redisPartitionCnt = dbServers.size(); + int32_t idx = -1; + // Now stay in a loop and connect to each of them. + for (std::set::iterator it=dbServers.begin(); it!=dbServers.end(); ++it) { + std::string serverName = *it; + // In the case of client side Redis server partitioning, we expect the user to configure the ports of + // their Redis servers starting from our REDIS_SERVER_PORT (base port number 6379) + 2 and go up by one for each new Redis server. + idx++; + + // Redis server name can have port number, password and other things specified along with it. + // server:port:RedisPassword:ConnectionTimeoutValue:use_tls + string targetServerPassword = ""; + string targetServerName = ""; + int targetServerPort = 0; + int connectionTimeout = 0; + // TLS makes sense only when a single non-cluster redis server is configured (e-g: IBM Compose Redis server or AWS Elasticache Redis server or an on-prem single server non-cluster Redis). + // When the user configures multiple Redis servers, it is not practical to do TLS on all of them. + // In the case of multi-server Redis configuration, user can ignore the useTls configuration field. + // Even if user specifies something for that field, we will ignore it in the logic below. + int useTls = -1; + + // Redis server name can have port number, password, connection timeout value and + // use_tls=1 specified along with it --> MyHost:2345:xyz:5:use_tls + int32_t tokenCnt = 0; + + // This technique to get the empty tokens while tokenizing a string is described in this URL: + // https://stackoverflow.com/questions/22331648/boosttokenizer-point-separated-but-also-keeping-empty-fields + // + typedef streams_boost::tokenizer > tokenizer; + streams_boost::char_separator sep( + ":", // dropped delimiters + "", // kept delimiters + streams_boost::keep_empty_tokens); // empty token policy + + STREAMS_BOOST_FOREACH(std::string token, tokenizer(serverName, sep)) { + tokenCnt++; + + if (tokenCnt == 1) { + // This must be our first token. + if (token != "") { + targetServerName = token; + } + } else if (tokenCnt == 2){ + // This must be our second token. + if (token != "") { + targetServerPort = atoi(token.c_str()); + } + + if (targetServerPort <= 0) { + targetServerPort = REDIS_SERVER_PORT; + } + } else if (tokenCnt == 3) { + // This must be our third token. + if (token != "") { + targetServerPassword = token; + } + } else if (tokenCnt == 4) { + // This must be our fourth token. + if (token != "") { + connectionTimeout = atoi(token.c_str()); + } - // If we get a NULL reply, then it indicates a redis server connection error. - if (redis_reply == NULL) { - // This is how we can detect that a wrong redis server name is configured by the user or - // not even a single redis server daemon being up and running. - // On such errors, redis context will carry an error string. - // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. - dbError.set("Unable to connect to the redis server(s). " + std::string(redisPartitions[partitionIdx].rdsc->errstr), DPS_CONNECTION_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error " << DPS_CONNECTION_ERROR, "RedisDBLayer"); - return; - } + if (connectionTimeout <= 0) { + // Set it to a default of 3 seconds. + connectionTimeout = 3; + } + } else if (tokenCnt == 5) { + // This must be our fifth token. + if (token != "") { + useTls = atoi(token.c_str()); + } - if (redis_reply->type == REDIS_REPLY_ERROR) { - dbError.set("Unable to check the existence of the dps GUID key. Error=" + string(redis_reply->str), DPS_KEY_EXISTENCE_CHECK_ERROR); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed. Error=" << string(redis_reply->str) << ", rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisDBLayer"); - freeReplyObject(redis_reply); - return; - } + if (useTls < 0) { + // Set it to a default of 0 i.e. no TLS needed. + useTls = 0; + } - if (redis_reply->integer == (int)0) { - // It could be that our global store id is not there now. - // Let us create one with an initial value of 0. - // Redis setnx is an atomic operation. It will succeed only for the very first operator that - // attempts to do this setting after a redis server is started fresh. If some other operator - // already raced us ahead and created this guid_key, then our attempt below will be safely rejected. - freeReplyObject(redis_reply); - cmd = string(REDIS_SETNX_CMD) + keyString + string(" ") + string("0"); - redis_reply = (redisReply*)redisCommand(redisPartitions[partitionIdx].rdsc, cmd.c_str()); - } + if (useTls > 0) { + // Set it to 1 i.e. TLS needed. + useTls = 1; + } + } // End of the long if-else block. + } // End of the Boost FOREACH loop. + + if (targetServerName == "") { + // User only specified the server name and no port. + // (This is the case of server name followed by a : character with a missing port number) + targetServerName = serverName; + // In this case, use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } - freeReplyObject(redis_reply); - SPLAPPTRC(L_DEBUG, "Inside connectToDatabase done", "RedisDBLayer"); + if (targetServerPort <= 0) { + // User didn't give a Redis server port. + // Only a server name was given not followed by a : character. + // Use the default Redis server port. + targetServerPort = REDIS_SERVER_PORT; + } + + // Password is already set to empty string at the time of variable declaration. + // Because of that, we are good even if the redis configuration string has this field as blank. + + if (connectionTimeout <= 0) { + // User didn't configure a connectionTimeout field at all. So, set it to 3 seconds. + connectionTimeout = 3; + } + + // As noted in the commentary above, useTls is not applicable in a multi-server + // Redis configuration as it is not practical to do TLS for all of them. + // TLS makes sense only for a cloud service or on-prem based single Redis server. + // So, the useTls field will get ignored in the connection logic below. + if (useTls < 0) { + // User didn't configure a useTls field at all, So, set it to 0 i.e. no TLS needed. + useTls = 0; + } + + // Use this line to test out the parsed tokens from the Redis configuration string. + string clusterPasswordUsage = "a"; + + if (targetServerPassword.length() <= 0) { + clusterPasswordUsage = "no"; + } + + cout << connectionAttemptCnt << ") ThreadId=" << threadId << ". Attempting to connect to the Redis serve " << targetServerName << " on port " << targetServerPort << " with " << clusterPasswordUsage << " password. " << "connectionTimeout=" << connectionTimeout << ", use_tls=" << useTls << "." << endl; + + // Redis connection timeout structure format: {tv_sec, tv_microsecs} + struct timeval timeout = { connectionTimeout, 0 }; + redisPartitions[idx].rdsc = redisConnectWithTimeout(targetServerName.c_str(), targetServerPort, timeout); + + if (redisPartitions[idx].rdsc == NULL || redisPartitions[idx].rdsc->err) { + if (redisPartitions[idx].rdsc) { + char serverNumber[50]; + sprintf(serverNumber, "%d", idx+1); + redisConnectionErrorMsg += " Connection error for Redis server " + serverName + ". Error=" + string(redisPartitions[idx].rdsc->errstr); + } + + // Since we got a connection error on one of the servers, let us disconnect from the servers that we successfully connected to so far. + // Loop backwards. + for(int32_t cnt=idx; cnt >=0; cnt--) { + if (redisPartitions[cnt].rdsc != NULL) { + redisFree(redisPartitions[cnt].rdsc); + redisPartitions[cnt].rdsc = NULL; + } + } + + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + dbError.set(redisConnectionErrorMsg, DPS_INITIALIZE_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error '" << redisConnectionErrorMsg << "'. " << DPS_INITIALIZE_ERROR, "RedisDBLayer"); + return; + } + + // If the user configured it with a Redis auth password, then we must authenticate now. + if (targetServerPassword.length() > 0) { + std::string cmd = string(REDIS_AUTH_CMD) + targetServerPassword; + redis_reply = (redisReply*)redisCommand(redisPartitions[idx].rdsc, cmd.c_str()); + + // If we get a NULL reply, then it indicates a redis server connection error. + if (redis_reply == NULL) { + // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + dbError.set("Unable to authenticate to the redis server(s). Possible connection breakage. " + std::string(redisPartitions[idx].rdsc->errstr), DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication with an error " << string("Possible connection breakage. ") << DPS_CONNECTION_ERROR, "RedisDBLayer"); + + // Since we got a connection error on one of the servers, let us disconnect from the servers that we successfully connected to so far. + // Loop backwards. + for(int32_t cnt=idx; cnt >=0; cnt--) { + if (redisPartitions[cnt].rdsc != NULL) { + redisFree(redisPartitions[cnt].rdsc); + redisPartitions[cnt].rdsc = NULL; + } + } + + return; + } + + if (redis_reply->type == REDIS_REPLY_ERROR) { + cout << "Unable to connect to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + dbError.set("Unable to authenticate to the Redis server. Error msg=" + std::string(redis_reply->str), DPS_AUTHENTICATION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed during authentication. error=" << redis_reply->str << ", rc=" << DPS_AUTHENTICATION_ERROR, "RedisDBLayer"); + + // Since we got an authentication error on one of the servers, let us disconnect from the servers that we successfully connected to so far. + // Loop backwards. + for(int32_t cnt=idx; cnt >=0; cnt--) { + if (redisPartitions[cnt].rdsc != NULL) { + redisFree(redisPartitions[cnt].rdsc); + redisPartitions[cnt].rdsc = NULL; + } + } + + freeReplyObject(redis_reply); + return; + } + + freeReplyObject(redis_reply); + } // End of Redis authentication. + + cout << "Successfully connected to the Redis server " << targetServerName << " on port " << targetServerPort << endl; + } // End of for loop. + } // End of the else block that connects to multiple non-cluster Redis servers. + + // We have now made connection to one or more servers in a redis cluster. + // Let us check if the global storeId key:value pair is already there in the cache. + string keyString = string(DPS_AND_DL_GUID_KEY); + int32_t partitionIdx = getRedisServerPartitionIndex(keyString); + std::string cmd = string(REDIS_EXISTS_CMD) + keyString; + redis_reply = (redisReply*)redisCommand(redisPartitions[partitionIdx].rdsc, cmd.c_str()); + + // If we get a NULL reply, then it indicates a redis server connection error. + if (redis_reply == NULL) { + // This is how we can detect that a wrong redis server name is configured by the user or + // not even a single redis server daemon being up and running. + // On such errors, redis context will carry an error string. + // When this error occurs, we can't reuse that redis context for further server commands. This is a serious error. + dbError.set("Unable to connect to the redis server(s). " + std::string(redisPartitions[partitionIdx].rdsc->errstr), DPS_CONNECTION_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed with an error " << DPS_CONNECTION_ERROR, "RedisDBLayer"); + return; + } + + if (redis_reply->type == REDIS_REPLY_ERROR) { + dbError.set("Unable to check the existence of the dps GUID key. Error=" + string(redis_reply->str), DPS_KEY_EXISTENCE_CHECK_ERROR); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase, it failed. Error=" << string(redis_reply->str) << ", rc=" << DPS_KEY_EXISTENCE_CHECK_ERROR, "RedisDBLayer"); + freeReplyObject(redis_reply); + return; + } + + if (redis_reply->integer == (int)0) { + // It could be that our global store id is not there now. + // Let us create one with an initial value of 0. + // Redis setnx is an atomic operation. It will succeed only for the very first operator that + // attempts to do this setting after a redis server is started fresh. If some other operator + // already raced us ahead and created this guid_key, then our attempt below will be safely rejected. + freeReplyObject(redis_reply); + cmd = string(REDIS_SETNX_CMD) + keyString + string(" ") + string("0"); + redis_reply = (redisReply*)redisCommand(redisPartitions[partitionIdx].rdsc, cmd.c_str()); + } + + freeReplyObject(redis_reply); + SPLAPPTRC(L_DEBUG, "Inside connectToDatabase done", "RedisDBLayer"); } uint64_t RedisDBLayer::createStore(std::string const & name, diff --git a/com.ibm.streamsx.dps/info.xml b/com.ibm.streamsx.dps/info.xml index c2b2e7b..da5368f 100644 --- a/com.ibm.streamsx.dps/info.xml +++ b/com.ibm.streamsx.dps/info.xml @@ -5,7 +5,7 @@ The Distributed Process Store (DPS) toolkit enables multiple applications running processing elements (PEs) on one or more machines to share application specific state information. The shared information is stored in an external data store. This allows non-fused SPL, C++ and Java operators running on different machines to share information. The following external data stores are supported: -* Redis® versions 2.8.2+, 3.0.x and 3.2.x. +* Redis® versions 6.0 and higher. The toolkit consists of a set of native functions that provide access to shared state, and additional locking functionality to provide safe, concurrent access to the shared data. These functions can be used from anywhere inside a Streams application, whether it be SPL code, SPL functions, SPL native functions, C++ primitive operators. They also have counterparts written in Java that can be used from a Java primitive operator. The actual state information is stored separately in the distributed back-end in-memory store which is transparent to the Streams application. @@ -25,13 +25,26 @@ In order to ensure safe access (i.e. store operations do not override each other # Notes for supported data stores - * Redis 2.8.x: If you have a heavy workload of put/get requests, then using multiple Redis servers may give scaling to improve the overall throughput. You can manually configure and start multiple instances of Redis to listen and run on different ports and/or machines. The DPS toolkit will internally treat these Redis v2.8.x multiple instances as multiple shards and distribute the key-value pairs among those instances to improve performance with increased memory capacity. If you have multiple Redis servers configured, consider using the DPS Time To Live (TTL) APIs, discussed below. This will result in better scalability of your put/get requests than using the APIs that take the user created store id as a function argument. Choose the APIs according to your functional and scaling needs. - * Redis 3.0.x or 3.2.x: If you choose to use these versions to take advantage of the Redis built-in high availability and clustering, fail-over, replication and persistence features, it is important to note that, those new Redis 3.x features may impact the overall performance of reading from and writing data to the Redis cluster nodes. - * Redis 3.2.x servers are by default configured to block connections from remote machines and remote IP addresses other than the loopback (127.0.0.1) address. See the comments in the `redis.conf` configuration file for more information. This restriction means that if Streams is running on a machine different from the Redis server, you will need to reconfigure and restart the Redis server to accept remote connections. Make the following changes in your redis configuration file: + * Redis standalone (non-cluster): If you have a heavy workload of put/get requests, then using multiple Redis servers may give scaling to improve the overall throughput. You can manually configure and start multiple instances of Redis to listen and run on different ports and/or machines. The DPS toolkit will internally treat such multiple Redis non-cluster multiple instances as multiple shards and distribute the key-value pairs among those instances to improve performance with increased memory capacity. If you have multiple Redis servers configured, consider using the DPS Time To Live (TTL) APIs, discussed below. This will result in better scalability of your put/get requests than using the APIs that take the user created store id as a function argument. Choose the APIs according to your functional and scaling needs. + * Redis Cluster: If you choose to use v6.0 or higher to take advantage of the Redis built-in high availability and clustering, fail-over, replication and persistence features, it is important to note that the cluster features may impact the overall performance of reading from and writing data to the Redis cluster nodes. + * Redis servers are by default configured to block connections from remote machines and remote IP addresses other than the loopback (127.0.0.1) address. See the comments in the `redis.conf` configuration file for more information. This restriction means that if Streams is running on a machine different from the Redis server, you will need to reconfigure and restart the Redis server to accept remote connections. Make the following changes in your redis configuration file: * Comment out the line `bind 127.0.0.1` * Make sure `protected-mode` is set to `no`. * Restart Redis, making sure to specify the path to the modified configuration file. +* Redis server v6.0 and higher support TLS/SSL. To use the Redis TLS/SSL feature, it is necessary to have openssl and openssl-devel installed on all the IBM Streams machines. In particular, libssl.so and libcrypto.so files should be available on the Linux system directories (e-g: /lib64/libssl.so and /lib64/libcrypto.so.) + +* To successfully build this toolkit, your IBM Streams application development machine should have the following RPMs installed. + +curl +curl-devel +lua +lua-devel +openldap-devel +openssl-devel +cyrus-sasl +cyrus-sasl-devel + # Toolkit overview The functions provided by the DPS toolkit are divided into 3 categories: @@ -77,6 +90,10 @@ This is achieved using a trust based cooperative locking scheme to gain exclusiv To create applications that use the DPS Toolkit, you'll need to: 1. Install IBM InfoSphere Streams. Configure the product environment variables by entering the following command: source product-installation-root-directory/4.2.0.0/bin/streamsprofile.sh + + Download the most recent version of the DPS toolkit from here and extract it in your IBM Streams development machine. + https://github.com/IBMStreams/streamsx.dps + 2. Ensure that all of the following additional RPMs required by the DPS toolkit are present on your system: * curl * curl-devel @@ -84,12 +101,15 @@ To create applications that use the DPS Toolkit, you'll need to: * lua-devel * openssl-devel * openldap-devel + * cyrus-sasl + * cyrus-sasl-devel * ibverbs * ibverbs-devel 3. Install and configure an external key-value data store that is supported by DPS. -4. Configure the DPS toolkit to connect to the data store from step 3. Copy the DPS toolkit sample configuration file from `STREAMS_INSTALL/samples/com.ibm.streamsx.dps/DPSUsageFromSPL/etc/no-sql-kv-store-servers.cfg` to `your-project-directory/etc/no-sql-kv-store-servers.cfg` and edit the file as follows: +4. Configure the DPS toolkit to connect to the data store from step 3. Copy the DPS toolkit sample configuration file from `<YOUR_DPS_TOOLKIT_DIRECTORY>/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/etc/no-sql-kv-store-servers.cfg` to `your-project-directory/etc/no-sql-kv-store-servers.cfg` and edit the file as follows: * For Redis in non-cluster mode: `redis` should be the first line in the configuration file. Then specify the Redis server/IP address and port number separated by a colon, e.g. `Machine1:7002`. Each additional server should be specified on a new line. If you prefer to use the Unix domain socket instead of TCP, you can simply specify `unixsocket` instead of a server name. If you decide to use a unix domain socket, you must also ensure that your redis.conf file on the server side is configured properly for a unix domain socket pointing to `/tmp/redis.sock` file. - * For Redis v3.x in cluster mode: `redis-cluster` should be the first line in the file, followed by the server name or IP address and a port number for one of the master Redis nodes that is active in your Redis cluster. For example: `Machine1:30001`. + * For Redis in cluster mode: `redis-cluster-plus-plus` should be the first line in the file, followed by the server name or IP address and a port number for one of the master Redis nodes that is active in your Redis cluster. For example: `Machine1:30001`. + * It is suggested that you read the commentary section at the top of that configuration file to understand various options available for configuring your chosen no-sql database. * Each time you create a new SPL project, copy the configuration file from the example provided in the DPS toolkit to the /etc directory inside of your SPL project directory. Make any configuration changes needed to the file, based on your application's needs. * See the sample configuration file for additional examples. # Using the toolkit in SPL applications @@ -210,12 +230,12 @@ The following example shows how to check for errors, after a function call, in t # Additional Examples -To specifically learn how to call the DPS APIs from SPL native functions, C++ and Java primitive operators, see the samples included in `<STREAMS_INSTALL>/samples/com.ibm.streamsx.dps`. +To specifically learn how to call the DPS APIs from SPL native functions, C++ and Java primitive operators, see the samples included in `<STREAMS_INSTALL>/samples/com.ibm.streamsx.dps` or in `<YOUR_DPS_TOOLKIT_DIRECTORY>/samples` directory. The `advanced` sub-directory there contains examples that showcase bulk of the available DPS APIs. # Reference information [../../javadoc/dps/index.html| DPS Java API Reference] - 4.1.1 + 4.1.2 4.2.0.0 diff --git a/dependencies/Makefile b/dependencies/Makefile index 7438456..b3d68d9 100644 --- a/dependencies/Makefile +++ b/dependencies/Makefile @@ -8,6 +8,7 @@ PACKAGES += couchbase PACKAGES += curl PACKAGES += hiredis PACKAGES += hiredis-cluster +PACKAGES += redis-plus-plus PACKAGES += json-c PACKAGES += memcached PACKAGES += mongo-c diff --git a/dependencies/hiredis/Makefile b/dependencies/hiredis/Makefile index 80bdb63..3161708 100644 --- a/dependencies/hiredis/Makefile +++ b/dependencies/hiredis/Makefile @@ -1,7 +1,7 @@ include ../make.variable.include PKG_NAME := hiredis -VERSION := 0.13.3 +VERSION := 1.0.0 ARCHIVE := v$(VERSION).tar.gz ARCHIVE_INSTALL := $(PKG_NAME)-$(VERSION)-$(OS)-$(ARCH)-install-bin.tar.gz URL := https://github.com/redis/$(PKG_NAME)/archive/$(ARCHIVE) @@ -16,8 +16,8 @@ all: tar -C $(TARGETDIR) -xzvf $(ARCHIVE_INSTALL) $(ARCHIVE_INSTALL): $(PKG_DIR)/Makefile - $(MAKE) -C $(PKG_DIR) - $(MAKE) -C $(PKG_DIR) PREFIX=$(GEN_DIR) install + $(MAKE) -C $(PKG_DIR) USE_SSL=1 + $(MAKE) -C $(PKG_DIR) USE_SSL=1 PREFIX=$(GEN_DIR) install rm -f $(GEN_DIR)/lib/*.a rm -rf $(GEN_DIR)/lib/pkgconfig tar -C $(GEN_DIR) -czvf $(ARCHIVE_INSTALL) ./lib ./include diff --git a/dependencies/hiredis/hiredis-0.13.3-el6-ppc64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-el6-ppc64-install-bin.tar.gz deleted file mode 100644 index 5e6af2d..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-el6-ppc64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-el6-x86_64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-el6-x86_64-install-bin.tar.gz deleted file mode 100644 index e301621..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-el6-x86_64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-el7-ppc64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-el7-ppc64-install-bin.tar.gz deleted file mode 100644 index f76139d..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-el7-ppc64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-el7-ppc64le-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-el7-ppc64le-install-bin.tar.gz deleted file mode 100644 index 5cbe774..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-el7-ppc64le-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-el7-x86_64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-el7-x86_64-install-bin.tar.gz deleted file mode 100644 index 65b0cb5..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-el7-x86_64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-sles11-x86_64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-sles11-x86_64-install-bin.tar.gz deleted file mode 100644 index b7cc8fd..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-sles11-x86_64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-0.13.3-sles12-x86_64-install-bin.tar.gz b/dependencies/hiredis/hiredis-0.13.3-sles12-x86_64-install-bin.tar.gz deleted file mode 100644 index 32eaeed..0000000 Binary files a/dependencies/hiredis/hiredis-0.13.3-sles12-x86_64-install-bin.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/hiredis-1.0.0-el7-x86_64-install-bin.tar.gz b/dependencies/hiredis/hiredis-1.0.0-el7-x86_64-install-bin.tar.gz new file mode 100644 index 0000000..c65293b Binary files /dev/null and b/dependencies/hiredis/hiredis-1.0.0-el7-x86_64-install-bin.tar.gz differ diff --git a/dependencies/hiredis/patch/sds.h.patch b/dependencies/hiredis/patch/sds.h.patch index 1e8ec0e..9ecf8d9 100644 --- a/dependencies/hiredis/patch/sds.h.patch +++ b/dependencies/hiredis/patch/sds.h.patch @@ -1,20 +1,19 @@ ---- hiredis-0.13.3/sds.h.orig 2016-05-27 13:22:18.410499697 -0400 -+++ hiredis-0.13.3/sds.h 2016-05-27 13:37:08.880365569 -0400 -@@ -39,6 +39,9 @@ - #include "win32.h" - #endif +--- hiredis-1.0.0/sds.h.orig 2020-08-03 14:18:07.000000000 -0400 ++++ hiredis-1.0.0/sds.h 2020-10-10 13:43:56.988507551 -0400 +@@ -44,6 +44,9 @@ + #include + #include +#ifdef __cplusplus +extern "C" { +#endif typedef char *sds; - struct sdshdr { -@@ -102,4 +105,9 @@ - sds sdsRemoveFreeSpace(sds s); - size_t sdsAllocSize(sds s); + /* Note: sdshdr5 is never used, we just access the flags byte directly. +@@ -275,4 +278,8 @@ + int sdsTest(int argc, char *argv[]); + #endif -+ +#ifdef __cplusplus +} +#endif diff --git a/dependencies/hiredis/v0.13.3.tar.gz b/dependencies/hiredis/v0.13.3.tar.gz deleted file mode 100644 index d0270cf..0000000 Binary files a/dependencies/hiredis/v0.13.3.tar.gz and /dev/null differ diff --git a/dependencies/hiredis/v1.0.0.tar.gz b/dependencies/hiredis/v1.0.0.tar.gz new file mode 100644 index 0000000..c41632b Binary files /dev/null and b/dependencies/hiredis/v1.0.0.tar.gz differ diff --git a/dependencies/redis-plus-plus/Makefile b/dependencies/redis-plus-plus/Makefile new file mode 100644 index 0000000..c70fec8 --- /dev/null +++ b/dependencies/redis-plus-plus/Makefile @@ -0,0 +1,46 @@ +include ../make.variable.include + +PKG_NAME := redis-plus-plus +VERSION := 1.2.1 +ARCHIVE := $(PKG_NAME)-$(VERSION).tar.gz +ARCHIVE_INSTALL := $(PKG_NAME)-$(VERSION)-$(OS)-$(ARCH)-install-bin.tar.gz +PKG_DIR = $(PKG_NAME)-$(VERSION) +GEN_DIR := $(shell pwd)/gen +HIREDIS_PKG := hiredis +HIREDIS_VERSION := 1.0.0 +HIREDIS_PKG_DIR := $(HIREDIS_PKG)-$(HIREDIS_VERSION) +HIREDIS_SRC_DIR := $(HIREDIS_PKG)-$(HIREDIS_VERSION)-src + +all: + test -f $(ARCHIVE_INSTALL) || $(MAKE) $(ARCHIVE_INSTALL) + @echo "Checking to see TARGETDIR is set..." + @test -n "$(TARGETDIR)" || false + mkdir -p $(TARGETDIR) + tar -C $(TARGETDIR) -xzvf $(ARCHIVE_INSTALL) + rm -f $(ARCHIVE_INSTALL) + +$(ARCHIVE_INSTALL): $(PKG_DIR)/Makefile + $(MAKE) -C $(HIREDIS_SRC_DIR) PREFIX=../$(HIREDIS_PKG_DIR) USE_SSL=1 + $(MAKE) -C $(HIREDIS_SRC_DIR) PREFIX=../$(HIREDIS_PKG_DIR) USE_SSL=1 install + cd $(PKG_DIR) && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=../$(HIREDIS_PKG_DIR) -DCMAKE_INSTALL_PREFIX=../$(PKG_NAME) -DREDIS_PLUS_PLUS_BUILD_TEST=OFF -DREDIS_PLUS_PLUS_BUILD_STATIC=OFF -DREDIS_PLUS_PLUS_USE_TLS=ON + cd $(PKG_DIR) && $(MAKE) + cd $(PKG_DIR) && $(MAKE) install + rm -rf $(HIREDIS_SRC_DIR) + rm -rf $(HIREDIS_PKG_DIR) + rm -rf $(PKG_DIR) + tar -C $(PKG_NAME) -czvf $(ARCHIVE_INSTALL) ./lib ./include + rm -rf $(PKG_NAME) + +$(PKG_DIR)/Makefile: $(ARCHIVE) + tar -xvzf $(ARCHIVE) + tar -xvzf $(HIREDIS_PKG_DIR).tar.gz + mv $(HIREDIS_PKG_DIR) $(HIREDIS_SRC_DIR) + +clean: + rm -rf $(PKG_DIR) + rm -rf $(PKG_NAME) + rm -f $(ARCHIVE_INSTALL) + rm -rf $(HIREDIS_SRC_DIR) + rm -rf $(HIREDIS_PKG_DIR) + rm -rf $(TARGETDIR)/lib/libredis++* + rm -rf $(TARGETDIR)/include/sw diff --git a/dependencies/redis-plus-plus/hiredis-1.0.0.tar.gz b/dependencies/redis-plus-plus/hiredis-1.0.0.tar.gz new file mode 100644 index 0000000..c41632b Binary files /dev/null and b/dependencies/redis-plus-plus/hiredis-1.0.0.tar.gz differ diff --git a/dependencies/redis-plus-plus/redis-plus-plus-1.2.1.tar.gz b/dependencies/redis-plus-plus/redis-plus-plus-1.2.1.tar.gz new file mode 100644 index 0000000..d5b6ca5 Binary files /dev/null and b/dependencies/redis-plus-plus/redis-plus-plus-1.2.1.tar.gz differ diff --git a/samples/DPSUsageFromCpp/Makefile b/samples/DPSUsageFromCpp/Makefile index b0eb85f..717af59 100644 --- a/samples/DPSUsageFromCpp/Makefile +++ b/samples/DPSUsageFromCpp/Makefile @@ -22,7 +22,16 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +# +# This example includes a C++ operator that uses DPS APIs as well. +# In order to build that C++ operator with the correct .so files +# present in the DPS toolkit's impl directory, it is necessary to +# export the following. This will get used inside the C++ operator's +# impl/bin/archLevel file. +export DPS_TOOLKIT_HOME=$(HOME)/streamsx.dps/com.ibm.streamsx.dps SPLC_FLAGS ?= -a -t $(DPS_TOOLKIT_HOME) SPLC = $(STREAMS_INSTALL)/bin/sc diff --git a/samples/DPSUsageFromCpp/etc/no-sql-kv-store-servers.cfg b/samples/DPSUsageFromCpp/etc/no-sql-kv-store-servers.cfg index c7df47f..de4e93f 100644 --- a/samples/DPSUsageFromCpp/etc/no-sql-kv-store-servers.cfg +++ b/samples/DPSUsageFromCpp/etc/no-sql-kv-store-servers.cfg @@ -2,17 +2,189 @@ # # This file is a simple configuration file for the Streams DPS toolkit. # Any line starting with # is a comment line. -# You must correctly configure the following in this file: +# You must correctly configure the following in this file. # -# 1) In the very first non-comment regular line of this file indicate the name of the key value store that will be used. +# 1) In the very first non-comment regular line of this file, please tell us +# about the name of the key value store product you will be using. # It can ONLY be one of these NoSQL K/V data store product names supported by the DPS toolkit: # -# i) redis -# ii) redis-cluster +# i) memcached +# ii) redis +# iii) cassandra +# iv) cloudant +# v) hbase +# vi) mongo +# vii) couchbase +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. +# # # 2) Below the line containing the K/V data store product name, please list -# your valid server names or IP addresses. -# (Please see the toolkit documentation for the expected format of the server names for your chosen NoSQL data store product.) +# your valid memcached or redis or cassandra or cloudant or +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. +# (Please read below about the expected format of this for your chosen NoSQL data store product.) +# +# a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# b) For redis, (i.e. redis running in a non cluster mode) please specify one or more +# servers (name or IP address) one per line or if you are working on a single machine +# and if you prefer to use the Unix domain socket instead of TCP, you can simply +# specify unixsocket instead of a server name. For redis, if you decide to use the +# unixsocket, you must also ensure that your redis.conf file on the server side is +# configured properly for a unix domain socket pointing to /tmp/redis.sock file and +# the usual redis port set to 0. +# +# IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): +# (If you have a heavy workload of put/get requests, then using multiple Redis servers +# may give scaling to improve the overall throughput. If you are going to consider using +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit +# will automatically do the client side partitioning to shard (i.e. spread) your data across +# those servers. You may also decide to run multiple Redis server instances on a single +# machine to take advantage of the available CPU cores. +# If you run multiple Redis servers on a single machine, then you must configure each server +# instance with a unique port in a separate redis.conf.X file where X is your Redis server +# instance number. [For example, if you run 3 instances on the same machine, you can have +# redis.conf.1, redis.conf.2, and redis.conf.3] If you are planning to run multiple Redis server +# instances on the same machine, you may also want to assign unique PID file names in their +# respective configuration files. Having done this, you can start each of those +# redis server instances by running a command from the src directory of your redis installation +# directory: ./redis-server ../redis.conf.X (substitute X with your redis server instance number). +# When there are multiple Redis servers running on a single machine, then you must properly +# specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a +# Redis authentication password after the port number if the Redis environment is configured to +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 +# +# When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a +# good scaling factor for your high volume put/get requests in comparison to using the APIs that +# are based on the user created named stores. You are encouraged to choose between +# the TTL and non-TTL based APIs according to your functional and scaling needs. +# +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator +# server. For Cassandra, you can list either all or just only one of the +# seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# d) In Cloudant, you must have a personalized URL registered with the Cloudant public +# web offering or with a "Cloudant Local" on-premises private installation. +# Since you have to enter the user id and password below, it is better to create +# a user id specifically for working with the Streams dps toolkit. # +# For the server name, you must enter a single line in the following format: +# http://user:password@user.cloudant.com if you are using the Cloudant service on the web +# (OR) +# http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises +# Cloudant Local load balancer machine. +# +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# User id and password in the URL below should match the Linux user id and password on +# the machine where the HBase REST server program is running. If you have a multi-machine +# HBase cluster, then you can run the HBase REST server on multiple machines and configure +# one or more REST servers [one per line] below. Configuring multiple REST servers will +# let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple +# servers and that may be a factor in slightly improving the HBase read/write performance. +# REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# +# (e-g:) +# http://user:password@Machine1:8080 +# http://user:password@Machine3:8080 +# http://user:password@Machine3:8080 +# +# f) For Mongo, you can specify a single server or a replica set (for redundancy and +# high availability using automatic fail-over) or a sharded cluster's query router +# mongos seed servers (for HA, load balancing, high throughput and fail-over). +# Please provide it in the following way for one of those three modes of MongoDB configuration. +# +# (e-g for single server, replica set and sharded cluster in that order.) +# Machine1:27017 +# Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME +# Machine1:27069,Machine2:27069,Machine3:27069 +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] +# +# g) For Couchbase, you can specify one or more server names. Please specify one server name per line. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# +# (e-g:) +# user:password@Machine1 +# Machine2 +# Machine3 +# +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword +# (e-g:) +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# +# =============================================================================== redis -127.0.0.1:6379 +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/DPSUsageFromJava/Makefile b/samples/DPSUsageFromJava/Makefile index 2c60614..671997e 100644 --- a/samples/DPSUsageFromJava/Makefile +++ b/samples/DPSUsageFromJava/Makefile @@ -22,10 +22,14 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps - -# the dps-helper is copied from the toolkit to the impl/lib direcrory of the application -# to ensure it is included in the SAB file +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps + +# The dps-helper is copied from the DPS toolkit directory to the +# impl/java/lib directory of this application to ensure it is +# included in the SAB file. DPS_HELPER_JAR = $(DPS_TOOLKIT_HOME)/impl/java/lib/dps-helper.jar SPLC_FLAGS ?= -a -t $(DPS_TOOLKIT_HOME) @@ -40,8 +44,8 @@ distributed: java $(SPLC) $(SPLC_FLAGS) -M $(SPL_MAIN_COMPOSITE) $(SPL_CMD_ARGS) --output-directory=./output/JavaDPSDemo/Distributed java: $(JAVA_CLASS_FILES) - mkdir -p impl/lib - cp $(DPS_HELPER_JAR) impl/lib + mkdir -p impl/java/lib + cp $(DPS_HELPER_JAR) impl/java/lib JAVA_HOME=$(STREAMS_INSTALL)/java ant clean: diff --git a/samples/DPSUsageFromJava/build.xml b/samples/DPSUsageFromJava/build.xml index e812c08..b6b693d 100644 --- a/samples/DPSUsageFromJava/build.xml +++ b/samples/DPSUsageFromJava/build.xml @@ -48,8 +48,8 @@ - - + + diff --git a/samples/DPSUsageFromJava/etc/no-sql-kv-store-servers.cfg b/samples/DPSUsageFromJava/etc/no-sql-kv-store-servers.cfg old mode 100755 new mode 100644 index c7df47f..de4e93f --- a/samples/DPSUsageFromJava/etc/no-sql-kv-store-servers.cfg +++ b/samples/DPSUsageFromJava/etc/no-sql-kv-store-servers.cfg @@ -2,17 +2,189 @@ # # This file is a simple configuration file for the Streams DPS toolkit. # Any line starting with # is a comment line. -# You must correctly configure the following in this file: +# You must correctly configure the following in this file. # -# 1) In the very first non-comment regular line of this file indicate the name of the key value store that will be used. +# 1) In the very first non-comment regular line of this file, please tell us +# about the name of the key value store product you will be using. # It can ONLY be one of these NoSQL K/V data store product names supported by the DPS toolkit: # -# i) redis -# ii) redis-cluster +# i) memcached +# ii) redis +# iii) cassandra +# iv) cloudant +# v) hbase +# vi) mongo +# vii) couchbase +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. +# # # 2) Below the line containing the K/V data store product name, please list -# your valid server names or IP addresses. -# (Please see the toolkit documentation for the expected format of the server names for your chosen NoSQL data store product.) +# your valid memcached or redis or cassandra or cloudant or +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. +# (Please read below about the expected format of this for your chosen NoSQL data store product.) +# +# a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# b) For redis, (i.e. redis running in a non cluster mode) please specify one or more +# servers (name or IP address) one per line or if you are working on a single machine +# and if you prefer to use the Unix domain socket instead of TCP, you can simply +# specify unixsocket instead of a server name. For redis, if you decide to use the +# unixsocket, you must also ensure that your redis.conf file on the server side is +# configured properly for a unix domain socket pointing to /tmp/redis.sock file and +# the usual redis port set to 0. +# +# IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): +# (If you have a heavy workload of put/get requests, then using multiple Redis servers +# may give scaling to improve the overall throughput. If you are going to consider using +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit +# will automatically do the client side partitioning to shard (i.e. spread) your data across +# those servers. You may also decide to run multiple Redis server instances on a single +# machine to take advantage of the available CPU cores. +# If you run multiple Redis servers on a single machine, then you must configure each server +# instance with a unique port in a separate redis.conf.X file where X is your Redis server +# instance number. [For example, if you run 3 instances on the same machine, you can have +# redis.conf.1, redis.conf.2, and redis.conf.3] If you are planning to run multiple Redis server +# instances on the same machine, you may also want to assign unique PID file names in their +# respective configuration files. Having done this, you can start each of those +# redis server instances by running a command from the src directory of your redis installation +# directory: ./redis-server ../redis.conf.X (substitute X with your redis server instance number). +# When there are multiple Redis servers running on a single machine, then you must properly +# specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a +# Redis authentication password after the port number if the Redis environment is configured to +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 +# +# When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a +# good scaling factor for your high volume put/get requests in comparison to using the APIs that +# are based on the user created named stores. You are encouraged to choose between +# the TTL and non-TTL based APIs according to your functional and scaling needs. +# +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator +# server. For Cassandra, you can list either all or just only one of the +# seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# d) In Cloudant, you must have a personalized URL registered with the Cloudant public +# web offering or with a "Cloudant Local" on-premises private installation. +# Since you have to enter the user id and password below, it is better to create +# a user id specifically for working with the Streams dps toolkit. # +# For the server name, you must enter a single line in the following format: +# http://user:password@user.cloudant.com if you are using the Cloudant service on the web +# (OR) +# http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises +# Cloudant Local load balancer machine. +# +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# User id and password in the URL below should match the Linux user id and password on +# the machine where the HBase REST server program is running. If you have a multi-machine +# HBase cluster, then you can run the HBase REST server on multiple machines and configure +# one or more REST servers [one per line] below. Configuring multiple REST servers will +# let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple +# servers and that may be a factor in slightly improving the HBase read/write performance. +# REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# +# (e-g:) +# http://user:password@Machine1:8080 +# http://user:password@Machine3:8080 +# http://user:password@Machine3:8080 +# +# f) For Mongo, you can specify a single server or a replica set (for redundancy and +# high availability using automatic fail-over) or a sharded cluster's query router +# mongos seed servers (for HA, load balancing, high throughput and fail-over). +# Please provide it in the following way for one of those three modes of MongoDB configuration. +# +# (e-g for single server, replica set and sharded cluster in that order.) +# Machine1:27017 +# Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME +# Machine1:27069,Machine2:27069,Machine3:27069 +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] +# +# g) For Couchbase, you can specify one or more server names. Please specify one server name per line. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# +# (e-g:) +# user:password@Machine1 +# Machine2 +# Machine3 +# +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword +# (e-g:) +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# +# =============================================================================== redis -127.0.0.1:6379 +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/DPSUsageFromJava/impl/java/src/application/DataStoreTester.java b/samples/DPSUsageFromJava/impl/java/src/application/DataStoreTester.java index a8ebc4c..89dc333 100644 --- a/samples/DPSUsageFromJava/impl/java/src/application/DataStoreTester.java +++ b/samples/DPSUsageFromJava/impl/java/src/application/DataStoreTester.java @@ -27,16 +27,15 @@ @PrimitiveOperator(name="DataStoreTester", namespace="application", description="This operator is a no-op. It exists for the purpose of demonstrating how to successfully fuse two Java operators that use the DPS toolkit. Because it is fused with the TickerIdGenerator operator, the SharedLoader annotation is used in both classes. See the operators' source for more information.") @InputPorts({@InputPortSet(description="Port that ingests tuples", cardinality=1, optional=false, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious), @InputPortSet(description="Optional input ports", optional=true, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious)}) -//To compile Add the DPS toolkit's Java library (dps-helper.jar) to the toolkit path of this operator. -//There are 2 ways to do this: -//1. If your application will have access to the Streams install location at runtimen, then you can specify the full path to the location of the dps-helper.jar file present inside the DPS toolkit as follows: +//Add the DPS toolkit's Java library (dps-helper.jar) to the path of this operator. +//There are 2 ways to do this: +//1. If your application will have access to the Streams install location at runtime, then you can specify the full path to the location of the dps-helper.jar file present inside the DPS toolkit as follows: //@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") -//if that path will be accessible at runtime. +//if that path will be accessible at compile time and at runtime. -//2. Or, you can copy the dps-helper.jar from /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar into the impl/lib folder of this application and reference it as follows: -@Libraries("impl/lib/dps-helper.jar") -//we choose option 2, because the Makefile will copy the dps-helper from the toolkit location to impl/lib +//2. Or, you can copy the dps-helper.jar either from the /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar or from a more recent version of the DPS toolkit directory into the impl/java/lib folder of this application and reference it as follows. You will have to create the lib sub-directory inside impl/java of this application before copying the jar file there. It is done in the Makefile of this example. If you choose this option, then you must ensure that the @Libraries shown above is commented out and the following one is uncommented. +@Libraries("impl/java/lib/dps-helper.jar") // Add the following annotation if you are going to fuse this Java operator with other Java operators that will also use // the DPS APIs. In that case, it is necessary to add the following annotation so that the fused PE will use a shared class loader. diff --git a/samples/DPSUsageFromJava/impl/java/src/application/TickerIdGenerator.java b/samples/DPSUsageFromJava/impl/java/src/application/TickerIdGenerator.java index fa49f4e..60dcddf 100644 --- a/samples/DPSUsageFromJava/impl/java/src/application/TickerIdGenerator.java +++ b/samples/DPSUsageFromJava/impl/java/src/application/TickerIdGenerator.java @@ -46,16 +46,15 @@ " See Main.spl for examples on how to iterate over the contents of a store") @InputPorts({@InputPortSet(description="Port that ingests tuples", cardinality=1, optional=false, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious), @InputPortSet(description="Optional input ports", optional=true, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious)}) @OutputPorts({@OutputPortSet(description="Port that produces tuples", cardinality=1, optional=false, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating), @OutputPortSet(description="Optional output ports", optional=true, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating)}) + //Add the DPS toolkit's Java library (dps-helper.jar) to the path of this operator. //There are 2 ways to do this: - //1. If your application will have access to the Streams install location at runtime, then you can specify the full path to the location of the dps-helper.jar file present inside the DPS toolkit as follows: //@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") -//if that path will be accessible at runtime. +//if that path will be accessible at compile time and at runtime. -//2. Or, you can copy the dps-helper.jar from /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar into the impl/lib folder of this application and reference it as follows: -@Libraries("impl/lib/dps-helper.jar") -// we choose option 2, because the Makefile will copy the dps-helper from the toolkit location to impl/lib +//2. Or, you can copy the dps-helper.jar either from the /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar or from a more recent version of the DPS toolkit directory into the impl/java/lib folder of this application and reference it as follows. You will have to create the lib sub-directory inside impl/java of this application before copying the jar file there. It is done in the Makefile of this example. If you choose this option, then you must ensure that the @Libraries shown above is commented out and the following one is uncommented. +@Libraries("impl/java/lib/dps-helper.jar") //Next, add the following annotation if you are going to fuse this Java operator with other Java operators that will also use //the DPS APIs. In that case, it is necessary to add the following annotation so that the fused PE will use a shared class loader. diff --git a/samples/DPSUsageFromSPL/Makefile b/samples/DPSUsageFromSPL/Makefile index c6ca5ed..3dd9426 100644 --- a/samples/DPSUsageFromSPL/Makefile +++ b/samples/DPSUsageFromSPL/Makefile @@ -22,7 +22,10 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps SPLC_FLAGS ?= -a -t $(DPS_TOOLKIT_HOME) SPLC = $(STREAMS_INSTALL)/bin/sc diff --git a/samples/DPSUsageFromSPL/etc/no-sql-kv-store-servers.cfg b/samples/DPSUsageFromSPL/etc/no-sql-kv-store-servers.cfg old mode 100755 new mode 100644 index c7df47f..de4e93f --- a/samples/DPSUsageFromSPL/etc/no-sql-kv-store-servers.cfg +++ b/samples/DPSUsageFromSPL/etc/no-sql-kv-store-servers.cfg @@ -2,17 +2,189 @@ # # This file is a simple configuration file for the Streams DPS toolkit. # Any line starting with # is a comment line. -# You must correctly configure the following in this file: +# You must correctly configure the following in this file. # -# 1) In the very first non-comment regular line of this file indicate the name of the key value store that will be used. +# 1) In the very first non-comment regular line of this file, please tell us +# about the name of the key value store product you will be using. # It can ONLY be one of these NoSQL K/V data store product names supported by the DPS toolkit: # -# i) redis -# ii) redis-cluster +# i) memcached +# ii) redis +# iii) cassandra +# iv) cloudant +# v) hbase +# vi) mongo +# vii) couchbase +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. +# # # 2) Below the line containing the K/V data store product name, please list -# your valid server names or IP addresses. -# (Please see the toolkit documentation for the expected format of the server names for your chosen NoSQL data store product.) +# your valid memcached or redis or cassandra or cloudant or +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. +# (Please read below about the expected format of this for your chosen NoSQL data store product.) +# +# a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# b) For redis, (i.e. redis running in a non cluster mode) please specify one or more +# servers (name or IP address) one per line or if you are working on a single machine +# and if you prefer to use the Unix domain socket instead of TCP, you can simply +# specify unixsocket instead of a server name. For redis, if you decide to use the +# unixsocket, you must also ensure that your redis.conf file on the server side is +# configured properly for a unix domain socket pointing to /tmp/redis.sock file and +# the usual redis port set to 0. +# +# IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): +# (If you have a heavy workload of put/get requests, then using multiple Redis servers +# may give scaling to improve the overall throughput. If you are going to consider using +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit +# will automatically do the client side partitioning to shard (i.e. spread) your data across +# those servers. You may also decide to run multiple Redis server instances on a single +# machine to take advantage of the available CPU cores. +# If you run multiple Redis servers on a single machine, then you must configure each server +# instance with a unique port in a separate redis.conf.X file where X is your Redis server +# instance number. [For example, if you run 3 instances on the same machine, you can have +# redis.conf.1, redis.conf.2, and redis.conf.3] If you are planning to run multiple Redis server +# instances on the same machine, you may also want to assign unique PID file names in their +# respective configuration files. Having done this, you can start each of those +# redis server instances by running a command from the src directory of your redis installation +# directory: ./redis-server ../redis.conf.X (substitute X with your redis server instance number). +# When there are multiple Redis servers running on a single machine, then you must properly +# specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a +# Redis authentication password after the port number if the Redis environment is configured to +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 +# +# When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a +# good scaling factor for your high volume put/get requests in comparison to using the APIs that +# are based on the user created named stores. You are encouraged to choose between +# the TTL and non-TTL based APIs according to your functional and scaling needs. +# +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator +# server. For Cassandra, you can list either all or just only one of the +# seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# d) In Cloudant, you must have a personalized URL registered with the Cloudant public +# web offering or with a "Cloudant Local" on-premises private installation. +# Since you have to enter the user id and password below, it is better to create +# a user id specifically for working with the Streams dps toolkit. # +# For the server name, you must enter a single line in the following format: +# http://user:password@user.cloudant.com if you are using the Cloudant service on the web +# (OR) +# http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises +# Cloudant Local load balancer machine. +# +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# User id and password in the URL below should match the Linux user id and password on +# the machine where the HBase REST server program is running. If you have a multi-machine +# HBase cluster, then you can run the HBase REST server on multiple machines and configure +# one or more REST servers [one per line] below. Configuring multiple REST servers will +# let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple +# servers and that may be a factor in slightly improving the HBase read/write performance. +# REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# +# (e-g:) +# http://user:password@Machine1:8080 +# http://user:password@Machine3:8080 +# http://user:password@Machine3:8080 +# +# f) For Mongo, you can specify a single server or a replica set (for redundancy and +# high availability using automatic fail-over) or a sharded cluster's query router +# mongos seed servers (for HA, load balancing, high throughput and fail-over). +# Please provide it in the following way for one of those three modes of MongoDB configuration. +# +# (e-g for single server, replica set and sharded cluster in that order.) +# Machine1:27017 +# Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME +# Machine1:27069,Machine2:27069,Machine3:27069 +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] +# +# g) For Couchbase, you can specify one or more server names. Please specify one server name per line. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# +# (e-g:) +# user:password@Machine1 +# Machine2 +# Machine3 +# +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword +# (e-g:) +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# +# =============================================================================== redis -127.0.0.1:6379 +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/DpsTTLCompositesSample/Makefile b/samples/DpsTTLCompositesSample/Makefile index 439c0df..38ae9cc 100644 --- a/samples/DpsTTLCompositesSample/Makefile +++ b/samples/DpsTTLCompositesSample/Makefile @@ -22,7 +22,10 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps:../../com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps SPLC_FLAGS ?= -a -t $(DPS_TOOLKIT_HOME) SPLC = $(STREAMS_INSTALL)/bin/sc diff --git a/samples/DpsTTLCompositesSample/etc/no-sql-kv-store-servers.cfg b/samples/DpsTTLCompositesSample/etc/no-sql-kv-store-servers.cfg old mode 100755 new mode 100644 index dd5150f..de4e93f --- a/samples/DpsTTLCompositesSample/etc/no-sql-kv-store-servers.cfg +++ b/samples/DpsTTLCompositesSample/etc/no-sql-kv-store-servers.cfg @@ -1,2 +1,190 @@ +# =============================================================================== +# +# This file is a simple configuration file for the Streams DPS toolkit. +# Any line starting with # is a comment line. +# You must correctly configure the following in this file. +# +# 1) In the very first non-comment regular line of this file, please tell us +# about the name of the key value store product you will be using. +# It can ONLY be one of these NoSQL K/V data store product names supported by the DPS toolkit: +# +# i) memcached +# ii) redis +# iii) cassandra +# iv) cloudant +# v) hbase +# vi) mongo +# vii) couchbase +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. +# +# +# 2) Below the line containing the K/V data store product name, please list +# your valid memcached or redis or cassandra or cloudant or +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. +# (Please read below about the expected format of this for your chosen NoSQL data store product.) +# +# a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# b) For redis, (i.e. redis running in a non cluster mode) please specify one or more +# servers (name or IP address) one per line or if you are working on a single machine +# and if you prefer to use the Unix domain socket instead of TCP, you can simply +# specify unixsocket instead of a server name. For redis, if you decide to use the +# unixsocket, you must also ensure that your redis.conf file on the server side is +# configured properly for a unix domain socket pointing to /tmp/redis.sock file and +# the usual redis port set to 0. +# +# IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): +# (If you have a heavy workload of put/get requests, then using multiple Redis servers +# may give scaling to improve the overall throughput. If you are going to consider using +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit +# will automatically do the client side partitioning to shard (i.e. spread) your data across +# those servers. You may also decide to run multiple Redis server instances on a single +# machine to take advantage of the available CPU cores. +# If you run multiple Redis servers on a single machine, then you must configure each server +# instance with a unique port in a separate redis.conf.X file where X is your Redis server +# instance number. [For example, if you run 3 instances on the same machine, you can have +# redis.conf.1, redis.conf.2, and redis.conf.3] If you are planning to run multiple Redis server +# instances on the same machine, you may also want to assign unique PID file names in their +# respective configuration files. Having done this, you can start each of those +# redis server instances by running a command from the src directory of your redis installation +# directory: ./redis-server ../redis.conf.X (substitute X with your redis server instance number). +# When there are multiple Redis servers running on a single machine, then you must properly +# specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a +# Redis authentication password after the port number if the Redis environment is configured to +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 +# +# When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a +# good scaling factor for your high volume put/get requests in comparison to using the APIs that +# are based on the user created named stores. You are encouraged to choose between +# the TTL and non-TTL based APIs according to your functional and scaling needs. +# +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator +# server. For Cassandra, you can list either all or just only one of the +# seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# +# (e-g:) +# Machine1 +# Machine2 +# Machine3 +# +# d) In Cloudant, you must have a personalized URL registered with the Cloudant public +# web offering or with a "Cloudant Local" on-premises private installation. +# Since you have to enter the user id and password below, it is better to create +# a user id specifically for working with the Streams dps toolkit. +# +# For the server name, you must enter a single line in the following format: +# http://user:password@user.cloudant.com if you are using the Cloudant service on the web +# (OR) +# http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises +# Cloudant Local load balancer machine. +# +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# User id and password in the URL below should match the Linux user id and password on +# the machine where the HBase REST server program is running. If you have a multi-machine +# HBase cluster, then you can run the HBase REST server on multiple machines and configure +# one or more REST servers [one per line] below. Configuring multiple REST servers will +# let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple +# servers and that may be a factor in slightly improving the HBase read/write performance. +# REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# +# (e-g:) +# http://user:password@Machine1:8080 +# http://user:password@Machine3:8080 +# http://user:password@Machine3:8080 +# +# f) For Mongo, you can specify a single server or a replica set (for redundancy and +# high availability using automatic fail-over) or a sharded cluster's query router +# mongos seed servers (for HA, load balancing, high throughput and fail-over). +# Please provide it in the following way for one of those three modes of MongoDB configuration. +# +# (e-g for single server, replica set and sharded cluster in that order.) +# Machine1:27017 +# Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME +# Machine1:27069,Machine2:27069,Machine3:27069 +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] +# +# g) For Couchbase, you can specify one or more server names. Please specify one server name per line. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# +# (e-g:) +# user:password@Machine1 +# Machine2 +# Machine3 +# +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword +# (e-g:) +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile +# (e-g:) +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# +# =============================================================================== redis -localhost:6379 \ No newline at end of file +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/Makefile b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/Makefile index 3cf141d..fa7914d 100644 --- a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/Makefile +++ b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/Makefile @@ -22,7 +22,10 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps # If the user wants to use a different version of # the DPS toolkit than the one shipped in the @@ -30,7 +33,14 @@ DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps # always set the STREAMS_SPLPATH environment variable # and point to a specific version of the DPS toolkit. ifndef STREAMS_SPLPATH - SPLC_FLAGS ?= -a -z -t $(DPS_TOOLKIT_HOME) + #SPLC_FLAGS ?= -a -z -t $(DPS_TOOLKIT_HOME) + SPLC_FLAGS ?= -a -z + # This example includes a C++ operator that uses DPS APIs as well. + # In order to build that C++ operator with the correct .so files + # present in the DPS toolkit's impl directory, it is necessary to + # export the following. This will get used inside the C++ operator's + # impl/bin/archLevel file. + export STREAMS_SPLPATH=${DPS_TOOLKIT_HOME} else SPLC_FLAGS ?= -a -z endif diff --git a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/com.acme.test/Main.spl b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/com.acme.test/Main.spl index db89495..0e08e91 100644 --- a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/com.acme.test/Main.spl +++ b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/com.acme.test/Main.spl @@ -48,6 +48,10 @@ composite Main { // Hence, each operator will be running as individual PEs (Processing Elements) so that // we can verify whether the dps functions will work correctly across different PEs. graph + //This operator is required in order for the C++ operators to function properly. + () as DPSAux1 = DPSAux() { + } + // Let us kick-start our ride into the world of the distributed process store (dps). stream StartSignal = Beacon() { param diff --git a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/etc/no-sql-kv-store-servers.cfg b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/etc/no-sql-kv-store-servers.cfg index f5498b6..de4e93f 100644 --- a/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/etc/no-sql-kv-store-servers.cfg +++ b/samples/advanced/01_using_no_sql_db_in_spl_custom_and_cpp_primitive_operators/etc/no-sql-kv-store-servers.cfg @@ -15,16 +15,26 @@ # v) hbase # vi) mongo # vii) couchbase -# viii) aerospike -# ix) redis-cluster +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. # # # 2) Below the line containing the K/V data store product name, please list # your valid memcached or redis or cassandra or cloudant or -# hbase or mongo or couchbase or aerospike or redis-cluster server names or IP addresses. +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. # (Please read below about the expected format of this for your chosen NoSQL data store product.) # # a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# # (e-g:) # Machine1 # Machine2 @@ -41,7 +51,7 @@ # IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): # (If you have a heavy workload of put/get requests, then using multiple Redis servers # may give scaling to improve the overall throughput. If you are going to consider using -# multiple Redis servers that are not configured in a redis cluster mode, the dps toolkit +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit # will automatically do the client side partitioning to shard (i.e. spread) your data across # those servers. You may also decide to run multiple Redis server instances on a single # machine to take advantage of the available CPU cores. @@ -56,20 +66,31 @@ # When there are multiple Redis servers running on a single machine, then you must properly # specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a # Redis authentication password after the port number if the Redis environment is configured to -# require authentication before using it. +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls # (e-g:) -# Machine1:7001:MyRedisPassword -# Machine1:7002 -# Machine1:7003:MyRedisPassword +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 # # When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a # good scaling factor for your high volume put/get requests in comparison to using the APIs that # are based on the user created named stores. You are encouraged to choose between # the TTL and non-TTL based APIs according to your functional and scaling needs. # -# c) In Cassandra, all servers assume an equal role. There is no special master or coordinator +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator # server. For Cassandra, you can list either all or just only one of the # seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# # (e-g:) # Machine1 # Machine2 @@ -79,13 +100,14 @@ # web offering or with a "Cloudant Local" on-premises private installation. # Since you have to enter the user id and password below, it is better to create # a user id specifically for working with the Streams dps toolkit. +# # For the server name, you must enter a single line in the following format: # http://user:password@user.cloudant.com if you are using the Cloudant service on the web # (OR) # http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises # Cloudant Local load balancer machine. # -# e)For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. # User id and password in the URL below should match the Linux user id and password on # the machine where the HBase REST server program is running. If you have a multi-machine # HBase cluster, then you can run the HBase REST server on multiple machines and configure @@ -93,6 +115,7 @@ # let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple # servers and that may be a factor in slightly improving the HBase read/write performance. # REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# # (e-g:) # http://user:password@Machine1:8080 # http://user:password@Machine3:8080 @@ -102,40 +125,66 @@ # high availability using automatic fail-over) or a sharded cluster's query router # mongos seed servers (for HA, load balancing, high throughput and fail-over). # Please provide it in the following way for one of those three modes of MongoDB configuration. +# # (e-g for single server, replica set and sharded cluster in that order.) # Machine1:27017 # Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME # Machine1:27069,Machine2:27069,Machine3:27069 -# [If you are using a sharded cluster, then you must manually enable sharding for the ibm_dps database and -# the collections created by the DPS in Mongo.] +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] # # g) For Couchbase, you can specify one or more server names. Please specify one server name per line. -# You must have already created a Couchbase admin user id and password using the Couchbase web console. -# You must specify that admin user id and password as part of any one of the server names you will add below. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# # (e-g:) # user:password@Machine1 # Machine2 # Machine3 # -# h) For Aerospike, you can specify one or more server names. Please specify one server name per line. -# You can optionally specify the Aerospike port number along with the server name. +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword # (e-g:) -# Machine1:3000 -# Machine2 -# Machine3:3000 -# -# i) For redis-cluster, you must install and configure Redis instances enabled with -# the clustering option in the redis.conf file. This HA feature is available only -# in Redis version 3 and above. For error free application start-up as well as for -# proper reconnection to the auto reconfigured redis cluster on abrupt node outages, -# it is recommended that you specify more than one or even better all your redis-cluster -# master and slave nodes (server name or IP address and a port number). +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile # (e-g:) -# Machine1:7001 -# Machine2:7002 -# Machine3:7003 -# Machine4:7004 +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt # # =============================================================================== redis -# As documented above, specify below your NoSQL K/V store servers' names or IP addresses with port numbers and passwords wherever applicable. +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/Makefile b/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/Makefile index 3cf141d..7e8b5f9 100644 --- a/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/Makefile +++ b/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/Makefile @@ -22,7 +22,10 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps # If the user wants to use a different version of # the DPS toolkit than the one shipped in the @@ -30,9 +33,16 @@ DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps # always set the STREAMS_SPLPATH environment variable # and point to a specific version of the DPS toolkit. ifndef STREAMS_SPLPATH - SPLC_FLAGS ?= -a -z -t $(DPS_TOOLKIT_HOME) + #SPLC_FLAGS ?= -a -z -t $(DPS_TOOLKIT_HOME) + SPLC_FLAGS ?= -a -z + # This example includes C++ native funtions that use DPS APIs as well. + # In order to build the C++ native functions with the correct .so files + # present in the DPS toolkit's impl directory, it is necessary to + # export the following. This will get used inside the C++ native functions' + # impl/bin/archLevel file. + export STREAMS_SPLPATH=${DPS_TOOLKIT_HOME} else - SPLC_FLAGS ?= -a -z + SPLC_FLAGS ?= -a -z endif SPLC = $(STREAMS_INSTALL)/bin/sc diff --git a/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/etc/no-sql-kv-store-servers.cfg b/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/etc/no-sql-kv-store-servers.cfg index f5498b6..de4e93f 100644 --- a/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/etc/no-sql-kv-store-servers.cfg +++ b/samples/advanced/02_using_no_sql_db_in_spl_custom_operators_and_a_cpp_native_function/etc/no-sql-kv-store-servers.cfg @@ -15,16 +15,26 @@ # v) hbase # vi) mongo # vii) couchbase -# viii) aerospike -# ix) redis-cluster +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. # # # 2) Below the line containing the K/V data store product name, please list # your valid memcached or redis or cassandra or cloudant or -# hbase or mongo or couchbase or aerospike or redis-cluster server names or IP addresses. +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. # (Please read below about the expected format of this for your chosen NoSQL data store product.) # # a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# # (e-g:) # Machine1 # Machine2 @@ -41,7 +51,7 @@ # IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): # (If you have a heavy workload of put/get requests, then using multiple Redis servers # may give scaling to improve the overall throughput. If you are going to consider using -# multiple Redis servers that are not configured in a redis cluster mode, the dps toolkit +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit # will automatically do the client side partitioning to shard (i.e. spread) your data across # those servers. You may also decide to run multiple Redis server instances on a single # machine to take advantage of the available CPU cores. @@ -56,20 +66,31 @@ # When there are multiple Redis servers running on a single machine, then you must properly # specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a # Redis authentication password after the port number if the Redis environment is configured to -# require authentication before using it. +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls # (e-g:) -# Machine1:7001:MyRedisPassword -# Machine1:7002 -# Machine1:7003:MyRedisPassword +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 # # When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a # good scaling factor for your high volume put/get requests in comparison to using the APIs that # are based on the user created named stores. You are encouraged to choose between # the TTL and non-TTL based APIs according to your functional and scaling needs. # -# c) In Cassandra, all servers assume an equal role. There is no special master or coordinator +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator # server. For Cassandra, you can list either all or just only one of the # seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# # (e-g:) # Machine1 # Machine2 @@ -79,13 +100,14 @@ # web offering or with a "Cloudant Local" on-premises private installation. # Since you have to enter the user id and password below, it is better to create # a user id specifically for working with the Streams dps toolkit. +# # For the server name, you must enter a single line in the following format: # http://user:password@user.cloudant.com if you are using the Cloudant service on the web # (OR) # http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises # Cloudant Local load balancer machine. # -# e)For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. # User id and password in the URL below should match the Linux user id and password on # the machine where the HBase REST server program is running. If you have a multi-machine # HBase cluster, then you can run the HBase REST server on multiple machines and configure @@ -93,6 +115,7 @@ # let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple # servers and that may be a factor in slightly improving the HBase read/write performance. # REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# # (e-g:) # http://user:password@Machine1:8080 # http://user:password@Machine3:8080 @@ -102,40 +125,66 @@ # high availability using automatic fail-over) or a sharded cluster's query router # mongos seed servers (for HA, load balancing, high throughput and fail-over). # Please provide it in the following way for one of those three modes of MongoDB configuration. +# # (e-g for single server, replica set and sharded cluster in that order.) # Machine1:27017 # Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME # Machine1:27069,Machine2:27069,Machine3:27069 -# [If you are using a sharded cluster, then you must manually enable sharding for the ibm_dps database and -# the collections created by the DPS in Mongo.] +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] # # g) For Couchbase, you can specify one or more server names. Please specify one server name per line. -# You must have already created a Couchbase admin user id and password using the Couchbase web console. -# You must specify that admin user id and password as part of any one of the server names you will add below. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# # (e-g:) # user:password@Machine1 # Machine2 # Machine3 # -# h) For Aerospike, you can specify one or more server names. Please specify one server name per line. -# You can optionally specify the Aerospike port number along with the server name. +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword # (e-g:) -# Machine1:3000 -# Machine2 -# Machine3:3000 -# -# i) For redis-cluster, you must install and configure Redis instances enabled with -# the clustering option in the redis.conf file. This HA feature is available only -# in Redis version 3 and above. For error free application start-up as well as for -# proper reconnection to the auto reconfigured redis cluster on abrupt node outages, -# it is recommended that you specify more than one or even better all your redis-cluster -# master and slave nodes (server name or IP address and a port number). +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile # (e-g:) -# Machine1:7001 -# Machine2:7002 -# Machine3:7003 -# Machine4:7004 +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt # # =============================================================================== redis -# As documented above, specify below your NoSQL K/V store servers' names or IP addresses with port numbers and passwords wherever applicable. +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/Makefile b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/Makefile index 3bd487f..e5524f0 100644 --- a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/Makefile +++ b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/Makefile @@ -22,7 +22,15 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME ?= $(HOME)/streamsx.dps/com.ibm.streamsx.dps + +# The dps-helper is copied from the DPS toolkit directory to the +# impl/java/lib directory of this application to ensure it is +# included in the SAB file. +DPS_HELPER_JAR = $(DPS_TOOLKIT_HOME)/impl/java/lib/dps-helper.jar # If the user wants to use a different version of # the DPS toolkit than the one shipped in the @@ -32,20 +40,27 @@ DPS_TOOLKIT_HOME ?= $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps # # IMPORTANT # --------- -# If the user wants to point to a different DPS toolkit, +# If the user wants to point to a different DPS toolkit directory +# than the one shipped in the IBM Streams installation directory, # then it is necessary to make changes in the following -# three files present in this project directory as well. +# three files present in this project directory as well +# before running this Makefile. # 1) build.xml in the same directory as this Makefile. -# --> In that file, search for dps-helper.jar and change its path to +# --> In that file, search for dps-helper.jar and change its full path to # point to the new DPS toolkit directory being used. # # 2) impl/java/src/com/acme/test/TickerIdGenerator.java -# --> Search for @Libraries and change the path to point to the -# new DPS toolkit directory where the dps-helper.jar is located. +# --> Search for @Libraries, read the commentary there to copy +# the dps-helper.jar file locally within this application and +# activate the correct @Libraries annotation as explained there. # # 3) impl/java/src/com/ibm/acme/test/DataStoreTest.java -# --> Search for @Libraries and change the path to point to the -# new DPS toolkit directory where the dps-helper.jar is located. +# --> Search for @Libraries, read the commentary there to copy +# the dps-helper.jar file locally within this application and +# activate the correct @Libraries annotation as explained there. +# +# This example is already set up correctly with the 3 steps mentioned above. +# ifndef STREAMS_SPLPATH SPLC_FLAGS ?= -a -z -t $(DPS_TOOLKIT_HOME) else @@ -63,6 +78,8 @@ distributed: java $(SPLC) $(SPLC_FLAGS) -M $(SPL_MAIN_COMPOSITE) $(SPL_CMD_ARGS) --output-directory=./output/com.acme.test.Main/Distributed java: $(JAVA_CLASS_FILES) + mkdir -p impl/java/lib + cp $(DPS_HELPER_JAR) impl/java/lib JAVA_HOME=$(STREAMS_INSTALL)/java ant clean: diff --git a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/build.xml b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/build.xml index 7e86ac7..95ae787 100644 --- a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/build.xml +++ b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/build.xml @@ -45,7 +45,11 @@ - + + + + + diff --git a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/etc/no-sql-kv-store-servers.cfg b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/etc/no-sql-kv-store-servers.cfg index f5498b6..de4e93f 100644 --- a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/etc/no-sql-kv-store-servers.cfg +++ b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/etc/no-sql-kv-store-servers.cfg @@ -15,16 +15,26 @@ # v) hbase # vi) mongo # vii) couchbase -# viii) aerospike -# ix) redis-cluster +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. # # # 2) Below the line containing the K/V data store product name, please list # your valid memcached or redis or cassandra or cloudant or -# hbase or mongo or couchbase or aerospike or redis-cluster server names or IP addresses. +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. # (Please read below about the expected format of this for your chosen NoSQL data store product.) # # a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# # (e-g:) # Machine1 # Machine2 @@ -41,7 +51,7 @@ # IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): # (If you have a heavy workload of put/get requests, then using multiple Redis servers # may give scaling to improve the overall throughput. If you are going to consider using -# multiple Redis servers that are not configured in a redis cluster mode, the dps toolkit +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit # will automatically do the client side partitioning to shard (i.e. spread) your data across # those servers. You may also decide to run multiple Redis server instances on a single # machine to take advantage of the available CPU cores. @@ -56,20 +66,31 @@ # When there are multiple Redis servers running on a single machine, then you must properly # specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a # Redis authentication password after the port number if the Redis environment is configured to -# require authentication before using it. +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls # (e-g:) -# Machine1:7001:MyRedisPassword -# Machine1:7002 -# Machine1:7003:MyRedisPassword +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 # # When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a # good scaling factor for your high volume put/get requests in comparison to using the APIs that # are based on the user created named stores. You are encouraged to choose between # the TTL and non-TTL based APIs according to your functional and scaling needs. # -# c) In Cassandra, all servers assume an equal role. There is no special master or coordinator +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator # server. For Cassandra, you can list either all or just only one of the # seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# # (e-g:) # Machine1 # Machine2 @@ -79,13 +100,14 @@ # web offering or with a "Cloudant Local" on-premises private installation. # Since you have to enter the user id and password below, it is better to create # a user id specifically for working with the Streams dps toolkit. +# # For the server name, you must enter a single line in the following format: # http://user:password@user.cloudant.com if you are using the Cloudant service on the web # (OR) # http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises # Cloudant Local load balancer machine. # -# e)For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. # User id and password in the URL below should match the Linux user id and password on # the machine where the HBase REST server program is running. If you have a multi-machine # HBase cluster, then you can run the HBase REST server on multiple machines and configure @@ -93,6 +115,7 @@ # let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple # servers and that may be a factor in slightly improving the HBase read/write performance. # REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# # (e-g:) # http://user:password@Machine1:8080 # http://user:password@Machine3:8080 @@ -102,40 +125,66 @@ # high availability using automatic fail-over) or a sharded cluster's query router # mongos seed servers (for HA, load balancing, high throughput and fail-over). # Please provide it in the following way for one of those three modes of MongoDB configuration. +# # (e-g for single server, replica set and sharded cluster in that order.) # Machine1:27017 # Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME # Machine1:27069,Machine2:27069,Machine3:27069 -# [If you are using a sharded cluster, then you must manually enable sharding for the ibm_dps database and -# the collections created by the DPS in Mongo.] +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] # # g) For Couchbase, you can specify one or more server names. Please specify one server name per line. -# You must have already created a Couchbase admin user id and password using the Couchbase web console. -# You must specify that admin user id and password as part of any one of the server names you will add below. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# # (e-g:) # user:password@Machine1 # Machine2 # Machine3 # -# h) For Aerospike, you can specify one or more server names. Please specify one server name per line. -# You can optionally specify the Aerospike port number along with the server name. +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword # (e-g:) -# Machine1:3000 -# Machine2 -# Machine3:3000 -# -# i) For redis-cluster, you must install and configure Redis instances enabled with -# the clustering option in the redis.conf file. This HA feature is available only -# in Redis version 3 and above. For error free application start-up as well as for -# proper reconnection to the auto reconfigured redis cluster on abrupt node outages, -# it is recommended that you specify more than one or even better all your redis-cluster -# master and slave nodes (server name or IP address and a port number). +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile # (e-g:) -# Machine1:7001 -# Machine2:7002 -# Machine3:7003 -# Machine4:7004 +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt # # =============================================================================== redis -# As documented above, specify below your NoSQL K/V store servers' names or IP addresses with port numbers and passwords wherever applicable. +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable. diff --git a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/acme/test/TickerIdGenerator.java b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/acme/test/TickerIdGenerator.java index 04f064d..4274e98 100644 --- a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/acme/test/TickerIdGenerator.java +++ b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/acme/test/TickerIdGenerator.java @@ -141,13 +141,15 @@ allowed limit (240 characters). You will get an exception for test case 23 and 2 description="Java Operator TickerIdGenerator") @InputPorts({@InputPortSet(description="Port that ingests tuples", cardinality=1, optional=false, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious), @InputPortSet(description="Optional input ports", optional=true, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious)}) @OutputPorts({@OutputPortSet(description="Port that produces tuples", cardinality=1, optional=false, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating), @OutputPortSet(description="Optional output ports", optional=true, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating)}) + //Add the DPS toolkit's Java library (dps-helper.jar) to the path of this operator. //There are 2 ways to do this: //1. If your application will have access to the Streams install location at runtime, then you can specify the full path to the location of the dps-helper.jar file present inside the DPS toolkit as follows: -@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") -//if that path will be accessible at runtime. -//2. Or, you can copy the dps-helper.jar from /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar into the lib folder of this application and reference it as follows: -//@Libraries("lib/dps-helper.jar") +//@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") +//if that path will be accessible at compile time and at runtime. + +//2. Or, you can copy the dps-helper.jar either from the /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar or from a more recent version of the DPS toolkit directory into the impl/java/lib folder of this application and reference it as follows. You will have to create the lib sub-directory inside impl/java of this application before copying the jar file there. It is done in the Makefile of this example. If you choose this option, then you must ensure that the @Libraries shown above is commented out and the following one is uncommented. +@Libraries("impl/java/lib/dps-helper.jar") // Add the following annotation if you are going to fuse this Java operator with other Java operators that will also use // the DPS APIs. In that case, it is necessary to add the following annotation so that the fused PE will use a shared class loader. @@ -1530,7 +1532,8 @@ public final void process(StreamingInput inputStream, Tuple tuple) // command you are sending here. // if (dbProductName.equalsIgnoreCase("redis") == true || - dbProductName.equalsIgnoreCase("redis-cluster") == true) { + dbProductName.equalsIgnoreCase("redis-cluster") == true || + dbProductName.equalsIgnoreCase("redis-cluster-plus-plus") == true) { // Let us try some simple Redis native commands (one way calls that don't fetch anything from the DB) // (You can't do get command using this technique. Similarly, no complex type keys or values. // In that case, please use the regular dps APIs.) @@ -1567,6 +1570,16 @@ public final void process(StreamingInput inputStream, Tuple tuple) // a lot of Redis related work using this new API. List myCmdList = new ArrayList(); String redisResult = ""; + String hashTag = ""; + + // If it is Redis cluster, let us use hash tags when running commands that use multiple keys. This is needed to avoid the CROSSSLOT error. + // https://stackoverflow.com/questions/38042629/redis-cross-slot-error + // https://redis.io/topics/cluster-spec#keys-hash-tags + if(dbProductName.equalsIgnoreCase("redis-cluster") == true || dbProductName.equalsIgnoreCase("redis-cluster-plus-plus") == true) { + hashTag = "{my_h_tag}"; + } else { + hashTag = ""; + } // Redis command: SETEX 'My Key 1' 60 'This is MyValue1 with some JSON. ..." // From your Redis command, add the distinct parts into a List @@ -1726,6 +1739,7 @@ public final void process(StreamingInput inputStream, Tuple tuple) myCmdList.clear(); redisResult = ""; myCmdList.add(new RString("PING")); + myCmdList.add(new RString("Hello New York")); try { redisResult = sf.runDataStoreCommand(myCmdList); @@ -1739,15 +1753,15 @@ public final void process(StreamingInput inputStream, Tuple tuple) myCmdList.clear(); redisResult = ""; myCmdList.add(new RString("MSET")); - myCmdList.add(new RString("one")); + myCmdList.add(new RString(hashTag + "one")); myCmdList.add(new RString("1")); - myCmdList.add(new RString("two")); + myCmdList.add(new RString(hashTag + "two")); myCmdList.add(new RString("2")); - myCmdList.add(new RString("three")); + myCmdList.add(new RString(hashTag + "three")); myCmdList.add(new RString("3")); - myCmdList.add(new RString("four")); + myCmdList.add(new RString(hashTag + "four")); myCmdList.add(new RString("4")); - myCmdList.add(new RString("sixty two")); + myCmdList.add(new RString(hashTag + "sixty two")); myCmdList.add(new RString("62")); try { @@ -1771,16 +1785,36 @@ public final void process(StreamingInput inputStream, Tuple tuple) System.out.println("Redis command Test13 [KEYS]-->runDataStoreCommand redisResult=" + sfe.getErrorCode() + ", msg=" + sfe.getErrorMessage()); } + // Redis command: MGET one two three four 'sixty two' + // From your Redis command, add the distinct parts into a list + myCmdList.clear(); + redisResult = ""; + myCmdList.add(new RString("MGET")); + myCmdList.add(new RString(hashTag + "one")); + myCmdList.add(new RString(hashTag + "two")); + myCmdList.add(new RString(hashTag + "three")); + myCmdList.add(new RString(hashTag + "four")); + // This is a non-existing key for which there will be no result returned. + myCmdList.add(new RString(hashTag + "five")); + myCmdList.add(new RString(hashTag + "sixty two")); + + try { + redisResult = sf.runDataStoreCommand(myCmdList); + System.out.println("Redis command Test13b [MGET]-->runDataStoreCommand redisResult=" + redisResult); + } catch (StoreFactoryException sfe) { + System.out.println("Redis command Test13b [MGET]-->runDataStoreCommand redisResult=" + sfe.getErrorCode() + ", msg=" + sfe.getErrorMessage()); + } + // Redis command: DEL one two three four 'sixty two' // From your Redis command, add the distinct parts into a List myCmdList.clear(); redisResult = ""; myCmdList.add(new RString("DEL")); - myCmdList.add(new RString("one")); - myCmdList.add(new RString("two")); - myCmdList.add(new RString("three")); - myCmdList.add(new RString("four")); - myCmdList.add(new RString("sixty two")); + myCmdList.add(new RString(hashTag + "one")); + myCmdList.add(new RString(hashTag + "two")); + myCmdList.add(new RString(hashTag + "three")); + myCmdList.add(new RString(hashTag + "four")); + myCmdList.add(new RString(hashTag + "sixty two")); try { redisResult = sf.runDataStoreCommand(myCmdList); diff --git a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/ibm/acme/test/DataStoreTester.java b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/ibm/acme/test/DataStoreTester.java index d5bf957..24758c3 100644 --- a/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/ibm/acme/test/DataStoreTester.java +++ b/samples/advanced/03_using_no_sql_db_in_spl_custom_and_java_primitive_operators/impl/java/src/com/ibm/acme/test/DataStoreTester.java @@ -47,13 +47,15 @@ description="Java Operator DataStoreTester") @InputPorts({@InputPortSet(description="Port that ingests tuples", cardinality=1, optional=false, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious), @InputPortSet(description="Optional input ports", optional=true, windowingMode=WindowMode.NonWindowed, windowPunctuationInputMode=WindowPunctuationInputMode.Oblivious)}) @OutputPorts({@OutputPortSet(description="Port that produces tuples", cardinality=1, optional=false, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating), @OutputPortSet(description="Optional output ports", optional=true, windowPunctuationOutputMode=WindowPunctuationOutputMode.Generating)}) + //Add the DPS toolkit's Java library (dps-helper.jar) to the path of this operator. //There are 2 ways to do this: //1. If your application will have access to the Streams install location at runtime, then you can specify the full path to the location of the dps-helper.jar file present inside the DPS toolkit as follows: -@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") -//if that path will be accessible at runtime. -//2. Or, you can copy the dps-helper.jar from /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar into the lib folder of this application and reference it as follows: -//@Libraries("lib/dps-helper.jar") +//@Libraries("@STREAMS_INSTALL@/toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar") +//if that path will be accessible at compile time and at runtime. + +//2. Or, you can copy the dps-helper.jar either from the /toolkits/com.ibm.streamsx.dps/impl/java/lib/dps-helper.jar or from a more recent version of the DPS toolkit directory into the impl/java/lib folder of this application and reference it as follows. You will have to create the lib sub-directory inside impl/java of this application before copying the jar file there. It is done in the Makefile of this example. If you choose this option, then you must ensure that the @Libraries shown above is commented out and the following one is uncommented. +@Libraries("impl/java/lib/dps-helper.jar") // Add the following annotation if you are going to fuse this Java operator with other Java operators that will also use // the DPS APIs. In that case, it is necessary to add the following annotation so that the fused PE will use a shared class loader. diff --git a/samples/advanced/04_all_dps_apis_at_work_in_spl/Makefile b/samples/advanced/04_all_dps_apis_at_work_in_spl/Makefile index 9782536..69e8ffd 100644 --- a/samples/advanced/04_all_dps_apis_at_work_in_spl/Makefile +++ b/samples/advanced/04_all_dps_apis_at_work_in_spl/Makefile @@ -22,7 +22,10 @@ # # end_generated_IBM_copyright_prolog -DPS_TOOLKIT_HOME = $(STREAMS_INSTALL)/toolkits/com.ibm.streamsx.dps +# Since the DPS toolkit version you are using could be more +# recent than the one in the IBM Streams installation directory, +# you may want to ensure that the following DPS toolkit path is correct. +DPS_TOOLKIT_HOME = $(HOME)/streamsx.dps/com.ibm.streamsx.dps # If the user wants to use a different version of # the DPS toolkit than the one shipped in the diff --git a/samples/advanced/04_all_dps_apis_at_work_in_spl/com.acme.test/Main.splmm b/samples/advanced/04_all_dps_apis_at_work_in_spl/com.acme.test/Main.splmm index b391b21..479d93e 100644 --- a/samples/advanced/04_all_dps_apis_at_work_in_spl/com.acme.test/Main.splmm +++ b/samples/advanced/04_all_dps_apis_at_work_in_spl/com.acme.test/Main.splmm @@ -1,6 +1,6 @@ /* * Licensed Materials - Property of IBM - * Copyright IBM Corp. 2011, 2016 + * Copyright IBM Corp. 2011, 2020 * US Government Users Restricted Rights - Use, duplication or * disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ @@ -2037,6 +2037,7 @@ composite RunNativeDataStoreCommands() { mutable rstring cmd = ""; mutable uint64 err = 0ul; mutable boolean res = true; + mutable rstring hashTag = ""; rstring dbProductName = dpsGetNoSqlDbProductName(); if (dbProductName == "memcached" || dbProductName == "mongo" || @@ -2058,8 +2059,18 @@ composite RunNativeDataStoreCommands() { // We will simply take your command string and run it. So, be sure of what // command you are sending here. // - if (dbProductName == "redis" || dbProductName == "redis-cluster") { + if (dbProductName == "redis" || dbProductName == "redis-cluster" || dbProductName == "redis-cluster-plus-plus") { printStringLn("=== Start of the native data store command execution using Redis ==="); + + // If it is Redis cluster, let us use hash tags when running commands that use multiple keys. This is needed to avoid the CROSSSLOT error. + // https://stackoverflow.com/questions/38042629/redis-cross-slot-error + // https://redis.io/topics/cluster-spec#keys-hash-tags + if(dbProductName == "redis-cluster" || dbProductName == "redis-cluster-plus-plus") { + hashTag = "{my_h_tag}"; + } else { + hashTag = ""; + } + // Let us try some simple Redis native commands (one way calls that don't fetch anything from the DB) // (You can't do get command using this technique. Similarly, no complex type keys or values. // In that case, please use the regular dps APIs.) @@ -2269,6 +2280,7 @@ composite RunNativeDataStoreCommands() { clearM(myCmdList); redisResult = ""; appendM(myCmdList, "PING"); + appendM(myCmdList, "Hello New York"); myFlag = dpsRunDataStoreCommand(myCmdList, redisResult, err); @@ -2279,19 +2291,20 @@ composite RunNativeDataStoreCommands() { } // Redis command: MSET one 1 two 2 three 3 four 4 'sixty two' 62 - // From your Redis command, add the distinct parts into a list + // From your Redis command, add the distinct parts into a list + clearM(myCmdList); redisResult = ""; appendM(myCmdList, "MSET"); - appendM(myCmdList, "one"); + appendM(myCmdList, hashTag + "one"); appendM(myCmdList, "1"); - appendM(myCmdList, "two"); + appendM(myCmdList, hashTag + "two"); appendM(myCmdList, "2"); - appendM(myCmdList, "three"); + appendM(myCmdList, hashTag + "three"); appendM(myCmdList, "3"); - appendM(myCmdList, "four"); + appendM(myCmdList, hashTag + "four"); appendM(myCmdList, "4"); - appendM(myCmdList, "sixty two"); + appendM(myCmdList, hashTag + "sixty two"); appendM(myCmdList, "62"); myFlag = dpsRunDataStoreCommand(myCmdList, redisResult, err); @@ -2317,16 +2330,37 @@ composite RunNativeDataStoreCommands() { printStringLn("Redis command Test13 [KEYS]-->dpsRunDataStoreCommand resultValue=" + (rstring)redisResult); } + // Redis command: MGET one two three four 'sixty two' + // From your Redis command, add the distinct parts into a list + clearM(myCmdList); + redisResult = ""; + appendM(myCmdList, "MGET"); + appendM(myCmdList, hashTag + "one"); + appendM(myCmdList, hashTag + "two"); + appendM(myCmdList, hashTag + "three"); + appendM(myCmdList, hashTag + "four"); + // This is a non-existing key for which there will be no result returned. + appendM(myCmdList, hashTag + "five"); + appendM(myCmdList, hashTag + "sixty two"); + + myFlag = dpsRunDataStoreCommand(myCmdList, redisResult, err); + + if (err != 0ul) { + printStringLn("Redis command Test13b [MGET]-->dpsRunDataStoreCommand rc=" + (rstring)dpsGetLastStoreErrorCode() + ", msg=" + dpsGetLastStoreErrorString()); + } else { + printStringLn("Redis command Test13b [MGET]-->dpsRunDataStoreCommand resultValue=" + (rstring)redisResult); + } + // Redis command: DEL one two three four 'sixty two' // From your Redis command, add the distinct parts into a list clearM(myCmdList); redisResult = ""; appendM(myCmdList, "DEL"); - appendM(myCmdList, "one"); - appendM(myCmdList, "two"); - appendM(myCmdList, "three"); - appendM(myCmdList, "four"); - appendM(myCmdList, "sixty two"); + appendM(myCmdList, hashTag + "one"); + appendM(myCmdList, hashTag + "two"); + appendM(myCmdList, hashTag + "three"); + appendM(myCmdList, hashTag + "four"); + appendM(myCmdList, hashTag + "sixty two"); myFlag = dpsRunDataStoreCommand(myCmdList, redisResult, err); diff --git a/samples/advanced/04_all_dps_apis_at_work_in_spl/etc/no-sql-kv-store-servers.cfg b/samples/advanced/04_all_dps_apis_at_work_in_spl/etc/no-sql-kv-store-servers.cfg index f5498b6..de4e93f 100644 --- a/samples/advanced/04_all_dps_apis_at_work_in_spl/etc/no-sql-kv-store-servers.cfg +++ b/samples/advanced/04_all_dps_apis_at_work_in_spl/etc/no-sql-kv-store-servers.cfg @@ -15,16 +15,26 @@ # v) hbase # vi) mongo # vii) couchbase -# viii) aerospike -# ix) redis-cluster +# viii) redis-cluster +# ix) redis-cluster-plus-plus +# +# Note: A 'redis' configuration can be made to work with a non-cluster redis instance in +# TLS or non-TLS mode. +# +# A 'redis-cluster-plus-plus' configuration can be made to work with a redis cluster in +# TLS/SSL or non-TLS mode. +# +# A 'redis-cluster' configuration can be made to work with a redis cluster only in +# non-TLS mode. The other two redis configurations above makes this one redundant. # # # 2) Below the line containing the K/V data store product name, please list # your valid memcached or redis or cassandra or cloudant or -# hbase or mongo or couchbase or aerospike or redis-cluster server names or IP addresses. +# hbase or mongo or couchbase or redis-cluster server names or IP addresses. # (Please read below about the expected format of this for your chosen NoSQL data store product.) # # a) For memcached, you must list all the participating servers one per line (names or IP addresses). +# # (e-g:) # Machine1 # Machine2 @@ -41,7 +51,7 @@ # IMPORTANT TIP ABOUT RUNNING MULTIPLE REDIS SERVERS (in a non cluster redis configuration): # (If you have a heavy workload of put/get requests, then using multiple Redis servers # may give scaling to improve the overall throughput. If you are going to consider using -# multiple Redis servers that are not configured in a redis cluster mode, the dps toolkit +# multiple Redis servers that are not configured in a redis-cluster mode, the dps toolkit # will automatically do the client side partitioning to shard (i.e. spread) your data across # those servers. You may also decide to run multiple Redis server instances on a single # machine to take advantage of the available CPU cores. @@ -56,20 +66,31 @@ # When there are multiple Redis servers running on a single machine, then you must properly # specify below each ServerName:Port on a separate line. For Redis, one can optionally provide a # Redis authentication password after the port number if the Redis environment is configured to -# require authentication before using it. +# require authentication before using it. Along with that, one can optinally provide a +# redis server connection timeout value and the use_tls field to indicate whether to +# connect to the Redis server using TLS or not. Default Redis server connection timeout value is +# 3 seconds and user can override it with any non-zero value in seconds. +# For the use_tls field, a value of 1 means that TLS is needed and an absence of that field or +# a value of 0 for that field means that TLS is not needed. TLS makes sense only when a +# public cloud service based single redis server is configured i.e. IBM Compose Redis server or +# an AWS Elasticache Redis or Azure Cache for Redis or an on-prem Redis configured with a single server. +# It is also necessary to have Redis server 6 or above to enable/disable the TLS/SSL feature. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls # (e-g:) -# Machine1:7001:MyRedisPassword -# Machine1:7002 -# Machine1:7003:MyRedisPassword +# Machine1:7001:MyRedisPassword:7:1 +# Machine1:7002:::0 +# Machine1:7003:MyRedisPassword:6 # # When there are multiple Redis servers configured, using the dpsXXXXTTL APIs will provide a # good scaling factor for your high volume put/get requests in comparison to using the APIs that # are based on the user created named stores. You are encouraged to choose between # the TTL and non-TTL based APIs according to your functional and scaling needs. # -# c) In Cassandra, all servers assume an equal role. There is no special master or coordinator +# c) In Cassandra, all servers assume an equal role. There is no special primary or coordinator # server. For Cassandra, you can list either all or just only one of the # seed servers (names or IP addresses as it is specified in your cassandra.yaml file). +# # (e-g:) # Machine1 # Machine2 @@ -79,13 +100,14 @@ # web offering or with a "Cloudant Local" on-premises private installation. # Since you have to enter the user id and password below, it is better to create # a user id specifically for working with the Streams dps toolkit. +# # For the server name, you must enter a single line in the following format: # http://user:password@user.cloudant.com if you are using the Cloudant service on the web # (OR) # http://user:password@XXXXX where XXXXX is a name or IP address of your on-premises # Cloudant Local load balancer machine. # -# e)For HBase, you have to specify the HBase REST server address(es) in the format shown below. +# e) For HBase, you have to specify the HBase REST server address(es) in the format shown below. # User id and password in the URL below should match the Linux user id and password on # the machine where the HBase REST server program is running. If you have a multi-machine # HBase cluster, then you can run the HBase REST server on multiple machines and configure @@ -93,6 +115,7 @@ # let the DPS toolkit to spray (load balance) your heavy HBase workload to those multiple # servers and that may be a factor in slightly improving the HBase read/write performance. # REST server address format: http://user:password@HBase-REST-ServerNameOrIPAddress:port +# # (e-g:) # http://user:password@Machine1:8080 # http://user:password@Machine3:8080 @@ -102,40 +125,66 @@ # high availability using automatic fail-over) or a sharded cluster's query router # mongos seed servers (for HA, load balancing, high throughput and fail-over). # Please provide it in the following way for one of those three modes of MongoDB configuration. +# # (e-g for single server, replica set and sharded cluster in that order.) # Machine1:27017 # Machine1:27017,Machine1:27018,Machine1:27019/?replicaSet=YOUR_MONGO_REPLICA_SET_NAME # Machine1:27069,Machine2:27069,Machine3:27069 -# [If you are using a sharded cluster, then you must manually enable sharding for the ibm_dps database and -# the collections created by the DPS in Mongo.] +# [If you are using a sharded cluster, then you must manually enable sharding for the +# ibm_dps database and the collections created by the DPS in Mongo.] # # g) For Couchbase, you can specify one or more server names. Please specify one server name per line. -# You must have already created a Couchbase admin user id and password using the Couchbase web console. -# You must specify that admin user id and password as part of any one of the server names you will add below. +# You must have already created a Couchbase admin user id and password using the Couchbase +# web console. You must specify that admin user id and password as part of any one of the +# server names you will add below. +# # (e-g:) # user:password@Machine1 # Machine2 # Machine3 # -# h) For Aerospike, you can specify one or more server names. Please specify one server name per line. -# You can optionally specify the Aerospike port number along with the server name. +# h) For redis-cluster, you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This HA feature in the DPS toolkit +# is supported only in Redis version 3 and above. For error free application +# start-up as well as for proper reconnection to the auto reconfigured redis cluster +# on abrupt node outages, it is recommended that you specify more than one or even +# better all your redis-cluster primary and replica nodes (server name or IP address +# and a port number). Please note that this way of using the redis-cluster is done +# via an older version of redis-cluster client wrapper built in year 2016. Hence, with +# this configuration, you will not be able to use TLS/SSL in Redis server v6 +# and higher versions. It is good only for using a Redis cluster that is not +# configured with TLS/SSL. If you prefer to work with a more recent version of +# a redis-cluster client wrapper that is well tested with Redis server v6 and +# higher enabled for TLS/SSL or for non-TLS, it is recommended to skip to the next +# section and follow the steps from there. +# +# RedisServerNameOrIPAddress:port:RedisPassword # (e-g:) -# Machine1:3000 -# Machine2 -# Machine3:3000 -# -# i) For redis-cluster, you must install and configure Redis instances enabled with -# the clustering option in the redis.conf file. This HA feature is available only -# in Redis version 3 and above. For error free application start-up as well as for -# proper reconnection to the auto reconfigured redis cluster on abrupt node outages, -# it is recommended that you specify more than one or even better all your redis-cluster -# master and slave nodes (server name or IP address and a port number). +# Machine1:7001:MyRedisPassword +# Machine2:7001:MyRedisPassword +# Machine3:7001:MyRedisPassword +# Machine4:7001:MyRedisPassword +# +# i) redis-cluster-plus-plus: The previous section works only for non-TLS redis-cluster configurations. +# This section explains how to use a TLS/SSL enabled redis-cluster that became available in +# Redis v6 or above. It uses a much newer C++ redis-cluster client wrapper called +# redis-plus-plus that allows us to work with a redis-cluster v6 or above enabled with TLS/SSL or +# non-TLS. For redis-cluster-plus-plus you must install and configure Redis instances enabled with +# the clustering option in the redis.conf file. This particular HA feature with non-TLS or TLS/SSL +# in the DPS toolkit is supported only in Redis version 6 or above. For error free application start-up +# as well as for proper reconnection to the auto reconfigured redis cluster on abrupt node outages, +# it is recommended that you specify more than one or even better all your redis-cluster primary and +# replica nodes (server name or IP address and a port number). Please refer to the commentary above +# for the non-clustered Redis to understand the connection timeout and TLS usage. Both are supported in +# the clister-mode Redis as well when configuring your key value store product as redis-cluster-plus-plus. +# +# RedisServerNameOrIPAddress:port:RedisPassword:ConnectionTimeoutValue:use_tls:RedisClusterCACertificateFile # (e-g:) -# Machine1:7001 -# Machine2:7002 -# Machine3:7003 -# Machine4:7004 +# Machine1:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine2:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine3:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt +# Machine4:7001:MyRedisPassword:7:1:/home/streamsadmin/my-redis-ca.crt # # =============================================================================== redis -# As documented above, specify below your NoSQL K/V store servers' names or IP addresses with port numbers and passwords wherever applicable. +# Specify below your NoSQL K/V store servers' names or IP addresses, port numbers, passwords and other fields as applicable.