Skip to content

Commit 06f2fa4

Browse files
authored
Jetty 9.4.x : fix client remove idle destinations (#8495)
Fixes #8493: RemoveIdleDestinations's race condition and improve logging. Signed-off-by: Ludovic Orban <[email protected]>
1 parent 940455b commit 06f2fa4

File tree

7 files changed

+236
-54
lines changed

7 files changed

+236
-54
lines changed

jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ protected void tryCreate(boolean create)
306306
{
307307
pending.decrementAndGet();
308308
if (LOG.isDebugEnabled())
309-
LOG.debug("Not creating connection as pool is full, pending: {}", pending);
309+
LOG.debug("Not creating connection as pool {} is full, pending: {}", pool, pending);
310310
return;
311311
}
312312

@@ -516,15 +516,17 @@ public boolean sweep()
516516
@Override
517517
public String toString()
518518
{
519-
return String.format("%s@%x[c=%d/%d/%d,a=%d,i=%d,q=%d]",
519+
return String.format("%s@%x[s=%s,c=%d/%d/%d,a=%d,i=%d,q=%d,p=%s]",
520520
getClass().getSimpleName(),
521521
hashCode(),
522+
getState(),
522523
getPendingConnectionCount(),
523524
getConnectionCount(),
524525
getMaxConnectionCount(),
525526
getActiveConnectionCount(),
526527
getIdleConnectionCount(),
527-
destination.getQueuedRequestCount());
528+
destination.getQueuedRequestCount(),
529+
pool);
528530
}
529531

530532
private class FutureConnection extends Promise.Completable<Connection>

jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java

+75-10
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import org.eclipse.jetty.util.thread.QueuedThreadPool;
7878
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
7979
import org.eclipse.jetty.util.thread.Scheduler;
80+
import org.eclipse.jetty.util.thread.Sweeper;
8081
import org.eclipse.jetty.util.thread.ThreadPool;
8182

8283
/**
@@ -148,11 +149,12 @@ public class HttpClient extends ContainerLifeCycle
148149
private boolean tcpNoDelay = true;
149150
private boolean strictEventOrdering = false;
150151
private HttpField encodingField;
151-
private boolean removeIdleDestinations = false;
152+
private long destinationIdleTimeout;
152153
private boolean connectBlocking = false;
153154
private String name = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
154155
private HttpCompliance httpCompliance = HttpCompliance.RFC7230;
155156
private String defaultRequestContentType = "application/octet-stream";
157+
private Sweeper destinationSweeper;
156158

157159
/**
158160
* Creates a {@link HttpClient} instance that can perform requests to non-TLS destinations only
@@ -252,7 +254,14 @@ protected void doStart() throws Exception
252254
cookieStore = cookieManager.getCookieStore();
253255

254256
transport.setHttpClient(this);
257+
255258
super.doStart();
259+
260+
if (getDestinationIdleTimeout() > 0L)
261+
{
262+
destinationSweeper = new Sweeper(scheduler, 1000L);
263+
destinationSweeper.start();
264+
}
256265
}
257266

258267
private CookieManager newCookieManager()
@@ -263,6 +272,12 @@ private CookieManager newCookieManager()
263272
@Override
264273
protected void doStop() throws Exception
265274
{
275+
if (destinationSweeper != null)
276+
{
277+
destinationSweeper.stop();
278+
destinationSweeper = null;
279+
}
280+
266281
decoderFactories.clear();
267282
handlers.clear();
268283

@@ -318,6 +333,11 @@ CookieManager getCookieManager()
318333
return cookieManager;
319334
}
320335

336+
Sweeper getDestinationSweeper()
337+
{
338+
return destinationSweeper;
339+
}
340+
321341
/**
322342
* @return the authentication store associated with this instance
323343
*/
@@ -570,20 +590,27 @@ protected Origin createOrigin(String scheme, String host, int port, Object tag)
570590
*/
571591
public HttpDestination resolveDestination(Origin origin)
572592
{
573-
return destinations.computeIfAbsent(origin, o ->
593+
return destinations.compute(origin, (k, v) ->
574594
{
575-
HttpDestination newDestination = getTransport().newHttpDestination(o);
576-
addManaged(newDestination);
577-
if (LOG.isDebugEnabled())
578-
LOG.debug("Created {}", newDestination);
579-
return newDestination;
595+
if (v == null || v.stale())
596+
{
597+
HttpDestination newDestination = getTransport().newHttpDestination(k);
598+
addManaged(newDestination);
599+
if (LOG.isDebugEnabled())
600+
LOG.debug("Created {}; existing: '{}'", newDestination, v);
601+
return newDestination;
602+
}
603+
return v;
580604
});
581605
}
582606

583607
protected boolean removeDestination(HttpDestination destination)
584608
{
609+
boolean removed = destinations.remove(destination.getOrigin(), destination);
585610
removeBean(destination);
586-
return destinations.remove(destination.getOrigin(), destination);
611+
if (LOG.isDebugEnabled())
612+
LOG.debug("Removed {}; result: {}", destination, removed);
613+
return removed;
587614
}
588615

589616
/**
@@ -1080,14 +1107,50 @@ public void setStrictEventOrdering(boolean strictEventOrdering)
10801107
this.strictEventOrdering = strictEventOrdering;
10811108
}
10821109

1110+
/**
1111+
* The default value is 0
1112+
* @return the time in ms after which idle destinations are removed
1113+
* @see #setDestinationIdleTimeout(long)
1114+
*/
1115+
@ManagedAttribute("The time in ms after which idle destinations are removed, disabled when zero or negative")
1116+
public long getDestinationIdleTimeout()
1117+
{
1118+
return destinationIdleTimeout;
1119+
}
1120+
1121+
/**
1122+
* <p>
1123+
* Whether destinations that have no connections (nor active nor idle) and no exchanges
1124+
* should be removed after the specified timeout.
1125+
* </p>
1126+
* <p>
1127+
* If the specified {@code destinationIdleTimeout} is 0 or negative, then the destinations
1128+
* are not removed.
1129+
* </p>
1130+
* <p>
1131+
* Avoids accumulating destinations when applications (e.g. a spider bot or web crawler)
1132+
* hit a lot of different destinations that won't be visited again.
1133+
* </p>
1134+
*
1135+
* @param destinationIdleTimeout the time in ms after which idle destinations are removed
1136+
*/
1137+
public void setDestinationIdleTimeout(long destinationIdleTimeout)
1138+
{
1139+
if (isStarted())
1140+
LOG.warn("Calling setDestinationIdleTimeout() while started has no effect");
1141+
this.destinationIdleTimeout = destinationIdleTimeout;
1142+
}
1143+
10831144
/**
10841145
* @return whether destinations that have no connections should be removed
10851146
* @see #setRemoveIdleDestinations(boolean)
1147+
* @deprecated replaced by {@link #getDestinationIdleTimeout()}
10861148
*/
1149+
@Deprecated
10871150
@ManagedAttribute("Whether idle destinations are removed")
10881151
public boolean isRemoveIdleDestinations()
10891152
{
1090-
return removeIdleDestinations;
1153+
return destinationIdleTimeout > 0L;
10911154
}
10921155

10931156
/**
@@ -1101,10 +1164,12 @@ public boolean isRemoveIdleDestinations()
11011164
*
11021165
* @param removeIdleDestinations whether destinations that have no connections should be removed
11031166
* @see org.eclipse.jetty.client.DuplexConnectionPool
1167+
* @deprecated replaced by {@link #setDestinationIdleTimeout(long)}, calls the latter with a value of 10000 ms.
11041168
*/
1169+
@Deprecated
11051170
public void setRemoveIdleDestinations(boolean removeIdleDestinations)
11061171
{
1107-
this.removeIdleDestinations = removeIdleDestinations;
1172+
setDestinationIdleTimeout(removeIdleDestinations ? 10_000L : 0L);
11081173
}
11091174

11101175
/**

jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java

+94-30
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.List;
2727
import java.util.Queue;
2828
import java.util.concurrent.RejectedExecutionException;
29+
import java.util.concurrent.TimeUnit;
2930
import java.util.concurrent.TimeoutException;
3031

3132
import org.eclipse.jetty.client.api.Connection;
@@ -45,14 +46,16 @@
4546
import org.eclipse.jetty.util.component.ContainerLifeCycle;
4647
import org.eclipse.jetty.util.component.Dumpable;
4748
import org.eclipse.jetty.util.component.DumpableCollection;
49+
import org.eclipse.jetty.util.component.LifeCycle;
4850
import org.eclipse.jetty.util.log.Log;
4951
import org.eclipse.jetty.util.log.Logger;
5052
import org.eclipse.jetty.util.ssl.SslContextFactory;
53+
import org.eclipse.jetty.util.thread.Locker;
5154
import org.eclipse.jetty.util.thread.Scheduler;
5255
import org.eclipse.jetty.util.thread.Sweeper;
5356

5457
@ManagedObject
55-
public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable
58+
public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable, Sweeper.Sweepable
5659
{
5760
private static final Logger LOG = Log.getLogger(HttpDestination.class);
5861

@@ -65,7 +68,10 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
6568
private final ClientConnectionFactory connectionFactory;
6669
private final HttpField hostField;
6770
private final RequestTimeouts requestTimeouts;
71+
private final Locker staleLock = new Locker();
6872
private ConnectionPool connectionPool;
73+
private boolean stale;
74+
private long activeNanos;
6975

7076
public HttpDestination(HttpClient client, Origin origin)
7177
{
@@ -104,23 +110,78 @@ public HttpDestination(HttpClient client, Origin origin)
104110
hostField = new HttpField(HttpHeader.HOST, host);
105111
}
106112

113+
public boolean stale()
114+
{
115+
try (Locker.Lock l = staleLock.lock())
116+
{
117+
boolean stale = this.stale;
118+
if (!stale)
119+
this.activeNanos = System.nanoTime();
120+
if (LOG.isDebugEnabled())
121+
LOG.debug("Stale check done with result {} on {}", stale, this);
122+
return stale;
123+
}
124+
}
125+
126+
@Override
127+
public boolean sweep()
128+
{
129+
if (LOG.isDebugEnabled())
130+
LOG.debug("Sweep check in progress on {}", this);
131+
boolean remove = false;
132+
try (Locker.Lock l = staleLock.lock())
133+
{
134+
boolean stale = exchanges.isEmpty() && connectionPool.isEmpty();
135+
if (!stale)
136+
{
137+
this.activeNanos = System.nanoTime();
138+
}
139+
else if (isStaleDelayExpired())
140+
{
141+
this.stale = true;
142+
remove = true;
143+
}
144+
}
145+
if (remove)
146+
{
147+
getHttpClient().removeDestination(this);
148+
LifeCycle.stop(this);
149+
}
150+
if (LOG.isDebugEnabled())
151+
LOG.debug("Sweep check done with result {} on {}", remove, this);
152+
return remove;
153+
}
154+
155+
private boolean isStaleDelayExpired()
156+
{
157+
assert staleLock.isLocked();
158+
long destinationIdleTimeout = TimeUnit.MILLISECONDS.toNanos(getHttpClient().getDestinationIdleTimeout());
159+
return System.nanoTime() - activeNanos >= destinationIdleTimeout;
160+
}
161+
107162
@Override
108163
protected void doStart() throws Exception
109164
{
110165
this.connectionPool = newConnectionPool(client);
111166
addBean(connectionPool, true);
112167
super.doStart();
113-
Sweeper sweeper = client.getBean(Sweeper.class);
114-
if (sweeper != null && connectionPool instanceof Sweeper.Sweepable)
115-
sweeper.offer((Sweeper.Sweepable)connectionPool);
168+
Sweeper connectionPoolSweeper = client.getBean(Sweeper.class);
169+
if (connectionPoolSweeper != null && connectionPool instanceof Sweeper.Sweepable)
170+
connectionPoolSweeper.offer((Sweeper.Sweepable)connectionPool);
171+
Sweeper destinationSweeper = getHttpClient().getDestinationSweeper();
172+
if (destinationSweeper != null)
173+
destinationSweeper.offer(this);
116174
}
117175

118176
@Override
119177
protected void doStop() throws Exception
120178
{
121-
Sweeper sweeper = client.getBean(Sweeper.class);
122-
if (sweeper != null && connectionPool instanceof Sweeper.Sweepable)
123-
sweeper.remove((Sweeper.Sweepable)connectionPool);
179+
Sweeper destinationSweeper = getHttpClient().getDestinationSweeper();
180+
if (destinationSweeper != null)
181+
destinationSweeper.remove(this);
182+
Sweeper connectionPoolSweeper = client.getBean(Sweeper.class);
183+
if (connectionPoolSweeper != null && connectionPool instanceof Sweeper.Sweepable)
184+
connectionPoolSweeper.remove((Sweeper.Sweepable)connectionPool);
124185
super.doStop();
125186
removeBean(connectionPool);
126187
}
@@ -462,11 +523,7 @@ public boolean remove(Connection connection)
462523
{
463524
boolean removed = connectionPool.remove(connection);
464525

465-
if (getHttpExchanges().isEmpty())
466-
{
467-
tryRemoveIdleDestination();
468-
}
469-
else if (removed)
526+
if (removed)
470527
{
471528
// Process queued requests that may be waiting.
472529
// We may create a connection that is not
@@ -501,22 +558,6 @@ public void abort(Throwable cause)
501558
{
502559
exchange.getRequest().abort(cause);
503560
}
504-
if (exchanges.isEmpty())
505-
tryRemoveIdleDestination();
506-
}
507-
508-
private void tryRemoveIdleDestination()
509-
{
510-
if (getHttpClient().isRemoveIdleDestinations() && connectionPool.isEmpty())
511-
{
512-
// There is a race condition between this thread removing the destination
513-
// and another thread queueing a request to this same destination.
514-
// If this destination is removed, but the request queued, a new connection
515-
// will be opened, the exchange will be executed and eventually the connection
516-
// will idle timeout and be closed. Meanwhile a new destination will be created
517-
// in HttpClient and will be used for other requests.
518-
getHttpClient().removeDestination(this);
519-
}
520561
}
521562

522563
@Override
@@ -530,16 +571,39 @@ public String asString()
530571
return origin.asString();
531572
}
532573

574+
@ManagedAttribute("For how long this destination has been idle in ms")
575+
public long getIdle()
576+
{
577+
if (getHttpClient().getDestinationIdleTimeout() <= 0L)
578+
return -1;
579+
try (Locker.Lock l = staleLock.lock())
580+
{
581+
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - activeNanos);
582+
}
583+
}
584+
585+
@ManagedAttribute("Whether this destinations is stale")
586+
public boolean isStale()
587+
{
588+
try (Locker.Lock l = staleLock.lock())
589+
{
590+
return this.stale;
591+
}
592+
}
593+
533594
@Override
534595
public String toString()
535596
{
536-
return String.format("%s[%s]@%x%s,queue=%d,pool=%s",
597+
return String.format("%s[%s]@%x%s,state=%s,queue=%d,pool=%s,stale=%b,idle=%d",
537598
HttpDestination.class.getSimpleName(),
538599
asString(),
539600
hashCode(),
540601
proxy == null ? "" : "(via " + proxy + ")",
602+
getState(),
541603
getQueuedRequestCount(),
542-
getConnectionPool());
604+
getConnectionPool(),
605+
isStale(),
606+
getIdle());
543607
}
544608

545609
/**

jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java

-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ private void startClient() throws Exception
7373
clientThreads.setName("client");
7474
client = new HttpClient();
7575
client.setExecutor(clientThreads);
76-
client.setRemoveIdleDestinations(false);
7776
client.start();
7877
}
7978

0 commit comments

Comments
 (0)