// kick off a read of a tracked object, completing asynchronously if necessary
        public override void ReadAsync(PartitionReadEvent readEvent, EffectTracker effectTracker)
        {
            this.partition.Assert(readEvent != null);
            try
            {
                if (readEvent.Prefetch.HasValue)
                {
                    TryRead(readEvent.Prefetch.Value);
                }

                TryRead(readEvent.ReadTarget);

                void TryRead(Key key)
                {
                    TrackedObject target = null;
                    var           status = this.mainSession.Read(ref key, ref effectTracker, ref target, readEvent, 0);

                    switch (status)
                    {
                    case Status.NOTFOUND:
                    case Status.OK:
                        // fast path: we hit in the cache and complete the read
                        this.StoreStats.HitCount++;
                        effectTracker.ProcessReadResult(readEvent, key, target);
                        break;

                    case Status.PENDING:
                        // slow path: read continuation will be called when complete
                        this.StoreStats.MissCount++;
                        break;

                    case Status.ERROR:
                        this.partition.ErrorHandler.HandleError(nameof(ReadAsync), "FASTER reported ERROR status", null, true, this.partition.ErrorHandler.IsTerminated);
                        break;
                    }
                }
            }
            catch (Exception exception)
                when(this.terminationToken.IsCancellationRequested && !Utils.IsFatal(exception))
                {
                    throw new OperationCanceledException("Partition was terminated.", exception, this.terminationToken);
                }
        }