Skip to content

Commit 4c868a6

Browse files
committed
Added ability to enforce max connection lifetime
Driver keeps idle socket connections in a connection pool. These connections can get invalidated while resting idle in the pool. They can be killed by network equipment like load balancer, proxy or firewall. It is also safer to refresh connections once in a while so that database can renew corresponding resources. This commit adds `maxConnectionLifetime` setting and makes driver close too old connections. Checking and closing happens in an application thread that tries to acquire a connection. Feature can be used on it's own or in combination with `connectionLivenessCheckTimeout` to guarantee validity of acquired connections.
1 parent 4a77c8d commit 4c868a6

20 files changed

+545
-68
lines changed

driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ protected ConnectionPool createConnectionPool( AuthToken authToken, SecurityPlan
177177

178178
ConnectionSettings connectionSettings = new ConnectionSettings( authToken, config.connectionTimeoutMillis() );
179179
PoolSettings poolSettings = new PoolSettings( config.maxIdleConnectionPoolSize(),
180-
config.idleTimeBeforeConnectionTest() );
180+
config.idleTimeBeforeConnectionTest(), config.maxConnectionLifetime() );
181181
Connector connector = createConnector( connectionSettings, securityPlan, config.logging() );
182182

183183
return new SocketConnectionPool( poolSettings, connector, createClock(), config.logging() );

driver/src/main/java/org/neo4j/driver/internal/cluster/RoutingPooledConnection.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ public BoltServerAddress boltServerAddress()
221221
return delegate.boltServerAddress();
222222
}
223223

224+
@Override
225+
public long creationTimestamp()
226+
{
227+
return delegate.creationTimestamp();
228+
}
229+
224230
@Override
225231
public long lastUsedTimestamp()
226232
{

driver/src/main/java/org/neo4j/driver/internal/net/pooling/BlockingPooledConnectionQueue.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public PooledConnection acquire( Supplier<PooledConnection> supplier )
107107
return connection;
108108
}
109109

110-
void disposeBroken( PooledConnection connection )
110+
void dispose( PooledConnection connection )
111111
{
112112
acquiredConnections.remove( connection );
113113
disposeSafely( connection );

driver/src/main/java/org/neo4j/driver/internal/net/pooling/PoolSettings.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
public class PoolSettings
2222
{
2323
public static final int NO_IDLE_CONNECTION_TEST = -1;
24+
public static final int INFINITE_CONNECTION_LIFETIME = -1;
2425

2526
public static final int DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE = 10;
2627
public static final int DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST = NO_IDLE_CONNECTION_TEST;
28+
public static final int DEFAULT_MAX_CONNECTION_LIFETIME = INFINITE_CONNECTION_LIFETIME;
2729

2830
private final int maxIdleConnectionPoolSize;
2931
private final long idleTimeBeforeConnectionTest;
32+
private final long maxConnectionLifetime;
3033

31-
public PoolSettings( int maxIdleConnectionPoolSize, long idleTimeBeforeConnectionTest )
34+
public PoolSettings( int maxIdleConnectionPoolSize, long idleTimeBeforeConnectionTest, long maxConnectionLifetime )
3235
{
3336
this.maxIdleConnectionPoolSize = maxIdleConnectionPoolSize;
3437
this.idleTimeBeforeConnectionTest = idleTimeBeforeConnectionTest;
38+
this.maxConnectionLifetime = maxConnectionLifetime;
3539
}
3640

3741
public int maxIdleConnectionPoolSize()
@@ -53,4 +57,19 @@ public boolean idleTimeBeforeConnectionTestConfigured()
5357
{
5458
return idleTimeBeforeConnectionTest >= 0;
5559
}
60+
61+
public long maxConnectionLifetime()
62+
{
63+
if ( !maxConnectionLifetimeConfigured() )
64+
{
65+
throw new IllegalStateException(
66+
"Max connection lifetime is not configured: " + maxConnectionLifetime );
67+
}
68+
return maxConnectionLifetime;
69+
}
70+
71+
public boolean maxConnectionLifetimeConfigured()
72+
{
73+
return maxConnectionLifetime > 0;
74+
}
5675
}

driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledConnectionReleaseConsumer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void accept( PooledConnection pooledConnection )
4747
}
4848
else
4949
{
50-
connections.disposeBroken( pooledConnection );
50+
connections.dispose( pooledConnection );
5151
}
5252
}
5353
}

driver/src/main/java/org/neo4j/driver/internal/net/pooling/PooledSocketConnection.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,16 @@ public class PooledSocketConnection implements PooledConnection
6060
private boolean unrecoverableErrorsOccurred = false;
6161
private SessionResourcesHandler resourcesHandler;
6262
private final Clock clock;
63+
64+
private final long creationTimestamp;
6365
private long lastUsedTimestamp;
6466

6567
public PooledSocketConnection( Connection delegate, Consumer<PooledConnection> release, Clock clock )
6668
{
6769
this.delegate = delegate;
6870
this.release = release;
6971
this.clock = clock;
72+
this.creationTimestamp = clock.millis();
7073
updateLastUsedTimestamp();
7174
}
7275

@@ -280,6 +283,12 @@ public void setResourcesHandler( SessionResourcesHandler resourcesHandler )
280283
this.resourcesHandler = resourcesHandler;
281284
}
282285

286+
@Override
287+
public long creationTimestamp()
288+
{
289+
return creationTimestamp;
290+
}
291+
283292
@Override
284293
public long lastUsedTimestamp()
285294
{
@@ -315,6 +324,6 @@ private boolean isClientOrTransientError( RuntimeException e )
315324

316325
private void updateLastUsedTimestamp()
317326
{
318-
this.lastUsedTimestamp = clock.millis();
327+
lastUsedTimestamp = clock.millis();
319328
}
320329
}

driver/src/main/java/org/neo4j/driver/internal/net/pooling/SocketConnectionPool.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ private PooledConnection acquireConnection( BoltServerAddress address,
149149
// dispose previous connection that can't be acquired
150150
if ( connection != null )
151151
{
152-
connectionQueue.disposeBroken( connection );
152+
connectionQueue.dispose( connection );
153153
}
154154

155155
connection = connectionQueue.acquire( connectionSupplier );
@@ -162,18 +162,27 @@ private PooledConnection acquireConnection( BoltServerAddress address,
162162

163163
private boolean canBeAcquired( PooledConnection connection, boolean connectionCreated )
164164
{
165-
if ( poolSettings.idleTimeBeforeConnectionTestConfigured() )
165+
if ( connectionCreated )
166+
{
167+
return true;
168+
}
169+
170+
if ( poolSettings.maxConnectionLifetimeConfigured() )
166171
{
167-
if ( connectionCreated )
172+
if ( isTooOld( connection ) )
168173
{
169-
return true;
174+
return false;
170175
}
176+
}
171177

178+
if ( poolSettings.idleTimeBeforeConnectionTestConfigured() )
179+
{
172180
if ( hasBeenIdleForTooLong( connection ) )
173181
{
174182
return connectionValidator.isConnected( connection );
175183
}
176184
}
185+
177186
return true;
178187
}
179188

@@ -183,6 +192,12 @@ private boolean hasBeenIdleForTooLong( PooledConnection connection )
183192
return idleTime > poolSettings.idleTimeBeforeConnectionTest();
184193
}
185194

195+
private boolean isTooOld( PooledConnection connection )
196+
{
197+
long lifetime = clock.millis() - connection.creationTimestamp();
198+
return lifetime > poolSettings.maxConnectionLifetime();
199+
}
200+
186201
private void assertNotClosed( BoltServerAddress address, BlockingPooledConnectionQueue connections )
187202
{
188203
if ( closed.get() )

driver/src/main/java/org/neo4j/driver/internal/spi/PooledConnection.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ public interface PooledConnection extends Connection
4040
*/
4141
boolean hasUnrecoverableErrors();
4242

43+
/**
44+
* Timestamp of when this connection was created. This timestamp should never change.
45+
*
46+
* @return timestamp as returned by {@link Clock#millis()}.
47+
*/
48+
long creationTimestamp();
49+
4350
/**
4451
* Timestamp of when this connection was used. This timestamp is updated when connection is returned to the pool.
4552
*

driver/src/main/java/org/neo4j/driver/v1/Config.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,8 @@ public class Config
5858

5959
private final int maxIdleConnectionPoolSize;
6060

61-
/**
62-
* Connections that have been idle in the pool longer than this threshold will
63-
* be tested for validity before being returned to the user.
64-
*/
6561
private final long idleTimeBeforeConnectionTest;
62+
private final long maxConnectionLifetime;
6663

6764
/** Indicator for encrypted traffic */
6865
private final boolean encrypted;
@@ -83,6 +80,7 @@ private Config( ConfigBuilder builder)
8380
this.logLeakedSessions = builder.logLeakedSessions;
8481

8582
this.idleTimeBeforeConnectionTest = builder.idleTimeBeforeConnectionTest;
83+
this.maxConnectionLifetime = builder.maxConnectionLifetime;
8684
this.maxIdleConnectionPoolSize = builder.maxIdleConnectionPoolSize;
8785

8886
this.encrypted = builder.encrypted;
@@ -143,6 +141,16 @@ public long idleTimeBeforeConnectionTest()
143141
return idleTimeBeforeConnectionTest;
144142
}
145143

144+
/**
145+
* Pooled connections older than this threshold will be closed and removed from the pool.
146+
*
147+
* @return maximum lifetime in milliseconds
148+
*/
149+
public long maxConnectionLifetime()
150+
{
151+
return maxConnectionLifetime;
152+
}
153+
146154
/**
147155
* @return the configured connection timeout value in milliseconds.
148156
*/
@@ -223,6 +231,7 @@ public static class ConfigBuilder
223231
private boolean logLeakedSessions;
224232
private int maxIdleConnectionPoolSize = PoolSettings.DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE;
225233
private long idleTimeBeforeConnectionTest = PoolSettings.DEFAULT_IDLE_TIME_BEFORE_CONNECTION_TEST;
234+
private long maxConnectionLifetime = PoolSettings.DEFAULT_MAX_CONNECTION_LIFETIME;
226235
private boolean encrypted = true;
227236
private TrustStrategy trustStrategy = trustAllCertificates();
228237
private LoadBalancingStrategy loadBalancingStrategy = LoadBalancingStrategy.LEAST_CONNECTED;
@@ -348,7 +357,7 @@ public ConfigBuilder withSessionLivenessCheckTimeout( long timeout )
348357
* Value {@code 0} means connections will always be tested for
349358
* validity and negative values mean connections will never be tested.
350359
*
351-
* @param value the minimum idle time in milliseconds
360+
* @param value the minimum idle time
352361
* @param unit the unit in which the duration is given
353362
* @return this builder
354363
*/
@@ -358,6 +367,32 @@ public ConfigBuilder withConnectionLivenessCheckTimeout( long value, TimeUnit un
358367
return this;
359368
}
360369

370+
/**
371+
* Pooled connections older than this threshold will be closed and removed from the pool. Such discarding
372+
* happens during connection acquisition so that new session is never backed by an old connection.
373+
* <p>
374+
* Setting this option to a low value will cause a high connection churn and might result in a performance hit.
375+
* <p>
376+
* It is recommended to set maximum lifetime to a slightly smaller value than the one configured in network
377+
* equipment (load balancer, proxy, firewall, etc. can also limit maximum connection lifetime).
378+
* <p>
379+
* Setting can also be used in combination with {@link #withConnectionLivenessCheckTimeout(long, TimeUnit)}. In
380+
* this case, it is recommended to set liveness check to a value smaller than network equipment has and maximum
381+
* lifetime to a reasonably large value to "renew" connections once in a while.
382+
* <p>
383+
* No maximum lifetime limit is imposed by default. Zero and negative values result in lifetime not being
384+
* checked.
385+
*
386+
* @param value the maximum connection lifetime
387+
* @param unit the unit in which the duration is given
388+
* @return this builder
389+
*/
390+
public ConfigBuilder withMaxConnectionLifetime( long value, TimeUnit unit )
391+
{
392+
this.maxConnectionLifetime = unit.toMillis( value );
393+
return this;
394+
}
395+
361396
/**
362397
* Configure the {@link EncryptionLevel} to use, use this to control wether the driver uses TLS encryption or not.
363398
* @param level the TLS level to use

driver/src/test/java/org/neo4j/driver/internal/cluster/RoutingPooledConnectionErrorHandlingTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import static org.mockito.Mockito.when;
6060
import static org.neo4j.driver.internal.logging.DevNullLogging.DEV_NULL_LOGGING;
6161
import static org.neo4j.driver.internal.net.pooling.PoolSettings.DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE;
62+
import static org.neo4j.driver.internal.net.pooling.PoolSettings.INFINITE_CONNECTION_LIFETIME;
6263
import static org.neo4j.driver.internal.net.pooling.PoolSettings.NO_IDLE_CONNECTION_TEST;
6364
import static org.neo4j.driver.internal.spi.Collector.NO_OP;
6465
import static org.neo4j.driver.internal.util.Matchers.containsReader;
@@ -351,7 +352,8 @@ private static RoutingTable newRoutingTable( ClusterComposition clusterCompositi
351352
private static ConnectionPool newConnectionPool( Connector connector, BoltServerAddress... addresses )
352353
{
353354
int maxIdleConnections = DEFAULT_MAX_IDLE_CONNECTION_POOL_SIZE;
354-
PoolSettings settings = new PoolSettings( maxIdleConnections, NO_IDLE_CONNECTION_TEST );
355+
PoolSettings settings = new PoolSettings( maxIdleConnections, NO_IDLE_CONNECTION_TEST,
356+
INFINITE_CONNECTION_LIFETIME );
355357
SocketConnectionPool pool = new SocketConnectionPool( settings, connector, Clock.SYSTEM, DEV_NULL_LOGGING );
356358

357359
// force pool to create and memorize some connections
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2002-2017 "Neo Technology,"
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.driver.internal.cluster;
20+
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
import org.mockito.InjectMocks;
24+
import org.mockito.Mock;
25+
import org.mockito.runners.MockitoJUnitRunner;
26+
27+
import org.neo4j.driver.internal.spi.PooledConnection;
28+
29+
import static org.junit.Assert.assertEquals;
30+
import static org.mockito.Mockito.when;
31+
32+
@RunWith( MockitoJUnitRunner.class )
33+
public class RoutingPooledConnectionTest
34+
{
35+
@Mock
36+
private PooledConnection pooledConnection;
37+
@InjectMocks
38+
private RoutingPooledConnection routingPooledConnection;
39+
40+
@Test
41+
public void shouldExposeCreationTimestamp()
42+
{
43+
when( pooledConnection.creationTimestamp() ).thenReturn( 42L );
44+
45+
long timestamp = routingPooledConnection.creationTimestamp();
46+
47+
assertEquals( 42L, timestamp );
48+
}
49+
50+
@Test
51+
public void shouldExposeLastUsedTimestamp()
52+
{
53+
when( pooledConnection.lastUsedTimestamp() ).thenReturn( 42L );
54+
55+
long timestamp = routingPooledConnection.lastUsedTimestamp();
56+
57+
assertEquals( 42L, timestamp );
58+
}
59+
}

driver/src/test/java/org/neo4j/driver/internal/net/pooling/BlockingPooledConnectionQueueTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,33 @@ public void shouldReportActiveConnections()
287287

288288
@Test
289289
@SuppressWarnings( "unchecked" )
290-
public void shouldDisposeBrokenConnections()
290+
public void shouldDisposeConnections()
291291
{
292292
BlockingPooledConnectionQueue queue = newConnectionQueue( 5 );
293293

294294
queue.offer( mock( PooledConnection.class ) );
295295
PooledConnection connection = queue.acquire( mock( Supplier.class ) );
296296
assertEquals( 1, queue.activeConnections() );
297297

298-
queue.disposeBroken( connection );
298+
queue.dispose( connection );
299+
assertEquals( 0, queue.activeConnections() );
300+
verify( connection ).dispose();
301+
}
302+
303+
@Test
304+
@SuppressWarnings( "unchecked" )
305+
public void shouldDisposeConnectionsThatThrowOnDisposal()
306+
{
307+
BlockingPooledConnectionQueue queue = newConnectionQueue( 5 );
308+
309+
PooledConnection pooledConnection = mock( PooledConnection.class );
310+
doThrow( new RuntimeException() ).when( pooledConnection ).dispose();
311+
312+
queue.offer( pooledConnection );
313+
PooledConnection connection = queue.acquire( mock( Supplier.class ) );
314+
assertEquals( 1, queue.activeConnections() );
315+
316+
queue.dispose( connection );
299317
assertEquals( 0, queue.activeConnections() );
300318
verify( connection ).dispose();
301319
}

0 commit comments

Comments
 (0)