|
27 | 27 | import com.google.api.gax.core.NoCredentialsProvider; |
28 | 28 | import com.google.api.gax.paging.Page; |
29 | 29 | import com.google.api.gax.rpc.HeaderProvider; |
| 30 | +import com.google.api.gax.rpc.ServerStream; |
30 | 31 | import com.google.api.services.bigquery.model.ErrorProto; |
31 | 32 | import com.google.api.services.bigquery.model.GetQueryResultsResponse; |
32 | 33 | import com.google.api.services.bigquery.model.ProjectList; |
|
53 | 54 | import com.google.cloud.bigquery.storage.v1.BigQueryReadSettings; |
54 | 55 | import com.google.cloud.bigquery.storage.v1.CreateReadSessionRequest; |
55 | 56 | import com.google.cloud.bigquery.storage.v1.DataFormat; |
| 57 | +import com.google.cloud.bigquery.storage.v1.ReadRowsRequest; |
| 58 | +import com.google.cloud.bigquery.storage.v1.ReadRowsResponse; |
56 | 59 | import com.google.cloud.bigquery.storage.v1.ReadSession; |
57 | 60 | import com.google.common.annotations.VisibleForTesting; |
58 | 61 | import com.google.common.base.Function; |
|
70 | 73 | import io.opentelemetry.context.Scope; |
71 | 74 | import java.io.IOException; |
72 | 75 | import java.net.URI; |
| 76 | +import java.util.ArrayDeque; |
73 | 77 | import java.util.ArrayList; |
74 | 78 | import java.util.Collections; |
| 79 | +import java.util.Iterator; |
75 | 80 | import java.util.List; |
76 | 81 | import java.util.Map; |
| 82 | +import java.util.Queue; |
77 | 83 | import java.util.concurrent.Callable; |
78 | 84 | import java.util.concurrent.locks.ReentrantLock; |
79 | 85 | import java.util.regex.Matcher; |
@@ -280,9 +286,177 @@ public Page<FieldValueList> getNextPage() { |
280 | 286 | } |
281 | 287 | } |
282 | 288 |
|
| 289 | + /** |
| 290 | + * NextPageFetcher implementation for queries returning results in Arrow format. Reads subsequent |
| 291 | + * pages from the job's default gRPC storage read stream. |
| 292 | + * |
| 293 | + * <p>Note: Neither {@link Page} nor {@link TableResult} implements {@link AutoCloseable}. The |
| 294 | + * underlying gRPC stream is automatically canceled and resources released when iteration reaches |
| 295 | + * the end (or maximum results requested) or when an error occurs. Callers that do not iterate to |
| 296 | + * completion rely on server-side stream timeouts and garbage collection to release stream |
| 297 | + * resources. |
| 298 | + */ |
| 299 | + static class ArrowQueryPageFetcher implements NextPageFetcher<FieldValueList> { |
| 300 | + private static final long serialVersionUID = 1L; |
| 301 | + private static final long DEFAULT_PAGE_SIZE = 10000L; |
| 302 | + |
| 303 | + private final JobId jobId; |
| 304 | + private final Schema schema; |
| 305 | + private final byte[] arrowSchemaBytes; |
| 306 | + private final BigQueryOptions serviceOptions; |
| 307 | + private final long maxResults; |
| 308 | + private final Map<BigQueryRpc.Option, ?> optionsMap; |
| 309 | + |
| 310 | + private transient org.apache.arrow.vector.types.pojo.Schema arrowSchemaPojo; |
| 311 | + private transient BigQueryReadClient bqReadClient; |
| 312 | + private transient ServerStream<ReadRowsResponse> stream; |
| 313 | + private transient Iterator<ReadRowsResponse> streamIterator; |
| 314 | + private final Queue<FieldValueList> buffer = new ArrayDeque<>(); |
| 315 | + private long totalRowsReturned = 0L; |
| 316 | + private boolean streamClosed = false; |
| 317 | + |
| 318 | + ArrowQueryPageFetcher( |
| 319 | + JobId jobId, |
| 320 | + Schema schema, |
| 321 | + byte[] arrowSchemaBytes, |
| 322 | + org.apache.arrow.vector.types.pojo.Schema arrowSchemaPojo, |
| 323 | + BigQueryOptions serviceOptions, |
| 324 | + long initialRowOffset, |
| 325 | + Long maxResults, |
| 326 | + Map<BigQueryRpc.Option, ?> optionsMap) { |
| 327 | + this.jobId = jobId; |
| 328 | + this.schema = schema; |
| 329 | + this.arrowSchemaBytes = arrowSchemaBytes; |
| 330 | + this.arrowSchemaPojo = arrowSchemaPojo; |
| 331 | + this.serviceOptions = serviceOptions; |
| 332 | + this.totalRowsReturned = initialRowOffset; |
| 333 | + this.maxResults = maxResults != null ? maxResults : Long.MAX_VALUE; |
| 334 | + this.optionsMap = optionsMap; |
| 335 | + } |
| 336 | + |
| 337 | + @Override |
| 338 | + public Page<FieldValueList> getNextPage() { |
| 339 | + if (streamClosed || totalRowsReturned >= maxResults) { |
| 340 | + closeClient(); |
| 341 | + return null; |
| 342 | + } |
| 343 | + |
| 344 | + Number optionPageSize = |
| 345 | + optionsMap != null ? (Number) optionsMap.get(BigQueryRpc.Option.MAX_RESULTS) : null; |
| 346 | + long pageSize = |
| 347 | + optionPageSize != null && optionPageSize.longValue() > 0 |
| 348 | + ? optionPageSize.longValue() |
| 349 | + : DEFAULT_PAGE_SIZE; |
| 350 | + List<FieldValueList> rowBatch = new ArrayList<>((int) Math.min(pageSize, 10000L)); |
| 351 | + |
| 352 | + try { |
| 353 | + String location = |
| 354 | + jobId.getLocation() != null ? jobId.getLocation() : serviceOptions.getLocation(); |
| 355 | + if (location == null) { |
| 356 | + throw new BigQueryException( |
| 357 | + 0, "Location must be specified to read Arrow rows from storage stream"); |
| 358 | + } |
| 359 | + |
| 360 | + if (bqReadClient == null) { |
| 361 | + BigQuery service = serviceOptions.getService(); |
| 362 | + if (service instanceof BigQueryImpl) { |
| 363 | + BigQueryImpl impl = (BigQueryImpl) service; |
| 364 | + bqReadClient = impl.getBigQueryReadClient(); |
| 365 | + } else { |
| 366 | + throw new IllegalStateException( |
| 367 | + "Arrow query result pagination requires an instance of BigQueryImpl to manage BigQueryReadClient lifecycle"); |
| 368 | + } |
| 369 | + } |
| 370 | + |
| 371 | + if (streamIterator == null) { |
| 372 | + String streamName = |
| 373 | + String.format( |
| 374 | + "projects/%s/locations/%s/jobs/%s/streams/_default", |
| 375 | + jobId.getProject() != null ? jobId.getProject() : serviceOptions.getProjectId(), |
| 376 | + location, |
| 377 | + jobId.getJob()); |
| 378 | + |
| 379 | + ReadRowsRequest readRowsRequest = |
| 380 | + ReadRowsRequest.newBuilder() |
| 381 | + .setReadStream(streamName) |
| 382 | + .setOffset(totalRowsReturned) |
| 383 | + .build(); |
| 384 | + |
| 385 | + stream = bqReadClient.readRowsCallable().call(readRowsRequest); |
| 386 | + streamIterator = stream.iterator(); |
| 387 | + } |
| 388 | + |
| 389 | + if (arrowSchemaPojo == null && arrowSchemaBytes != null) { |
| 390 | + arrowSchemaPojo = ArrowDeserializer.deserializeSchema(arrowSchemaBytes); |
| 391 | + } |
| 392 | + |
| 393 | + boolean hasMore = |
| 394 | + ArrowDeserializer.loadArrowRows( |
| 395 | + streamIterator, |
| 396 | + arrowSchemaPojo, |
| 397 | + schema, |
| 398 | + rowBatch, |
| 399 | + buffer, |
| 400 | + pageSize, |
| 401 | + totalRowsReturned, |
| 402 | + maxResults); |
| 403 | + |
| 404 | + if (rowBatch.isEmpty()) { |
| 405 | + streamClosed = true; |
| 406 | + closeClient(); |
| 407 | + return null; |
| 408 | + } |
| 409 | + |
| 410 | + totalRowsReturned += rowBatch.size(); |
| 411 | + |
| 412 | + String nextPageToken = null; |
| 413 | + if (hasMore && totalRowsReturned < maxResults) { |
| 414 | + nextPageToken = String.valueOf(totalRowsReturned); |
| 415 | + } else { |
| 416 | + streamClosed = true; |
| 417 | + closeClient(); |
| 418 | + } |
| 419 | + |
| 420 | + return new PageImpl<>(this, nextPageToken, rowBatch); |
| 421 | + |
| 422 | + } catch (BigQueryException e) { |
| 423 | + streamClosed = true; |
| 424 | + closeClient(); |
| 425 | + throw e; |
| 426 | + } catch (Exception e) { |
| 427 | + streamClosed = true; |
| 428 | + closeClient(); |
| 429 | + throw new BigQueryException(0, "Failed to read Arrow rows from storage stream", e); |
| 430 | + } |
| 431 | + } |
| 432 | + |
| 433 | + private void closeClient() { |
| 434 | + if (stream != null) { |
| 435 | + try { |
| 436 | + stream.cancel(); |
| 437 | + } catch (Exception e) { |
| 438 | + // Ignore cancellation exceptions |
| 439 | + } |
| 440 | + } |
| 441 | + bqReadClient = null; |
| 442 | + streamIterator = null; |
| 443 | + stream = null; |
| 444 | + } |
| 445 | + } |
| 446 | + |
283 | 447 | private final ReentrantLock readClientLock = new ReentrantLock(); |
284 | 448 | private transient BigQueryReadClient bqReadClient; |
285 | 449 |
|
| 450 | + @VisibleForTesting |
| 451 | + void setBigQueryReadClient(BigQueryReadClient client) { |
| 452 | + readClientLock.lock(); |
| 453 | + try { |
| 454 | + this.bqReadClient = client; |
| 455 | + } finally { |
| 456 | + readClientLock.unlock(); |
| 457 | + } |
| 458 | + } |
| 459 | + |
286 | 460 | /** |
287 | 461 | * Lazily creates or retrieves the shared {@link BigQueryReadClient} instance used for streaming |
288 | 462 | * Arrow query results, reusing credentials and channel configuration from this {@link |
|
0 commit comments