Skip to content

Reuse JDBC connection in JDBC connectors#14653

Merged
kokosing merged 5 commits intotrinodb:masterfrom
kokosing:origin/master/152_reuse_connection
Nov 2, 2022
Merged

Reuse JDBC connection in JDBC connectors#14653
kokosing merged 5 commits intotrinodb:masterfrom
kokosing:origin/master/152_reuse_connection

Conversation

@kokosing
Copy link
Copy Markdown
Member

Reuse JDBC connection in JDBC connectors

@cla-bot cla-bot bot added the cla-signed label Oct 16, 2022
@kokosing
Copy link
Copy Markdown
Member Author

It does not work somehow. Things do not get committed.

@kokosing kokosing marked this pull request as draft October 16, 2022 05:41
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There is no need to make it functional. Just make the class not static.

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch from 52e86b3 to 1ee9091 Compare October 17, 2022 08:02
@lhofhansl
Copy link
Copy Markdown
Member

So these are cached in each worker for the duration of a query and after the query is done all connection(s) are released...?
Asking because holding connections open can tie up non-trivial resources - server-side - on some databases.

@kokosing
Copy link
Copy Markdown
Member Author

So these are cached in each worker for the duration of a query and after the query is done all connection(s) are released...?
Asking because holding connections open can tie up non-trivial resources - server-side - on some databases.

This is in progress. I don't want to hold connections open for a long time. Now I also added a change to keep it up to 2 seconds when innactive.

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch 2 times, most recently from 0c40b64 to 187cbe2 Compare October 19, 2022 14:29
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

good catch

Can you move it to a separate PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i have some comments in #14702.
please apply & rebase

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch 2 times, most recently from 5742255 to c07c9b6 Compare October 23, 2022 21:31
@kokosing kokosing marked this pull request as ready for review October 23, 2022 21:32
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

improve commit message grammar

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

inline kvCache

Copy link
Copy Markdown
Member

@findepi findepi left a comment

Choose a reason for hiding this comment

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

"Introduce NonLoadableCache"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i have some comments in #14702.
please apply & rebase

Comment on lines 98 to 129
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should also test that these make the cache empty.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think so. SafeCaches provides forwarding implementation of cache, so it is up to a delegate cache to clear things. So testing if methods are properly implemented should be on a different level, where a delegate cache implementation is.

Comment on lines 25 to 27
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the rationale to have NonLoadableCache interface and NonLoadableCacheImpl as separate classes?
I.e. would anything bad happen if the implementation class was called NonLoadableCache, was public and had a package-private constructor?

Comment on lines 22 to 24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The cache eviction problem isn't limited to get(K key, Callable<? extends V> loader) method.
Even if I call asMap().putIfAbsent etc i insert into a cache a value that's "fresh" from the calling thread perspective, but can already stale from some other thread perspective.
Thus this safety is an illusion, the NonLoadableCache isn't any more safe than any other Guava cache. It's just less convenient to use.

Copy link
Copy Markdown
Member

@findepi findepi left a comment

Choose a reason for hiding this comment

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

"Introduce QueryEventListener to JDBC connectors"

Copy link
Copy Markdown
Member

@findepi findepi left a comment

Choose a reason for hiding this comment

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

"Static import SINGLETON"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For some reason....

/trino master$ git grep '.in(SINGLETON)' | wc -l
69
/trino master$ git grep '.in(Scopes.SINGLETON)' | wc -l
675

Copy link
Copy Markdown
Member

@findepi findepi left a comment

Choose a reason for hiding this comment

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

"Test JDBC connection creations"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My IDE shows me a warning here.

'DriverConnectionFactory' used without 'try'-with-resources statement

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd suggest writing this like

return new ConnectionFactory()
{
    private final DriverConnectionFactory delegate = new DriverConnectionFactory(new Driver(), config, credentialProvider);

and then adding

@Override
public void close()
        throws SQLException
{
    delegate.close();
}

at the bottom

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My IDE shows me a warning here.

Thank you. I had this disabled somehow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

USING may prevent JOIN pushdown, which looks like something you want to test for #6781

BTW it would be good to assert the pushdown actually happened.
It won't happen under default configuration due to lack of stats, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will add something with even more easier predicate and without a predicate at all too. I don't want to go very deeply about what is supported and what is not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add SHOW STATS

also, we should test with a connector that supports table stats

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also, we should test with a connector that supports table stats

SHOW STATS is interesting as it is using JDBI which double closes connections. However I would prefer not to use postgresql connector here. Also using different connectors may have different number of opened connections. Maybe I should not count them at all?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

java.lang.AssertionError: 
Expecting:
 <14>
to be less than or equal to:
 <11> 

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it is related to reusing connections.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assert that connectionCreations is empty.

also, rename the field to openConnections

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is verified after setup of test class and after each test already.

Copy link
Copy Markdown
Contributor

@ssheikin ssheikin left a comment

Choose a reason for hiding this comment

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

This is a first pass of review and comments which I left here mostly for myself for the second path :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not really follow why cache has to be represented as map. Looks very similar to how Set is backed by Map, but I'd say it's not necessary abstraction here.

Chain of not very structured thoughts:
What if we try to add something to map? Would it mean that map is readonly? Logically it leads that Cache has to implement Map interface (too when readonly).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You need to reverse your thinking. It is not cache represent as map, it is a map that is presented to you as cache. Internally that cache is implemented as map, basically I change the API here, behavior is same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.remove I'd expect that connections holder (this class) is called pool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's not a typical pool
This is effectively a queryId-keyd cache of 1-element pools.
It should be documented by keying by queryId is used here. This is not obvious especially that we don't reuse any "dirty" connections.

Comment on lines 197 to 202
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like dirty path could be improved even more.
Do we have statistics how many connections fit to the dirty bucket?

Copy link
Copy Markdown
Member

@findepi findepi left a comment

Choose a reason for hiding this comment

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

"Reuse JDBC connection in JDBC connectors"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

import statically

Comment on lines 110 to 111
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should be enough:

Suggested change
innerBinder.bind(ReusingConnectionFactory.class).in(SINGLETON);
innerBinder.bind(ConnectionFactory.class).to(Key.get(ReusingConnectionFactory.class)).in(SINGLETON);
innerBinder.bind(ConnectionFactory.class).to(ReusingConnectionFactory.class).in(SINGLETON);

but i cannot verify that since this code isn't exercised by tests

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is tested by TestJdbcConnectionsCreation. They Key is a key here, otherwise you would get two different instances.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assert that now one is retained in the cache and one underlying has been closed already

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's not a typical pool
This is effectively a queryId-keyd cache of 1-element pools.
It should be documented by keying by queryId is used here. This is not obvious especially that we don't reuse any "dirty" connections.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add separate test coverage for planning and execution

eg

assertOpenConnections(String sql, int expectedPlanningConections, int expectedExecutionConnections)

this would do

assertOpenConnections("EXPLAIN" + sql, expectedPlanningConections);
assertOpenConnections(sql, expectedPlanningConections + expectedExecutionConnections);

-- this would help understand why we're not reaching 1 connection per query, even with reuse

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

:-)

Nice try. It is way beyond the scope of this commit (PR). Connector and even more ConnectionFactory has no notion about planning and execution. There is plenty of plumbing already and ask to test SHOW STATS for a real connector requires even more plumbing. Let's do go crazy with feature requests :)

@kokosing
Copy link
Copy Markdown
Member Author

Extracted #14732

Copy link
Copy Markdown
Member Author

@kokosing kokosing left a comment

Choose a reason for hiding this comment

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

I am going to reimplement this a bit. Having this "dirty" thing is very fragile, it is hard to tell what connection and when will be reused. Instead I will make part of the implementation of JdbcClient only for metadata queries.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it is related to reusing connections.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

:-)

Nice try. It is way beyond the scope of this commit (PR). Connector and even more ConnectionFactory has no notion about planning and execution. There is plenty of plumbing already and ask to test SHOW STATS for a real connector requires even more plumbing. Let's do go crazy with feature requests :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will add something with even more easier predicate and without a predicate at all too. I don't want to go very deeply about what is supported and what is not.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My IDE shows me a warning here.

Thank you. I had this disabled somehow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is verified after setup of test class and after each test already.

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch from a340a10 to 271d634 Compare October 26, 2022 09:27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

move it top.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It matches the order in which fields are defined now.

Comment on lines 91 to 93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it a possible situation that connection returned from the cache is already closed? Is it a harmful?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think it is possible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duration.ofSeconds(5) -> const FOREVER = Duration.ofDays(5)
10 -> const VERY_LARGE = 1000

Comment on lines 71 to 73
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If I understood correctly Connection ignored is not yet closed, but connectionFactory.openConnection(ALICE) tries to open new connection in the same session.

Does it mean that ReusableConnectionFactory is designed not only for metadata retrieval, but a general implementation? (I guess yes)
Is it a real situation? (I guess not for metadata)
Does it mean that Metadata retrieval is parallel? (guess no)

Copy link
Copy Markdown
Member Author

@kokosing kokosing Oct 29, 2022

Choose a reason for hiding this comment

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

The point is that JDBC connection are so-so thread safe. So it is better not to share a single connection between different threads. Also the next step is to verify we don't open nested connections in the same thread.

Yes. Yes. Not for the same Trino query.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what if
Duration.ofMillis(0) ?
10 -> 0 ?
Is it covered by tests?

Comment on lines 754 to 756
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not related

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What does session.getQueryId() returns for ALICE?

looks like this heavily relies on that metadata requests are not parallel.

and this test could be extended:

    @Test
    public void testConnectionIsNotShared()
            throws Exception
    {
        try (MockConnectionFactory mockConnectionFactory = new MockConnectionFactory();
                ReusableConnectionFactory connectionFactory = new CacheBasedReusableConnectionFactory(mockConnectionFactory, Duration.ofSeconds(5), 10);
                Connection ignored = connectionFactory.openConnection(ALICE)) {
            connectionFactory.openConnection(ALICE).close();
            assertThat(mockConnectionFactory.openedConnections).isEqualTo(2);
        }
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

may be close it explicitly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That is the point that connection is still open.

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch 2 times, most recently from 76c71d2 to c12c852 Compare October 29, 2022 21:12
Copy link
Copy Markdown
Member Author

@kokosing kokosing left a comment

Choose a reason for hiding this comment

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

AC, PTAL

Notice that this is based on #14749

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

consider previous connection to be closed and add test

putIfAbsent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It matches the order in which fields are defined now.

Comment on lines 91 to 93
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think it is possible.

Comment on lines 71 to 73
Copy link
Copy Markdown
Member Author

@kokosing kokosing Oct 29, 2022

Choose a reason for hiding this comment

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

The point is that JDBC connection are so-so thread safe. So it is better not to share a single connection between different threads. Also the next step is to verify we don't open nested connections in the same thread.

Yes. Yes. Not for the same Trino query.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That is the point that connection is still open.

@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch 3 times, most recently from 6309924 to 8ad2293 Compare October 31, 2022 15:23
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

still need to see this in detail

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Test something like:

aliceConnection1 = connectionFactory.openConnection(ALICE);
aliceConnection2 = connectionFactory.openConnection(ALICE);
aliceConnection1.close();
aliceConnection2.close();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added as testSingleUserCanCreateMultipleConnections

Statements executed during operation like commitCreateTable
are going to change the state of the remote data base so auto commit mode is expected.
@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch from 8ad2293 to 673a92d Compare October 31, 2022 20:57
Copy link
Copy Markdown
Member Author

@kokosing kokosing left a comment

Choose a reason for hiding this comment

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

AC

Copy link
Copy Markdown
Member

@hashhar hashhar Nov 1, 2022

Choose a reason for hiding this comment

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

assumes connections are going to be acquired from a single thread - this is probably true for metadata queries.

To elaborate this assumes that metadata queries happen in single-threaded manner so for same queryId we'll only ever have 1 connection.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is not necessary. This factory can be used by multiple threads. The point is that only one connection can be stored for future use and no connection is not shared between threads.

There are no issues if two threads wants to create a connection for same queryId or single thread that wants to create two connections. This factory is not optimized for such use case so hit rate will be low, but there will be no regression and no correctness issues.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Test something like:

aliceConnection1 = connectionFactory.openConnection(ALICE);
aliceConnection2 = connectionFactory.openConnection(ALICE);
aliceConnection1.close();
aliceConnection2.close();

JdbcQueryEventListener will be notified when query is started or finished.
Otherwise data processing queries may happen on coordinator and
sometimes they may reuse connections. That makes a test to be difficult
to deterministic.
@kokosing kokosing force-pushed the origin/master/152_reuse_connection branch from 673a92d to dc02f1a Compare November 1, 2022 11:14
@kokosing
Copy link
Copy Markdown
Member Author

kokosing commented Nov 1, 2022

AC

Copy link
Copy Markdown
Member

@hashhar hashhar left a comment

Choose a reason for hiding this comment

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

Looks good to me (for the goal of connection reuse only for metadata queries).

@kokosing
Copy link
Copy Markdown
Member Author

kokosing commented Nov 2, 2022

All @findepi comments got addressed. I am happy to address more in follow up PRs.

@kokosing kokosing merged commit 7219062 into trinodb:master Nov 2, 2022
@kokosing kokosing deleted the origin/master/152_reuse_connection branch November 2, 2022 12:44
@kokosing kokosing mentioned this pull request Nov 2, 2022
@github-actions github-actions bot added this to the 402 milestone Nov 2, 2022
Comment on lines +123 to +129
@Config("query.reuse-connection")
@ConfigDescription("Enables reusing JDBC connection for metadata queries to data source within a single Trino query")
public BaseJdbcConfig setReuseConnection(boolean reuseConnection)
{
this.reuseConnection = reuseConnection;
return this;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs to be documented. Will submit a follow-up PR.

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

Development

Successfully merging this pull request may close these issues.

7 participants