Example #1
0
        public static async Task RemoveEventFromSubscriptionAsync(string accountId, string subscriptionId, string eventName, string eventScope)
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            // convert the event scope to all lower-case characters
            eventScope = eventScope.ToLowerInvariant();

            // sanity-check: eventScope must belong to the account; if the scope is null or empty then set it to scope to the account itself
            if (string.IsNullOrWhiteSpace(accountId) == false)
            {
                // accountId is present
                string baseAccountScope = "/accounts/" + accountId;

                if (eventScope != null)
                {
                    if (eventScope.IndexOf(baseAccountScope) != 0)
                    {
                        return; // return false;
                    }
                }
                else
                {
                    eventScope = baseAccountScope;
                }
            }

            // NOTE: we do this entire operation in one atomic Lua script to ensure that we all the collections we touch stay intact
            // NOTE: if the subscription is already subscribed to this event...then this Lua function will effectively have no effect

            // create our script-builder object placeholders
            StringBuilder luaBuilder;
            List <string> arguments;
            int           iArgument;
            List <string> keys;

            int RESULT_SUCCESS = 0;

            // generate Lua script
            luaBuilder = new StringBuilder();
            arguments  = new List <string>();
            iArgument  = 1;
            //
            luaBuilder.Append("redis.call(\"SREM\", KEYS[1], ARGV[" + iArgument.ToString() + "])\n");
            arguments.Add(accountId + REDIS_SLASH + subscriptionId);
            iArgument++;
            //
            luaBuilder.Append("redis.call(\"SREM\", KEYS[2], ARGV[" + iArgument.ToString() + "])\n");
            arguments.Add(eventName + "[scope@" + eventScope + "]");
            iArgument++;
            //
            luaBuilder.Append("return " + RESULT_SUCCESS.ToString() + "\n");

            keys = new List <string>();
            keys.Add(REDIS_PREFIX_EVENT + REDIS_PREFIX_SEPARATOR + eventName + "[scope@" + eventScope + "]" + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SUBSCRIPTIONS);
            keys.Add(REDIS_PREFIX_SUBSCRIPTION + REDIS_PREFIX_SEPARATOR + accountId + REDIS_SLASH + subscriptionId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_EVENTS);
            long luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);
        }
        public static async Task <Subscription> LoadSubscriptionAsync(string accountId, string subscriptionId)
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            string fullyQualifiedSubscriptionKey = REDIS_PREFIX_SUBSCRIPTION + REDIS_PREFIX_SEPARATOR + (accountId != null ? accountId.ToLowerInvariant() : REDIS_ASTERISK) + REDIS_SLASH + subscriptionId.ToLowerInvariant();
            bool   subscriptionExists            = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedSubscriptionKey }) > 0);

            if (subscriptionExists)
            {
                Dictionary <string, string> subscriptionDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedSubscriptionKey);

                // extract serverId from subscriptionId
                string serverId = ParsingHelper.ExtractServerDetailsFromAccountServerIdIdentifier(subscriptionId).Value.ToServerTypeServerIdIdentifierString();
                //
                string clientId = subscriptionDictionary.ContainsKey("client-id") ? subscriptionDictionary["client-id"] : null;
                //
                string userId = subscriptionDictionary.ContainsKey("user-id") ? subscriptionDictionary["user-id"] : null;

                Subscription resultSubscription = new Subscription();
                // account-id
                resultSubscription._accountId = accountId;
                // subscription-id
                resultSubscription._subscriptionId = subscriptionId;
                // server-id
                resultSubscription._serverId = serverId;
                // client-id
                resultSubscription._clientId = clientId;
                // user-id
                resultSubscription._userId = userId;

                return(resultSubscription);
            }

            // subscription (or its owner) could not be found
            return(null);
        }
Example #3
0
        public static async Task DispatchEventsAsync()
        {
            // connect to Redis
            _redisClient = await Singletons.CreateNewRedisClientAsync();

            // create a list of all eventPriorityKeys
            List <string> eventPriorityKeyList = new List <string>();

            for (int i = 7; i >= 0; i--)
            {
                eventPriorityKeyList.Add("service:*/event#incoming-notifications" + i.ToString());
            }
            ;
            string[] eventPriorityKeys = eventPriorityKeyList.ToArray();

            while (true)
            {
                // wait for a new event to be enqueued; then dequeue it (always dequeuing from the highest-priority event queue first)
                var queuedEvent = await _redisClient.ListPopLeftBlockingAsync <string, byte[]>(eventPriorityKeys, null);

                // retrieve our event priority
                string keyName  = queuedEvent.Key;
                int    priority = int.Parse(keyName.Substring("service:*/event#incoming-notifications".Length));

                // parse out the event creation timestamp and the resource path of the event source
                // NOTE: the event value is: [8-byte unix timestamp in milliseconds][resourceuri#eventname][1 byte of 0x00][event json]
                byte[] eventAsBytes     = queuedEvent.Value;
                long   createdTimestamp = BitConverter.ToInt64(eventAsBytes, 0);
                int    resourceStartPos = 8;
                int    resourceLength   = 0;
                for (int i = resourceStartPos; i < eventAsBytes.Length; i++)
                {
                    if (eventAsBytes[i] == 0x00)
                    {
                        resourceLength = i - resourceStartPos;
                    }
                }
                string resourcePathAndEvent = Encoding.UTF8.GetString(eventAsBytes, resourceStartPos, resourceLength);
                int    jsonStartPos         = resourceStartPos + resourceLength + 1;
                string jsonEncodedEvent     = Encoding.UTF8.GetString(eventAsBytes, jsonStartPos, eventAsBytes.Length - jsonStartPos);

                // split the resourcePath and its eventName
                if (resourcePathAndEvent.IndexOf("#") < 0)
                {
                    continue;                                        // safety check: if the resource is not validly formatted, skip to the next event
                }
                string resourcePath = resourcePathAndEvent.Substring(0, resourcePathAndEvent.IndexOf("#"));
                string eventName    = resourcePathAndEvent.Substring(resourcePathAndEvent.IndexOf("#") + 1);

                // calculate the root resource for the account (to make sure we don't "bubble" the event handler any higher than the account's root)
                if (resourcePath.IndexOf("/accounts/") != 0)
                {
                    continue;                                          // sanity and security check: resource must start with the accounts tag
                }
                if (resourcePath.Length <= "/accounts/".Length)
                {
                    continue;                                             // sanity and security check: resource must contain an account id
                }
                //int accountStartPos = "/accounts/".Length;
                //int rootAccountResourcePos = resourcePath.IndexOf("/", accountStartPos);
                //if (rootAccountResourcePos < 0) continue; // sanity and security check: resource must contain an account id

                // find all subscriptions which are subscribed to receive this event
                List <string> listeningSubscriptions = new List <string>();
                List <string> resourcePathList       = new List <string>();
                string        tempResourcePath       = resourcePath;
                // bubble up the resource tree, to find all parent resources which could also be subscribed to this event
                while (tempResourcePath.Length > "/accounts/".Length)
                {
                    resourcePathList.Add(tempResourcePath);
                    int parentPathElementPos = tempResourcePath.LastIndexOf('/' /*, tempResourcePath.Length - 2 */);
                    tempResourcePath = tempResourcePath.Substring(0, parentPathElementPos);
                }
                // search all event-subscription keys to determine which subscriptions are subscribed to receive this event
                foreach (string resourcePathElement in resourcePathList)
                {
                    List <string> subscriptionIds = await _redisClient.SetMembersAsync <string, string>("event:" + eventName + "[scope@" + resourcePathElement + "]" + "#" + "subscriptions");

                    foreach (string subscriptionId in subscriptionIds)
                    {
                        //string subscriptionId = subscriptionIdAndTag.Substring(0, subscriptionId.IndexOf(" "));
                        //string tag = subscriptionId.Substring(subscriptionIdAndTag.IndexOf(" ") + 1);

                        // establish our subscription's base key
                        string        subscriptionBaseKey = "subscription:" + subscriptionId;
                        long          currentTimestamp;
                        long          subMillisecondIndex;
                        StringBuilder luaBuilder;
                        List <string> arguments;
                        int           iArgument;
                        List <string> keys;

                        // NOTE: we put this code in a loop for the rare-to-impossible case that we experience more than 1 million events/subscription/millisecond
                        while (true)
                        {
                            // prefix the current timestamp to the event json
                            currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

                            /* now, we make sure that the current timestamp is unique:
                             *   - the following lua function passes our UnixTimeMilliseconds timestamp to Redis,
                             *   - then compares the timestamp to "subscription:account_id/subscription_id" field "last-incoming-notification-time"
                             *   - if the timestamp is different than the existing timestamp: stores the new timestamp in memory,
                             *     sets the field "last-incoming-notification-time-incrementer" to 0 and returns that "incrementer" value "0".
                             *   - if the timestamp is the same then we instead increment the field "last-incoming-notification-time-incrementer" and return its new value.
                             */
                            // NOTE: this function is written in Lua because this operation must happen atomically
                            // if the subscription cannot be found, the function immediately returns 0 (and does not inadvertently recreate the key)
                            luaBuilder = new StringBuilder();
                            arguments  = new List <string>();
                            iArgument  = 1;
                            /* switch replication to "commands replication" instead of "script replication" for this script (required because we obtain a non-static piece of data, the timestamp, in this script and sometimes then write it out as data */
                            // NOTE: this feature requires Redis 3.2.0 or newer
                            luaBuilder.Append("redis.replicate_commands()\n");
                            //
                            // check and make sure that the subscription still exists (for that edge case) and, if not then return "-1" as error.
                            luaBuilder.Append("if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n");
                            luaBuilder.Append("  return {-1, -1}\n");
                            luaBuilder.Append("end\n");

                            luaBuilder.Append("local currentTime = tonumber(ARGV[" + iArgument.ToString() + "])\n");
                            arguments.Add(currentTimestamp.ToString());
                            iArgument++;
                            luaBuilder.Append("local currentTimeArray = redis.call(\"TIME\")\n");
                            luaBuilder.Append("local currentTime = (currentTimeArray[1] * 1000) + math.floor(currentTimeArray[2] / 1000)\n");
                            //
                            luaBuilder.Append("local returnValue = 0\n");
                            // get current timestamp
                            luaBuilder.Append("local previousTime = tonumber(redis.call(\"HGET\", KEYS[1], \"last-incoming-notification-time\"))\n");
                            // if the currentTime is newer than the previousTime, then overwrite "last-incoming-notification-time", etc.
                            luaBuilder.Append("if (previousTime == false) or (currentTime > previousTime) then\n");
                            luaBuilder.Append("  redis.call(\"HSET\", KEYS[1], \"last-incoming-notification-time\", currentTime)\n");
                            luaBuilder.Append("  redis.call(\"HSET\", KEYS[1], \"last-incoming-notification-time-incrementer\", 0)\n");
                            luaBuilder.Append("  returnValue = 0\n");
                            luaBuilder.Append("else\n");
                            luaBuilder.Append("  returnValue = redis.call(\"HINCRBY\", KEYS[1], \"last-incoming-notification-time-incrementer\", 1)\n");
                            luaBuilder.Append("end\n");
                            // return our returnValue (the incrementer)
                            luaBuilder.Append("return {currentTime, returnValue}\n");

                            keys = new List <string>();
                            keys.Add(subscriptionBaseKey);

                            // NOTE: if the returned incrementer value is >= 1 million, it's too large and we need to wait until the next millisecond and try again.
                            //       (practically speaking, anywhere close to 1 million events per millisecond should never ever ever happen in our software)
                            object[] returnObjects = await _redisClient.EvalAsync <string, string, object[]>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                            currentTimestamp    = (long)returnObjects[0];
                            subMillisecondIndex = (long)returnObjects[1];

                            if (subMillisecondIndex < 1000000)
                            {
                                break;
                            }
                        }

                        byte[] currentTimeAsByteArray            = BitConverter.GetBytes((currentTimestamp * 1000000) + subMillisecondIndex);
                        byte[] jsonAsByteArray                   = Encoding.UTF8.GetBytes(jsonEncodedEvent);
                        byte[] subscriptionEventValueAsByteArray = new byte[currentTimeAsByteArray.Length + jsonAsByteArray.Length];
                        Array.Copy(currentTimeAsByteArray, 0, subscriptionEventValueAsByteArray, 0, currentTimeAsByteArray.Length);
                        Array.Copy(jsonAsByteArray, 0, subscriptionEventValueAsByteArray, currentTimeAsByteArray.Length, jsonAsByteArray.Length);

                        // now queue this event into each subscription's respective incoming notification queue, check for the current waitkey and
                        // if the waikey is empty we should add a dummy value so that the subscriber dequeues the new incoming event
                        //
                        // NOTE: this function is written in Lua because this operation must happen atomically
                        // if the subscription cannot be found, the function immediately fails so that the event key is not inadvertenly created
                        luaBuilder = new StringBuilder();
                        List <byte[]> argumentsAsByteArrays = new List <byte[]>();
                        iArgument = 1;
                        // check and make sure that the subscription still exists (for that edge case) and, if not then return "-1" as error.
                        luaBuilder.Append("if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n");
                        luaBuilder.Append("  return {-1, -1}\n"); // return -1 if the subscription doesn't exist (so we can short-circuit our verification logic
                        luaBuilder.Append("end\n");
                        // add the event to the appropriate incoming event queue
                        luaBuilder.Append("local addCount = redis.call(\"ZADD\", KEYS[2], ARGV[" + iArgument.ToString() + "], ARGV[" + (iArgument + 1).ToString() + "])\n");
                        argumentsAsByteArrays.Add(_redisClient.Encoding.GetBytes(createdTimestamp.ToString()));
                        iArgument++;
                        argumentsAsByteArrays.Add(subscriptionEventValueAsByteArray);
                        iArgument++;
                        // check for the current subscription waitkey; if a waitkey exists and is empty then add a dummy value so that it dequeues the new incoming event
                        luaBuilder.Append("local connectionId = redis.call(\"HGET\", KEYS[1], \"connection-id\")\n");
                        luaBuilder.Append("if connectionId == false then\n"); // null values are if-evaluated as false; we'll change this to -1 to mean "no connection"
                        luaBuilder.Append("  connectionId = -1\n");
                        luaBuilder.Append("end\n");
                        luaBuilder.Append("return {addCount, tonumber(connectionId)}\n");

                        keys = new List <string>();
                        keys.Add(subscriptionBaseKey);
                        keys.Add(subscriptionBaseKey + "#incoming-notifications" + priority.ToString());

                        object[] addCountAndConnectionIdArray = await _redisClient.EvalAsync <string, byte[], object[]>(luaBuilder.ToString(), keys.ToArray(), argumentsAsByteArrays.ToArray()).ConfigureAwait(false);

                        //object[] addCountAndConnectionIdArray = (object[])addCountAndConnectionId;

                        bool subscriptionAndWaitKeyExist = true;
                        var  addCount = (long)addCountAndConnectionIdArray[0];
                        if (addCount == -1)
                        {
                            // this is no subscription
                            subscriptionAndWaitKeyExist = false;
                        }
                        else if (addCount == 0)
                        {
                            // we failed to add the event; error!
                        }
                        long connectionId = (long)addCountAndConnectionIdArray[1];
                        if (connectionId == -1)
                        {
                            // there is no connectionId
                            subscriptionAndWaitKeyExist = false;
                        }
                        else
                        {
                            connectionId = (long)addCountAndConnectionIdArray[1];
                        }
                        // if our subscription exists and a wait key exists, check to see if the wait key has any elements; if not then add one
                        if (subscriptionAndWaitKeyExist)
                        {
                            luaBuilder = new StringBuilder();
                            arguments  = new List <string>();
                            iArgument  = 1;
                            //
                            luaBuilder.Append("local connectionId = ARGV[" + iArgument.ToString() + "]\n");
                            arguments.Add(connectionId.ToString());
                            iArgument++;
                            // check to make sure the connection-id has not changed
                            luaBuilder.Append("local verifyConnectionId = redis.call(\"HGET\", KEYS[1], ARGV[" + iArgument.ToString() + "])\n");
                            arguments.Add("connection-id");
                            iArgument++;
                            luaBuilder.Append("if connectionId == verifyConnectionId then\n");
                            // if the connection matches, then check to see if the wait key exists; if the wait key does not exist, push it a blank element
                            luaBuilder.Append("  if redis.call(\"EXISTS\", KEYS[2]) == 0 then\n");
                            luaBuilder.Append("    redis.call(\"RPUSH\", KEYS[2], ARGV[" + iArgument.ToString() + "])\n");
                            arguments.Add("");
                            iArgument++;
                            luaBuilder.Append("  end\n");
                            luaBuilder.Append("end\n");

                            keys = new List <string>();
                            keys.Add(subscriptionBaseKey);
                            keys.Add(subscriptionBaseKey + "#waitkey-" + connectionId.ToString());

                            await _redisClient.EvalAsync <string, string, object>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);
                        }
                    }
                }
            }
        }
        public async Task DeleteSubscriptionAsync()
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            //int RESULT_KEY_CONFLICT = -1;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            // if the subscription has already been deleted, return success
            luaBuilder.Append(
                "if redis.call(\"EXISTS\", KEYS[1]) == 0 then\n" +
                "  return {1, \"\"}\n" +
                "end\n");
            // read the connection-id from the subscription; we will delete this connection's waitkey in a follow-up call
            luaBuilder.Append("local connection_id = redis.call(\"HGET\", KEYS[1], \"connection-id\")\n");
            // delete the subscription itself
            luaBuilder.Append("redis.call(\"DEL\", KEYS[1])\n");
            // delete the subscription's event registrations
            luaBuilder.Append("redis.call(\"DEL\", KEYS[2])\n");
            // delete the subscription's notifications list
            luaBuilder.Append("redis.call(\"DEL\", KEYS[3])\n");
            // delete the subscription's incoming notifications priority queues
            for (int iPriority = 0; iPriority < 8; iPriority++)
            {
                luaBuilder.Append("redis.call(\"DEL\", KEYS[" + (4 + iPriority).ToString() + "])\n");
            }
            //
            luaBuilder.Append("return {1, connection-id}\n");

            object[]      luaResult           = null;
            List <string> keys                = new List <string>();
            string        subscriptionKeyBase = REDIS_PREFIX_SUBSCRIPTION + REDIS_PREFIX_SEPARATOR + (_accountId != null ? _accountId : REDIS_ASTERISK) + REDIS_SLASH + _subscriptionId;;

            keys.Add(subscriptionKeyBase);
            keys.Add(subscriptionKeyBase + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_EVENTS);
            keys.Add(subscriptionKeyBase + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_NOTIFICATIONS);
            for (int iPriority = 0; iPriority < 8; iPriority++)
            {
                keys.Add(subscriptionKeyBase + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_INCOMING_NOTIFICATIONS + iPriority.ToString());
            }
            luaResult = await _redisClient.EvalAsync <string, string, object[]>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

            long?resultValue = (luaResult != null && luaResult.Length >= 1 ? (long)luaResult[0] : (long?)null);

            if (resultValue != null && resultValue == 1)
            {
                if (luaResult.Length >= 2 && string.IsNullOrWhiteSpace((string)luaResult[1]) == false)
                {
                    string oldConnectionId = (string)luaResult[1];

                    // success; dispose of the current connection's waitkey, if one existed (by pushing a dummy value into it and then expiring it in ten seconds)
                    string oldConnectionWaitKey = subscriptionKeyBase + "#waitkey-" + oldConnectionId.ToString();
                    // NOTE: we push a value into the key so that the previous connection will trigger, see that it's not the current connection, and terminate
                    await _redisClient.ListPushRightAsync(oldConnectionWaitKey, new string[] { string.Empty });

                    // NOTE: we also put a timeout on the previous connection's waitkey so that, if no connection was actively listening, the key will be auto-deleted
                    // NOTE: we use a fixed expiration of ten seconds.  We actually do not need any timeout--but we want to make sure that a caller who is currently blocked gets a chance to
                    //       fetch the empty value we just enqueued...while automatically cleaning up afterwards.  ten seconds should be far more than enough time for a previous caller to
                    //       start blocking on its waitkey and dequeue the empty value we pushed into its queue.
                    await _redisClient.ExpireAsync(oldConnectionWaitKey, 10);
                }

                // finally, reset our server-assigned values
                _subscriptionId = null;
            }
            else
            {
                // unknown error
                throw new Exception("Critical Redis error!");
            }
        }
        public async Task SaveSubscriptionAsync()
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            // create our script-builder object placeholders; we re-use these below
            StringBuilder luaBuilder;
            List <string> arguments;
            int           iArgument;
            List <string> keys;

            /* if the subscription is a new subscription, allocate the subscription-id now */
            if (_objectIsNew)
            {
                // allocate a new subscriptionId for this subscription on this account; we use an atomic lua script to do this so that the first subscriptionId is zero for consistency (since HINCRBY would set the first entry to one)
                // NOTE: we allocate the subscriptionId in a unique script because Lua-on-Redis requires that we pass all keys pre-built--so we can't allocate the subscriptionId and create its key(s) in the same script
                // generate Lua script
                luaBuilder = new StringBuilder();
                arguments  = new List <string>();
                iArgument  = 1;
                //
                // default our subscription_id to -1 (as a marker indicating that the subscription_id is not valid)
                luaBuilder.Append("local subscription_id = -1\n");
                // create or increment the last-subscription-id for this account
                luaBuilder.Append(
                    "  if redis.call(\"HEXISTS\", KEYS[1], \"last-subscription-id\") == 0 then\n" +
                    // if no subscription has been added to this account for this server_id, set the subscription_id to zero and store it.
                    "    redis.call(\"HSET\", KEYS[1], \"last-subscription-id\", 0)\n" +
                    "    subscription_id = 0\n" +
                    "  else\n" +
                    // otherwise...increment the last-subscription-id and return the new value as subscription_id.
                    "    subscription_id = redis.call(\"HINCRBY\", KEYS[1], \"last-subscription-id\", 1)\n" +
                    "  end\n" +
                    "  return subscription_id\n"
                    );
                //
                keys = new List <string>();
                keys.Add(REDIS_PREFIX_EVENT_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _serverId);
                long newSubscriptionLuaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                //
                // obtain the subscription_id (if subscription_id == -1 then we had an error)
                if (newSubscriptionLuaResult >= 0)
                {
                    _subscriptionId = _serverId + "-" + newSubscriptionLuaResult.ToString();
                }
                else
                {
                    // subscriptionId could not be allocated
                    throw new Exception("Critical Redis error!");
                }
            }

            int RESULT_SUCCESS         = 0;
            int RESULT_KEY_CONFLICT    = -1;
            int RESULT_DATA_CORRUPTION = -2;

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            luaBuilder = new StringBuilder();
            arguments  = new List <string>();
            iArgument  = 1;
            if (_objectIsNew)
            {
                // for new subscriptions: if a subscription with this accountId+'/'+subscriptionId already exists, return RESULT_KEY_CONFLICT.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" +
                    "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                    "end\n");
            }
            //
            luaBuilder.Append("redis.call(\"HSET\", KEYS[1], \"client-id\", ARGV[" + iArgument.ToString() + "])\n");
            arguments.Add(_clientId);
            iArgument++;
            //
            if (_userId != null)
            {
                luaBuilder.Append("redis.call(\"HSET\", KEYS[1], \"user-id\", ARGV[" + iArgument.ToString() + "])\n");
                arguments.Add(_userId);
                iArgument++;
            }
            //
            luaBuilder.Append("return " + RESULT_SUCCESS.ToString() + "\n"); //

            keys = new List <string>();
            string subscriptionKeyBase = REDIS_PREFIX_SUBSCRIPTION + REDIS_PREFIX_SEPARATOR + (_accountId != null ? _accountId : REDIS_ASTERISK) + REDIS_SLASH + _subscriptionId;

            keys.Add(subscriptionKeyBase);
            long luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

            if (luaResult == RESULT_SUCCESS)
            {
                // success

                // add the subscription to the set of all subscriptions on this fullServerId
                string subscriptionsSetKey = REDIS_PREFIX_EVENT_SERVICE + REDIS_PREFIX_SEPARATOR + (_accountId != null ? _accountId : REDIS_ASTERISK) + REDIS_SLASH + _serverId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SUBSCRIPTIONS;
                await _redisClient.SetAddAsync(subscriptionsSetKey, new string[] { _subscriptionId });
            }
            else if (luaResult == RESULT_KEY_CONFLICT)
            {
                // key name conflict
                // TODO: consider returning an error to the caller
            }
            else if (luaResult == RESULT_DATA_CORRUPTION)
            {
                // data corruption
                throw new Exception("Critical Redis error!");
            }
            else
            {
                // unknown error
                throw new Exception("Critical Redis error!");
            }
        }