blob: b55d5b8bb8174e4aa3a6a92f36641161800af779 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Apache.Arrow.Adbc.Drivers.Apache.Hive2;
using Apache.Arrow.Adbc.Drivers.Databricks.Reader.CloudFetch;
using Apache.Arrow.Adbc.Tracing;
using Apache.Hive.Service.Rpc.Thrift;
namespace Apache.Arrow.Adbc.Drivers.Databricks.Reader
{
/// <summary>
/// A composite reader for Databricks that delegates to either CloudFetchReader or DatabricksReader
/// based on CloudFetch configuration and result set characteristics. This was introduced because some
/// older DBR do not accurately report the result set characteristics in the MetadataResponse
/// </summary>
internal class DatabricksCompositeReader : TracingReader
{
public override string AssemblyName => DatabricksConnection.s_assemblyName;
public override string AssemblyVersion => DatabricksConnection.s_assemblyVersion;
public override Schema Schema { get { return _schema; } }
private BaseDatabricksReader? _activeReader;
private readonly IHiveServer2Statement _statement;
private readonly Schema _schema;
private readonly IResponse _response;
private readonly bool _isLz4Compressed;
private IOperationStatusPoller? operationStatusPoller;
private bool _disposed;
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="DatabricksCompositeReader"/> class.
/// </summary>
/// <param name="statement">The Databricks statement.</param>
/// <param name="schema">The Arrow schema.</param>
/// <param name="isLz4Compressed">Whether the results are LZ4 compressed.</param>
/// <param name="httpClient">The HTTP client for CloudFetch operations.</param>
internal DatabricksCompositeReader(
IHiveServer2Statement statement,
Schema schema,
IResponse response,
bool isLz4Compressed,
HttpClient httpClient,
IOperationStatusPoller? operationPoller = null)
: base(statement)
{
_statement = statement ?? throw new ArgumentNullException(nameof(statement));
_schema = schema ?? throw new ArgumentNullException(nameof(schema));
_response = response;
_isLz4Compressed = isLz4Compressed;
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
// use direct results if available
if (_statement.TryGetDirectResults(_response, out TSparkDirectResults? directResults)
&& directResults!.__isset.resultSet
&& directResults.ResultSet != null)
{
_activeReader = DetermineReader(directResults.ResultSet);
}
if (_response.DirectResults?.ResultSet?.HasMoreRows ?? true)
{
operationStatusPoller = operationPoller ?? new DatabricksOperationStatusPoller(_statement, response, GetHeartbeatIntervalFromConnection(), GetRequestTimeoutFromConnection());
operationStatusPoller.Start();
}
}
/// <summary>
/// Determines whether CloudFetch should be used based on the fetch results.
/// </summary>
/// <param name="initialResults">The initial fetch results.</param>
/// <returns>True if CloudFetch should be used, false otherwise.</returns>
internal static bool ShouldUseCloudFetch(TFetchResultsResp initialResults)
{
return initialResults.__isset.results &&
initialResults.Results.__isset.resultLinks &&
initialResults.Results.ResultLinks?.Count > 0;
}
private BaseDatabricksReader DetermineReader(TFetchResultsResp initialResults)
{
if (ShouldUseCloudFetch(initialResults))
{
return CreateCloudFetchReader(initialResults);
}
else
{
return CreateDatabricksReader(initialResults);
}
}
/// <summary>
/// Reads the next record batch from the active reader.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The next record batch, or null if there are no more batches.</returns>
private async ValueTask<RecordBatch?> ReadNextRecordBatchInternalAsync(CancellationToken cancellationToken = default)
{
// Initialize the active reader if not already done
if (_activeReader == null)
{
// if no reader, we did not have direct results
// Make a FetchResults call to get the initial result set
// and determine the reader based on the result set
TFetchResultsReq request = new TFetchResultsReq(_response.OperationHandle!, TFetchOrientation.FETCH_NEXT, this._statement.BatchSize);
// Set MaxBytes from DatabricksStatement
if (this._statement is DatabricksStatement databricksStatement)
{
request.MaxBytes = databricksStatement.MaxBytesPerFetchRequest;
}
TFetchResultsResp response = await this._statement.Client!.FetchResults(request, cancellationToken);
_activeReader = DetermineReader(response);
}
return await _activeReader.ReadNextRecordBatchAsync(cancellationToken);
}
/// <summary>
/// Creates a CloudFetchReader instance. Virtual to allow testing.
/// </summary>
/// <param name="initialResults">The initial fetch results.</param>
/// <returns>A new CloudFetchReader instance.</returns>
protected virtual BaseDatabricksReader CreateCloudFetchReader(TFetchResultsResp initialResults)
{
return new CloudFetchReader(_statement, _schema, _response, initialResults, _isLz4Compressed, _httpClient);
}
/// <summary>
/// Creates a DatabricksReader instance. Virtual to allow testing.
/// </summary>
/// <param name="initialResults">The initial fetch results.</param>
/// <returns>A new DatabricksReader instance.</returns>
protected virtual BaseDatabricksReader CreateDatabricksReader(TFetchResultsResp initialResults)
{
return new DatabricksReader(_statement, _schema, _response, initialResults, _isLz4Compressed);
}
public override async ValueTask<RecordBatch?> ReadNextRecordBatchAsync(CancellationToken cancellationToken = default)
{
var result = await ReadNextRecordBatchInternalAsync(cancellationToken);
// Stop the poller when we've reached the end of results
if (result == null)
{
StopOperationStatusPoller();
}
return result;
}
protected override void Dispose(bool disposing)
{
try
{
if (!_disposed)
{
if (disposing)
{
StopOperationStatusPoller();
if (_activeReader == null)
{
_ = HiveServer2Reader.CloseOperationAsync(_statement, _response)
.ConfigureAwait(false).GetAwaiter().GetResult();
}
else
{
// Note: Have the contained reader close the operation to avoid duplicate calls.
_activeReader.Dispose();
_activeReader = null;
}
}
}
}
finally
{
base.Dispose(disposing);
_disposed = true;
}
}
private void StopOperationStatusPoller()
{
operationStatusPoller?.Stop();
operationStatusPoller?.Dispose();
operationStatusPoller = null;
}
/// <summary>
/// Gets the heartbeat interval from the statement's connection.
/// </summary>
/// <returns>The heartbeat interval in seconds, or default if not available.</returns>
private int GetHeartbeatIntervalFromConnection()
{
if (_statement is DatabricksStatement databricksStatement)
{
var connection = databricksStatement.Connection;
if (connection is DatabricksConnection databricksConnection)
{
return databricksConnection.FetchHeartbeatIntervalSeconds;
}
}
return DatabricksConstants.DefaultOperationStatusPollingIntervalSeconds;
}
/// <summary>
/// Gets the request timeout from the statement's connection.
/// </summary>
/// <returns>The request timeout in seconds, or default if not available.</returns>
private int GetRequestTimeoutFromConnection()
{
if (_statement is DatabricksStatement databricksStatement)
{
var connection = databricksStatement.Connection;
if (connection is DatabricksConnection databricksConnection)
{
return databricksConnection.OperationStatusRequestTimeoutSeconds;
}
}
return DatabricksConstants.DefaultOperationStatusRequestTimeoutSeconds;
}
}
}