private ItemDescriptor Deserialize(DataKind kind, SerializedItemDescriptor serializedItemDesc)
        {
            if (serializedItemDesc.Deleted || serializedItemDesc.SerializedItem is null)
            {
                return(ItemDescriptor.Deleted(serializedItemDesc.Version));
            }
            var deserializedItem = kind.Deserialize(serializedItemDesc.SerializedItem);

            if (serializedItemDesc.Version == 0 || serializedItemDesc.Version == deserializedItem.Version ||
                deserializedItem.Item is null)
            {
                return(deserializedItem);
            }
            // If the store gave us a version number that isn't what was encoded in the object, trust it
            return(new ItemDescriptor(serializedItemDesc.Version, deserializedItem.Item));
        }
 public bool Upsert(DataKind kind, string key, SerializedItemDescriptor item)
 {
     MaybeThrowError();
     if (!Data.ContainsKey(kind))
     {
         Data[kind] = new Dictionary <string, SerializedItemDescriptor>();
     }
     if (Data[kind].TryGetValue(key, out var oldItem))
     {
         // If PersistOnlyAsString is true, simulate the kind of implementation where we can't see the
         // version as a separate attribute in the database and must deserialize the item to get it.
         var oldVersion = PersistOnlyAsString ?
                          kind.Deserialize(oldItem.SerializedItem).Version :
                          oldItem.Version;
         if (oldVersion >= item.Version)
         {
             return(false);
         }
     }
     Data[kind][key] = StorableItem(kind, item);
     return(true);
 }
Example #3
0
        public async Task <bool> UpsertAsync(DataKind kind, string key, SerializedItemDescriptor newItem)
        {
            var fullKey = ItemKey(kind, key);

            // We will potentially keep retrying indefinitely until someone's write succeeds
            while (true)
            {
                var oldValue   = (await _client.KV.Get(fullKey)).Response;
                var oldVersion = oldValue is null ? 0 :
                                 kind.Deserialize(Encoding.UTF8.GetString(oldValue.Value)).Version;

                // Check whether the item is stale. If so, don't do the update (and return the existing item to
                // FeatureStoreWrapper so it can be cached)
                if (oldVersion >= newItem.Version)
                {
                    return(false);
                }

                // Otherwise, try to write. We will do a compare-and-set operation, so the write will only succeed if
                // the key's ModifyIndex is still equal to the previous value returned by getEvenIfDeleted. If the
                // previous ModifyIndex was zero, it means the key did not previously exist and the write will only
                // succeed if it still doesn't exist.
                var modifyIndex = oldValue == null ? 0 : oldValue.ModifyIndex;
                var pair        = new KVPair(fullKey)
                {
                    Value       = Encoding.UTF8.GetBytes(newItem.SerializedItem),
                    ModifyIndex = modifyIndex
                };
                var result = await _client.KV.CAS(pair);

                if (result.Response)
                {
                    return(true);
                }

                // If we failed, retry the whole shebang
                _log.Debug("Concurrent modification detected, retrying");
            }
        }
        public bool Upsert(DataKind kind, string key, SerializedItemDescriptor newItem)
        {
            IDatabase db      = _redis.GetDatabase();
            string    baseKey = ItemsKey(kind);

            while (true)
            {
                string oldData;
                try
                {
                    oldData = db.HashGet(baseKey, key);
                }
                catch (RedisTimeoutException e)
                {
                    _log.Error("Timeout in update when reading {0} from {1}: {2}", key, baseKey, e.ToString());
                    throw;
                }
                // Here, unfortunately, we have to deserialize the old item (if any) just to find
                // out its version number (see implementation notes).
                var oldVersion = (oldData is null) ? 0 : kind.Deserialize(oldData).Version;
                if (oldVersion >= newItem.Version)
                {
                    _log.Debug("Attempted to {0} key: {1} version: {2} with a version that is" +
                               " the same or older: {3} in \"{4}\"",
                               newItem.Deleted ? "delete" : "update",
                               key, oldVersion, newItem.Version, kind.Name);
                    return(false);
                }

                // This hook is used only in unit tests
                _updateHook?.Invoke();

                // Note that transactions work a bit differently in StackExchange.Redis than in other
                // Redis clients. The same Redis connection is shared across all threads, so it can't
                // set a WATCH at the moment we start the transaction. Instead, it saves up all of
                // the actions we send during the transaction, and replays them all within a MULTI
                // when the transaction. AddCondition() is this client's way of doing a WATCH, and it
                // can only use an equality match on the whole value (which is unfortunate since a
                // serialized flag value could be fairly large).
                ITransaction txn = db.CreateTransaction();
                txn.AddCondition(oldData is null ? Condition.HashNotExists(baseKey, key) :
                                 Condition.HashEqual(baseKey, key, oldData));

                txn.HashSetAsync(baseKey, key, newItem.SerializedItem);

                try
                {
                    bool success = txn.Execute();
                    if (!success)
                    {
                        // The watch was triggered, we should retry
                        _log.Debug("Concurrent modification detected, retrying");
                        continue;
                    }
                }
                catch (RedisTimeoutException e)
                {
                    _log.Error("Timeout on update of {0} in {1}: {2}", key, baseKey, e.ToString());
                    throw;
                }
                return(true);
            }
        }