/// <summary>
        /// Fetches document snapshots from the server, based on an optional transaction ID.
        /// </summary>
        /// <param name="documents">The document references to fetch. Must not be null, or contain null references.</param>
        /// <param name="transactionId">A transaction ID, or null to not include any transaction ID.</param>
        /// <param name="fieldMask">The field mask to use to restrict which fields are retrieved. May be null, in which
        /// case no field mask is applied, and the complete documents are retrieved.</param>
        /// <param name="cancellationToken">A cancellation token for the operation.</param>
        /// <returns>The document snapshots, in the order they are provided in the response. (This may not be the order of <paramref name="documents"/>.)</returns>
        internal async Task <IList <DocumentSnapshot> > GetDocumentSnapshotsAsync(IEnumerable <DocumentReference> documents, ByteString transactionId, FieldMask fieldMask, CancellationToken cancellationToken)
        {
            GaxPreconditions.CheckNotNull(documents, nameof(documents));
            var request = new BatchGetDocumentsRequest
            {
                Database  = RootPath,
                Documents = { documents.Select(ExtractPath) },
                Mask      = fieldMask?.ToProto()
            };

            if (transactionId != null)
            {
                request.Transaction = transactionId;
            }
            var stream = Client.BatchGetDocuments(request, CallSettings.FromCancellationToken(cancellationToken));

            using (var responseStream = stream.ResponseStream)
            {
                List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>();

                // Note: no need to worry about passing the cancellation token in here, as we've passed it into the overall call.
                // If the token is cancelled, the call will be aborted.
                while (await responseStream.MoveNext().ConfigureAwait(false))
                {
                    var response = responseStream.Current;
                    var readTime = Timestamp.FromProto(response.ReadTime);
                    switch (response.ResultCase)
                    {
                    case BatchGetDocumentsResponse.ResultOneofCase.Found:
                        snapshots.Add(DocumentSnapshot.ForDocument(this, response.Found, readTime));
                        break;

                    case BatchGetDocumentsResponse.ResultOneofCase.Missing:
                        snapshots.Add(DocumentSnapshot.ForMissingDocument(this, response.Missing, readTime));
                        break;

                    default:
                        throw new InvalidOperationException($"Unknown response type: {response.ResultCase}");
                    }
                }
                return(snapshots);
            }

            string ExtractPath(DocumentReference documentReference)
            {
                GaxPreconditions.CheckArgument(documentReference != null, nameof(documents), "DocumentReference sequence must not contain null elements.");
                return(documentReference.Path);
            }
        }
Пример #2
0
        internal async Task <QuerySnapshot> GetSnapshotAsync(ByteString transactionId, CancellationToken cancellationToken)
        {
            var       responses = StreamResponsesAsync(transactionId, cancellationToken);
            Timestamp?readTime  = null;
            List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>();
            await responses.ForEachAsync(response =>
            {
                if (response.Document != null)
                {
                    snapshots.Add(DocumentSnapshot.ForDocument(Database, response.Document, Timestamp.FromProto(response.ReadTime)));
                }
                if (readTime == null && response.ReadTime != null)
                {
                    readTime = Timestamp.FromProto(response.ReadTime);
                }
            }, cancellationToken).ConfigureAwait(false);

            GaxPreconditions.CheckState(readTime != null, "The stream returned from RunQuery did not provide a read timestamp.");

            return(new QuerySnapshot(this, snapshots.AsReadOnly(), readTime.Value));
        }
Пример #3
0
        /// <summary>
        /// Fetches document snapshots from the server, based on an optional transaction ID.
        /// </summary>
        /// <param name="documents">The document references to fetch. Must not be null, or contain null references.</param>
        /// <param name="transactionId">A transaction ID, or null to not include any transaction ID.</param>
        /// <param name="fieldMask">The field mask to use to restrict which fields are retrieved. May be null, in which
        /// case no field mask is applied, and the complete documents are retrieved.</param>
        /// <param name="cancellationToken">A cancellation token for the operation.</param>
        /// <returns>The document snapshots, in the order they are provided in the response. (This may not be the order of <paramref name="documents"/>.)</returns>
        internal async Task <IList <DocumentSnapshot> > GetDocumentSnapshotsAsync(IEnumerable <DocumentReference> documents, ByteString transactionId, FieldMask fieldMask, CancellationToken cancellationToken)
        {
            GaxPreconditions.CheckNotNull(documents, nameof(documents));
            var request = new BatchGetDocumentsRequest
            {
                Database  = RootPath,
                Documents = { documents.Select(ExtractPath) },
                Mask      = fieldMask?.ToProto()
            };

            if (transactionId != null)
            {
                request.Transaction = transactionId;
            }

            var clock        = Client.Settings.Clock ?? SystemClock.Instance;
            var scheduler    = Client.Settings.Scheduler ?? SystemScheduler.Instance;
            var callSettings = _batchGetCallSettings.WithCancellationToken(cancellationToken);

            // This is the function that we'll retry. We can't use the built-in retry functionality, because it's not a unary gRPC call.
            // (We could potentially simulate a unary call, but it would be a little odd to do so.)
            // Note that we perform a "whole request" retry. In theory we could collect some documents, then see an error, and only
            // request the remaining documents. Given how rarely we retry anyway in practice, that's probably not worth doing.
            Func <BatchGetDocumentsRequest, CallSettings, Task <List <DocumentSnapshot> > > function = async(req, settings) =>
            {
                var stream = Client.BatchGetDocuments(req, settings);
                using (var responseStream = stream.ResponseStream)
                {
                    List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>();

                    // Note: no need to worry about passing the cancellation token in here, as we've passed it into the overall call.
                    // If the token is cancelled, the call will be aborted.
                    while (await responseStream.MoveNext().ConfigureAwait(false))
                    {
                        var response = responseStream.Current;
                        var readTime = Timestamp.FromProto(response.ReadTime);
                        switch (response.ResultCase)
                        {
                        case BatchGetDocumentsResponse.ResultOneofCase.Found:
                            snapshots.Add(DocumentSnapshot.ForDocument(this, response.Found, readTime));
                            break;

                        case BatchGetDocumentsResponse.ResultOneofCase.Missing:
                            snapshots.Add(DocumentSnapshot.ForMissingDocument(this, response.Missing, readTime));
                            break;

                        default:
                            throw new InvalidOperationException($"Unknown response type: {response.ResultCase}");
                        }
                    }
                    return(snapshots);
                }
            };

            var retryingTask = RetryHelper.Retry(function, request, callSettings, clock, scheduler);

            return(await retryingTask.ConfigureAwait(false));

            string ExtractPath(DocumentReference documentReference)
            {
                GaxPreconditions.CheckArgument(documentReference != null, nameof(documents), "DocumentReference sequence must not contain null elements.");
                return(documentReference.Path);
            }
        }
Пример #4
0
        public async Task <WatchResponseResult> HandleResponseAsync(ListenResponse response, CancellationToken cancellationToken)
        {
            switch (response.ResponseTypeCase)
            {
            case ListenResponse.ResponseTypeOneofCase.TargetChange:
                TargetChange change      = response.TargetChange;
                bool         noTargetIds = change.TargetIds.Count == 0;

                switch (change.TargetChangeType)
                {
                case NoChange:
                    if (noTargetIds && change.ReadTime != null && _current)
                    {
                        // This means everything is up-to-date, so emit the current set of docs as a snapshot,
                        // if there were changes.
                        await PushSnapshotAsync(Timestamp.FromProto(change.ReadTime), change.ResumeToken, cancellationToken).ConfigureAwait(false);
                    }
                    break;

                case Add:
                    GaxPreconditions.CheckState(WatchStream.WatchTargetId == change.TargetIds[0], "Target ID must be 0x{0:x}", WatchStream.WatchTargetId);
                    break;

                case Remove:
                    // TODO: Do we really want an RpcException here?
                    StatusCode status = (StatusCode?)change.Cause?.Code ?? StatusCode.Cancelled;
                    throw new RpcException(new Status(status, $"Backend ended listen stream: {change.Cause?.Message}"));

                case Current:
                    _current = true;
                    break;

                case Reset:
                    ResetDocs();
                    return(WatchResponseResult.Continue);

                default:
                    // TODO: Do we really want an RpcException here?
                    throw new RpcException(new Status(StatusCode.InvalidArgument, $"Encountered invalid target change type: {change.Cause.Message}"));
                }

                bool healthy = change.ResumeToken != null && (change.TargetIds.Count == 0 || change.TargetIds.Contains(WatchStream.WatchTargetId));
                // Possibly tell the watch stream that it's now healthy (so reset backoff), or just continue.
                return(healthy ? WatchResponseResult.StreamHealthy : WatchResponseResult.Continue);

            case ListenResponse.ResponseTypeOneofCase.DocumentChange:
                // No other targetIds can show up here, but we still need to see if the targetId was in the
                // added list or removed list.
                var               changed  = response.DocumentChange.TargetIds.Contains(WatchStream.WatchTargetId);
                var               removed  = response.DocumentChange.RemovedTargetIds.Contains(WatchStream.WatchTargetId);
                Document          document = response.DocumentChange.Document;
                DocumentReference docRef   = CreateDocumentReference(document.Name);
                if (changed && removed)
                {
                    throw new InvalidOperationException("Server error: document was both changed and removed");
                }
                if (!changed && !removed)
                {
                    throw new InvalidOperationException("Server error: document was neither changed nor removed");
                }
                _changeMap[docRef] = changed ? document : null;
                return(WatchResponseResult.Continue);

            case ListenResponse.ResponseTypeOneofCase.DocumentDelete:
                _changeMap[CreateDocumentReference(response.DocumentDelete.Document)] = null;
                return(WatchResponseResult.Continue);

            case ListenResponse.ResponseTypeOneofCase.DocumentRemove:
                _changeMap[CreateDocumentReference(response.DocumentRemove.Document)] = null;
                return(WatchResponseResult.Continue);

            case ListenResponse.ResponseTypeOneofCase.Filter:
                // TODO: Do we really want to create the change set itself, rather than just count? It seems a bit wasteful.
                ChangeSet changeSet   = ExtractChanges(default);
                int       currentSize = _documentSet.Count + changeSet.Adds.Count - changeSet.Deletes.Count;
                // Reset the stream if we don't have the right number of documents.
                if (response.Filter.Count != currentSize)
                {
                    ResetDocs();
                    return(WatchResponseResult.ResetStream);
                }
                return(WatchResponseResult.Continue);

            default:
                throw new RpcException(new Status(StatusCode.InvalidArgument, $"Encountered invalid listen response type: {response.ResponseTypeCase}"));
            }
        }
Пример #5
0
 internal IAsyncEnumerable <DocumentSnapshot> StreamAsync(ByteString transactionId, CancellationToken cancellationToken) =>
 StreamResponsesAsync(transactionId, cancellationToken)
 .Where(resp => resp.Document != null)
 .Select(resp => DocumentSnapshot.ForDocument(Database, resp.Document, Timestamp.FromProto(resp.ReadTime)));
Пример #6
0
 internal static WriteResult FromProto(V1.WriteResult result, wkt::Timestamp commitTime)
 {
     GaxPreconditions.CheckNotNull(result, nameof(result));
     GaxPreconditions.CheckNotNull(commitTime, nameof(commitTime));
     return(new WriteResult(Timestamp.FromProto(result.UpdateTime ?? commitTime)));
 }