Skip to content

Commit 5fca890

Browse files
committed
Make socket connection timeout configurable
Previously socket connect did not have a timeout and was bounded only by the timeout configured on the OS level. Such timeout could be rather long and result in repeated stalls trying to refresh routing table against a failed router.
1 parent 5de9312 commit 5fca890

File tree

11 files changed

+255
-75
lines changed

11 files changed

+255
-75
lines changed

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
*/
3030
public class ConnectionSettings
3131
{
32-
public static final String DEFAULT_USER_AGENT = format( "neo4j-java/%s", driverVersion() );
32+
private static final String DEFAULT_USER_AGENT = format( "neo4j-java/%s", driverVersion() );
3333

3434
/**
3535
* Extracts the driver version from the driver jar MANIFEST.MF file.
@@ -52,16 +52,18 @@ private static String driverVersion()
5252

5353
private final AuthToken authToken;
5454
private final String userAgent;
55+
private final int timeoutMillis;
5556

56-
public ConnectionSettings( AuthToken authToken, String userAgent )
57+
public ConnectionSettings( AuthToken authToken, String userAgent, int timeoutMillis )
5758
{
5859
this.authToken = authToken;
5960
this.userAgent = userAgent;
61+
this.timeoutMillis = timeoutMillis;
6062
}
6163

62-
public ConnectionSettings( AuthToken authToken )
64+
public ConnectionSettings( AuthToken authToken, int timeoutMillis )
6365
{
64-
this( authToken, DEFAULT_USER_AGENT );
66+
this( authToken, DEFAULT_USER_AGENT, timeoutMillis );
6567
}
6668

6769
public AuthToken authToken()
@@ -74,4 +76,8 @@ public String userAgent()
7476
return userAgent;
7577
}
7678

79+
public int timeoutMillis()
80+
{
81+
return timeoutMillis;
82+
}
7783
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ ConnectionPool createConnectionPool( AuthToken authToken, SecurityPlan securityP
120120
{
121121
authToken = authToken == null ? AuthTokens.none() : authToken;
122122

123-
ConnectionSettings connectionSettings = new ConnectionSettings( authToken );
123+
ConnectionSettings connectionSettings = new ConnectionSettings( authToken, config.connectionTimeoutMillis() );
124124
PoolSettings poolSettings = new PoolSettings( config.maxIdleConnectionPoolSize() );
125125
Connector connector = new SocketConnector( connectionSettings, securityPlan, config.logging() );
126126

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2002-2016 "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.net;
20+
21+
import java.io.IOException;
22+
import java.net.Socket;
23+
import java.net.SocketTimeoutException;
24+
import java.net.StandardSocketOptions;
25+
import java.nio.channels.ByteChannel;
26+
import java.nio.channels.SocketChannel;
27+
import java.security.GeneralSecurityException;
28+
29+
import org.neo4j.driver.internal.security.SecurityPlan;
30+
import org.neo4j.driver.internal.security.TLSSocketChannel;
31+
import org.neo4j.driver.v1.Logger;
32+
33+
class ChannelFactory
34+
{
35+
static ByteChannel create( BoltServerAddress address, SecurityPlan securityPlan, int timeoutMillis, Logger log )
36+
throws IOException, GeneralSecurityException
37+
{
38+
SocketChannel soChannel = SocketChannel.open();
39+
soChannel.setOption( StandardSocketOptions.SO_REUSEADDR, true );
40+
soChannel.setOption( StandardSocketOptions.SO_KEEPALIVE, true );
41+
connect( soChannel, address, timeoutMillis );
42+
43+
ByteChannel channel;
44+
45+
if ( securityPlan.requiresEncryption() )
46+
{
47+
channel = TLSSocketChannel.create( address, securityPlan, soChannel, log );
48+
}
49+
else
50+
{
51+
channel = soChannel;
52+
}
53+
54+
if ( log.isTraceEnabled() )
55+
{
56+
channel = new LoggingByteChannel( channel, log );
57+
}
58+
59+
return channel;
60+
}
61+
62+
private static void connect( SocketChannel soChannel, BoltServerAddress address, int timeoutMillis )
63+
throws IOException
64+
{
65+
Socket socket = soChannel.socket();
66+
try
67+
{
68+
socket.connect( address.toSocketAddress(), timeoutMillis );
69+
}
70+
catch ( SocketTimeoutException e )
71+
{
72+
throw new IOException( "Timeout " + timeoutMillis + "ms expired", e );
73+
}
74+
}
75+
}

driver/src/main/java/org/neo4j/driver/internal/net/SocketClient.java

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,14 @@
2020

2121
import java.io.IOException;
2222
import java.net.ConnectException;
23-
import java.net.StandardSocketOptions;
2423
import java.nio.ByteBuffer;
2524
import java.nio.channels.ByteChannel;
26-
import java.nio.channels.SocketChannel;
2725
import java.security.GeneralSecurityException;
2826
import java.util.Queue;
2927

3028
import org.neo4j.driver.internal.messaging.Message;
3129
import org.neo4j.driver.internal.messaging.MessageFormat;
3230
import org.neo4j.driver.internal.security.SecurityPlan;
33-
import org.neo4j.driver.internal.security.TLSSocketChannel;
3431
import org.neo4j.driver.internal.util.BytePrinter;
3532
import org.neo4j.driver.v1.Logger;
3633
import org.neo4j.driver.v1.exceptions.ClientException;
@@ -49,6 +46,7 @@ public class SocketClient
4946

5047
private final BoltServerAddress address;
5148
private final SecurityPlan securityPlan;
49+
private final int timeoutMillis;
5250
private final Logger logger;
5351

5452
private SocketProtocol protocol;
@@ -57,10 +55,11 @@ public class SocketClient
5755

5856
private ByteChannel channel;
5957

60-
public SocketClient( BoltServerAddress address, SecurityPlan securityPlan, Logger logger )
58+
public SocketClient( BoltServerAddress address, SecurityPlan securityPlan, int timeoutMillis, Logger logger )
6159
{
6260
this.address = address;
6361
this.securityPlan = securityPlan;
62+
this.timeoutMillis = timeoutMillis;
6463
this.logger = logger;
6564
this.channel = null;
6665
}
@@ -121,7 +120,7 @@ public void start()
121120
try
122121
{
123122
logger.debug( "~~ [CONNECT] %s", address );
124-
setChannel( ChannelFactory.create( address, securityPlan, logger ) );
123+
setChannel( ChannelFactory.create( address, securityPlan, timeoutMillis, logger ) );
125124
protocol = negotiateProtocol();
126125
reader = protocol.reader();
127126
writer = protocol.writer();
@@ -280,36 +279,6 @@ public String toString()
280279
return "SocketClient[protocolVersion=" + version + "]";
281280
}
282281

283-
private static class ChannelFactory
284-
{
285-
public static ByteChannel create( BoltServerAddress address, SecurityPlan securityPlan, Logger logger )
286-
throws IOException, GeneralSecurityException
287-
{
288-
SocketChannel soChannel = SocketChannel.open();
289-
soChannel.setOption( StandardSocketOptions.SO_REUSEADDR, true );
290-
soChannel.setOption( StandardSocketOptions.SO_KEEPALIVE, true );
291-
soChannel.connect( address.toSocketAddress() );
292-
293-
ByteChannel channel;
294-
295-
if (securityPlan.requiresEncryption())
296-
{
297-
channel = TLSSocketChannel.create( address, securityPlan, soChannel, logger );
298-
}
299-
else
300-
{
301-
channel = soChannel;
302-
}
303-
304-
if ( logger.isTraceEnabled() )
305-
{
306-
channel = new LoggingByteChannel( channel, logger );
307-
}
308-
309-
return channel;
310-
}
311-
}
312-
313282
public BoltServerAddress address()
314283
{
315284
return address;

driver/src/main/java/org/neo4j/driver/internal/net/SocketConnection.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ public class SocketConnection implements Connection
5858

5959
private final Logger logger;
6060

61-
public SocketConnection( BoltServerAddress address, SecurityPlan securityPlan, Logging logging )
61+
public SocketConnection( BoltServerAddress address, SecurityPlan securityPlan, int timeoutMillis, Logging logging )
6262
{
6363
this.logger = logging.getLog( format( "conn-%s", UUID.randomUUID().toString() ) );
64-
this.socket = new SocketClient( address, securityPlan, logger );
64+
this.socket = new SocketClient( address, securityPlan, timeoutMillis, logger );
6565
this.responseHandler = createResponseHandler( logger );
6666
this.socket.start();
6767
}

driver/src/main/java/org/neo4j/driver/internal/net/SocketConnector.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public SocketConnector( ConnectionSettings connectionSettings, SecurityPlan secu
4747
@Override
4848
public final Connection connect( BoltServerAddress address )
4949
{
50-
Connection connection = createConnection( address, securityPlan, logging );
50+
Connection connection = createConnection( address, securityPlan, connectionSettings.timeoutMillis(),
51+
logging );
5152

5253
// Because SocketConnection is not thread safe, wrap it in this guard
5354
// to ensure concurrent access leads causes application errors
@@ -71,9 +72,10 @@ public final Connection connect( BoltServerAddress address )
7172
* <p>
7273
* <b>This method is package-private only for testing</b>
7374
*/
74-
Connection createConnection( BoltServerAddress address, SecurityPlan securityPlan, Logging logging )
75+
Connection createConnection( BoltServerAddress address, SecurityPlan securityPlan, int timeoutMillis,
76+
Logging logging )
7577
{
76-
return new SocketConnection( address, securityPlan, logging );
78+
return new SocketConnection( address, securityPlan, timeoutMillis, logging );
7779
}
7880

7981
private static Map<String,Value> tokenAsMap( AuthToken token )

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class Config
6161

6262
private final int routingFailureLimit;
6363
private final long routingRetryDelayMillis;
64+
private final int connectionTimeoutMillis;
6465

6566
private Config( ConfigBuilder builder)
6667
{
@@ -73,6 +74,7 @@ private Config( ConfigBuilder builder)
7374
this.trustStrategy = builder.trustStrategy;
7475
this.routingFailureLimit = builder.routingFailureLimit;
7576
this.routingRetryDelayMillis = builder.routingRetryDelayMillis;
77+
this.connectionTimeoutMillis = builder.connectionTimeoutMillis;
7678
}
7779

7880
/**
@@ -127,6 +129,14 @@ public long idleTimeBeforeConnectionTest()
127129
return -1;
128130
}
129131

132+
/**
133+
* @return the configured connection timeout value in milliseconds.
134+
*/
135+
public int connectionTimeoutMillis()
136+
{
137+
return connectionTimeoutMillis;
138+
}
139+
130140
/**
131141
* @return the level of encryption required for all connections.
132142
*/
@@ -176,7 +186,8 @@ public static class ConfigBuilder
176186
private EncryptionLevel encryptionLevel = EncryptionLevel.REQUIRED;
177187
private TrustStrategy trustStrategy = trustAllCertificates();
178188
private int routingFailureLimit = 1;
179-
private long routingRetryDelayMillis = 5_000;
189+
private long routingRetryDelayMillis = TimeUnit.SECONDS.toMillis( 5 );
190+
private int connectionTimeoutMillis = (int) TimeUnit.SECONDS.toMillis( 30 );
180191

181192
private ConfigBuilder() {}
182193

@@ -367,6 +378,40 @@ public ConfigBuilder withRoutingRetryDelay( long delay, TimeUnit unit )
367378
return this;
368379
}
369380

381+
/**
382+
* Specify socket connection timeout.
383+
* <p>
384+
* A timeout of zero is treated as an infinite timeout and will be bound by the timeout configured on the
385+
* operating system level. The connection will block until established or an error occurs.
386+
* <p>
387+
* Timeout value should be greater or equal to zero and represent a valid {@code int} value when converted to
388+
* {@link TimeUnit#MILLISECONDS milliseconds}.
389+
*
390+
* @param value the timeout duration
391+
* @param unit the unit in which duration is given
392+
* @return this builder
393+
* @throws IllegalArgumentException when given value is negative or does not fit in {@code int} when
394+
* converted to milliseconds.
395+
*/
396+
public ConfigBuilder withConnectionTimeout( long value, TimeUnit unit )
397+
{
398+
long connectionTimeoutMillis = unit.toMillis( value );
399+
if ( connectionTimeoutMillis < 0 )
400+
{
401+
throw new IllegalArgumentException( String.format(
402+
"The connection timeout may not be smaller than 0, but was %d %s.", value, unit ) );
403+
}
404+
int connectionTimeoutMillisInt = (int) connectionTimeoutMillis;
405+
if ( connectionTimeoutMillisInt != connectionTimeoutMillis )
406+
{
407+
throw new IllegalArgumentException( String.format(
408+
"The connection timeout must represent int value when converted to milliseconds %d.",
409+
connectionTimeoutMillis ) );
410+
}
411+
this.connectionTimeoutMillis = connectionTimeoutMillisInt;
412+
return this;
413+
}
414+
370415
/**
371416
* Create a config instance from this builder.
372417
* @return a {@link Config} instance

0 commit comments

Comments
 (0)