21
21
import com .google .api .core .SettableApiFuture ;
22
22
import com .google .api .gax .rpc .ResponseObserver ;
23
23
import com .google .api .gax .rpc .ServerStreamingCallable ;
24
+ import com .google .api .gax .rpc .StatusCode ;
24
25
import com .google .api .gax .rpc .StreamController ;
25
26
import com .google .cloud .Timestamp ;
27
+ import com .google .cloud .firestore .v1 .FirestoreSettings ;
26
28
import com .google .firestore .v1 .RunAggregationQueryRequest ;
27
29
import com .google .firestore .v1 .RunAggregationQueryResponse ;
28
30
import com .google .firestore .v1 .RunQueryRequest ;
29
31
import com .google .firestore .v1 .StructuredAggregationQuery ;
30
32
import com .google .firestore .v1 .Value ;
31
33
import com .google .protobuf .ByteString ;
34
+ import java .util .Set ;
32
35
import java .util .concurrent .atomic .AtomicBoolean ;
33
36
import javax .annotation .Nonnull ;
34
37
import javax .annotation .Nullable ;
@@ -68,25 +71,68 @@ public ApiFuture<AggregateQuerySnapshot> get() {
68
71
69
72
@ Nonnull
70
73
ApiFuture <AggregateQuerySnapshot > get (@ Nullable final ByteString transactionId ) {
71
- RunAggregationQueryRequest request = toProto (transactionId );
72
- AggregateQueryResponseObserver responseObserver = new AggregateQueryResponseObserver ();
74
+ AggregateQueryResponseDeliverer responseDeliverer =
75
+ new AggregateQueryResponseDeliverer (
76
+ transactionId , /* startTimeNanos= */ query .rpcContext .getClock ().nanoTime ());
77
+ runQuery (responseDeliverer );
78
+ return responseDeliverer .getFuture ();
79
+ }
80
+
81
+ private void runQuery (AggregateQueryResponseDeliverer responseDeliverer ) {
82
+ RunAggregationQueryRequest request = toProto (responseDeliverer .getTransactionId ());
83
+ AggregateQueryResponseObserver responseObserver =
84
+ new AggregateQueryResponseObserver (responseDeliverer );
73
85
ServerStreamingCallable <RunAggregationQueryRequest , RunAggregationQueryResponse > callable =
74
86
query .rpcContext .getClient ().runAggregationQueryCallable ();
75
-
76
87
query .rpcContext .streamRequest (request , responseObserver , callable );
88
+ }
89
+
90
+ private final class AggregateQueryResponseDeliverer {
91
+
92
+ @ Nullable private final ByteString transactionId ;
93
+ private final long startTimeNanos ;
94
+ private final SettableApiFuture <AggregateQuerySnapshot > future = SettableApiFuture .create ();
95
+ private final AtomicBoolean isFutureCompleted = new AtomicBoolean (false );
96
+
97
+ AggregateQueryResponseDeliverer (@ Nullable ByteString transactionId , long startTimeNanos ) {
98
+ this .transactionId = transactionId ;
99
+ this .startTimeNanos = startTimeNanos ;
100
+ }
101
+
102
+ ApiFuture <AggregateQuerySnapshot > getFuture () {
103
+ return future ;
104
+ }
105
+
106
+ @ Nullable
107
+ ByteString getTransactionId () {
108
+ return transactionId ;
109
+ }
110
+
111
+ long getStartTimeNanos () {
112
+ return startTimeNanos ;
113
+ }
114
+
115
+ void deliverResult (long count , Timestamp readTime ) {
116
+ if (isFutureCompleted .compareAndSet (false , true )) {
117
+ future .set (new AggregateQuerySnapshot (AggregateQuery .this , readTime , count ));
118
+ }
119
+ }
77
120
78
- return responseObserver .getFuture ();
121
+ void deliverError (Throwable throwable ) {
122
+ if (isFutureCompleted .compareAndSet (false , true )) {
123
+ future .setException (throwable );
124
+ }
125
+ }
79
126
}
80
127
81
128
private final class AggregateQueryResponseObserver
82
129
implements ResponseObserver <RunAggregationQueryResponse > {
83
130
84
- private final SettableApiFuture <AggregateQuerySnapshot > future = SettableApiFuture .create ();
85
- private final AtomicBoolean isFutureNotified = new AtomicBoolean (false );
131
+ private final AggregateQueryResponseDeliverer responseDeliverer ;
86
132
private StreamController streamController ;
87
133
88
- SettableApiFuture < AggregateQuerySnapshot > getFuture ( ) {
89
- return future ;
134
+ AggregateQueryResponseObserver ( AggregateQueryResponseDeliverer responseDeliverer ) {
135
+ this . responseDeliverer = responseDeliverer ;
90
136
}
91
137
92
138
@ Override
@@ -96,14 +142,10 @@ public void onStart(StreamController streamController) {
96
142
97
143
@ Override
98
144
public void onResponse (RunAggregationQueryResponse response ) {
99
- // Ignore subsequent response messages. The RunAggregationQuery RPC returns a stream of
100
- // responses (rather than just a single response); however, only the first response of the
101
- // stream is actually used. Any more responses are technically errors, but since the Future
102
- // will have already been notified, we just drop any unexpected responses.
103
- if (!isFutureNotified .compareAndSet (false , true )) {
104
- return ;
105
- }
145
+ // Close the stream to avoid it dangling, since we're not expecting any more responses.
146
+ streamController .cancel ();
106
147
148
+ // Extract the count and read time from the RunAggregationQueryResponse.
107
149
Timestamp readTime = Timestamp .fromProto (response .getReadTime ());
108
150
Value value = response .getResult ().getAggregateFieldsMap ().get (ALIAS_COUNT );
109
151
if (value == null ) {
@@ -118,19 +160,30 @@ public void onResponse(RunAggregationQueryResponse response) {
118
160
}
119
161
long count = value .getIntegerValue ();
120
162
121
- future .set (new AggregateQuerySnapshot (AggregateQuery .this , readTime , count ));
122
-
123
- // Close the stream to avoid it dangling, since we're not expecting any more responses.
124
- streamController .cancel ();
163
+ // Deliver the result; even though the `RunAggregationQuery` RPC is a "streaming" RPC, meaning
164
+ // that `onResponse()` can be called multiple times, it _should_ only be called once for count
165
+ // queries. But even if it is called more than once, `responseDeliverer` will drop superfluous
166
+ // results.
167
+ responseDeliverer .deliverResult (count , readTime );
125
168
}
126
169
127
170
@ Override
128
171
public void onError (Throwable throwable ) {
129
- if (!isFutureNotified .compareAndSet (false , true )) {
130
- return ;
172
+ if (shouldRetry (throwable )) {
173
+ runQuery (responseDeliverer );
174
+ } else {
175
+ responseDeliverer .deliverError (throwable );
131
176
}
177
+ }
132
178
133
- future .setException (throwable );
179
+ private boolean shouldRetry (Throwable throwable ) {
180
+ Set <StatusCode .Code > retryableCodes =
181
+ FirestoreSettings .newBuilder ().runAggregationQuerySettings ().getRetryableCodes ();
182
+ return query .shouldRetryQuery (
183
+ throwable ,
184
+ responseDeliverer .getTransactionId (),
185
+ responseDeliverer .getStartTimeNanos (),
186
+ retryableCodes );
134
187
}
135
188
136
189
@ Override
0 commit comments