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