/// <summary>
        /// The internal lock extender.
        /// </summary>
        /// <returns></returns>
        private async Task LockExtender()
        {
            while (!_disposeCancellation.IsCancellationRequested)
            {
                // wait
                await Task.Delay(_refreshSpan).ConfigureAwait(false);

                // get expiry time
                TimeSpan expiryTime = _expirySpan;

                // gather all locks that need to be refreshed or expired
                RedisLockHandle[]             refreshableLocks = null;
                IEnumerable <RedisLockHandle> expiredLocks     = null;

                lock (_handles) {
                    refreshableLocks = _handles.Where(h => h.RefreshedAt <(DateTime.UtcNow - TimeSpan.FromSeconds(30)) || h.ExpiresAt> DateTime.UtcNow).ToArray();
                    expiredLocks     = _handles.Where(h => h.ExpiresAt < DateTime.UtcNow);

                    // process any expires locks
                    foreach (RedisLockHandle handle in expiredLocks)
                    {
                        _handles.Remove(handle);
                    }
                }

                // build refresh lock tasks
                List <Task <bool> > refreshLockTasks = new List <Task <bool> >(refreshableLocks.Length);
                Dictionary <Task <bool>, RedisLockHandle> refreshLockHandles = new Dictionary <Task <bool>, RedisLockHandle>();
                Dictionary <Task <bool>, DateTime>        refreshLockTimes   = new Dictionary <Task <bool>, DateTime>();

                foreach (RedisLockHandle handle in refreshableLocks)
                {
                    // create task
                    Task <bool> task = _database.LockExtendAsync($"tandem.{handle.ResourceURI.ToString()}", handle.Token.ToString(), expiryTime);

                    // add and map
                    refreshLockTasks.Add(task);
                    refreshLockHandles[task] = handle;
                    refreshLockTimes[task]   = DateTime.UtcNow;
                }

                // refresh all locks
                try {
                    await Task.WhenAll(refreshLockTasks).ConfigureAwait(false);
                } catch (Exception) { }

                // map any sucesses
                List <RedisLockHandle> refreshFailures = new List <RedisLockHandle>();

                foreach (var task in refreshLockTasks)
                {
                    // find associated handle
                    RedisLockHandle handle = refreshLockHandles[task];
                    DateTime        time   = refreshLockTimes[task];

                    // ignore faulty refreshes, if it expires eventually we'll catch that too
                    if (task.IsFaulted)
                    {
                        // invoke event
                        try {
                            OnInvalidated(new RedisLockInvalidatedEventArgs()
                            {
                                Handle = handle
                            });
                        } catch (Exception) { }

                        continue;
                    }

                    // check if we successfully refreshed the handle
                    if (task.Result == true)
                    {
                        handle.RefreshedAt = time;
                        handle.ExpiresAt   = time + expiryTime;
                    }
                    else
                    {
                        refreshFailures.Add(handle);
                    }
                }

                lock (_handles) {
                    foreach (var handle in refreshFailures)
                    {
                        try {
                            _handles.Remove(handle);
                        } catch (Exception) { }
                    }
                }
            }
        }
        /// <summary>
        /// Locks the specified resource.
        /// </summary>
        /// <param name="resourceUri">The resource URI.</param>
        /// <param name="waitTime">The maximum amount of time to wait.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns>The lock handle or null if the lock could not be obtained.</returns>
        public async Task <ILockHandle> LockAsync(Uri resourceUri, TimeSpan waitTime = default(TimeSpan), CancellationToken cancellationToken = default(CancellationToken))
        {
            if (_disposed == 1)
            {
                throw new ObjectDisposedException("The lock manager has been disposed");
            }

            if (!resourceUri.Scheme.Equals("tandem", StringComparison.CurrentCultureIgnoreCase))
            {
                throw new FormatException("The protocol scheme must be tandem");
            }

            // generate random token
            LockToken token = new LockToken(Guid.NewGuid(), _owner);

            // get remaining time
            TimeSpan remainingTime = waitTime;

            while (remainingTime >= TimeSpan.Zero)
            {
                // check if cancelled
                cancellationToken.ThrowIfCancellationRequested();

                // try and get the lock
                bool gotLock = await _database.LockTakeAsync($"tandem.{resourceUri.ToString()}", token.ToString(), _expirySpan).ConfigureAwait(false);

                if (gotLock)
                {
                    // create handle
                    RedisLockHandle handle = new RedisLockHandle(this)
                    {
                        ResourceURI = resourceUri,
                        Token       = token
                    };

                    // add handle
                    lock (_handles) {
                        _handles.Add(handle);
                    }

                    return(handle);
                }
                else
                {
                    if (waitTime == TimeSpan.Zero)
                    {
                        return(null);
                    }

                    if (remainingTime > TimeSpan.FromMilliseconds(3000))
                    {
                        // no point waiting anymore, no cigar for the lock
                        return(null);
                    }
                    else
                    {
                        // wait 3 seconds until we try again
                        await Task.Delay(3000, cancellationToken).ConfigureAwait(false);

                        remainingTime -= TimeSpan.FromSeconds(3);
                    }
                }
            }

            return(null);
        }