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); }
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!"); } }