Skip to content

Commit 3c2903d

Browse files
authored
Revisit Write Timeout Handling (#1128)
1 parent 481191a commit 3c2903d

13 files changed

+255
-127
lines changed

src/examples/java/io/nats/examples/benchmark/NatsBench2.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.util.concurrent.atomic.AtomicBoolean;
2828
import java.util.concurrent.atomic.AtomicLong;
2929

30+
import static io.nats.client.support.NatsConstants.OUTPUT_QUEUE_IS_FULL;
31+
3032
/**
3133
* A utility class for measuring NATS performance, similar to the version in go
3234
* and node. The various tradeoffs to make this code act/work like the other
@@ -279,7 +281,7 @@ public void run() {
279281
nc.publish(subject, payload);
280282
success = true;
281283
} catch (IllegalStateException ex) {
282-
if (ex.getMessage().contains("Output queue is full")) {
284+
if (ex.getMessage().contains(OUTPUT_QUEUE_IS_FULL)) {
283285
success = false;
284286
Thread.sleep(1000);
285287
} else {

src/main/java/io/nats/client/Connection.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -541,15 +541,14 @@ enum Status {
541541
/**
542542
* Immediately flushes the underlying connection buffer if the connection is valid.
543543
* @throws IOException the connection flush fails
544-
* @throws IllegalStateException the connection is not connected
545544
*/
546545
void flushBuffer() throws IOException;
547546

548547
/**
549548
* Forces reconnect behavior. Stops the current connection including the reading and writing,
550549
* copies already queued outgoing messages, and then begins the reconnect logic.
551-
* @throws IOException the force reconnected fails
552-
* @throws InterruptedException if one is thrown, in order to propagate it up
550+
* @throws IOException the forceReconnect fails
551+
* @throws InterruptedException the connection is not connected
553552
*/
554553
void forceReconnect() throws IOException, InterruptedException;
555554

src/main/java/io/nats/client/Options.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ public class Options {
120120
*/
121121
public static final Duration DEFAULT_SOCKET_WRITE_TIMEOUT = Duration.ofMinutes(1);
122122

123+
/**
124+
* Constant used for calculating if a socket write timeout is large enough.
125+
*/
126+
public static final long MINIMUM_SOCKET_WRITE_TIMEOUT_GT_CONNECTION_TIMEOUT = 100;
127+
123128
/**
124129
* Default server ping interval. The client will send a ping to the server on this interval to insure liveness.
125130
* The server may send pings to the client as well, these are handled automatically by the library,
@@ -1240,11 +1245,23 @@ public Builder maxControlLine(int bytes) {
12401245
* Set the timeout for connection attempts. Each server in the options is allowed this timeout
12411246
* so if 3 servers are tried with a timeout of 5s the total time could be 15s.
12421247
*
1243-
* @param time the time to wait
1248+
* @param connectionTimeout the time to wait
1249+
* @return the Builder for chaining
1250+
*/
1251+
public Builder connectionTimeout(Duration connectionTimeout) {
1252+
this.connectionTimeout = connectionTimeout;
1253+
return this;
1254+
}
1255+
1256+
/**
1257+
* Set the timeout for connection attempts. Each server in the options is allowed this timeout
1258+
* so if 3 servers are tried with a timeout of 5s the total time could be 15s.
1259+
*
1260+
* @param connectionTimeoutMillis the time to wait in milliseconds
12441261
* @return the Builder for chaining
12451262
*/
1246-
public Builder connectionTimeout(Duration time) {
1247-
this.connectionTimeout = time;
1263+
public Builder connectionTimeout(long connectionTimeoutMillis) {
1264+
this.connectionTimeout = Duration.ofMillis(connectionTimeoutMillis);
12481265
return this;
12491266
}
12501267

@@ -1728,9 +1745,17 @@ else if (useDefaultTls) {
17281745
new DefaultThreadFactory(threadPrefix));
17291746
}
17301747

1731-
if (socketWriteTimeout != null && socketWriteTimeout.toMillis() < 1) {
1748+
if (socketWriteTimeout == null || socketWriteTimeout.toMillis() < 1) {
17321749
socketWriteTimeout = null;
17331750
}
1751+
else {
1752+
long swtMin = connectionTimeout.toMillis() + MINIMUM_SOCKET_WRITE_TIMEOUT_GT_CONNECTION_TIMEOUT;
1753+
if (socketWriteTimeout.toMillis() < swtMin) {
1754+
throw new IllegalStateException("Socket Write Timeout must be at least "
1755+
+ MINIMUM_SOCKET_WRITE_TIMEOUT_GT_CONNECTION_TIMEOUT
1756+
+ " milliseconds greater than the Connection Timeout");
1757+
}
1758+
}
17341759

17351760
if (errorListener == null) {
17361761
errorListener = new ErrorListenerLoggerImpl();

src/main/java/io/nats/client/impl/MessageQueue.java

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.function.Predicate;
2525

2626
import static io.nats.client.support.NatsConstants.EMPTY_BODY;
27+
import static io.nats.client.support.NatsConstants.OUTPUT_QUEUE_IS_FULL;
2728

2829
class MessageQueue {
2930
protected static final int STOPPED = 0;
@@ -35,9 +36,10 @@ class MessageQueue {
3536
protected final AtomicInteger running;
3637
protected final boolean singleReaderMode;
3738
protected final LinkedBlockingQueue<NatsMessage> queue;
38-
protected final Lock filterLock;
39+
protected final Lock editLock;
3940
protected final int publishHighwaterMark;
4041
protected final boolean discardWhenFull;
42+
protected final long offerLockMillis;
4143
protected final long offerTimeoutMillis;
4244
protected final Duration requestCleanupInterval;
4345

@@ -47,7 +49,11 @@ class MessageQueue {
4749
protected final NatsMessage poisonPill;
4850

4951
MessageQueue(boolean singleReaderMode, Duration requestCleanupInterval) {
50-
this(singleReaderMode, -1, false, requestCleanupInterval);
52+
this(singleReaderMode, -1, false, requestCleanupInterval, null);
53+
}
54+
55+
MessageQueue(boolean singleReaderMode, Duration requestCleanupInterval, MessageQueue source) {
56+
this(singleReaderMode, -1, false, requestCleanupInterval, source);
5157
}
5258

5359
/**
@@ -61,31 +67,40 @@ class MessageQueue {
6167
* @param requestCleanupInterval is used to figure the offerTimeoutMillis
6268
*/
6369
MessageQueue(boolean singleReaderMode, int publishHighwaterMark, boolean discardWhenFull, Duration requestCleanupInterval) {
70+
this(singleReaderMode, publishHighwaterMark, discardWhenFull, requestCleanupInterval, null);
71+
}
72+
73+
MessageQueue(boolean singleReaderMode, int publishHighwaterMark, boolean discardWhenFull, Duration requestCleanupInterval, MessageQueue source) {
6474
this.publishHighwaterMark = publishHighwaterMark;
6575
this.queue = publishHighwaterMark > 0 ? new LinkedBlockingQueue<>(publishHighwaterMark) : new LinkedBlockingQueue<>();
6676
this.discardWhenFull = discardWhenFull;
6777
this.running = new AtomicInteger(RUNNING);
6878
this.sizeInBytes = new AtomicLong(0);
6979
this.length = new AtomicLong(0);
70-
this.offerTimeoutMillis = calculateOfferTimeoutMillis(requestCleanupInterval);
80+
this.offerLockMillis = requestCleanupInterval.toMillis();
81+
this.offerTimeoutMillis = Math.max(1, requestCleanupInterval.toMillis() * 95 / 100);
7182

7283
// The poisonPill is used to stop poll and accumulate when the queue is stopped
7384
this.poisonPill = new NatsMessage("_poison", null, EMPTY_BODY);
7485

75-
this.filterLock = new ReentrantLock();
86+
editLock = new ReentrantLock();
7687

7788
this.singleReaderMode = singleReaderMode;
7889
this.requestCleanupInterval = requestCleanupInterval;
90+
91+
if (source != null) {
92+
source.drainTo(this);
93+
}
7994
}
8095

81-
MessageQueue(MessageQueue source) {
82-
this(source.singleReaderMode, source.publishHighwaterMark, source.discardWhenFull, source.requestCleanupInterval);
83-
source.queue.drainTo(queue);
84-
length.set(queue.size());
85-
}
86-
87-
private static long calculateOfferTimeoutMillis(Duration requestCleanupInterval) {
88-
return Math.max(1, requestCleanupInterval.toMillis() * 95 / 100);
96+
void drainTo(MessageQueue target) {
97+
editLock.lock();
98+
try {
99+
queue.drainTo(target.queue);
100+
target.length.set(queue.size());
101+
} finally {
102+
editLock.unlock();
103+
}
89104
}
90105

91106
boolean isSingleReaderMode() {
@@ -124,21 +139,36 @@ boolean push(NatsMessage msg) {
124139
}
125140

126141
boolean push(NatsMessage msg, boolean internal) {
127-
this.filterLock.lock();
142+
long start = System.currentTimeMillis();
143+
try {
144+
// try to get the lock, but don't wait forever
145+
// assuming that if we are waiting for the lock
146+
// another push likely has the lock and
147+
if (!editLock.tryLock(offerLockMillis, TimeUnit.MILLISECONDS)) {
148+
throw new IllegalStateException(OUTPUT_QUEUE_IS_FULL + queue.size());
149+
}
150+
}
151+
catch (InterruptedException e) {
152+
return false;
153+
}
154+
128155
try {
129-
// If we aren't running, then we need to obey the filter lock
130-
// to avoid ordering problems
131156
if (!internal && this.discardWhenFull) {
132157
return this.queue.offer(msg);
133158
}
134-
if (!this.offer(msg)) {
135-
throw new IllegalStateException("Output queue is full " + queue.size());
159+
160+
long timeoutLeft = Math.max(100, offerTimeoutMillis - (System.currentTimeMillis() - start));
161+
162+
if (!this.queue.offer(msg, timeoutLeft, TimeUnit.MILLISECONDS)) {
163+
throw new IllegalStateException(OUTPUT_QUEUE_IS_FULL + queue.size());
136164
}
137165
this.sizeInBytes.getAndAdd(msg.getSizeInBytes());
138166
this.length.incrementAndGet();
139167
return true;
168+
} catch (InterruptedException ie) {
169+
return false;
140170
} finally {
141-
this.filterLock.unlock();
171+
editLock.unlock();
142172
}
143173
}
144174

@@ -154,14 +184,6 @@ void poisonTheQueue() {
154184
}
155185
}
156186

157-
boolean offer(NatsMessage msg) {
158-
try {
159-
return this.queue.offer(msg, offerTimeoutMillis, TimeUnit.MILLISECONDS);
160-
} catch (InterruptedException ie) {
161-
return false;
162-
}
163-
}
164-
165187
NatsMessage poll(Duration timeout) throws InterruptedException {
166188
NatsMessage msg = null;
167189

@@ -289,7 +311,7 @@ long sizeInBytes() {
289311
}
290312

291313
void filter(Predicate<NatsMessage> p) {
292-
this.filterLock.lock();
314+
editLock.lock();
293315
try {
294316
if (this.isRunning()) {
295317
throw new IllegalStateException("Filter is only supported when the queue is paused");
@@ -307,7 +329,7 @@ void filter(Predicate<NatsMessage> p) {
307329
}
308330
this.queue.addAll(newQueue);
309331
} finally {
310-
this.filterLock.unlock();
332+
editLock.unlock();
311333
}
312334
}
313-
}
335+
}

0 commit comments

Comments
 (0)