Skip to content

Commit fd1a1ad

Browse files
committed
Retry with exponential backoff
This commit changes default retry policy from fixed backoff to exponential backoff with random jitter. Retry policy is user when retrying transactions executed via `Session#readTransaction()` and `Session#writeTransaction()` methods. Exponential backoff with random jitter is a better policy because it spreads out clusters of retries and progressively reduces load on the database. The only configuration option available is: `ConfigBuilder#withMaxTransactionRetryTime()` which sets a cap on how long particular transaction can be retried with exponential backoff.
1 parent 6c9eac7 commit fd1a1ad

19 files changed

+718
-453
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
import org.neo4j.driver.internal.net.SocketConnector;
2929
import org.neo4j.driver.internal.net.pooling.PoolSettings;
3030
import org.neo4j.driver.internal.net.pooling.SocketConnectionPool;
31+
import org.neo4j.driver.internal.retry.ExponentialBackoff;
3132
import org.neo4j.driver.internal.retry.RetryDecision;
3233
import org.neo4j.driver.internal.retry.RetryLogic;
3334
import org.neo4j.driver.internal.retry.RetrySettings;
34-
import org.neo4j.driver.internal.retry.RetryWithDelay;
3535
import org.neo4j.driver.internal.security.SecurityPlan;
3636
import org.neo4j.driver.internal.spi.ConnectionPool;
3737
import org.neo4j.driver.internal.spi.ConnectionProvider;
@@ -57,7 +57,7 @@ public final Driver newInstance( URI uri, AuthToken authToken, RoutingSettings r
5757
BoltServerAddress address = BoltServerAddress.from( uri );
5858
SecurityPlan securityPlan = createSecurityPlan( address, config );
5959
ConnectionPool connectionPool = createConnectionPool( authToken, securityPlan, config );
60-
RetryLogic<RetryDecision> retryLogic = RetryWithDelay.create( retrySettings, createClock() );
60+
RetryLogic<RetryDecision> retryLogic = createRetryLogic( retrySettings );
6161

6262
try
6363
{
@@ -195,6 +195,16 @@ protected SessionFactory createSessionFactory( ConnectionProvider connectionProv
195195
return new SessionFactoryImpl( connectionProvider, retryLogic, config );
196196
}
197197

198+
/**
199+
* Creates new {@link RetryLogic<RetryDecision>}.
200+
* <p>
201+
* <b>This method is protected only for testing</b>
202+
*/
203+
protected RetryLogic<RetryDecision> createRetryLogic( RetrySettings settings )
204+
{
205+
return ExponentialBackoff.create( settings, createClock() );
206+
}
207+
198208
private static SecurityPlan createSecurityPlan( BoltServerAddress address, Config config )
199209
{
200210
try
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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.retry;
20+
21+
import java.util.Objects;
22+
import java.util.concurrent.ThreadLocalRandom;
23+
24+
import org.neo4j.driver.internal.util.Clock;
25+
import org.neo4j.driver.v1.exceptions.ServiceUnavailableException;
26+
import org.neo4j.driver.v1.exceptions.SessionExpiredException;
27+
import org.neo4j.driver.v1.exceptions.TransientException;
28+
29+
import static java.util.concurrent.TimeUnit.SECONDS;
30+
31+
public class ExponentialBackoff implements RetryLogic<ExponentialBackoffDecision>
32+
{
33+
public static final long DEFAULT_MAX_RETRY_TIME_MS = SECONDS.toMillis( 30 );
34+
35+
private static final long INITIAL_RETRY_DELAY_MS = SECONDS.toMillis( 1 );
36+
private static final double RETRY_DELAY_MULTIPLIER = 2.0;
37+
private static final double RETRY_DELAY_JITTER_FACTOR = 0.2;
38+
39+
private final long maxRetryTimeMs;
40+
private final long initialRetryDelayMs;
41+
private final double multiplier;
42+
private final double jitterFactor;
43+
private final Clock clock;
44+
45+
ExponentialBackoff( long maxRetryTimeMs, long initialRetryDelayMs, double multiplier, double jitterFactor,
46+
Clock clock )
47+
{
48+
this.maxRetryTimeMs = maxRetryTimeMs;
49+
this.initialRetryDelayMs = initialRetryDelayMs;
50+
this.multiplier = multiplier;
51+
this.jitterFactor = jitterFactor;
52+
this.clock = clock;
53+
54+
verifyAfterConstruction();
55+
}
56+
57+
public static RetryLogic<RetryDecision> defaultRetryLogic()
58+
{
59+
return create( RetrySettings.DEFAULT, Clock.SYSTEM );
60+
}
61+
62+
public static RetryLogic<RetryDecision> noRetries()
63+
{
64+
return create( new RetrySettings( 0 ), Clock.SYSTEM );
65+
}
66+
67+
@SuppressWarnings( "unchecked" )
68+
public static RetryLogic<RetryDecision> create( RetrySettings settings, Clock clock )
69+
{
70+
return (RetryLogic) new ExponentialBackoff( settings.maxRetryTimeMs(), INITIAL_RETRY_DELAY_MS,
71+
RETRY_DELAY_MULTIPLIER, RETRY_DELAY_JITTER_FACTOR, clock );
72+
}
73+
74+
@Override
75+
public ExponentialBackoffDecision apply( Throwable error, ExponentialBackoffDecision previousDecision )
76+
{
77+
Objects.requireNonNull( error );
78+
ExponentialBackoffDecision decision = decision( previousDecision );
79+
80+
long elapsedTimeMs = clock.millis() - decision.startTimestamp();
81+
if ( elapsedTimeMs > maxRetryTimeMs || !canRetryOn( error ) )
82+
{
83+
return decision.stopRetrying();
84+
}
85+
86+
long delayWithJitterMs = computeDelayWithJitter( decision.delay() );
87+
sleep( delayWithJitterMs );
88+
89+
long nextDelayWithoutJitterMs = (long) (decision.delay() * multiplier);
90+
return decision.withDelay( nextDelayWithoutJitterMs );
91+
}
92+
93+
private ExponentialBackoffDecision decision( ExponentialBackoffDecision previous )
94+
{
95+
return previous == null ? new ExponentialBackoffDecision( clock.millis(), initialRetryDelayMs ) : previous;
96+
}
97+
98+
private long computeDelayWithJitter( long delayMs )
99+
{
100+
long jitter = (long) (delayMs * jitterFactor);
101+
long min = delayMs - jitter;
102+
long max = delayMs + jitter;
103+
if ( max < 0 )
104+
{
105+
// overflow detected, truncate min and max values
106+
min -= 1;
107+
max = min;
108+
}
109+
return ThreadLocalRandom.current().nextLong( min, max + 1 );
110+
}
111+
112+
private void sleep( long delayMs )
113+
{
114+
try
115+
{
116+
clock.sleep( delayMs );
117+
}
118+
catch ( InterruptedException e )
119+
{
120+
Thread.currentThread().interrupt();
121+
throw new IllegalStateException( "Retries interrupted", e );
122+
}
123+
}
124+
125+
private void verifyAfterConstruction()
126+
{
127+
if ( maxRetryTimeMs < 0 )
128+
{
129+
throw new IllegalArgumentException( "Max retry time should be >= 0: " + maxRetryTimeMs );
130+
}
131+
if ( initialRetryDelayMs < 0 )
132+
{
133+
throw new IllegalArgumentException( "Initial retry delay should >= 0: " + initialRetryDelayMs );
134+
}
135+
if ( multiplier < 1.0 )
136+
{
137+
throw new IllegalArgumentException( "Multiplier should be >= 1.0: " + multiplier );
138+
}
139+
if ( jitterFactor < 0 || jitterFactor > 1 )
140+
{
141+
throw new IllegalArgumentException( "Jitter factor should be in [0.0, 1.0]: " + jitterFactor );
142+
}
143+
if ( clock == null )
144+
{
145+
throw new IllegalArgumentException( "Clock should not be null" );
146+
}
147+
}
148+
149+
private static boolean canRetryOn( Throwable error )
150+
{
151+
return error instanceof SessionExpiredException ||
152+
error instanceof ServiceUnavailableException ||
153+
error instanceof TransientException;
154+
}
155+
}

driver/src/main/java/org/neo4j/driver/internal/retry/RetryWithDelayDecision.java renamed to driver/src/main/java/org/neo4j/driver/internal/retry/ExponentialBackoffDecision.java

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,16 @@
1818
*/
1919
package org.neo4j.driver.internal.retry;
2020

21-
class RetryWithDelayDecision implements RetryDecision
21+
public class ExponentialBackoffDecision implements RetryDecision
2222
{
23-
private int attempt = 1;
23+
private final long startTimestamp;
24+
private long delay;
2425
private boolean shouldRetry = true;
2526

26-
RetryWithDelayDecision()
27+
ExponentialBackoffDecision( long startTimestamp, long initialDelay )
2728
{
28-
}
29-
30-
/**
31-
* Create a decision with the specified attempt.
32-
* <p>
33-
* <b>Note:</b> only for testing.
34-
*/
35-
RetryWithDelayDecision( int attempt )
36-
{
37-
this.attempt = attempt;
29+
this.startTimestamp = startTimestamp;
30+
this.delay = initialDelay;
3831
}
3932

4033
@Override
@@ -43,22 +36,23 @@ public boolean shouldRetry()
4336
return shouldRetry;
4437
}
4538

46-
int attempt()
39+
long startTimestamp()
40+
{
41+
return startTimestamp;
42+
}
43+
44+
long delay()
4745
{
48-
return attempt;
46+
return delay;
4947
}
5048

51-
RetryWithDelayDecision incrementAttempt()
49+
ExponentialBackoffDecision withDelay( long delay )
5250
{
53-
attempt++;
54-
if ( attempt == Integer.MAX_VALUE )
55-
{
56-
stopRetrying();
57-
}
51+
this.delay = delay;
5852
return this;
5953
}
6054

61-
RetryWithDelayDecision stopRetrying()
55+
ExponentialBackoffDecision stopRetrying()
6256
{
6357
shouldRetry = false;
6458
return this;

driver/src/main/java/org/neo4j/driver/internal/retry/RetrySettings.java

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,17 @@
2020

2121
public final class RetrySettings
2222
{
23-
public static final int DEFAULT_MAX_ATTEMPTS = 3;
24-
public static final int DEFAULT_DELAY_MS = 2_000;
25-
public static final RetrySettings DEFAULT = new RetrySettings( DEFAULT_MAX_ATTEMPTS, DEFAULT_DELAY_MS );
23+
public static final RetrySettings DEFAULT = new RetrySettings( ExponentialBackoff.DEFAULT_MAX_RETRY_TIME_MS );
2624

27-
private final int maxAttempts;
28-
private final long delayMs;
25+
private final long maxRetryTimeMs;
2926

30-
public RetrySettings( int maxAttempts, long delayMs )
27+
public RetrySettings( long maxRetryTimeMs )
3128
{
32-
this.maxAttempts = maxAttempts;
33-
this.delayMs = delayMs;
29+
this.maxRetryTimeMs = maxRetryTimeMs;
3430
}
3531

36-
public int maxAttempts()
32+
public long maxRetryTimeMs()
3733
{
38-
return maxAttempts;
39-
}
40-
41-
public long delayMs()
42-
{
43-
return delayMs;
34+
return maxRetryTimeMs;
4435
}
4536
}

driver/src/main/java/org/neo4j/driver/internal/retry/RetryWithDelay.java

Lines changed: 0 additions & 93 deletions
This file was deleted.

0 commit comments

Comments
 (0)