Skip to content

Introduce abstract base classes for Arrow result handling#850

Closed
jayantsing-db wants to merge 1618 commits into
mainfrom
jayantsing-db/http-client
Closed

Introduce abstract base classes for Arrow result handling#850
jayantsing-db wants to merge 1618 commits into
mainfrom
jayantsing-db/http-client

Conversation

@jayantsing-db

Copy link
Copy Markdown
Collaborator

Description

This PR is the first of two and splits the changes originally proposed in #634.

Key changes:

  • Create AbstractRemoteChunkProvider and AbstractArrowResultChunk as unified base implementations for chunk management
  • Add state machine (ArrowResultChunkStateMachine) to handle chunk lifecycle transitions
  • Improve thread synchronization between main thread and IO/download threads

The new async implementation (2nd PR) will provide an alternative path for handling large Arrow datasets with improved scalability, while maintaining the existing synchronous approach for backwards compatibility.

Below is a class diagram that provides a clearer understanding and concise summary of the class structure:

class-diagram

Testing

  • Fake service tests
  • Unit tests
  • Multi-DBR tests
  • Local testing in a highly concurrent environment

Additional Notes to the Reviewer

Benchmarking document: https://docs.google.com/document/d/1MvKeSnrQVKFGdkuaSPXFCrbb4TDaUqFwHrIW2rxy3zI/edit?usp=sharing

madhav-db and others added 30 commits December 17, 2024 05:30
* parser

* more complex data type logic

* complete complex data types

* more tests

* refine

* integration test

* add test to driver test

* removed secret

* fmt

* fix unsupported test

* integration test working

* fmt

* comments

* fmt

* update method sig

* fmt

* comments

* comments

* comments

* fmt

* fmt

* add

* more extensive testing

* more extensive testing

* more extensive testing

* more extensive testing

* more extensive testing
* Send intial connection log to unauth telemetry endpoint

* Add tests + remove debug logs

* fmt

* Add tests

* Add tests

* Add tests for classes i didn't add:)

* add getters/setters in systemConfiguration + improve coverage
- Run execution queries in `async` mode
- Run metadata queries in `async` mode
- Refactor result retrieval in Thrift by improving response handling and fixing polling
- Refactor credential refreshing by transitioning from Thrift accessor to Thrift transport
- Competing driver, SQL Gateway, and SQL Execution API also execute the queries in async mode
- Minor code cleanup

Running queries in async mode and using polling will be beneficial:
- Freeing up server resources under high load instead of holding the connection for ~5 seconds (direct results)
- If query takes more than 5 seconds, server will anyway require polling from client
- Didn't notice any regressions (slightly better performance) under high concurrent load when running queries in async mode. Some empirical results below:
  - Running a simple select query on a table with 10-15 columns for 10K rows in a concurrent environment (250 connections executing query in parallel)
- Average results when running on `main`
```
Average query time across connections: 73873.58ms
Min query time: 22075ms
Max query time: 88997ms
```
- Average results when running in `async` mode
```
Average query time across connections: 68382.95ms
Min query time: 24718ms
Max query time: 95282ms
```
…ch (#622)

This adds a workflow to run JDBC comparator in a separate branch for GA readiness. Note that this does not affect the main branch.
Add databricks protected runners
Use databricks runners
Setup PAT correctly
…y paths (#620)

* change file paths

* change file paths

* change file paths

* change file paths

* change file paths

* change file paths

* change file paths
Earlier it was PWD which is already set to some path!!
This is because we need the workflow to proceed further and email the differences.
The test will instead send the output to STDERR.
Minor changes to github workflow around JDBC comparator
This reverts commit 5124293.
This will send a report around 00:00 UTC. The code is in the branch jdbc-comparator.
The reports will be saved as artifacts of github workflow run.
jayantsing-db and others added 12 commits May 30, 2025 13:43
- Add nvdApiKey configuration to OWASP dependency-check-maven plugin
- Configure retry settings and API delay for better performance
- Update GitHub Actions workflow to pass NVD_API_KEY secret
- Resolves warning about missing NVD API key and improves scan performance

Without this, release shows a warning: An NVD API Key was not provided -
it is highly recommended to use an NVD API key as the update can take a
VERY long time without an API Key
- Add `Support for INTERVAL data type.`
- This was not added to NEXT_CHANGELOG by authors
… Query (#838)

* Basic working

* Added tests

* nit

* nit

* PR comments

* nit

* Added e2e tests

* Removed Describe query logs

* log fix

* Updated error handling
* fixed actions

* nit

* nit

* nit

* check

* nit
* added application name to track BiTool

* add tests

* some changes

* refactored code; update user agent and telemetry client app

* update user agent thrift mode as well

* changes fn to package-private.

* make parameterized test

* spotless

* merge UserAgentHelper and UserAgentManager

* addressed reviews

* update DatabricksHttpClient
* Add statement ID and session ID wherever possible

* Add null check tests
* Add support for SSL client certificate authentication

- Implemented client certificate authentication via keystore configuration parameters.
- Updated `ConfiguratorUtils` to handle loading and managing keystores.
- Enhanced unit tests to cover keystore loading and client certificate scenarios.

* - Removed redundant null check for the keystore in createKeyManagers method.
- Simplified key entry verification by replacing Enumeration with a stream-based approach for better readability and performance.

* addresses comments

* address comments

* fix next changelog

* Remove unnecessary copy command for intermediateCA.crt in sslTesting workflow

* Update sslTesting workflow to correct client certificate configuration for mutual TLS

* Update sslTesting workflow to include intermediate CA certificate and remove obsolete DNS directive

* revert to main

* Refactor HTTP exception handling to use DatabricksSSLException for SSL/TLS errors. Update relevant classes and tests to replace DatabricksHttpException with DatabricksSSLException, ensuring proper error handling during SSL configuration and handshake processes.

* Update error message in DatabricksClientConfiguratorManager to reflect SSL error handling instead of HTTP error. This change enhances clarity in logging and exception management related to SSL configuration failures.
…d in databricks/databricks-jdbc#634.

Key changes:
- Create AbstractRemoteChunkProvider and AbstractArrowResultChunk as unified base implementations for chunk management
- Add state machine (ArrowResultChunkStateMachine) to handle chunk lifecycle transitions
- Improve thread synchronization between main thread and IO/download threads

The new async implementation (2nd PR) will provide an alternative path for handling large Arrow datasets with improved scalability, while maintaining the existing synchronous approach for backwards compatibility.

Below is a class diagram that provides a clearer understanding and concise summary of the class structure:

![class-diagram](https://github.com/user-attachments/assets/e9503d47-5895-439a-9d39-f4963da3e5df)

- Fake service tests
- Unit tests
- Multi-DBR tests
- Local testing in a highly concurrent environment

Benchmarking document: https://docs.google.com/document/d/1MvKeSnrQVKFGdkuaSPXFCrbb4TDaUqFwHrIW2rxy3zI/edit?usp=sharing
@github-actions

Copy link
Copy Markdown

Please ensure that the NEXT_CHANGELOG.md file is updated with any relevant changes.
If this is not necessary for your PR, please include the following in your PR description:
NO_CHANGELOG=true
and rerun the job.

@jayantsing-db jayantsing-db changed the title Jayantsing db/http client Introduce abstract base classes for Arrow result handling Jun 11, 2025
@jayantsing-db jayantsing-db requested a review from gopalldb June 11, 2025 12:21
return false;
}

if (getStatus() == ChunkStatus.PROCESSING_SUCCEEDED) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when is this status set?

@jayantsing-db jayantsing-db Jun 20, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This status signifies that the chunk data has been deserialised and is ready to be read. It happens after the download.

protected void downloadData(IDatabricksHttpClient httpClient, CompressionCodec compressionCodec)
throws DatabricksParsingException, IOException {
// Inject error if enabled for testing
if (injectError && errorInjectionCount < errorInjectionCountMaxValue) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to check for inject errors? Not sure that I understand these errors :|

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a throw-away code and was added to test the retry behaviour of chunk download using DriverTester

response.getEntity().getContent(), compressionCodec, decompressionContext);
initializeData(uncompressedStream);
setStatus(ChunkStatus.DOWNLOAD_SUCCEEDED);
setStatus(ChunkStatus.PROCESSING_SUCCEEDED);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we do this as part of initializeData itself?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense will do.

this.numRows = rowCount;
this.inputStream = stream;
this.status = ChunkStatus.PENDING;
this.status = status == null ? ChunkStatus.DOWNLOAD_SUCCEEDED : status;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it is pending, then also we can set Download_Succeeded

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the special case of inline results where we have the results in one go. So the builder method is called with all results downloaded. Hence the status, DOWNLOAD_SUCCEEDED. Note that it will have status PROCESSING_SUCCEEDED only if processing is successful. Another reason to distinguish between DOWNLOAD_SUCCEEDED and PROCESSING_SUCCEEDED :)

@gopalldb gopalldb requested a review from samikshya-db June 19, 2025 04:43
return; // No transition needed
}

Set<ChunkStatus> validTargets = VALID_TRANSITIONS.get(currentStatus);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can call isValidTransition(..) here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

*
* @return Set of valid target states
*/
public synchronized Set<ChunkStatus> getValidTargetStates() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this used?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not used yet in this PR. will be used in later PR for logging purposes. The method returns the valid states from current state.

} finally {
if (!downloadSuccessful) {
if (downloadSuccessful) {
chunk.getChunkReadyFuture().complete(null);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comment on what is null, e.g. null /*.paramName */

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* advanced debugging).
*/
@Test
void exampleChunkDownloadRetry() throws Exception {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not being used anymore. There are separate tests for the retry tests.

return getStatus() == ChunkStatus.PENDING
|| (!DriverUtil.isRunningAgainstFake()
&& expiryTime.minusSeconds(SECONDS_BUFFER_FOR_EXPIRY).isBefore(Instant.now()));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

60s seems too long for 22mb download, what's the validity of presigned URLs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, URL is valid for 15 minutes. Buffer to check expiry is 1 minute. I have retained the old code for now.

/**
* Releases all resources associated with this chunk.
*
* @return true if chunk was released, false if it was already released

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this contract needed, how is this boolean used? seems like we would need to return false only when release failed, not if already released

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value is used in

if (chunkIndexToChunksMap.get(currentChunkIndex).releaseChunk()) {
to decrement the chunk count in memory and queue next chunks for download.

* @return number of record batches
*/
protected int getRecordBatchCountInChunk() {
return getStatus() == ChunkStatus.PROCESSING_SUCCEEDED ? recordBatchList.size() : 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we fail here instead of returning 0?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to fail than to read some junk or throw NPE in chunk iterator. but I kept this processing logic same as before to limit the changes. I will create a separate PR for this.

LOGGER.warn(
"Failed to transition to state [%s] from state [%s] for chunk [%d] and statement [%s]. Stack trace: %s",
targetStatus, getStatus(), chunkIndex, statementId, ExceptionUtils.getStackTrace(e));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if it is ok to proceed if transition fails?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes initially i was throwing exception for an invalid transitions. But when i introduced this, there were quite a few invalid state transitions (all transparent to user correctness but invalid technically) in the existing code/logic which I fixed. But there could be 1-2 somewhere hidden for which I am throwing the warning for now. I will make this strict sometime later.

chunkReadyFuture.get(CHUNK_READY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw e;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log this probably?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

IDatabricksSession session,
IDatabricksHttpClient httpClient,
int maxParallelChunkDownloadsPerQuery,
CompressionCodec compressionCodec)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one constructor should use the other?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant we should re-use the constructor.

protected AbstractRemoteChunkProvider(
      StatementId statementId,
      ResultManifest resultManifest,
      ResultData resultData,
      IDatabricksSession session,
      IDatabricksHttpClient httpClient,
      int maxParallelChunkDownloadsPerQuery,
      CompressionCodec compressionCodec)
protected AbstractRemoteChunkProvider(
      IDatabricksStatementInternal parentStatement,
      TFetchResultsResp resultsResp,
      IDatabricksSession session,
      IDatabricksHttpClient httpClient,
      int maxParallelChunkDownloadsPerQuery,
      CompressionCodec compressionCodec)

RemoteChunkProvider (now most of the logic sits in AbstractRemoteChunkProvider) had these two constructors for thrift and SEA flow. Constructor chaining here will look a bit too inflated. Ideally I think we should have separate classes for thrift and SEA in such cases. But I have kept this construction untouched to limit the scope.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i think you meant re-using for the common fields. I can do that now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i think you meant re-using for the common fields. I can do that now.

it is getting slightly complex without introducing a third telescope constructor. I will leave this as is for now 😅

return chunkIndexMap;
}

private void populateChunkIndexMap(TRowSet resultData, ConcurrentMap<Long, T> chunkIndexMap)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add javadoc to these methods

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants