• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

synadia-io / nats.java.v3 / 38956993549

16 Oct 2026 07:43PM UTC coverage: 89.5% (+0.2%) from 89.277%
38956993549

push

github

scottf
Rejected server counts as a failed attempt; V3 connect cleanup; V3 vs Classic doc

- A server the connection rejects is skipped every time the pool supplies it, reported once per connect or reconnect, and also passed to connectFailed, so a pool that keeps it counts it out under the reconnect limit. Replaces the per-loop rule that ended the loop.
- NatsConnectionV3: connectV3 and reconnectAndResume folded into the connectImpl and reconnectImpl overrides; the entry methods call the Impl methods, then run a close or reconnect requested while the guard was held. Shares delayBeforeFirstRound and lameDuckTriggered with NatsConnection.
- CONNECTION_IMPLEMENTATIONS.md: user-facing comparison of V3 and Classic, linked from the README; connectionImplementation added to the README properties table.

4690 of 5499 branches covered (85.29%)

Branch coverage included in aggregate %.

36 of 42 new or added lines in 2 files covered. (85.71%)

3 existing lines in 2 files now uncovered.

11650 of 12758 relevant lines covered (91.32%)

4.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.25
/core/src/main/java/io/synadia/client/impl/NatsConnection.java
1
package io.synadia.client.impl;
2

3
import io.synadia.client.*;
4
import io.synadia.client.api.ServerInfo;
5
import io.synadia.client.global.NatsInetAddress;
6
import io.synadia.client.global.NatsSystemClock;
7
import io.synadia.client.utils.*;
8
import org.jspecify.annotations.NonNull;
9
import org.jspecify.annotations.Nullable;
10

11
import java.io.IOException;
12
import java.net.InetAddress;
13
import java.net.URISyntaxException;
14
import java.nio.ByteBuffer;
15
import java.nio.CharBuffer;
16
import java.util.*;
17
import java.util.concurrent.*;
18
import java.util.concurrent.atomic.AtomicBoolean;
19
import java.util.concurrent.atomic.AtomicLong;
20
import java.util.concurrent.atomic.AtomicReference;
21
import java.util.concurrent.locks.Condition;
22
import java.util.concurrent.locks.ReentrantLock;
23
import java.util.function.Predicate;
24

25
import static io.synadia.client.ConnectionStatus.*;
26
import static io.synadia.client.utils.NatsConstants.*;
27
import static io.synadia.client.utils.NatsRequestCompletableFuture.CancelAction;
28
import static java.nio.charset.StandardCharsets.UTF_8;
29

30
/**
31
 * The connection to a NATS server.
32
 *
33
 * <p>Publish, subscribe, and request methods validate their arguments and throw {@link IllegalArgumentException} for an invalid subject, reply subject, or message.
34
 */
35
public class NatsConnection implements AutoCloseable {
36

37
    protected final Options options;
38
    protected final boolean forceFlushOnRequest;
39

40
    protected final StatisticsCollector statistics;
41

42
    protected boolean connecting; // you can only connect in one thread
43
    protected boolean disconnecting; // you can only disconnect in one thread
44
    protected boolean closing; // respect a close call regardless
45
    protected Exception exceptionDuringConnectChange; // exception occurred in another thread while dis/connecting
46
    protected final ReentrantLock closeSocketLock;
47

48
    private volatile ConnectionStatus status;
49
    protected final ReentrantLock statusLock;
50
    protected final Condition statusChanged;
51

52
    protected CompletableFuture<DataPort> dataPortFuture;
53
    protected DataPort dataPort;
54
    // Volatile so that a caller reading getConnectedUrl() and getStatus() back to back cannot see
55
    // the pair out of the order teardown writes them in.
56
    protected volatile NatsUri currentServer;
57
    protected NatsUri connectingServer;
58
    protected NatsUri lastServer;
59
    protected CompletableFuture<Boolean> reconnectWaiter;
60
    protected volatile boolean lameDuckTriggered = false;
3 ✔
61
    protected final ConcurrentHashMap<NatsUri, String> serverAuthErrors;
62

63
    protected NatsConnectionReader reader;
64
    protected NatsConnectionWriter writer;
65

66
    protected final AtomicReference<ServerInfo> serverInfo;
67

68
    protected final Map<String, SubscriptionInfo> subscriptions;
69
    protected final Set<NatsDispatcher> dispatchers; // use a concurrent set so we get more consistent iteration behavior
70
    protected final Map<String, ConnectionListener> connectionListeners;
71
    protected final Map<String, ErrorListener> errorListeners;
72
    protected final Map<String, NatsRequestCompletableFuture> responsesAwaiting;
73
    protected final Map<String, NatsRequestCompletableFuture> responsesRespondedTo;
74
    protected final ConcurrentLinkedDeque<CompletableFuture<Boolean>> pongQueue;
75

76
    protected final String mainInbox;
77
    protected final AtomicReference<NatsDispatcher> inboxDispatcher;
78
    protected final ReentrantLock inboxDispatcherLock;
79
    protected ScheduledTask pingTask;
80
    protected ScheduledTask cleanupTask;
81

82
    protected final AtomicBoolean needPing;
83

84
    protected final AtomicLong nextSid;
85
    protected final NUID nuid;
86

87
    protected final AtomicReference<String> connectError;
88
    protected final AtomicReference<String> lastError;
89
    protected final AtomicReference<CompletableFuture<Boolean>> draining;
90
    protected final AtomicBoolean blockPublishForDrain;
91
    protected final AtomicBoolean tryingToConnect;
92

93
    // these are not final so they can be nullified on close
94
    protected ExecutorService callbackExecutor;
95
    protected ExecutorService executor;
96
    protected ExecutorService connectExecutor;
97
    protected ExecutorService readerExecutor;
98
    protected ExecutorService writerExecutor;
99
    protected ScheduledExecutorService scheduledExecutor;
100

101
    protected final boolean advancedTracking;
102

103
    protected final ServerPool serverPool;
104
    protected final DispatcherFactory dispatcherFactory;
105

106
    // allows user to opt into the level of subject validation they want
107
    protected interface SubjectReplyValidator {
108
        String validate(String subject, boolean required);
109
    }
110

111
    protected final SubjectReplyValidator subjectValidator;
112
    protected final SubjectReplyValidator replyValidator;
113

114
    protected String subjectValidate(String subject) {
115
        return subjectValidator.validate(subject, true);
6 ✔
116
    }
117

118
    protected String replyValidate(String replyTo) {
119
        return replyValidator.validate(replyTo, false);
6 ✔
120
    }
121

122
    protected NatsConnection(@NonNull Options options) {
2 ✔
123
        this.options = options;
3 ✔
124
        forceFlushOnRequest = options.forceFlushOnRequest();
4 ✔
125

126
        advancedTracking = options.isTrackAdvancedStats();
4 ✔
127
        this.statistics = options.getStatisticsCollector() == null ? new NatsStatistics() : options.getStatisticsCollector();
9 !
128
        statistics.setAdvancedTracking(advancedTracking);
5 ✔
129

130
        this.closeSocketLock = new ReentrantLock();
5 ✔
131

132
        this.statusLock = new ReentrantLock();
5 ✔
133
        this.statusChanged = statusLock.newCondition();
5 ✔
134
        this.status = DISCONNECTED;
3 ✔
135
        this.reconnectWaiter = new CompletableFuture<>();
5 ✔
136
        reconnectWaiter.complete(Boolean.TRUE);
5 ✔
137

138
        // seeded from the options; two listeners sharing an id means the last one wins
139
        this.connectionListeners = new ConcurrentHashMap<>();
5 ✔
140
        for (ConnectionListener cl : options.getConnectionListeners()) {
11 ✔
141
            addConnectionListener(cl);
3 ✔
142
        }
1 ✔
143

144
        this.errorListeners = new ConcurrentHashMap<>();
5 ✔
145
        for (ErrorListener el : options.getErrorListeners()) {
11 ✔
146
            addErrorListener(el);
3 ✔
147
        }
1 ✔
148

149
        this.dispatchers = ConcurrentHashMap.newKeySet();
3 ✔
150
        this.subscriptions = new ConcurrentHashMap<>();
5 ✔
151
        this.responsesAwaiting = new ConcurrentHashMap<>();
5 ✔
152
        this.responsesRespondedTo = new ConcurrentHashMap<>();
5 ✔
153
        this.serverAuthErrors = new ConcurrentHashMap<>();
5 ✔
154

155
        this.nextSid = new AtomicLong(1);
6 ✔
156
        this.nuid = new NUID();
5 ✔
157
        this.mainInbox = createInbox() + ".*";
5 ✔
158

159
        this.lastError = new AtomicReference<>();
5 ✔
160
        this.connectError = new AtomicReference<>();
5 ✔
161

162
        this.serverInfo = new AtomicReference<>(ServerInfo.EMPTY_INFO); // we want serverInfo.get to never return a null
6 ✔
163
        this.inboxDispatcher = new AtomicReference<>();
5 ✔
164
        this.inboxDispatcherLock = new ReentrantLock();
5 ✔
165
        this.pongQueue = new ConcurrentLinkedDeque<>();
5 ✔
166
        this.draining = new AtomicReference<>();
5 ✔
167
        this.blockPublishForDrain = new AtomicBoolean();
5 ✔
168
        this.tryingToConnect = new AtomicBoolean();
5 ✔
169

170
        options.incrementExecutorUse();
2 ✔
171
        this.executor = options.getExecutor();
4 ✔
172
        this.callbackExecutor = options.getCallbackExecutor();
4 ✔
173
        this.connectExecutor = options.getConnectExecutor();
4 ✔
174
        this.readerExecutor = options.getReaderExecutor();
4 ✔
175
        this.writerExecutor = options.getWriterExecutor();
4 ✔
176
        this.scheduledExecutor = options.getScheduledExecutor();
4 ✔
177

178
        this.reader = new NatsConnectionReader(this);
6 ✔
179
        this.writer = new NatsConnectionWriter(this);
6 ✔
180

181
        this.needPing = new AtomicBoolean(true);
6 ✔
182

183
        serverPool = options.getServerPool() == null ? new NatsServerPool() : options.getServerPool();
11 ✔
184
        serverPool.initialize(options);
4 ✔
185
        dispatcherFactory = options.getDispatcherFactory() == null ? new DispatcherFactory() : options.getDispatcherFactory();
9 !
186

187
        switch (options.subjectValidationType()) {
6 ✔
188
            case None:
189
                subjectValidator = (subject, required) -> required
6 !
190
                    ? Validator.required(subject, "Subject")
4 ✔
191
                    : Validator.emptyAsNull(subject);
×
192
                replyValidator = (replyTo, required) -> Validator.emptyAsNull(replyTo);
3 ✔
193
                break;
1 ✔
194
            case Strict:
195
                subjectValidator = (subject, required) ->
3 ✔
196
                    Validator.validateSubjectTermStrict(subject, "Subject", required);
5 ✔
197
                replyValidator = Validator::validateReplyTo;
3 ✔
198
                break;
1 ✔
199
            default:
200
                subjectValidator = (subject, required) ->
3 ✔
201
                    Validator.validateSubjectTerm(subject, "Subject", required);
5 ✔
202
                replyValidator = Validator::validateReplyTo;
3 ✔
203
                break;
204
        }
205
    }
1 ✔
206

207
    /**
208
     * Set the ReadListener, replacing any listener supplied in the {@link Options Options} or set earlier.
209
     * <p>Unlike {@link #addErrorListener(ErrorListener) addErrorListener()}
210
     * and {@link #addConnectionListener(ConnectionListener) addConnectionListener()}, there can be only one... read listener.
211
     * This is intentional, not an oversight. See {@link ReadListener ReadListener}.
212
     *
213
     * @param rl the ReadListener, or null to clear the current one
214
     */
215
    public void setReadListener(@Nullable ReadListener rl) {
216
        reader.setReadListener(rl);
×
217
    }
×
218

219
    // Connect is only called after creation
220
    protected void connect(boolean reconnectOnConnect) throws InterruptedException, IOException {
221
        if (tryingToConnect.compareAndSet(false, true)) {
6 !
222
            try {
223
                connectImpl(reconnectOnConnect);
3 ✔
224
            }
225
            finally {
226
                tryingToConnect.set(false);
4 ✔
227
            }
228
        }
229
    }
1 ✔
230

231
    protected void connectImpl(boolean reconnectOnConnect) throws InterruptedException, IOException {
232
        if (options.getServers().isEmpty()) {
5 !
233
            throw new IllegalArgumentException("No servers provided in options");
×
234
        }
235

236
        lastError.set("");
4 ✔
237

238
        Set<NatsUri> failList = new HashSet<>();
4 ✔
239
        boolean keepGoing = true;
2 ✔
240
        NatsUri first = null;
2 ✔
241
        NatsUri cur;
242
        // The pool is told to drop a rejected server, and the rejection is also a fail, so a pool that keeps it counts it out.
243
        // A server it supplies again is skipped again, but reported only once.
244
        Set<NatsUri> rejected = new HashSet<>();
4 ✔
245
        while (keepGoing && (cur = serverPool.peekNextServer()) != null) {
8 ✔
246
            String rejection = rejectionReason(cur);
4 ✔
247
            if (rejection != null) {
2 !
248
                serverPool.nextServer(); // b/c we only peeked, and the pool must have handed it out before it is told
×
NEW
249
                if (rejected.add(cur)) {
×
250
                    rejectServer(cur, rejection);
×
251
                    failList.add(cur);
×
252
                }
NEW
253
                serverPool.connectFailed(cur); // also a fail, so a pool that keeps it counts it out
×
NEW
254
                continue; // before first is set: the pool drops it, so it must not be the server that ends the pass
×
255
            }
256
            if (first == null) {
2 !
257
                first = cur;
3 ✔
258
            }
259
            else if (cur.equals(first)) {
×
260
                break;  // connect only goes through loop once
×
261
            }
262
            serverPool.nextServer(); // b/c we only peeked.
4 ✔
263

264
            // let server pool resolve hostnames, then loop through resolved
265
            List<NatsUri> resolvedList = resolveHost(cur);
4 ✔
266
            for (NatsUri resolved : resolvedList) {
10 ✔
267
                if (isClosed()) {
3 !
268
                    keepGoing = false;
×
269
                    break;
×
270
                }
271
                connectError.set(""); // new on each attempt
4 ✔
272

273
                updateStatus(CONNECTING, resolved, cur);
5 ✔
274

275
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
5 ✔
276

277
                if (isConnected()) {
3 ✔
278
                    serverPool.connectSucceeded(cur);
4 ✔
279
                    keepGoing = false;
2 ✔
280
                    break;
1 ✔
281
                }
282

283
                updateStatus(DISCONNECTED, resolved, cur);
5 ✔
284

285
                String err = connectError.get();
5 ✔
286

287
                if (isAuthenticationError(err)) {
4 !
288
                    serverAuthErrors.put(resolved, err);
×
289
                }
290
            }
1 ✔
291

292
            if (!isConnected() && !isClosed()) {
6 !
293
                failList.add(cur);
4 ✔
294
                serverPool.connectFailed(cur);
4 ✔
295
            }
296
        }
1 ✔
297

298
        if (!isConnected() && !isClosed()) {
6 !
299
            if (reconnectOnConnect) {
2 !
300
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
×
301
            }
302
            else {
303
                close(true, false);
4 ✔
304

305
                String err = connectError.get();
5 ✔
306
                if (isAuthenticationError(err)) {
4 !
307
                    throw new AuthenticationException("Authentication error connecting to NATS server: " + err);
×
308
                }
309
                throw new IOException("Unable to connect to NATS servers: " + failList);
7 ✔
310
            }
311
        }
312
    }
1 ✔
313

314
    /**
315
     * Forces reconnect behavior. Stops the current connection including the reading and writing,
316
     * copies already queued outgoing messages, and then begins the reconnect logic.
317
     * Does not flush. Does not force close the connection. See {@link ForceReconnectOptions}.
318
     * @throws IOException the forceReconnect fails
319
     * @throws InterruptedException the connection is not connected
320
     */
321
    public void forceReconnect() throws IOException, InterruptedException {
322
        forceReconnect(ForceReconnectOptions.DEFAULT_INSTANCE);
3 ✔
323
    }
1 ✔
324

325
    /**
326
     * Forces reconnect behavior. Stops the current connection including the reading and writing,
327
     * copies already queued outgoing messages, and then begins the reconnect logic.
328
     * If options are not provided, the default options are used meaning Does not flush and Does not force close the connection.
329
     * See {@link ForceReconnectOptions}.
330
     * @param options options for how the forceReconnect works.
331
     * @throws IOException the forceReconnect fails
332
     * @throws InterruptedException the connection is not connected
333
     */
334
    public void forceReconnect(ForceReconnectOptions options) throws IOException, InterruptedException {
335
        if (tryingToConnect.compareAndSet(false, true)) {
×
336
            try {
337
                forceReconnectImpl(options == null ? ForceReconnectOptions.DEFAULT_INSTANCE : options);
×
338
            }
339
            finally {
340
                tryingToConnect.set(false);
×
341
            }
342
        }
343
    }
×
344

345
    protected void forceReconnectImpl(@NonNull ForceReconnectOptions frOpts) throws InterruptedException {
346
        if (frOpts.isFlush()) {
×
347
            try {
348
                flush(frOpts.getFlushWait());
×
349
            }
350
            catch (TimeoutException e) {
×
351
                // Ignored. Manual test demonstrates that if the connection is dropped
352
                // in the middle of the flush, the most likely reason for a TimeoutException,
353
                // the socket is closed.
354
            }
×
355
        }
356

357
        closeSocketLock.lock();
×
358
        try {
359
            updateStatus(DISCONNECTED);
×
360

361
            // Close and reset the current data port and future
362
            if (dataPortFuture != null) {
×
363
                dataPortFuture.cancel(true);
×
364
                dataPortFuture = null;
×
365
            }
366

367
            // Stop i/o BEFORE closing the port. stop(false) clears the reader's running flag without
368
            // shutting down input, so when the close below unblocks the pending read, the reader sees the
369
            // resulting IOException as an expected shutdown rather than a communication issue. Both stop
370
            // methods return a future completed when the thread actually exits; both are initialized to an
371
            // already-completed future at construction, so joining one that never started returns at once.
372
            Future<Boolean> readerStopped = reader.stop(false);
×
373
            Future<Boolean> writerStopped = writer.stop();
×
374

375
            // close the data port as a task so as not to block reconnecting
376
            if (dataPort != null) {
×
377
                final DataPort dataPortToClose = dataPort;
×
378
                dataPort = null;
×
379
                executor.submit(() -> {
×
380
                    try {
381
                        if (frOpts.isForceClose()) {
×
382
                            dataPortToClose.forceClose();
×
383
                        }
384
                        else {
385
                            dataPortToClose.close();
×
386
                        }
387
                    }
388
                    catch (IOException ignore) {
×
389
                        // ignored since running as a task and nothing we can do.
390
                    }
×
391
                });
×
392
            }
393

394
            // Join the i/o threads with the full connection timeout, not a token wait. These reader and
395
            // writer instances are reused by the reconnect, so a thread that outlives this point can still
396
            // fire handleCommunicationIssue on the healthy connection and stomp the shared running flag.
397
            // The close above normally lands sub-millisecond, so these return immediately in the common case.
398
            long timeoutMillis = options.getConnectionTimeout();
×
399
            try {
400
                readerStopped.get(timeoutMillis, TimeUnit.MILLISECONDS);
×
401
            }
402
            catch (Exception ex) {
×
403
                processException(ex);
×
404
            }
×
405
            try {
406
                writerStopped.get(timeoutMillis, TimeUnit.MILLISECONDS);
×
407
            }
408
            catch (Exception ex) {
×
409
                processException(ex);
×
410
            }
×
411
        }
412
        finally {
413
            closeSocketLock.unlock();
×
414
        }
415

416
        reconnectImpl();
×
417
    }
×
418

419
    protected void reconnect() throws InterruptedException {
420
        if (tryingToConnect.compareAndSet(false, true)) {
×
421
            try {
422
                reconnectImpl();
×
423
            }
424
            finally {
425
                tryingToConnect.set(false);
×
426
            }
427
        }
428
    }
×
429

430
    // Reconnect can only be called when the connection is disconnected
431
    protected void reconnectImpl() throws InterruptedException {
432
        if (isClosed()) {
3 !
433
            return;
×
434
        }
435

436
        if (options.getMaxReconnects() == 0) {
4 !
437
            close(true, false);
×
438
            return;
×
439
        }
440

441
        writer.enterReconnectMode();
3 ✔
442

443
        if (!isConnected() && !isClosed() && !isClosing()) {
9 !
444
            reconnectImplConnect();
2 ✔
445
        }
446

447
        if (!isConnected()) {
3 ✔
448
            close(true, false);
4 ✔
449
            return;
1 ✔
450
        }
451

452
        subscriptions.forEach((sid, subscription) -> {
5 ✔
453
            if (subscription.dispatcher == null && !subscription.sub.isDraining()) {
3 !
454
                sendSubscriptionMessage(sid, subscription.sub.getSubject(), subscription.sub.getQueueName(), true);
×
455
            }
456
        });
1 ✔
457

458
        dispatchers.forEach(d -> {
4 ✔
459
            if (!d.isDraining()) {
3 !
460
                d.resendSubscriptions();
2 ✔
461
            }
462
        });
1 ✔
463

464
        writer.enterWaitingForEndReconnectMode();
3 ✔
465

466
        processConnectionEvent(ConnectionEvent.RESUBSCRIBED, uriDetail(currentServer));
7 ✔
467
    }
1 ✔
468

469
    protected void reconnectImplConnect() {
470
        long round = 0;
2 ✔
471
        NatsUri first = null;
2 ✔
472
        NatsUri cur;
473
        // see connect: a rejected server is skipped and failed every time it is supplied, but reported only once
474
        Set<NatsUri> rejected = new HashSet<>();
4 ✔
475
        while ((cur = serverPool.nextServer()) != null) {
6 ✔
476
            String rejection = rejectionReason(cur);
4 ✔
477
            if (rejection != null) {
2 !
NEW
478
                if (rejected.add(cur)) {
×
479
                    rejectServer(cur, rejection);
×
480
                }
NEW
481
                serverPool.connectFailed(cur); // also a fail, so a pool that keeps it counts it out
×
NEW
482
                continue; // before first is set and before the delay: the pool drops it, so it must not be the server that ends the pass
×
483
            }
484
            if (first == null) {
2 ✔
485
                first = cur;
2 ✔
486
                ++round;   // round becomes 1
4 ✔
487
                // Consume the lame duck signal here whatever the behavior does with it, otherwise a
488
                // signal received under a behavior that ignores it survives into a later campaign.
489
                boolean lameDuck = lameDuckTriggered;
3 ✔
490
                lameDuckTriggered = false;
3 ✔
491
                if (delayBeforeFirstRound(lameDuck)) {
4 !
492
                    invokeReconnectDelayHandler(round, lameDuck);
×
493
                }
494
            }
1 ✔
495
            else if (first.equals(cur)) {
4 !
496
                // went around the pool an entire time
497
                invokeReconnectDelayHandler(++round, false);
8 ✔
498
            }
499

500
            // let server list provider resolve hostnames
501
            // then loop through resolved
502
            List<NatsUri> resolvedList = resolveHost(cur);
4 ✔
503
            for (NatsUri resolved : resolvedList) {
10 ✔
504
                if (isClosed()) {
3 !
505
                    return;
×
506
                }
507
                connectError.set(""); // reset on each loop
4 ✔
508
                if (isDisconnectingOrClosed() || isClosing()) {
6 !
509
                    return;
×
510
                }
511
                updateStatus(RECONNECTING, resolved, cur);
5 ✔
512

513
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
5 ✔
514

515
                if (isConnected()) {
3 ✔
516
                    serverPool.connectSucceeded(cur);
4 ✔
517
                    statistics.incrementReconnects();
3 ✔
518
                    return;
1 ✔
519
                }
520

521
                String err = connectError.get();
5 ✔
522
                if (isAuthenticationError(err)) {
4 !
523
                    if (err.equals(serverAuthErrors.get(resolved))) {
×
524
                        return; // double auth error
×
525
                    }
526
                    serverAuthErrors.put(resolved, err);
×
527
                }
528
            }
1 ✔
529

530
            if (!isConnected() && !isClosed()) {
6 !
531
                serverPool.connectFailed(cur);
4 ✔
532
            }
533
        }
1 ✔
534
    }
1 ✔
535

536
    // A server the connection will never attempt because it cannot be used with the options: a ws:// server is
537
    // a plain websocket that is never upgraded to TLS, so with an SSL context it would be reached without TLS.
538
    // The connect and reconnect loops of both implementations ask this when the pool hands a server out, before
539
    // anything is sent to it. The bootstrap list cannot hold such a server, so one comes from connect_urls or
540
    // from a custom ServerPool.
541
    protected String rejectionReason(NatsUri nuri) {
542
        if (options.isTLSRequired() && WEBSOCKET_PROTOCOL.equals(nuri.getScheme())) {
9 ✔
543
            return "the options require TLS and a ws:// server is never upgraded to TLS";
2 ✔
544
        }
545
        return null;
2 ✔
546
    }
547

548
    // The pool has already handed the server out. Tell it so it stops supplying the server, and report it.
549
    protected void rejectServer(NatsUri nuri, String reason) {
550
        serverPool.serverRejected(nuri);
4 ✔
551
        notifyErrorListener((c, el) -> el.serverRejected(c, nuri.toString(), reason));
12 ✔
552
    }
1 ✔
553

554
    protected long timeCheck(long endNanos) throws TimeoutException {
555
        long remainingNanos = endNanos - NatsSystemClock.nanoTime();
4 ✔
556
        if (remainingNanos < 0) {
4 !
557
            throw new TimeoutException("connection timed out");
×
558
        }
559
        return remainingNanos;
2 ✔
560
    }
561

562
    // is called from reconnect and connect
563
    // will wait for any previous attempt to complete, using the reader.stop and
564
    // writer.stop
565
    protected void tryToConnect(NatsUri cur, NatsUri resolved, long nowNanos) {
566
        clearCurrentServer();
2 ✔
567
        connectingServer = cur; // the origin of discovered servers in the initial INFO, before currentServer is set
3 ✔
568

569
        try {
570
            long end = nowNanos + (options.getConnectionTimeout() * NANOS_PER_MILLI);
8 ✔
571
            timeCheck(end);
4 ✔
572

573
            statusLock.lock();
3 ✔
574
            try {
575
                if (connecting) {
3 !
576
                    return;
×
577
                }
578
                this.connecting = true;
3 ✔
579
                statusChanged.signalAll();
3 ✔
580
            }
581
            finally {
582
                statusLock.unlock();
3 ✔
583
            }
584

585
            // Create a new future for the DataPort, the reader/writer will use this
586
            // to wait for the connect/failure.
587
            this.dataPortFuture = new CompletableFuture<>();
5 ✔
588

589
            // Make sure the reader and writer are stopped
590
            long timeLeftNanos = timeCheck(end);
4 ✔
591
            if (reader.isRunning()) {
4 !
592
                reader.stop().get(timeLeftNanos, TimeUnit.NANOSECONDS);
×
593
            }
594
            timeLeftNanos = timeCheck(end);
4 ✔
595
            if (writer.isRunning()) {
4 !
596
                writer.stop().get(timeLeftNanos, TimeUnit.NANOSECONDS);
×
597
            }
598

599
            timeCheck(end);
4 ✔
600
            cleanUpPongQueue();
2 ✔
601

602
            timeLeftNanos = timeCheck(end);
4 ✔
603
            DataPort newDataPort = options.createDataPort();
4 ✔
604
            newDataPort.connect(this, resolved, cur, timeLeftNanos);
6 ✔
605

606
            // Notify any threads waiting on the sockets
607
            this.dataPort = newDataPort;
3 ✔
608
            dataPortFuture.complete(dataPort);
6 ✔
609

610
            // Wait for the INFO message manually.
611
            // All other traffic will use the reader and writer
612
            // TLS First, don't read info until after upgrade
613
            // ---
614
            // Also this task does not have any exception catching
615
            // Since it is submitted as an async task, the future
616
            // will be aware of any exception thrown, and the future.get()
617
            // will throw an ExecutionException which is handled futher down
618
            Callable<Object> connectTask = () -> {
4 ✔
619
                if (!options.isTlsFirst()) {
4 ✔
620
                    readInitialInfo();
2 ✔
621
                    checkVersionRequirements();
2 ✔
622
                }
623
                upgradeToSecureIfNeeded(resolved);
3 ✔
624
                if (options.isTlsFirst()) {
4 ✔
625
                    readInitialInfo();
2 ✔
626
                    checkVersionRequirements();
2 ✔
627
                }
628
                return null;
2 ✔
629
            };
630

631
            timeLeftNanos = timeCheck(end);
4 ✔
632
            Future<Object> future = connectExecutor.submit(connectTask);
5 ✔
633
            try {
634
                future.get(timeLeftNanos, TimeUnit.NANOSECONDS);
5 ✔
635
            }
636
            finally {
637
                future.cancel(true);
4 ✔
638
            }
639

640
            // start the reader and writer after we secured the connection, if necessary
641
            timeCheck(end);
4 ✔
642
            reader.start(dataPortFuture);
5 ✔
643
            timeCheck(end);
4 ✔
644
            writer.start(dataPortFuture);
5 ✔
645

646
            timeCheck(end);
4 ✔
647
            sendConnect(resolved);
3 ✔
648

649
            timeLeftNanos = timeCheck(end);
4 ✔
650
            Future<Boolean> pongFuture = sendPing();
3 ✔
651

652
            if (pongFuture != null) {
2 !
653
                pongFuture.get(timeLeftNanos, TimeUnit.NANOSECONDS);
5 ✔
654
            }
655

656
            if (pingTask == null) {
3 ✔
657
                timeCheck(end);
4 ✔
658
                long pingMillis = options.getPingInterval();
4 ✔
659
                if (pingMillis > 0) {
4 !
660
                    pingTask = new ScheduledTask(scheduledExecutor, pingMillis, () -> {
10 ✔
661
                        if (isConnected() && !isClosing()) {
3 !
662
                            try {
663
                                softPing(); // The timer always uses the standard queue
×
664
                            }
665
                            catch (Exception e) {
×
666
                                // it's running in a thread, there is no point throwing here
667
                            }
×
668
                        }
669
                    });
1 ✔
670
                }
671

672
                long cleanMillis = options.getRequestCleanupInterval();
4 ✔
673
                if (cleanMillis > 0) {
4 !
674
                    cleanupTask = new ScheduledTask(scheduledExecutor, cleanMillis, () -> cleanResponses(false));
14 ✔
675
                }
676
            }
677

678
            // Set connected status
679
            timeCheck(end);
4 ✔
680
            statusLock.lock();
3 ✔
681
            try {
682
                this.connecting = false;
3 ✔
683

684
                if (exceptionDuringConnectChange != null) {
3 !
685
                    throw exceptionDuringConnectChange;
×
686
                }
687

688
                this.currentServer = cur;
3 ✔
689
                serverAuthErrors.clear(); // reset on successful connection
3 ✔
690
                updateStatus(CONNECTED); // will signal status change, we also signal in finally
3 ✔
691
            }
692
            finally {
693
                statusLock.unlock();
3 ✔
694
            }
695
        }
696
        catch (Exception exp) {
1 ✔
697
            processException(exp);
3 ✔
698
            try {
699
                // allow force reconnect since this is pretty exceptional,
700
                // a connection failure while trying to connect
701
                closeSocket(false, true);
4 ✔
702
            }
703
            catch (InterruptedException e) {
×
704
                processException(e);
×
705
                Thread.currentThread().interrupt();
×
706
            }
1 ✔
707
        }
708
        finally {
709
            statusLock.lock();
3 ✔
710
            try {
711
                this.connecting = false;
3 ✔
712
                statusChanged.signalAll();
3 ✔
713
            }
714
            finally {
715
                statusLock.unlock();
3 ✔
716
            }
717
        }
718
    }
1 ✔
719

720
    protected void clearCurrentServer() {
721
        if (currentServer != null) {
3 ✔
722
            lastServer = currentServer;
4 ✔
723
        }
724
        currentServer = null;
3 ✔
725
    }
1 ✔
726

727
    protected void checkVersionRequirements() throws IOException {
728
        Options opts = getOptions();
3 ✔
729
        ServerInfo info = getServerInfo();
3 ✔
730

731
        if (opts.isNoEcho() && info.getProtocolVersion() < 1) {
7 ✔
732
            throw new IOException("Server does not support no echo.");
5 ✔
733
        }
734
    }
1 ✔
735

736
    protected void upgradeToSecureIfNeeded(NatsUri nuri) throws IOException {
737
        // When already communicating over "https" websocket, do NOT try to upgrade to secure.
738
        if (!nuri.isWebsocket()) {
3 ✔
739
            if (options.isTlsFirst()) {
4 ✔
740
                dataPort.upgradeToSecure();
4 ✔
741
            }
742
            else {
743
                // server    | client options      | result
744
                // --------- | ------------------- | --------
745
                // required  | not isTLSRequired() | mismatch
746
                // available | not isTLSRequired() | ok
747
                // neither   | not isTLSRequired() | ok
748
                // required  | isTLSRequired()     | ok
749
                // available | isTLSRequired()     | ok
750
                // neither   | isTLSRequired()     | mismatch
751
                ServerInfo serverInfo = getServerInfo();
3 ✔
752
                if (options.isTLSRequired()) {
4 ✔
753
                    if (!serverInfo.isTLSRequired() && !serverInfo.isTLSAvailable()) {
6 ✔
754
                        throw new IOException("SSL connection wanted by client.");
5 ✔
755
                    }
756
                    dataPort.upgradeToSecure();
4 ✔
757
                }
758
                else if (serverInfo.isTLSRequired()) {
3 ✔
759
                    throw new IOException("SSL required by server.");
5 ✔
760
                }
761
            }
762
        }
763
    }
1 ✔
764

765
    /**
766
     * Called by a reader that hit an I/O error. The reader passes itself so an implementation that uses
767
     * more than one reader over the connection's life can ignore a report from one that is no longer current.
768
     * @param source the reader reporting
769
     * @param io the error
770
     */
771
    protected void handleCommunicationIssue(NatsConnectionReader source, Exception io) {
772
        handleCommunicationIssue(io);
3 ✔
773
    }
1 ✔
774

775
    // Called from reader/writer thread
776
    protected void handleCommunicationIssue(Exception io) {
777
        // If we are connecting or disconnecting, note exception and leave
778
        statusLock.lock();
3 ✔
779
        try {
780
            if (connecting || disconnecting || status == CLOSED || isDraining()) {
13 !
781
                this.exceptionDuringConnectChange = io;
×
782
                return;
×
783
            }
784
        }
785
        finally {
786
            statusLock.unlock();
3 ✔
787
        }
788

789
        processException(io);
3 ✔
790
        if (currentServer != null) {
3 !
791
            serverPool.connectFailed(currentServer);
5 ✔
792
        }
793

794
        // Spawn a thread so we don't have timing issues with
795
        // waiting on read/write threads
796
        executor.submit(() -> {
6 ✔
797
            if (tryingToConnect.compareAndSet(false, true)) {
6 !
798
                try {
799
                    // any issue that brings us here is pretty serious
800
                    // so we are comfortable forcing the close
801
                    closeSocket(true, true);
4 ✔
802
                }
803
                catch (InterruptedException e) {
×
804
                    processException(e);
×
805
                    Thread.currentThread().interrupt();
×
806
                }
807
                finally {
808
                    tryingToConnect.set(false);
4 ✔
809
                }
810
            }
811
        });
1 ✔
812
    }
1 ✔
813

814
    // Close socket is called when another connect attempt is possible
815
    // Close is called when the connection should shut down, period
816
    protected void closeSocket(boolean tryReconnectIfConnected, boolean forceClose) throws InterruptedException {
817
        // Ensure we close the socket exclusively within one thread.
818
        closeSocketLock.lock();
3 ✔
819
        try {
820
            boolean wasConnected;
821
            statusLock.lock();
3 ✔
822
            try {
823
                if (isDisconnectingOrClosed()) {
3 !
824
                    waitForDisconnectOrClose(options.getConnectionTimeout());
×
825
                    return;
×
826
                }
827
                this.disconnecting = true;
3 ✔
828
                this.exceptionDuringConnectChange = null;
3 ✔
829
                wasConnected = (status == CONNECTED);
8 ✔
830

831
                // Update the status before tearing the socket down, not after. closeSocketImpl
832
                // clears the current server as its first act and can then block for as long as
833
                // the reader and writer stop timeouts allow, so updating afterwards leaves a
834
                // window where the connection reports CONNECTED with a null connected url.
835
                // This is also the order forceReconnectImpl already uses.
836
                updateStatus(DISCONNECTED);
3 ✔
837

838
                statusChanged.signalAll();
3 ✔
839
            }
840
            finally {
841
                statusLock.unlock();
3 ✔
842
            }
843

844
            closeSocketImpl(forceClose);
3 ✔
845

846
            statusLock.lock();
3 ✔
847
            try {
848
                this.exceptionDuringConnectChange = null; // Ignore IOExceptions during closeSocketImpl()
3 ✔
849
                this.disconnecting = false;
3 ✔
850
                statusChanged.signalAll();
3 ✔
851
            }
852
            finally {
853
                statusLock.unlock();
3 ✔
854
            }
855

856
            if (isClosing()) { // isClosing() means we are in the close method or were asked to be
3 !
857
                close(true, false);
×
858
            }
859
            else if (wasConnected && tryReconnectIfConnected) {
4 !
860
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
2 ✔
861
            }
862
        }
863
        finally {
864
            closeSocketLock.unlock();
3 ✔
865
        }
866
    }
1 ✔
867

868
    // Close socket is called when another connect attempt is possible
869
    // Close is called when the connection should shut down, period
870

871
    /**
872
     * Close the connection and release all blocking calls like {@link #flush flush}
873
     * and {@code Subscription.nextMessage}.
874
     * If close() is called after {@link #drain(long) drain} it will wait up to the connection timeout
875
     * to return, but it will not initiate a close. The drain takes precedence and will initiate the close.
876
     * <p>If the calling thread is interrupted while close is waiting, the close stops waiting,
877
     * the thread's interrupt status is restored, and the method returns.
878
     */
879
    @Override
880
    public void close() {
881
        try {
882
            close(true, false);
4 ✔
883
        }
884
        catch (InterruptedException e) {
×
885
            Thread.currentThread().interrupt(); // restore interrupt status; do not propagate from close()
×
886
        }
1 ✔
887
    }
1 ✔
888

889
    // Several paths reach this (public close(), connect/reconnect failure, closeSocket, drain, final flush),
890
    // but every caller passes forceClose=false, hence the SameParameterValue suppression.
891
    @SuppressWarnings("SameParameterValue")
892
    protected void close(boolean checkDrainStatus, boolean forceClose) throws InterruptedException {
893
        statusLock.lock();
3 ✔
894
        try {
895
            if (checkDrainStatus && isDraining()) {
5 !
896
                waitForDisconnectOrClose(options.getConnectionTimeout());
×
897
                return;
×
898
            }
899

900
            this.closing = true;// We were asked to close, so do it
3 ✔
901
            if (isDisconnectingOrClosed()) {
3 !
902
                waitForDisconnectOrClose(options.getConnectionTimeout());
×
903
                return;
×
904
            }
905
            else {
906
                this.disconnecting = true;
3 ✔
907
                this.exceptionDuringConnectChange = null;
3 ✔
908
                statusChanged.signalAll();
3 ✔
909
            }
910
        }
911
        finally {
912
            statusLock.unlock();
3 ✔
913
        }
914

915
        // Stop the reconnect wait timer after we stop the writer/reader (only if we are
916
        // really closing, not on errors)
917
        if (reconnectWaiter != null) {
3 !
918
            reconnectWaiter.cancel(true);
5 ✔
919
        }
920

921
        closeSocketImpl(forceClose);
3 ✔
922

923
        dispatchers.forEach(d -> d.stop(false));
8 ✔
924

925
        subscriptions.forEach((sid, subscription) -> subscription.sub.invalidate());
8 ✔
926

927
        dispatchers.clear();
3 ✔
928
        subscriptions.clear();
3 ✔
929

930
        if (pingTask != null) {
3 ✔
931
            pingTask.shutdown();
3 ✔
932
            pingTask = null;
3 ✔
933
        }
934
        if (cleanupTask != null) {
3 ✔
935
            cleanupTask.shutdown();
3 ✔
936
            cleanupTask = null;
3 ✔
937
        }
938

939
        cleanResponses(true);
3 ✔
940

941
        cleanUpPongQueue();
2 ✔
942

943
        statusLock.lock();
3 ✔
944
        try {
945
            updateStatus(CLOSED); // will signal, we also signal when we stop disconnecting
3 ✔
946

947
            /*
948
             * if (exceptionDuringConnectChange != null) {
949
             * processException(exceptionDuringConnectChange); exceptionDuringConnectChange
950
             * = null; }
951
             */
952
        }
953
        finally {
954
            statusLock.unlock();
3 ✔
955
        }
956

957
        callbackExecutor = null;
3 ✔
958
        executor = null;
3 ✔
959
        connectExecutor = null;
3 ✔
960
        readerExecutor = null;
3 ✔
961
        writerExecutor = null;
3 ✔
962
        scheduledExecutor = null;
3 ✔
963
        options.shutdownExecutors();
3 ✔
964

965
        statusLock.lock();
3 ✔
966
        try {
967
            this.disconnecting = false;
3 ✔
968
            statusChanged.signalAll();
3 ✔
969
        }
970
        finally {
971
            statusLock.unlock();
3 ✔
972
        }
973
    }
1 ✔
974

975
    // these *ExecutorIsClosed() are only used for tests
976
    protected boolean callbackExecutorIsClosed() { return callbackExecutor == null; }
7 ✔
977
    protected boolean executorIsClosed() { return executor == null; }
7 ✔
978
    protected boolean connectExecutorIsClosed() { return connectExecutor == null; }
7 ✔
979
    protected boolean readerExecutorIsClosed() { return readerExecutor == null; }
7 ✔
980
    protected boolean writerExecutorIsClosed() { return writerExecutor == null; }
7 ✔
981
    protected boolean scheduledExecutorIsClosed() { return scheduledExecutor == null; }
7 ✔
982

983
    // Should only be called from closeSocket or close
984
    protected void closeSocketImpl(boolean forceClose) {
985
        clearCurrentServer();
2 ✔
986

987
        // Signal both to stop.
988
        final Future<Boolean> readStop = reader.stop();
4 ✔
989
        final Future<Boolean> writeStop = writer.stop();
4 ✔
990

991
        // Now wait until they both stop before closing the socket.
992
        try {
993
            readStop.get(1, TimeUnit.SECONDS);
5 ✔
994
        }
995
        catch (Exception ex) {
1 ✔
996
            //
997
        }
1 ✔
998
        try {
999
            writeStop.get(1, TimeUnit.SECONDS);
5 ✔
1000
        }
1001
        catch (Exception ex) {
×
1002
            //
1003
        }
1 ✔
1004

1005
        // Close and reset the current data port and future
1006
        if (dataPortFuture != null) {
3 ✔
1007
            dataPortFuture.cancel(true);
5 ✔
1008
            dataPortFuture = null;
3 ✔
1009
        }
1010

1011
        // Close the current socket and cancel anyone waiting for it
1012
        try {
1013
            if (dataPort != null) {
3 ✔
1014
                if (forceClose) {
2 ✔
1015
                    dataPort.forceClose();
4 ✔
1016
                }
1017
                else {
1018
                    dataPort.close();
3 ✔
1019
                }
1020
            }
1021

1022
        }
1023
        catch (IOException ex) {
×
1024
            processException(ex);
×
1025
        }
1 ✔
1026
        cleanUpPongQueue();
2 ✔
1027

1028
        try {
1029
            reader.stop().get(10, TimeUnit.SECONDS);
7 ✔
1030
        }
1031
        catch (Exception ex) {
×
1032
            processException(ex);
×
1033
        }
1 ✔
1034
        try {
1035
            writer.stop().get(10, TimeUnit.SECONDS);
7 ✔
1036
        }
1037
        catch (Exception ex) {
×
1038
            processException(ex);
×
1039
        }
1 ✔
1040
    }
1 ✔
1041

1042
    protected void cleanUpPongQueue() {
1043
        Future<Boolean> b;
1044
        while ((b = pongQueue.poll()) != null) {
7 ✔
1045
            b.cancel(true);
5 ✔
1046
        }
1047
    }
1 ✔
1048

1049
    /**
1050
     * Send a message to the specified subject.
1051
     * @param subject the subject to send the message to
1052
     * @param data the message data
1053
     * @throws IllegalArgumentException if the subject is invalid
1054
     */
1055
    public void publish(@NonNull String subject, byte @Nullable [] data) {
1056
        publish(subject, null, null, data, false);
7 ✔
1057
    }
1 ✔
1058

1059
    /**
1060
     * Send a message to the specified subject. The message data <strong>will
1061
     * not</strong> be copied. The expected usage with string content is something
1062
     * like:
1063
     *
1064
     * <pre>
1065
     * nc = Nats.connect()
1066
     * Headers h = new Headers().put("key", "value");
1067
     * nc.publish("destination", h, "message".getBytes("UTF-8"))
1068
     * </pre>
1069
     *
1070
     * where the sender creates a byte array immediately before calling publish.
1071
     * See {@link #publish(String, String, byte[]) publish()} for more details on
1072
     * publish during reconnect.
1073
     *
1074
     * @param subject the subject to send the message to
1075
     * @param headers Optional headers to publish with the message.
1076
     * @param data the message data
1077
     * @throws IllegalStateException if the reconnect buffer is exceeded
1078
     * @throws IllegalArgumentException if the subject is invalid, or the headers are not supported by the connected server
1079
     */
1080
    public void publish(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data) {
1081
        publish(subject, null, headers, data, false);
×
1082
    }
×
1083

1084
    /**
1085
     * Send a request to the specified subject, providing a replyTo subject. The
1086
     * message data <strong>will not</strong> be copied. The expected usage with
1087
     * string content is something like:
1088
     *
1089
     * <pre>
1090
     * nc = Nats.connect()
1091
     * nc.publish("destination", "reply-to", "message".getBytes("UTF-8"))
1092
     * </pre>
1093
     *
1094
     * where the sender creates a byte array immediately before calling publish.
1095
     * <p>
1096
     * During reconnect the client will try to buffer messages. The buffer size is set
1097
     * in the connect options, see {@link OptionsBuilder#reconnectBufferSize(long) reconnectBufferSize()}
1098
     * with a default value of {@link OptionsConstants#DEFAULT_RECONNECT_BUF_SIZE 8 * 1024 * 1024} bytes.
1099
     * If the buffer is exceeded an IllegalStateException is thrown. Applications should use
1100
     * this exception as a signal to wait for reconnect before continuing.
1101
     * </p>
1102
     * @param subject the subject to send the message to
1103
     * @param replyTo the subject the receiver should send any response to
1104
     * @param data the message data
1105
     * @throws IllegalStateException if the reconnect buffer is exceeded
1106
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid
1107
     */
1108
    public void publish(@NonNull String subject, @Nullable String replyTo, byte @Nullable [] data) {
1109
        publish(subject, replyTo, null, data, false);
×
1110
    }
×
1111

1112
    /**
1113
     * Send a message to the specified subject.
1114
     * @param subject the subject to send the message to
1115
     * @param replyTo the subject the receiver should send any response to
1116
     * @param headers Optional headers to publish with the message.
1117
     * @param data the message data
1118
     * @throws IllegalStateException if the reconnect buffer is exceeded
1119
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid, or the headers are not supported by the connected server
1120
     */
1121
    public void publish(@NonNull String subject, @Nullable String replyTo, @Nullable Headers headers, byte @Nullable [] data) {
1122
        subject = subjectValidate(subject);
×
1123
        replyTo = replyValidate(replyTo);
×
1124
        validateNotClosed();
×
1125
        _publish(new InternalPublishableMessage(data, subject, replyTo, headers, false));
×
1126
    }
×
1127

1128
    /**
1129
     * Send a message to the specified subject.
1130
     * @param subject the subject to send the message to
1131
     * @param replyTo the subject the receiver should send any response to
1132
     * @param headers Optional headers to publish with the message.
1133
     * @param data the message data
1134
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing
1135
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid, or the headers are not supported by the connected server
1136
     */
1137
    public void publish(@NonNull String subject, @Nullable String replyTo, @Nullable Headers headers, byte @Nullable [] data, boolean flushImmediatelyAfterPublish) {
1138
        subject = subjectValidate(subject);
4 ✔
1139
        replyTo = replyValidate(replyTo);
4 ✔
1140
        validateNotClosed();
2 ✔
1141
        _publish(new InternalPublishableMessage(data, subject, replyTo, headers, flushImmediatelyAfterPublish));
10 ✔
1142
    }
1 ✔
1143

1144
    /**
1145
     * Send a message.
1146
     * @param message the message to send
1147
     * @throws IllegalArgumentException if the message is null, the subject or reply subject is invalid, or the headers are not supported by the connected server
1148
     */
1149
    public void publish(@NonNull NatsMessage message) {
1150
        Validator.validateNotNull(message, "Message");
×
1151
        subjectValidate(message.getSubject());
×
1152
        replyValidate(message.getReplyTo());
×
1153
        validateNotClosed();
×
1154
        _publish(new InternalPublishableMessage(message, false));
×
1155
    }
×
1156

1157
    /**
1158
     * Send a message.
1159
     * @param message the message to send
1160
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing
1161
     * @throws IllegalArgumentException if the message is null, the subject or reply subject is invalid, or the headers are not supported by the connected server
1162
     */
1163
    public void publish(@NonNull NatsMessage message, boolean flushImmediatelyAfterPublish) {
1164
        Validator.validateNotNull(message, "Message");
×
1165
        subjectValidate(message.getSubject());
×
1166
        replyValidate(message.getReplyTo());
×
1167
        validateNotClosed();
×
1168
        _publish(new InternalPublishableMessage(message, flushImmediatelyAfterPublish));
×
1169
    }
×
1170

1171
    // MUST CALL validateNotClosed(); BEFORE CALLING THIS
1172
    private void _publish(InternalPublishableMessage ipm) {
1173
        if (ipm.hasHeaders && !serverInfo.get().isHeadersSupported()) {
3 !
1174
            throw new IllegalArgumentException("Headers are not supported by the server, version: " + serverInfo.get().getVersion());
×
1175
        }
1176

1177
        if ((status == RECONNECTING || status == DISCONNECTED)
8 !
1178
            && !writer.canQueueDuringReconnect(ipm)) {
×
1179
            throw new IllegalStateException(
×
1180
                "Unable to queue any more messages during reconnect, max buffer is " + options.getReconnectBufferSize());
×
1181
        }
1182

1183
        queueOutgoing(ipm);
4 ✔
1184
    }
1 ✔
1185

1186
    private void validateNotClosed() {
1187
        if (isClosed()) {
3 !
1188
            throw new IllegalStateException("NatsConnection is Closed");
×
1189
        }
1190
        else if (blockPublishForDrain.get()) {
4 !
1191
            throw new IllegalStateException("NatsConnection is Draining"); // Ok to publish while waiting on subs
×
1192
        }
1193
    }
1 ✔
1194

1195
    /**
1196
     * Create a synchronous subscription to the specified subject.
1197
     *
1198
     * <p>Use the {@code Subscription.nextMessage}
1199
     * method to read messages for this subscription.
1200
     *
1201
     * <p>See {@link #createDispatcher(MessageHandler) createDispatcher} for
1202
     * information about creating an asynchronous subscription with callbacks.
1203
     *
1204
     * <p>This method will throw an IllegalArgumentException if the subject is invalid.
1205
     *
1206
     * @param subject the subject to subscribe to
1207
     * @return an object representing the subscription
1208
     * @throws IllegalArgumentException if the subject is invalid
1209
     */
1210
    @NonNull
1211
    public NatsSubscription subscribe(@NonNull String subject) {
1212
        subjectValidate(subject);
4 ✔
1213
        return _createSubscriptionByFactory(subject, null, null, null);
7 ✔
1214
    }
1215

1216
    /**
1217
     * Create a synchronous subscription to the specified subject and queue.
1218
     *
1219
     * <p>Use the {@code Subscription.nextMessage} method to read
1220
     * messages for this subscription.
1221
     *
1222
     * <p>See {@link #createDispatcher(MessageHandler) createDispatcher} for
1223
     * information about creating an asynchronous subscription with callbacks.
1224
     *
1225
     * <p>This method will throw an IllegalArgumentException if the subject is invalid.
1226
     *
1227
     * @param subject the subject to subscribe to
1228
     * @param queueName the queue group to join
1229
     * @return an object representing the subscription
1230
     * @throws IllegalArgumentException if the subject or the queue name is invalid
1231
     */
1232
    @NonNull
1233
    public NatsSubscription subscribe(@NonNull String subject, @NonNull String queueName) {
1234
        subjectValidate(subject);
4 ✔
1235
        Validator.validateQueueName(queueName, true);
4 ✔
1236
        return _createSubscriptionByFactory(subject, queueName, null, null);
7 ✔
1237
    }
1238

1239
    protected void invalidate(NatsSubscription sub) {
1240
        remove(sub);
3 ✔
1241
        sub.invalidate();
2 ✔
1242
    }
1 ✔
1243

1244
    protected void remove(NatsSubscription sub) {
1245
        SubscriptionInfo subscription = subscriptions.remove(sub.getSID());
7 ✔
1246

1247
        if (subscription != null && subscription.dispatcher != null) {
5 ✔
1248
            subscription.dispatcher.remove(sub);
4 ✔
1249
        }
1250
    }
1 ✔
1251

1252
    protected void unsubscribe(NatsSubscription sub, int after) {
1253
        if (isClosed()) { // last chance, usually sub will catch this
3 !
1254
            throw new IllegalStateException("NatsConnection is Closed");
×
1255
        }
1256

1257
        if (after <= 0) {
2 ✔
1258
            invalidate(sub); // Will clean it up
4 ✔
1259
        }
1260
        else {
1261
            sub.setUnsubLimit(after);
4 ✔
1262

1263
            if (sub.reachedUnsubLimit()) {
3 ✔
1264
                invalidate(sub); // takes the sid out of subscriptions, not just the sub out of service
3 ✔
1265
            }
1266
        }
1267

1268
        if (!isConnected()) {
3 !
1269
            return; // We will set up sub on reconnect or ignore
×
1270
        }
1271

1272
        sendUnsub(sub, after);
4 ✔
1273
    }
1 ✔
1274

1275
    protected void sendUnsub(@NonNull NatsSubscription sub, int after) {
1276
        ByteArrayBuilder bab =
×
1277
            new ByteArrayBuilder().append(UNSUB_SP_BYTES).append(sub.getSID());
×
1278
        if (after > 0) {
×
1279
            bab.append(SP).append(after);
×
1280
        }
1281
        queueOutgoing(new ProtocolMessage(bab, true));
×
1282
    }
×
1283

1284
    /**
1285
     * What the connection knows about one sid: the subscription, and the dispatcher delivering to it
1286
     * if there is one. The connection owns this link, so delivery, reconnect and drain never have to
1287
     * ask a subscription what it belongs to.
1288
     */
1289
    protected static final class SubscriptionInfo {
1290
        protected final NatsSubscription sub;
1291
        protected final @Nullable NatsDispatcher dispatcher;
1292

1293
        protected SubscriptionInfo(NatsSubscription sub, @Nullable NatsDispatcher dispatcher) {
2 ✔
1294
            this.sub = sub;
3 ✔
1295
            this.dispatcher = dispatcher;
3 ✔
1296
        }
1 ✔
1297
    }
1298

1299
    // Assumes the null/empty checks were handled elsewhere
1300
    @NonNull
1301
    NatsSubscription _createSubscriptionByFactory(@NonNull String subject,
1302
                                                  @Nullable String queueGroup,
1303
                                                  @Nullable NatsDispatcher dispatcher,
1304
                                                  @Nullable NatsSubscriptionFactory factory) {
1305
        if (isClosed()) {
3 ✔
1306
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1307
        }
1308
        else if (isDraining() && (dispatcher == null || dispatcher != inboxDispatcher.get())) {
10 !
1309
            throw new IllegalStateException("NatsConnection is Draining");
5 ✔
1310
        }
1311

1312
        NatsSubscription sub;
1313
        String sid = getNextSid();
3 ✔
1314

1315
        if (factory == null) {
2 ✔
1316
            sub = new NatsSubscription(sid, subject, queueGroup, this, dispatcher);
10 ✔
1317
        }
1318
        else {
1319
            sub = factory.createNatsSubscription(sid, subject, queueGroup, this, dispatcher);
8 ✔
1320
        }
1321
        subscriptions.put(sid, new SubscriptionInfo(sub, dispatcher));
10 ✔
1322

1323
        sendSubscriptionMessage(sid, subject, queueGroup, false);
6 ✔
1324
        return sub;
2 ✔
1325
    }
1326

1327
    protected String getNextSid() {
1328
        return Long.toString(nextSid.getAndIncrement());
5 ✔
1329
    }
1330

1331
    // The sid comes from the caller so that anything keyed to it - a dispatcher's handler - can be
1332
    // registered before this runs. The put stays ahead of the send for the same reason: the server
1333
    // can push as soon as it has the SUB, and deliverMessage drops a message with no entry here.
1334
    void reSubscribe(String resubSid, NatsSubscription sub, String subject, String queueGroup, @Nullable NatsDispatcher dispatcher) {
1335
        subscriptions.put(resubSid, new SubscriptionInfo(sub, dispatcher));
10 ✔
1336
        sendSubscriptionMessage(resubSid, subject, queueGroup, false);
6 ✔
1337
    }
1 ✔
1338

1339
    protected void sendSubscriptionMessage(String sid, String subject, String queueGroup, boolean treatAsInternal) {
1340
        if (!isConnected()) {
3 !
1341
            return; // We will set up sub on reconnect or ignore
×
1342
        }
1343

1344
        ByteArrayBuilder bab = new ByteArrayBuilder(UTF_8).append(SUB_SP_BYTES).append(subject);
9 ✔
1345
        if (queueGroup != null) {
2 !
1346
            bab.append(SP).append(queueGroup);
6 ✔
1347
        }
1348
        bab.append(SP).append(sid);
6 ✔
1349

1350
        // setting this to filter on stop.
1351
        // if it's an "internal" message, it won't be filtered
1352
        // if it's a normal message, the subscription will already be registered
1353
        // and therefore will be re-subscribed after a stop anyway
1354
        ProtocolMessage subMsg = new ProtocolMessage(bab, true);
6 ✔
1355
        if (treatAsInternal) {
2 ✔
1356
            queueInternalOutgoing(subMsg);
5 ✔
1357
        }
1358
        else {
1359
            queueOutgoing(subMsg);
4 ✔
1360
        }
1361
    }
1 ✔
1362

1363
    /**
1364
     * Create a new inbox subject, can be used for directed replies from
1365
     * subscribers. These are guaranteed to be unique, but can be shared and subscribed
1366
     * to by others.
1367
     * @return the inbox
1368
     */
1369
    @NonNull
1370
    public String createInbox() {
1371
        return options.getInboxPrefix() + nuid.next();
8 ✔
1372
    }
1373

1374
    protected int getRespInboxLength() {
1375
        return options.getInboxPrefix().length() + 22 + 1; // 22 for nuid, 1 for .
9 ✔
1376
    }
1377

1378
    protected String createResponseInbox(String inbox) {
1379
        // Substring gets rid of the * [trailing]
1380
        return inbox.substring(0, getRespInboxLength()) + nuid.next();
10 ✔
1381
    }
1382

1383
    // If the inbox is long enough, pull out the end part, otherwise, just use the
1384
    // full thing
1385
    protected String getResponseToken(String responseInbox) {
1386
        int len = getRespInboxLength();
3 ✔
1387
        if (responseInbox.length() <= len) {
4 !
1388
            return responseInbox;
×
1389
        }
1390
        return responseInbox.substring(len);
4 ✔
1391
    }
1392

1393
    protected void cleanResponses(boolean closing) {
1394
        ArrayList<String> toRemove = new ArrayList<>();
4 ✔
1395
        boolean wasInterrupted = false;
2 ✔
1396

1397
        for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesAwaiting.entrySet()) {
12 ✔
1398
            boolean remove = false;
2 ✔
1399
            NatsRequestCompletableFuture future = entry.getValue();
4 ✔
1400
            if (future.hasExceededTimeout()) {
3 ✔
1401
                remove = true;
2 ✔
1402
                future.cancelTimedOut();
3 ✔
1403
            }
1404
            else if (closing) {
2 ✔
1405
                remove = true;
2 ✔
1406
                future.cancelClosing();
3 ✔
1407
            }
1408
            else if (future.isDone()) {
3 ✔
1409
                // done should have already been removed, not sure if
1410
                // this even needs checking, but it won't hurt
1411
                remove = true;
2 ✔
1412
                try {
1413
                    future.get();
×
1414
                }
1415
                catch (InterruptedException e) {
×
1416
                    Thread.currentThread().interrupt();
×
1417
                    // We might have collected some entries already, but were interrupted.
1418
                    // Break out so we finish as quick as possible,
1419
                    // cleanResponses will be called again anyway
1420
                    wasInterrupted = true;
×
1421
                    break;
×
1422
                }
1423
                catch (Throwable ignore) {
1 ✔
1424
                }
×
1425
            }
1426

1427
            if (remove) {
2 ✔
1428
                toRemove.add(entry.getKey());
6 ✔
1429
                statistics.decrementOutstandingRequests();
3 ✔
1430
            }
1431
        }
1 ✔
1432

1433
        for (String key : toRemove) {
10 ✔
1434
            responsesAwaiting.remove(key);
5 ✔
1435
        }
1 ✔
1436

1437
        if (advancedTracking && !wasInterrupted) {
5 !
1438
            toRemove.clear(); // we can reuse this but it needs to be cleared
2 ✔
1439
            for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesRespondedTo.entrySet()) {
12 ✔
1440
                NatsRequestCompletableFuture future = entry.getValue();
4 ✔
1441
                if (future.hasExceededTimeout()) {
3 ✔
1442
                    toRemove.add(entry.getKey());
6 ✔
1443
                    future.cancelTimedOut();
2 ✔
1444
                }
1445
            }
1 ✔
1446

1447
            for (String token : toRemove) {
10 ✔
1448
                responsesRespondedTo.remove(token);
5 ✔
1449
            }
1 ✔
1450
        }
1451
    }
1 ✔
1452

1453
    /**
1454
     * Send a request and returns the reply or null. This version of request is equivalent
1455
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1456
     * the timeout and handling the ExecutionException and TimeoutException.
1457
     *
1458
     * @param subject the subject for the service that will handle the request
1459
     * @param data the content of the message
1460
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1461
     * @return the reply message or null if the timeout is reached
1462
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1463
     * @throws IllegalArgumentException if the subject is invalid
1464
     */
1465
    @Nullable
1466
    public Message request(@NonNull String subject, byte @Nullable [] data, long timeoutMillis) throws InterruptedException {
1467
        return request(subject, null, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1468
    }
1469

1470
    /**
1471
     * Send a request and returns the reply or null. This version of request is equivalent
1472
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1473
     * the timeout and handling the ExecutionException and TimeoutException.
1474
     *
1475
     * @param subject the subject for the service that will handle the request
1476
     * @param headers Optional headers to publish with the message.
1477
     * @param data the content of the message
1478
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1479
     * @return the reply message or null if the timeout is reached
1480
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1481
     * @throws IllegalArgumentException if the subject is invalid
1482
     */
1483
    @Nullable
1484
    public Message request(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis) throws InterruptedException {
1485
        return request(subject, headers, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1486
    }
1487

1488
    /**
1489
     * Send a request and returns the reply or null. This version of request is equivalent
1490
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1491
     * the timeout and handling the ExecutionException and TimeoutException.
1492
     *
1493
     * <p>The Message object allows you to set a replyTo, but in requests,
1494
     * the replyTo is reserved for internal use as the address for the
1495
     * server to respond to the client with the consumer's reply.</p>
1496
     *
1497
     * @param message the message
1498
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1499
     * @return the reply message or null if the timeout is reached
1500
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1501
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1502
     */
1503
    @Nullable
1504
    public Message request(@NonNull Message message, long timeoutMillis) throws InterruptedException {
1505
        Validator.validateNotNull(message, "Message");
4 ✔
1506
        return request(message.getSubject(), message.getHeaders(), message.getData(), timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
13 ✔
1507
    }
1508

1509
    /**
1510
     * Send a request and returns the reply or null. Same as the other request methods except that it lets
1511
     * the caller choose what happens to the outstanding request if no reply arrives in time.
1512
     *
1513
     * @param subject the subject for the service that will handle the request
1514
     * @param headers optional headers to publish with the message
1515
     * @param data the content of the message
1516
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1517
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1518
     * @return the reply message or null if the timeout is reached
1519
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1520
     * @throws IllegalArgumentException if the subject is invalid
1521
     */
1522
    @Nullable
1523
    public Message request(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis, @NonNull CancelAction cancelAction) throws InterruptedException {
1524
        return request(subject, headers, data, timeoutMillis, cancelAction, forceFlushOnRequest);
×
1525
    }
1526

1527
    /**
1528
     * Send a request and returns the reply or null. This is the core blocking request used by all the
1529
     * other request methods; it publishes, then waits on the future for the reply.
1530
     *
1531
     * @param subject the subject for the service that will handle the request
1532
     * @param headers optional headers to publish with the message
1533
     * @param data the content of the message
1534
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1535
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1536
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing the request
1537
     * @return the reply message or null if the timeout is reached
1538
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1539
     * @throws IllegalArgumentException if the subject is invalid
1540
     */
1541
    @Nullable
1542
    public Message request(@NonNull String subject,
1543
                           @Nullable Headers headers,
1544
                           byte @Nullable [] data,
1545
                           long timeoutMillis,
1546
                           @NonNull CancelAction cancelAction,
1547
                           boolean flushImmediatelyAfterPublish) throws InterruptedException
1548
    {
1549
        CompletableFuture<Message> incoming = requestAsync(subject, headers, data, timeoutMillis, cancelAction, flushImmediatelyAfterPublish);
9 ✔
1550
        try {
1551
            long getMillis = timeoutMillis <= 0 ? getOptions().getConnectionTimeout() : timeoutMillis;
6 !
1552
            return incoming.get(getMillis, TimeUnit.MILLISECONDS);
6 ✔
1553
        }
1554
        catch (TimeoutException | ExecutionException | CancellationException e) {
×
1555
            return null;
×
1556
        }
1557
    }
1558

1559
    /**
1560
     * Send a request. The returned future will be completed when the
1561
     * response comes back.
1562
     *
1563
     * @param subject the subject for the service that will handle the request
1564
     * @param data the content of the message
1565
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1566
     * @throws IllegalArgumentException if the subject is invalid
1567
     */
1568
    @NonNull
1569
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data) {
1570
        return requestAsync(subject, null, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1571
    }
1572

1573
    /**
1574
     * Send a request. The returned future will be completed when the
1575
     * response comes back.
1576
     *
1577
     * @param subject the subject for the service that will handle the request
1578
     * @param headers Optional headers to publish with the message.
1579
     * @param data the content of the message
1580
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1581
     * @throws IllegalArgumentException if the subject is invalid
1582
     */
1583
    @NonNull
1584
    public CompletableFuture<Message> requestAsync(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data) {
1585
        return requestAsync(subject, headers, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1586
    }
1587

1588
    /**
1589
     * Send a request. The returned future will be completed when the
1590
     * response comes back.
1591
     *
1592
     * @param subject the subject for the service that will handle the request
1593
     * @param data the content of the message
1594
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1595
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1596
     * @throws IllegalArgumentException if the subject is invalid
1597
     */
1598
    @NonNull
1599
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data, long timeoutMillis) {
1600
        return requestAsync(subject, null, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1601
    }
1602

1603
    /**
1604
     * Send a request. The returned future will be completed when the
1605
     * response comes back.
1606
     *
1607
     * @param subject the subject for the service that will handle the request
1608
     * @param data the content of the message
1609
     * @param headers Optional headers to publish with the message.
1610
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1611
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1612
     * @throws IllegalArgumentException if the subject is invalid
1613
     */
1614
    @NonNull
1615
    public CompletableFuture<Message> requestAsync(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis) {
1616
        return requestAsync(subject, headers, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1617
    }
1618

1619
    /**
1620
     * Send a request. The returned future will be completed when the
1621
     * response comes back.
1622
     *
1623
     * <p>The Message object allows you to set a replyTo, but in requests,
1624
     * the replyTo is reserved for internal use as the address for the
1625
     * server to respond to the client with the consumer's reply.</p>
1626
     *
1627
     * @param message the message
1628
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1629
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1630
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1631
     */
1632
    @NonNull
1633
    public CompletableFuture<Message> requestAsync(@NonNull Message message, long timeoutMillis) {
1634
        Validator.validateNotNull(message, "Message");
×
1635
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
×
1636
    }
1637

1638
    /**
1639
     * Send a request. The returned future will be completed when the
1640
     * response comes back.
1641
     *
1642
     * <p>The Message object allows you to set a replyTo, but in requests,
1643
     * the replyTo is reserved for internal use as the address for the
1644
     * server to respond to the client with the consumer's reply.</p>
1645
     *
1646
     * @param message the message
1647
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1648
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1649
     */
1650
    @NonNull
1651
    public CompletableFuture<Message> request(@NonNull Message message) {
1652
        Validator.validateNotNull(message, "Message");
4 ✔
1653
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), -1, CancelAction.REPORT, forceFlushOnRequest);
13 ✔
1654
    }
1655

1656
    /**
1657
     * Send a request, returning a future for the response. This is the core request-send used by all the
1658
     * other request / requestAsync methods.
1659
     *
1660
     * @param subject the subject for the service that will handle the request
1661
     * @param headers optional headers to publish with the message
1662
     * @param data the content of the message
1663
     * @param timeoutMillis the time in milliseconds before the outstanding request is cleaned up; a value
1664
     *                       less than 0 uses the request cleanup interval default
1665
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1666
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing the request
1667
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1668
     * @throws IllegalArgumentException if the subject is invalid
1669
     */
1670
    @NonNull
1671
    public CompletableFuture<Message> requestAsync(@NonNull String subject,
1672
                                                   @Nullable Headers headers,
1673
                                                   byte @Nullable [] data,
1674
                                                   long timeoutMillis,
1675
                                                   @NonNull CancelAction cancelAction,
1676
                                                   boolean flushImmediatelyAfterPublish)
1677
    {
1678
        validateNotClosed();
×
1679
        subjectValidate(subject);
×
1680

1681
        if (inboxDispatcher.get() == null) {
×
1682
            inboxDispatcherLock.lock();
×
1683
            try {
1684
                if (inboxDispatcher.get() == null) {
×
1685
                    NatsDispatcher d = dispatcherFactory.createDispatcher(this, this::deliverReply);
×
1686

1687
                    // Ensure the dispatcher is started before publishing messages
1688
                    dispatchers.add(d);
×
1689
                    d.start();
×
1690
                    d.subscribe(mainInbox);
×
1691
                    inboxDispatcher.set(d);
×
1692
                }
1693
            }
1694
            finally {
1695
                inboxDispatcherLock.unlock();
×
1696
            }
1697
        }
1698

1699
        String responseInbox = createResponseInbox(mainInbox);
×
1700
        String responseToken = getResponseToken(responseInbox);
×
1701
        NatsRequestCompletableFuture future =
×
1702
            new NatsRequestCompletableFuture(cancelAction,
1703
                timeoutMillis <= 0 ? options.getRequestCleanupInterval() : timeoutMillis, options.useTimeoutException());
×
1704

1705
        responsesAwaiting.put(responseToken, future);
×
1706
        statistics.incrementOutstandingRequests();
×
1707

1708
        try {
1709
            _publish(new InternalPublishableMessage(data, subject, responseInbox, headers, flushImmediatelyAfterPublish));
×
1710
        }
1711
        catch (RuntimeException e) {
×
1712
            // The publish never made it onto the queue (queue full/busy, or the calling thread was
1713
            // interrupted). Undo the registration so we don't leak a never-completable future waiting
1714
            // for the cleanup timer, then let the caller see the failure.
1715
            responsesAwaiting.remove(responseToken);
×
1716
            statistics.decrementOutstandingRequests();
×
1717
            throw e;
×
1718
        }
×
1719

1720
        statistics.incrementRequestsSent();
×
1721

1722
        return future;
×
1723
    }
1724

1725
    protected void deliverReply(Message msg) {
1726
        String subject = msg.getSubject();
3 ✔
1727
        String key = getResponseToken(subject);
4 ✔
1728
        NatsRequestCompletableFuture f = responsesAwaiting.remove(key);
6 ✔
1729
        if (f != null) {
2 ✔
1730
            if (advancedTracking) {
3 ✔
1731
                responsesRespondedTo.put(key, f);
6 ✔
1732
            }
1733
            statistics.decrementOutstandingRequests();
3 ✔
1734
            if (msg.isStatusMessage() && msg.getStatus().getCode() == 503) {
8 !
1735
                switch (f.getCancelAction()) {
6 !
1736
                    case COMPLETE:
1737
                        f.complete(msg);
×
1738
                        break;
×
1739
                    case REPORT:
1740
                        f.completeExceptionally(new StatusException(msg.getStatus()));
8 ✔
1741
                        break;
1 ✔
1742
                    case CANCEL:
1743
                    default:
1744
                        f.cancel(true);
×
1745
                }
1746
            }
1747
            else {
1748
                f.complete(msg);
4 ✔
1749
            }
1750
            statistics.incrementRepliesReceived();
4 ✔
1751
        }
1752
        else if (!subject.startsWith(mainInbox)) {
5 !
1753
            if (advancedTracking) {
3 ✔
1754
                if (responsesRespondedTo.get(key) != null) {
5 ✔
1755
                    statistics.incrementDuplicateRepliesReceived();
4 ✔
1756
                }
1757
                else {
1758
                    statistics.incrementOrphanRepliesReceived();
3 ✔
1759
                }
1760
            }
1761
        }
1762
    }
1 ✔
1763

1764
    /**
1765
     * Convenience method to create a dispatcher with no default handler. Only used
1766
     * with JetStream subscriptions that require specific handlers per subscription.
1767
     *
1768
     * @return a new Dispatcher
1769
     */
1770
    @NonNull
1771
    public NatsDispatcher createDispatcher() {
1772
        return createDispatcher(null);
4 ✔
1773
    }
1774

1775
    /**
1776
     * Create a {@code Dispatcher} for this connection. The dispatcher can group one
1777
     * or more subscriptions into a single callback thread. All messages go to the
1778
     * same {@code MessageHandler}.
1779
     *
1780
     * <p>Use the Dispatcher's {@link Dispatcher#subscribe(String)} and
1781
     * {@link Dispatcher#subscribe(String, String)} methods to add subscriptions.
1782
     *
1783
     * <pre>
1784
     * nc = Nats.connect()
1785
     * d = nc.createDispatcher((m) -&gt; System.out.println(m)).subscribe("hello");
1786
     * </pre>
1787
     *
1788
     * @param handler The target for the messages. If the handler is null, subscribing without
1789
     *                using its API that accepts a handler will discard messages.
1790
     * @return a new Dispatcher
1791
     */
1792
    @NonNull
1793
    public NatsDispatcher createDispatcher(@Nullable MessageHandler handler) {
1794
        if (isClosed()) {
3 ✔
1795
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1796
        }
1797
        else if (isDraining()) {
3 ✔
1798
            throw new IllegalStateException("NatsConnection is Draining");
5 ✔
1799
        }
1800

1801
        NatsDispatcher dispatcher = dispatcherFactory.createDispatcher(this, handler);
6 ✔
1802
        dispatchers.add(dispatcher);
5 ✔
1803
        dispatcher.start();
2 ✔
1804
        return dispatcher;
2 ✔
1805
    }
1806

1807
    /**
1808
     * Close a dispatcher. This will unsubscribe any subscriptions and stop the delivery thread.
1809
     *
1810
     * <p>Once closed the dispatcher will throw an exception on subsequent subscribe or unsubscribe calls.
1811
     *
1812
     * @param d the dispatcher to close
1813
     * @throws IllegalArgumentException if the dispatcher was not created by this connection, or has already been closed
1814
     */
1815
    public void closeDispatcher(@NonNull Dispatcher d) {
1816
        if (isClosed()) {
3 ✔
1817
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1818
        }
1819
        else if (!(d instanceof NatsDispatcher)) {
3 !
1820
            throw new IllegalArgumentException("NatsConnection can only manage its own dispatchers");
×
1821
        }
1822

1823
        NatsDispatcher nd = (NatsDispatcher) d;
3 ✔
1824

1825
        if (nd.isDraining()) {
3 !
1826
            return; // No op while draining
×
1827
        }
1828

1829
        if (!dispatchers.contains(nd)) {
5 ✔
1830
            throw new IllegalArgumentException("Dispatcher is already closed.");
5 ✔
1831
        }
1832

1833
        cleanupDispatcher(nd);
3 ✔
1834
    }
1 ✔
1835

1836
    protected void cleanupDispatcher(NatsDispatcher nd) {
1837
        nd.stop(true);
3 ✔
1838
        dispatchers.remove(nd);
5 ✔
1839
    }
1 ✔
1840

1841
    protected Set<Dispatcher> getDispatchers() {
1842
        return Collections.unmodifiableSet(dispatchers);
×
1843
    }
1844

1845
    /**
1846
     * Attach another ConnectionListener. Adding a listener whose id is already registered replaces it.
1847
     *
1848
     * <p>The ConnectionListener will only receive NatsConnection events arriving after it has been attached.  When
1849
     * a NatsConnection event is raised, the invocation order and parallelism of multiple ConnectionListeners is not
1850
     * specified.
1851
     *
1852
     * <p>Listeners attached here are not written back to the Options - another connection built from the same
1853
     * Options starts with only the listeners the Options carries.
1854
     *
1855
     * @param connectionListener the ConnectionListener to attach
1856
     */
1857
    public void addConnectionListener(@NonNull ConnectionListener connectionListener) {
1858
        connectionListeners.put(connectionListener.getConnectionListenerId(), connectionListener);
7 ✔
1859
    }
1 ✔
1860

1861
    /**
1862
     * Detach a ConnectionListener. This will cease delivery of any further NatsConnection events to this instance.
1863
     *
1864
     * @param connectionListener the ConnectionListener to detach
1865
     */
1866
    public void removeConnectionListener(@NonNull ConnectionListener connectionListener) {
1867
        connectionListeners.remove(connectionListener.getConnectionListenerId());
6 ✔
1868
    }
1 ✔
1869

1870
    /**
1871
     * Detach the ConnectionListener registered under the given id. See {@link ConnectionListener#getConnectionListenerId()}.
1872
     * Removing an id that is not registered is a no-op.
1873
     *
1874
     * @param id the id of the ConnectionListener to detach
1875
     */
1876
    public void removeConnectionListenerById(@NonNull String id) {
1877
        connectionListeners.remove(id);
5 ✔
1878
    }
1 ✔
1879

1880
    /**
1881
     * Attach an ErrorListener, which will receive error events for this connection in addition to any
1882
     * supplied in the {@link Options Options}. Adding a listener whose id is already registered replaces it.
1883
     *
1884
     * <p>The ErrorListener will only receive events arriving after it has been attached. When an event is
1885
     * raised, the invocation order and parallelism of multiple ErrorListeners is not specified.
1886
     *
1887
     * <p>Listeners attached here are not written back to the Options - another connection built from the same
1888
     * Options starts with only the listeners the Options carries.
1889
     *
1890
     * @param errorListener the ErrorListener to attach
1891
     */
1892
    public void addErrorListener(@NonNull ErrorListener errorListener) {
1893
        errorListeners.put(errorListener.getErrorListenerId(), errorListener);
7 ✔
1894
    }
1 ✔
1895

1896
    /**
1897
     * Detach an ErrorListener. This will cease delivery of any further error events to this instance.
1898
     *
1899
     * @param errorListener the ErrorListener to detach
1900
     */
1901
    public void removeErrorListener(@NonNull ErrorListener errorListener) {
1902
        errorListeners.remove(errorListener.getErrorListenerId());
6 ✔
1903
    }
1 ✔
1904

1905
    /**
1906
     * Detach the ErrorListener registered under the given id. See {@link ErrorListener#getErrorListenerId()}.
1907
     * Removing an id that is not registered is a no-op.
1908
     *
1909
     * @param id the id of the ErrorListener to detach
1910
     */
1911
    public void removeErrorListenerById(@NonNull String id) {
1912
        errorListeners.remove(id);
5 ✔
1913
    }
1 ✔
1914

1915
    /**
1916
     * Flush the connection's buffer of outgoing messages, including sending a
1917
     * protocol message to and from the server.
1918
     * If called while the connection is closed, this method will immediately
1919
     * throw a TimeoutException, regardless of the timeout.
1920
     * If called while the connection is disconnected due to network issues this
1921
     * method will wait for up to the timeout for a reconnect or close.
1922
     *
1923
     * @param timeoutMillis the time in milliseconds to wait for the flush to succeed; pass 0 (or a negative value) to wait forever.
1924
     * @throws TimeoutException if the timeout is exceeded
1925
     * @throws InterruptedException if the underlying thread is interrupted
1926
     */
1927
    public void flush(long timeoutMillis) throws TimeoutException, InterruptedException {
1928
        // The timeout comes in as millis, but we operate in nanos: we do work below
1929
        // (waitForConnectOrClose, then sendPing) before blocking on the future, so we
1930
        // recompute the remaining time right at the get() call for a precise wait.
1931
        long startNanos = NatsSystemClock.nanoTime();
2 ✔
1932
        boolean forever = timeoutMillis <= 0; // 0 (or negative) waits forever
8 ✔
1933
        long timeoutNanos = forever ? 0 : timeoutMillis * NANOS_PER_MILLI;
8 ✔
1934
        waitForConnectOrClose(forever ? 0 : timeoutMillis);
7 ✔
1935

1936
        if (isClosed()) {
3 ✔
1937
            throw new TimeoutException("Attempted to flush while closed");
5 ✔
1938
        }
1939

1940
        if (!forever && NatsSystemClock.nanoTime() - startNanos >= timeoutNanos) {
8 ✔
1941
            throw new TimeoutException("Timeout out waiting for connection before flush.");
5 ✔
1942
        }
1943

1944
        try {
1945
            Future<Boolean> waitForIt = sendPing();
3 ✔
1946

1947
            if (waitForIt == null) { // error in the send ping code
2 !
1948
                return;
×
1949
            }
1950

1951
            if (forever) {
2 ✔
1952
                waitForIt.get();
4 ✔
1953
            }
1954
            else {
1955
                // recompute the time left here, after the work above, so the future waits for a precise remainder
1956
                long remainingNanos = timeoutNanos - (NatsSystemClock.nanoTime() - startNanos);
6 ✔
1957
                if (remainingNanos <= 0) {
4 !
1958
                    remainingNanos = 1; // let the future timeout if it isn't resolved
×
1959
                }
1960
                waitForIt.get(remainingNanos, TimeUnit.NANOSECONDS);
5 ✔
1961
            }
1962

1963
            statistics.incrementFlushCounter();
3 ✔
1964
        }
1965
        catch (ExecutionException | CancellationException e) {
1 ✔
1966
            throw new TimeoutException(e.toString());
6 ✔
1967
        }
1 ✔
1968
    }
1 ✔
1969

1970
    protected void sendConnect(NatsUri nuri) throws IOException {
1971
        try {
1972
            ServerInfo info = serverInfo.get();
5 ✔
1973
            // This is changed - we used to use info.isAuthRequired(), but are changing it to
1974
            // better match older versions of the server. It may change again in the future.
1975
            CharBuffer connectOptions = options.buildProtocolConnectOptionsString(
5 ✔
1976
                nuri.toString(), true, info.getNonce());
4 ✔
1977
            ByteArrayBuilder bab =
4 ✔
1978
                new ByteArrayBuilder(OP_CONNECT_SP_LEN + connectOptions.limit(), UTF_8)
5 ✔
1979
                    .append(CONNECT_SP_BYTES).append(connectOptions);
4 ✔
1980
            queueInternalOutgoing(new ProtocolMessage(bab, false));
8 ✔
1981
        }
1982
        catch (Exception exp) {
×
1983
            throw new IOException("Error sending connect string", exp);
×
1984
        }
1 ✔
1985
    }
1 ✔
1986

1987
    protected CompletableFuture<Boolean> sendPing() {
1988
        return sendPing(true);
4 ✔
1989
    }
1990

1991
    protected void softPing() {
1992
        sendPing(false);
4 ✔
1993
    }
1 ✔
1994

1995
    /**
1996
     * Calculates the round trip time between this client and the server,
1997
     * waiting up to the connection timeout for the response.
1998
     * @return the round trip time in nanoseconds
1999
     * @throws IllegalStateException if the connection is not connected, or the max outgoing ping count would be exceeded
2000
     * @throws IOException if the connection is closed while waiting for the response
2001
     * @throws TimeoutException if the server does not respond in time
2002
     * @throws InterruptedException if the thread is interrupted
2003
     */
2004
    public long RTT() throws IOException, TimeoutException, InterruptedException {
2005
        return RTT(options.getConnectionTimeout());
6 ✔
2006
    }
2007

2008
    /**
2009
     * Calculates the round trip time between this client and the server.
2010
     * <p>A completed round trip also confirms the server has processed everything this
2011
     * connection sent before it, since the server reads one connection's stream in order.
2012
     * @param timeoutMillis the maximum time to wait, in milliseconds; less than 1 uses the connection timeout
2013
     * @return the round trip time in nanoseconds. Note the wait is bounded in millis but the
2014
     *         measurement is returned in nanos, since a round trip is routinely sub-millisecond
2015
     * @throws IllegalStateException if the connection is not connected, or the max outgoing ping count would be exceeded
2016
     * @throws IOException if the connection is closed while waiting for the response
2017
     * @throws TimeoutException if the server does not respond in time
2018
     * @throws InterruptedException if the thread is interrupted
2019
     */
2020
    public long RTT(long timeoutMillis) throws IOException, TimeoutException, InterruptedException {
2021
        if (!isConnected()) {
3 ✔
2022
            throw new IllegalStateException("Must be connected to do RTT.");
5 ✔
2023
        }
2024

2025
        int max = options.getMaxPingsOut();
4 ✔
2026
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
2027
            throw new IllegalStateException("Max outgoing Ping count exceeded.");
×
2028
        }
2029

2030
        long timeout = timeoutMillis < 1 ? options.getConnectionTimeout() : timeoutMillis;
10 ✔
2031
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
4 ✔
2032
        pongQueue.add(pongFuture);
5 ✔
2033
        boolean ponged = false;
2 ✔
2034
        try {
2035
            long startNanos = NatsSystemClock.nanoTime();
2 ✔
2036
            if (!queueOutgoing(new ProtocolMessage(PING_PROTO))) {
7 ✔
2037
                throw new IOException("RTT PING was not queued.");
5 ✔
2038
            }
2039
            pongFuture.get(timeout, TimeUnit.MILLISECONDS);
5 ✔
2040
            ponged = true;
2 ✔
2041
            return NatsSystemClock.nanoTime() - startNanos;
6 ✔
2042
        }
2043
        catch (CancellationException e) {
×
2044
            // cleanUpPongQueue cancels every waiter when the connection drops
2045
            throw new IOException("Connection closed while waiting for RTT.", e);
×
2046
        }
2047
        catch (ExecutionException e) {
×
2048
            throw new IOException(e.getCause());
×
2049
        }
2050
        finally {
2051
            // a future left in the queue would be completed by the next PONG,
2052
            // consuming the one a live waiter (a flush) is owed
2053
            if (!ponged) {
2 ✔
2054
                pongQueue.remove(pongFuture);
5 ✔
2055
            }
2056
        }
2057
    }
2058

2059
    // Send a ping request and push a pong future on the queue.
2060
    // Futures are completed in order, keep this one if a thread wants to wait
2061
    // for a specific pong. Note, if no pong returns, the wait will not return
2062
    // without setting a timeout.
2063
    @Nullable
2064
    protected CompletableFuture<Boolean> sendPing(boolean treatAsInternal) {
2065
        if (!isConnectedOrConnecting()) {
3 !
2066
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
×
2067
            retVal.complete(Boolean.FALSE);
×
2068
            return retVal;
×
2069
        }
2070

2071
        if (!treatAsInternal && !needPing.get()) {
2 !
2072
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
×
2073
            retVal.complete(Boolean.TRUE);
×
2074
            needPing.set(true);
×
2075
            return retVal;
×
2076
        }
2077

2078
        int max = options.getMaxPingsOut();
4 ✔
2079
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
2080
            handleCommunicationIssue(new IllegalStateException("Max outgoing Ping count exceeded."));
×
2081
            return null;
×
2082
        }
2083

2084
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
4 ✔
2085
        pongQueue.add(pongFuture);
5 ✔
2086

2087
        // a future left in the queue without its PING sent would be completed by the next PONG,
2088
        // consuming the one a live waiter (a flush) is owed, so it is removed if the PING is not queued
2089
        boolean queued;
2090
        try {
2091
            ProtocolMessage ping = new ProtocolMessage(PING_PROTO);
5 ✔
2092
            queued = treatAsInternal ? queueInternalOutgoing(ping) : queueOutgoing(ping);
7 !
2093
        }
2094
        catch (RuntimeException e) {
×
2095
            pongQueue.remove(pongFuture);
×
2096
            throw e;
×
2097
        }
1 ✔
2098
        if (!queued) {
2 !
2099
            return pingNotQueued(pongFuture);
×
2100
        }
2101

2102
        needPing.set(true);
4 ✔
2103
        statistics.incrementPingCount();
3 ✔
2104
        return pongFuture;
2 ✔
2105
    }
2106

2107
    // A PING that was not queued gets no PONG: its future leaves the queue and fails, so a flush waiting on it fails
2108
    protected CompletableFuture<Boolean> pingNotQueued(CompletableFuture<Boolean> pongFuture) {
2109
        pongQueue.remove(pongFuture);
5 ✔
2110
        pongFuture.completeExceptionally(new IllegalStateException("PING was not queued."));
7 ✔
2111
        return pongFuture;
2 ✔
2112
    }
2113

2114
    // This is a minor speed / memory enhancement.
2115
    // We can't reuse the same instance of any NatsMessage b/c of the "NatsMessage next" state,
2116
    // but it is safe to share the data bytes and the size since those fields are just being read
2117
    // This constructor "ProtocolMessage(ProtocolMessage pm)" shares the data and size
2118
    // reducing allocation of data for something that is often created and used.
2119
    // These static instances are the ones that are used for copying in sendPing and sendPong
2120
    protected static final ProtocolMessage PING_PROTO = new ProtocolMessage(OP_PING_BYTES, true);
6 ✔
2121
    protected static final ProtocolMessage PONG_PROTO = new ProtocolMessage(OP_PONG_BYTES, true);
7 ✔
2122

2123
    protected void sendPong() {
2124
        queueInternalOutgoing(new ProtocolMessage(PONG_PROTO));
7 ✔
2125
    }
1 ✔
2126

2127
    // Called by the reader
2128
    protected void handlePong() {
2129
        CompletableFuture<Boolean> pongFuture = pongQueue.pollFirst();
5 ✔
2130
        if (pongFuture != null) {
2 !
2131
            pongFuture.complete(Boolean.TRUE);
4 ✔
2132
        }
2133
    }
1 ✔
2134

2135
    protected void readInitialInfo() throws IOException {
2136
        byte[] readBuffer = new byte[options.getBufferSize()];
5 ✔
2137
        ByteBuffer protocolBuffer = ByteBuffer.allocate(options.getBufferSize());
5 ✔
2138
        boolean gotCRLF = false;
2 ✔
2139
        boolean gotCR = false;
2 ✔
2140

2141
        while (!gotCRLF) {
2 ✔
2142
            int read = dataPort.read(readBuffer, 0, readBuffer.length);
8 ✔
2143

2144
            if (read < 0) {
2 ✔
2145
                break;
1 ✔
2146
            }
2147

2148
            int i = 0;
2 ✔
2149
            while (i < read) {
3 ✔
2150
                byte b = readBuffer[i++];
5 ✔
2151

2152
                if (gotCR) {
2 ✔
2153
                    if (b != LF) {
3 ✔
2154
                        throw new IOException("Missed LF after CR waiting for INFO.");
5 ✔
2155
                    }
2156
                    else if (i < read) {
3 ✔
2157
                        throw new IOException("Read past initial info message.");
5 ✔
2158
                    }
2159

2160
                    gotCRLF = true;
2 ✔
2161
                    break;
1 ✔
2162
                }
2163

2164
                if (b == CR) {
3 ✔
2165
                    gotCR = true;
3 ✔
2166
                }
2167
                else {
2168
                    if (!protocolBuffer.hasRemaining()) {
3 ✔
2169
                        protocolBuffer = enlargeBuffer(protocolBuffer); // just double it
4 ✔
2170
                    }
2171
                    protocolBuffer.put(b);
4 ✔
2172
                }
2173
            }
1 ✔
2174
        }
1 ✔
2175

2176
        if (!gotCRLF) {
2 ✔
2177
            throw new IOException("Failed to read initial info message.");
5 ✔
2178
        }
2179

2180
        protocolBuffer.flip();
3 ✔
2181

2182
        String infoJson = UTF_8.decode(protocolBuffer).toString();
5 ✔
2183
        infoJson = infoJson.trim();
3 ✔
2184
        String[] msg = infoJson.split("\\s");
4 ✔
2185
        String op = msg[0].toUpperCase();
5 ✔
2186

2187
        if (!OP_INFO.equals(op)) {
4 ✔
2188
            throw new IOException("Received non-info initial message.");
5 ✔
2189
        }
2190

2191
        handleInfo(infoJson);
3 ✔
2192
    }
1 ✔
2193

2194
    protected void handleInfo(String infoJson) {
2195
        ServerInfo newServerInfo = new ServerInfo(infoJson);
5 ✔
2196
        serverInfo.set(newServerInfo);
4 ✔
2197

2198
        List<String> urls = newServerInfo.getConnectURLs();
3 ✔
2199
        if (!urls.isEmpty()) {
3 ✔
2200
            NatsUri origin = currentServer == null ? connectingServer : currentServer;
9 ✔
2201
            if (serverPool.acceptDiscoveredUrls(urls, origin)) {
6 ✔
2202
                processConnectionEvent(ConnectionEvent.DISCOVERED_SERVERS, urls.toString());
5 ✔
2203
            }
2204
        }
2205

2206
        if (newServerInfo.isLameDuckMode()) {
3 ✔
2207
            processConnectionEvent(ConnectionEvent.LAME_DUCK, uriDetail(currentServer));
7 ✔
2208
            this.lameDuckTriggered = true;
3 ✔
2209
        }
2210
    }
1 ✔
2211

2212
    protected void validatePayloadAndControlLineSizes(NatsMessage msg) {
2213
        if (options.clientSideLimitChecks()) {
4 !
2214
            if (getMaxPayload() > 0 && msg.getPayloadSize() > getMaxPayload()) {
12 !
2215
                throw new IllegalArgumentException(
×
2216
                    "Message payload size exceed server configuration " + msg.getPayloadSize() + " vs " + getMaxPayload());
×
2217
            }
2218
            if (msg.getControlLineLength() > options.getMaxControlLine()) {
6 !
2219
                throw new IllegalArgumentException("Control line is too long");
×
2220
            }
2221
        }
2222
    }
1 ✔
2223

2224
    // false if the message was not queued (discarded, or the thread was interrupted)
2225
    protected boolean queueOutgoing(NatsMessage msg) {
2226
        validatePayloadAndControlLineSizes(msg);
3 ✔
2227
        if (writer.queue(msg)) {
5 !
2228
            return true;
2 ✔
2229
        }
2230
        notifyErrorListener((c, el) -> el.messageDiscarded(c, msg));
×
2231
        return false;
×
2232
    }
2233

2234
    // false if the message was not queued (the thread was interrupted)
2235
    protected boolean queueInternalOutgoing(NatsMessage msg) {
2236
        validatePayloadAndControlLineSizes(msg);
3 ✔
2237
        return writer.queueInternalMessage(msg);
5 ✔
2238
    }
2239

2240
    protected void deliverMessage(NatsMessage msg) {
2241
        needPing.set(false);
4 ✔
2242
        statistics.incrementIn(msg.getSizeInBytes());
5 ✔
2243

2244
        SubscriptionInfo subscription = subscriptions.get(msg.getSID());
7 ✔
2245

2246
        if (subscription != null) {
2 !
2247
            NatsSubscription sub = subscription.sub;
3 ✔
2248
            msg.setSubscription(sub);
3 ✔
2249

2250
            NatsDispatcher d = subscription.dispatcher;
3 ✔
2251
            NatsMessageSink s = (d == null) ? sub : d;
6 ✔
2252
            // The only read of the queue reference on this path: q is what is judged below and q is
2253
            // what is pushed to, so an invalidate() on another thread cannot change it underneath
2254
            // the decision.
2255
            ConsumerMessageQueue q = ((d == null) ? sub.getMessageQueue() : d.getMessageQueue());
8 ✔
2256

2257
            switch (s.getDeliverabilityState(q)) {
7 !
2258
                case AVAILABLE -> {
2259
                    s.markNotSlow();
2 ✔
2260
                    // the before queue processor contract is to return true if the message is allowed to be queued
2261
                    if (sub.getBeforeQueueProcessor().apply(msg)) {
7 !
2262
                        q.push(msg);
4 ✔
2263
                    }
2264
                }
2265
                case FULL -> {
2266
                    // Drop the message and count it
2267
                    statistics.incrementDroppedCount();
3 ✔
2268
                    s.incrementDroppedCount();
2 ✔
2269

2270
                    // Notify the first time
2271
                    if (!s.isMarkedSlow()) {
3 ✔
2272
                        s.markSlow();
2 ✔
2273
                        processSlowConsumer(sub);
4 ✔
2274
                    }
2275
                }
2276
                case NOT_AVAILABLE -> {
2277
                    // The subscription went away between registration and delivery. Count the drop,
2278
                    // but do not mark slow - a subscription that is gone is not a slow consumer.
2279
                    statistics.incrementDroppedCount();
3 ✔
2280
                    s.incrementDroppedCount();
2 ✔
2281
                }
2282
            }
2283
        }
2284
//      else Drop messages we don't have a subscriber for (could be extras on an auto-unsub for example)
2285
    }
1 ✔
2286

2287
    protected void processOK() {
2288
        statistics.incrementOkCount();
3 ✔
2289
    }
1 ✔
2290

2291
    protected void makeCallback(Runnable callback) {
2292
        if (callbackExecutor != null) {
3 ✔
2293
            try {
2294
                callbackExecutor.execute(() -> {
6 ✔
2295
                    try {
2296
                        callback.run();
2 ✔
2297
                    }
2298
                    catch (Exception ex) {
1 ✔
2299
                        statistics.incrementExceptionCount();
3 ✔
2300
                    }
1 ✔
2301
                });
1 ✔
2302
            }
2303
            catch (RejectedExecutionException re) {
×
2304
                // Timing with shutdown probably, let it go
2305
            }
1 ✔
2306
        }
2307
    }
1 ✔
2308

2309
    /**
2310
     * Report that a subscription or dispatcher has fallen behind its pending limits. Only the first
2311
     * detection is reported until the consumer catches up again.
2312
     * @param subscription the subscription or dispatcher that is behind
2313
     */
2314
    public void processSlowConsumer(Subscription subscription) {
2315
        notifyErrorListener((c, el) -> el.slowConsumerDetected(c, subscription));
9 ✔
2316
    }
1 ✔
2317

2318
    /**
2319
     * Report an exception the client caught and handled, incrementing the exception statistic.
2320
     * @param exp the exception
2321
     */
2322
    public void processException(Exception exp) {
2323
        statistics.incrementExceptionCount();
3 ✔
2324
        notifyErrorListener((c, el) -> el.exceptionOccurred(c, exp));
9 ✔
2325
    }
1 ✔
2326

2327
    /**
2328
     * Handle a -ERR protocol message from the server. The text becomes the last error, and an
2329
     * authentication error is also remembered against the current server so the connect logic can
2330
     * stop retrying a server that will never accept these credentials.
2331
     * @param errorText the error text as sent by the server
2332
     */
2333
    public void processError(String errorText) {
2334
        statistics.incrementErrCount();
3 ✔
2335

2336
        lastError.set(errorText);
4 ✔
2337
        connectError.set(errorText); // even if this isn't during connection, save it just in case
4 ✔
2338

2339
        // If we get an authentication error, save it
2340
        if (isAuthenticationError(errorText) && currentServer != null) {
7 ✔
2341
            serverAuthErrors.put(currentServer, errorText);
7 ✔
2342
        }
2343

2344
        notifyErrorListener((c, el) -> el.errorOccurred(c, errorText));
9 ✔
2345
    }
1 ✔
2346

2347
    /**
2348
     * Picks which {@link ErrorListener} method to invoke, so {@link #notifyErrorListener(ErrorListenerCaller)}
2349
     * can do the fan-out once for every event.
2350
     */
2351
    public interface ErrorListenerCaller {
2352
        /**
2353
         * Invoke the event method on one listener.
2354
         * @param conn the connection the event is for
2355
         * @param el the listener to call
2356
         */
2357
        void call(NatsConnection conn, ErrorListener el);
2358
    }
2359

2360
    /**
2361
     * The single fan-out point for every ErrorListener event. One callback per listener, so a slow or
2362
     * throwing listener cannot block the others.
2363
     * @param elc the call to make on each listener
2364
     */
2365
    public void notifyErrorListener(ErrorListenerCaller elc) {
2366
        for (ErrorListener listener : errorListeners.values()) {
12 ✔
2367
            makeCallback(() -> elc.call(this, listener));
11 ✔
2368
        }
1 ✔
2369
    }
1 ✔
2370

2371
    protected String uriDetail(NatsUri uri) {
2372
        return uri == null ? null : uri.toString();
7 ✔
2373
    }
2374

2375
    protected String uriDetail(NatsUri uri, NatsUri hostOrlast) {
2376
        if (uri != null) {
2 !
2377
            if (hostOrlast == null || uri.equals(hostOrlast)) {
6 !
2378
                return uri.toString();
3 ✔
2379
            }
2380
            return uri + " [" + hostOrlast + "]";
6 ✔
2381
        }
2382
        return hostOrlast == null ? null : hostOrlast.toString();
×
2383
    }
2384

2385
    protected void processConnectionEvent(ConnectionEvent type, String uriDetails) {
2386
        long time = System.currentTimeMillis();
2 ✔
2387
        for (ConnectionListener listener : connectionListeners.values()) {
12 ✔
2388
            makeCallback(() -> listener.connectionEvent(this, type, time, uriDetails));
16 ✔
2389
        }
1 ✔
2390
    }
1 ✔
2391

2392
    /**
2393
     * Return the server info object. Will never be null, but will be an instance of {@link ServerInfo#EMPTY_INFO}
2394
     * before a connection is made, and will represent the last connected server once connected and while disconnected
2395
     * until a new connection is made.
2396
     * @return the server information such as id, client info, etc.
2397
     */
2398
    @NonNull
2399
    public ServerInfo getServerInfo() {
2400
        return serverInfo.get();
5 ✔
2401
    }
2402

2403
    /**
2404
     * the InetAddress of client as known by the NATS server, otherwise null.
2405
     * @return the InetAddress
2406
     */
2407
    @Nullable
2408
    public InetAddress getClientInetAddress() {
2409
        try {
2410
            ServerInfo si = getServerInfo();
3 ✔
2411
            return si == ServerInfo.EMPTY_INFO ? null : NatsInetAddress.getByName(si.getClientIp());
7 !
2412
        }
2413
        catch (Exception e) {
×
2414
            return null;
×
2415
        }
2416
    }
2417

2418
    /**
2419
     * the read-only options used to create this connection
2420
     * @return the Options
2421
     */
2422
    @NonNull
2423
    public Options getOptions() {
2424
        return options;
3 ✔
2425
    }
2426

2427
    /**
2428
     * a wrapper for useful statistics about the connection
2429
     * @return the Statistics implementation
2430
     */
2431
    @NonNull
2432
    public Statistics getStatistics() {
2433
        return statistics.getStatistics();
4 ✔
2434
    }
2435

2436
    protected StatisticsCollector getStatisticsCollector() {
2437
        return statistics;
3 ✔
2438
    }
2439

2440
    protected DataPort getDataPort() {
2441
        return dataPort;
3 ✔
2442
    }
2443

2444
    // Used for testing
2445
    protected int getSinkCount() {
2446
        return subscriptions.size() + dispatchers.size();
8 ✔
2447
    }
2448

2449
    /**
2450
     * MaxPayload returns the size limit that a message payload can have. This is
2451
     * set by the server configuration and delivered to the client upon connect.
2452
     *
2453
     * @return the maximum size of a message payload
2454
     */
2455
    public long getMaxPayload() {
2456
        ServerInfo info = serverInfo.get();
5 ✔
2457

2458
        if (info == null) {
2 !
2459
            return -1;
×
2460
        }
2461

2462
        return info.getMaxPayload();
3 ✔
2463
    }
2464

2465
    /**
2466
     * Return the list of known server urls, including additional servers discovered
2467
     * after a connection has been established.
2468
     * Will be empty (but not null) before a connection is made and will represent the last connected server while disconnected
2469
     * @return this connection's list of known server URLs
2470
     */
2471
    @NonNull
2472
    public Collection<String> getServers() {
2473
        return serverPool.getServerList();
4 ✔
2474
    }
2475

2476
    protected List<NatsUri> resolveHost(NatsUri nuri) {
2477
        // 1. If the nuri host is not already an ip address
2478
        //      -and- the nuri is not for websocket
2479
        //      -and- the HostnameResolveMode is Resolve
2480
        //    let the pool resolve it.
2481
        HostnameResolveMode resolveMode = options.hostnameResolveMode();
4 ✔
2482
        List<NatsUri> results = new ArrayList<>();
4 ✔
2483
        if (!nuri.hostIsIpAddress()
4 ✔
2484
            && !nuri.isWebsocket()
5 !
2485
            && resolveMode.resolve)
2486
        {
2487
            List<String> ips = serverPool.resolveHostToIps(
5 ✔
2488
                nuri.getHost(), resolveMode.maxOneResult, resolveMode.includeIPV6);
5 ✔
2489
            if (ips != null) {
2 !
2490
                for (String ip : ips) {
10 ✔
2491
                    try {
2492
                        results.add(nuri.reHost(ip));
6 ✔
2493
                    }
2494
                    catch (URISyntaxException u) {
×
2495
                        // ??? should never happen
2496
                        throw new RuntimeException(u);
×
2497
                    }
1 ✔
2498
                }
1 ✔
2499
            }
2500
        }
2501

2502
        // 2. If there were no results,
2503
        //    - host was already an ip address
2504
        //    - host was for websocket
2505
        //    - hostnameResolveMode did not want to be resolved
2506
        //    - pool returned nothing
2507
        //    - resolving failed...
2508
        //    so the list just becomes the original host.
2509
        if (results.isEmpty()) {
3 ✔
2510
            results.add(nuri);
4 ✔
2511
        }
2512
        return results;
2 ✔
2513
    }
2514

2515
    /**
2516
     * the url used for the current connection, or null if disconnected
2517
     * @return the url string
2518
     */
2519
    @Nullable
2520
    public String getConnectedUrl() {
2521
        return currentServer == null ? null : currentServer.toString();
9 ✔
2522
    }
2523

2524
    /**
2525
     * Returns the connection's current status.
2526
     *
2527
     * @return the connection's status
2528
     */
2529
    @NonNull
2530
    public ConnectionStatus getStatus() {
2531
        return status;
3 ✔
2532
    }
2533

2534
    /**
2535
     * the error text from the last error sent by the server to this client
2536
     * @return the last error text
2537
     */
2538
    @Nullable
2539
    public String getLastError() {
2540
        return lastError.get();
5 ✔
2541
    }
2542

2543
    /**
2544
     * Clear the last error from the server
2545
     */
2546
    public void clearLastError() {
2547
        lastError.set(null);
4 ✔
2548
    }
1 ✔
2549

2550
    protected ExecutorService getExecutor() {
2551
        return executor;
3 ✔
2552
    }
2553

2554
    protected ExecutorService getReaderExecutor() {
2555
        return readerExecutor;
3 ✔
2556
    }
2557

2558
    protected ExecutorService getWriterExecutor() {
2559
        return writerExecutor;
3 ✔
2560
    }
2561

2562
    protected ScheduledExecutorService getScheduledExecutor() {
2563
        return scheduledExecutor;
3 ✔
2564
    }
2565

2566
    protected void updateStatus(ConnectionStatus newStatus) {
2567
        updateStatus(newStatus, uriDetail(currentServer == null ? lastServer : currentServer));
13 ✔
2568
    }
1 ✔
2569

2570
    protected void updateStatus(ConnectionStatus newStatus, NatsUri resolvedUri, NatsUri hostUri) {
2571
        updateStatus(newStatus, uriDetail(resolvedUri, hostUri));
7 ✔
2572
    }
1 ✔
2573

2574
    protected void updateStatus(ConnectionStatus newStatus, String uriDetail) {
2575
        // The status is read under the lock and the event is chosen from the transition this
2576
        // call actually made, not from re-reading the field afterwards. Re-reading raises the
2577
        // event for whatever status another thread has since installed - or none at all, which
2578
        // is how a DISCONNECTED event goes missing when a reconnect follows closely behind.
2579
        ConnectionStatus oldStatus;
2580
        statusLock.lock();
3 ✔
2581
        try {
2582
            oldStatus = status;
3 ✔
2583
            if (oldStatus == CLOSED || newStatus == oldStatus) {
6 !
2584
                return;
1 ✔
2585
            }
2586
            this.status = newStatus;
3 ✔
2587
            statusChanged.signalAll();
3 ✔
2588
        }
2589
        finally {
2590
            statusLock.unlock();
3 ✔
2591
        }
2592

2593
        if (newStatus == DISCONNECTED) {
3 ✔
2594
            processConnectionEvent(ConnectionEvent.DISCONNECTED, uriDetail);
5 ✔
2595
        }
2596
        else if (newStatus == CLOSED) {
3 ✔
2597
            processConnectionEvent(ConnectionEvent.CLOSED, uriDetail);
5 ✔
2598
        }
2599
        else if (oldStatus == RECONNECTING && newStatus == CONNECTED) {
6 !
2600
            processConnectionEvent(ConnectionEvent.RECONNECTED, uriDetail);
5 ✔
2601
        }
2602
        else if (newStatus == CONNECTED) {
3 ✔
2603
            processConnectionEvent(ConnectionEvent.CONNECTED, uriDetail);
4 ✔
2604
        }
2605
    }
1 ✔
2606

2607
    protected boolean isClosing() {
2608
        return closing;
3 ✔
2609
    }
2610

2611
    protected boolean isClosed() {
2612
        return status == CLOSED;
8 ✔
2613
    }
2614

2615
    protected boolean isConnected() {
2616
        return status == CONNECTED;
8 ✔
2617
    }
2618

2619
    protected boolean isDisconnected() {
2620
        return status == DISCONNECTED;
×
2621
    }
2622

2623
    protected boolean isConnectedOrConnecting() {
2624
        statusLock.lock();
3 ✔
2625
        try {
2626
            return status == CONNECTED || connecting;
12 !
2627
        } finally {
2628
            statusLock.unlock();
3 ✔
2629
        }
2630
    }
2631

2632
    protected boolean isDisconnectingOrClosed() {
2633
        statusLock.lock();
3 ✔
2634
        try {
2635
            return status == CLOSED || disconnecting;
11 !
2636
        } finally {
2637
            statusLock.unlock();
3 ✔
2638
        }
2639
    }
2640

2641
    protected boolean isDisconnecting() {
2642
        statusLock.lock();
3 ✔
2643
        try {
2644
            return disconnecting;
5 ✔
2645
        } finally {
2646
            statusLock.unlock();
3 ✔
2647
        }
2648
    }
2649

2650
    protected void waitForDisconnectOrClose(long timeoutMillis) throws InterruptedException {
2651
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> isDisconnecting() && !isClosed() );
16 !
2652
    }
1 ✔
2653

2654
    protected void waitForConnectOrClose(long timeoutMillis) throws InterruptedException {
2655
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> !isConnected() && !isClosed());
21 !
2656
    }
1 ✔
2657

2658
    // operates purely in nanoseconds (precise, monotonic); the millis-facing wrappers above convert once at the edge.
2659
    // < 0 returns immediately, 0 waits forever, > 0 waits up to that many nanos.
2660
    protected void waitWhile(long timeoutNanos, Predicate<Void> waitWhileTrue) throws InterruptedException {
2661
        statusLock.lock();
3 ✔
2662
        try {
2663
            long currentWaitNanos = timeoutNanos;
2 ✔
2664
            long start = NatsSystemClock.nanoTime();
2 ✔
2665
            while (currentWaitNanos >= 0 && waitWhileTrue.test(null)) {
8 !
2666
                if (currentWaitNanos > 0) {
4 !
2667
                    if (statusChanged.await(currentWaitNanos, TimeUnit.NANOSECONDS) && !waitWhileTrue.test(null)) {
10 ✔
2668
                        break;
1 ✔
2669
                    }
2670
                    long now = NatsSystemClock.nanoTime();
2 ✔
2671
                    currentWaitNanos = currentWaitNanos - (now - start);
6 ✔
2672
                    start = now;
2 ✔
2673

2674
                    if (currentWaitNanos <= 0) {
4 ✔
2675
                        break;
1 ✔
2676
                    }
2677
                }
1 ✔
2678
                else {
2679
                    statusChanged.await();
×
2680
                }
2681
            }
2682
        }
2683
        finally {
2684
            statusLock.unlock();
3 ✔
2685
        }
2686
    }
1 ✔
2687

2688
    /**
2689
     * Whether the reconnect delay handler is invoked before the first round of a reconnect campaign.
2690
     * Rounds after the first always invoke it. This is the only place {@link ReconnectDelayBehavior}
2691
     * is read, so the setting applies to every handler, including a custom one.
2692
     * @param lameDuck whether the campaign was triggered by a server lame duck signal
2693
     * @return true to invoke the handler before round 1
2694
     */
2695
    protected boolean delayBeforeFirstRound(boolean lameDuck) {
2696
        return switch (options.reconnectDelayBehavior()) {
8 ✔
2697
            case BeforeAllRounds -> true;
2 ✔
2698
            case LameDuckAware -> lameDuck;
2 ✔
2699
            default -> false;   // BeforeSubsequentRounds
1 ✔
2700
        };
2701
    }
2702

2703
    protected void invokeReconnectDelayHandler(long round, boolean lameDuckTriggered) {
2704
        long currentWaitMillis = options.getReconnectDelayHandler()
8 ✔
2705
            .getWaitTimeMillis(round, options, serverPool.hasSecureServer(), lameDuckTriggered);
4 ✔
2706

2707
        this.reconnectWaiter = new CompletableFuture<>();
5 ✔
2708

2709
        long start = NatsSystemClock.nanoTime();
2 ✔
2710
        while (currentWaitMillis > 0 && !isDisconnectingOrClosed() && !isConnected() && !reconnectWaiter.isDone()) {
14 !
2711
            try {
2712
                reconnectWaiter.get(currentWaitMillis, TimeUnit.MILLISECONDS);
×
2713
            } catch (Exception exp) {
1 ✔
2714
                // ignore, try to loop again
2715
            }
×
2716
            long elapsedMillis = (NatsSystemClock.nanoTime() - start) / 1_000_000L;
6 ✔
2717
            currentWaitMillis -= elapsedMillis;
4 ✔
2718
            start = NatsSystemClock.nanoTime();
2 ✔
2719
        }
1 ✔
2720

2721
        reconnectWaiter.complete(Boolean.TRUE);
5 ✔
2722
    }
1 ✔
2723

2724
    protected ByteBuffer enlargeBuffer(ByteBuffer buffer) {
2725
        int current = buffer.capacity();
3 ✔
2726
        int newSize = current * 2;
4 ✔
2727
        ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
3 ✔
2728
        buffer.flip();
3 ✔
2729
        newBuffer.put(buffer);
4 ✔
2730
        return newBuffer;
2 ✔
2731
    }
2732

2733
    // For testing
2734
    protected NatsConnectionReader getReader() {
2735
        return reader;
3 ✔
2736
    }
2737

2738
    // For testing
2739
    protected NatsConnectionWriter getWriter() {
2740
        return writer;
×
2741
    }
2742

2743
    // For testing
2744
    protected Future<DataPort> getDataPortFuture() {
2745
        return dataPortFuture;
×
2746
    }
2747

2748
    protected boolean isDraining() {
2749
        return draining.get() != null;
8 ✔
2750
    }
2751

2752
    protected boolean isDrained() {
2753
        CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2754

2755
        try {
2756
            if (tracker != null && tracker.getNow(false)) {
9 !
2757
                return true;
2 ✔
2758
            }
2759
        } catch (Exception e) {
×
2760
            // These indicate the tracker was cancelled/timed out
2761
        }
1 ✔
2762

2763
        return false;
2 ✔
2764
    }
2765

2766
    /**
2767
     * Drain tells the connection to process in flight messages before closing.
2768
     * Drain initially drains all the sinks, stopping incoming messages.
2769
     * Next, publishing is halted and a flush call is used to insure all published
2770
     * messages have reached the server.
2771
     * Finally, the connection is closed.
2772
     * In order to drain subscribers, an unsub protocol message is sent to the server followed by a flush.
2773
     * These two steps occur before drain returns. The remaining steps occur in a background thread.
2774
     * This method tries to manage the timeout properly, so that if the timeout is 1 second, and the flush
2775
     * takes 100ms, the remaining steps have 900ms in the background thread.
2776
     * The connection will try to let all messages be drained, but when the timeout is reached
2777
     * the connection is closed and any outstanding dispatcher threads are interrupted.
2778
     * A future allows this call to be treated as synchronous or asynchronous as
2779
     * needed by the application. The value of the future will be true if all the subscriptions
2780
     * were drained in the timeout, and false otherwise. The future completes after the connection
2781
     * is closed, so any connection handler notifications will happen before the future completes.
2782
     *
2783
     * @param timeoutMillis The time in milliseconds to wait for the drain to succeed, pass 0 or less to wait
2784
     *                    forever. Drain involves moving messages to and from the server
2785
     *                    so a very short timeout is not recommended. If the timeout is reached before
2786
     *                    the drain completes, the connection is simply closed, which can result in message
2787
     *                    loss.
2788
     * @return A future that can be used to check if the drain has completed
2789
     * @throws InterruptedException if the thread is interrupted
2790
     * @throws TimeoutException if the initial flush times out
2791
     */
2792
    @NonNull
2793
    public CompletableFuture<Boolean> drain(long timeoutMillis) throws TimeoutException, InterruptedException {
2794

2795
        if (isClosing() || isClosed()) {
6 !
2796
            throw new IllegalStateException("A connection can't be drained during close.");
5 ✔
2797
        }
2798

2799
        statusLock.lock();
3 ✔
2800
        try {
2801
            if (isDraining()) {
3 ✔
2802
                return draining.get();
7 ✔
2803
            }
2804
            draining.set(new CompletableFuture<>());
6 ✔
2805
        } finally {
2806
            statusLock.unlock();
3 ✔
2807
        }
2808

2809
        final CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2810
        long startNanos = NatsSystemClock.nanoTime();
2 ✔
2811

2812
        // Don't include subscriptions with dispatchers
2813
        HashSet<NatsSubscription> pureSubscriptions = new HashSet<>();
4 ✔
2814
        for (SubscriptionInfo subscription : subscriptions.values()) {
12 ✔
2815
            if (subscription.dispatcher == null) {
3 ✔
2816
                pureSubscriptions.add(subscription.sub);
5 ✔
2817
            }
2818
        }
1 ✔
2819

2820
        final HashSet<NatsMessageSink> sinks = new HashSet<>();
4 ✔
2821
        sinks.addAll(pureSubscriptions);
4 ✔
2822
        sinks.addAll(dispatchers);
5 ✔
2823

2824
        NatsDispatcher inboxer = inboxDispatcher.get();
5 ✔
2825

2826
        if (inboxer != null) {
2 ✔
2827
            sinks.add(inboxer);
4 ✔
2828
        }
2829

2830
        // Stop the sinks NOW so that when this method returns they are blocked
2831
        sinks.forEach((sink) -> {
4 ✔
2832
            sink.markDraining(tracker);
3 ✔
2833
            sink.sendUnsubForDrain();
2 ✔
2834
        });
1 ✔
2835

2836
        try {
2837
            flush(timeoutMillis); // Flush and wait up to the timeout, if this fails, let the caller know
3 ✔
2838
        } catch (Exception e) {
1 ✔
2839
            close(false, false);
4 ✔
2840
            throw e;
2 ✔
2841
        }
1 ✔
2842

2843
        sinks.forEach(NatsMessageSink::markUnsubedForDrain);
3 ✔
2844

2845
        // Wait for the timeout or all sinks are drained
2846
        executor.submit(() -> {
10 ✔
2847
            try {
2848
                long timeoutNanos = timeoutMillis <= 0 ? Long.MAX_VALUE : timeoutMillis * NANOS_PER_MILLI;
10 ✔
2849
                long startTime = System.nanoTime();
2 ✔
2850
                while (NatsSystemClock.nanoTime() - startTime < timeoutNanos && !Thread.interrupted()) {
8 !
2851
                    sinks.removeIf(NatsMessageSink::isDrained);
4 ✔
2852
                    if (sinks.isEmpty()) {
3 ✔
2853
                        break;
1 ✔
2854
                    }
2855
                    //noinspection BusyWait
2856
                    Thread.sleep(1); // Sleep 1 milli
3 ✔
2857
                }
2858

2859
                // Stop publishing
2860
                blockPublishForDrain.set(true);
4 ✔
2861

2862
                // One last flush
2863
                if (timeoutMillis <= 0) {
4 ✔
2864
                    flush(0);
4 ✔
2865
                } else {
2866
                    long remainingMillis = timeoutMillis - (NatsSystemClock.nanoTime() - startNanos) / NANOS_PER_MILLI;
8 ✔
2867
                    if (remainingMillis > 0) {
4 ✔
2868
                        flush(remainingMillis);
3 ✔
2869
                    }
2870
                }
2871
                close(false, false); // close the connection after the last flush
4 ✔
2872
                tracker.complete(sinks.isEmpty());
6 ✔
2873
            } catch (TimeoutException e) {
×
2874
                processException(e);
×
2875
            } catch (InterruptedException e) {
×
2876
                processException(e);
×
2877
                Thread.currentThread().interrupt();
×
2878
            } finally {
2879
                try {
2880
                    close(false, false);// close the connection after the last flush
4 ✔
2881
                } catch (InterruptedException e) {
×
2882
                    processException(e);
×
2883
                    Thread.currentThread().interrupt();
×
2884
                }
1 ✔
2885
                tracker.complete(false);
5 ✔
2886
            }
2887
        });
1 ✔
2888

2889
        return tracker;
2 ✔
2890
    }
2891

2892
    protected boolean isAuthenticationError(String err) {
2893
        if (err == null) {
2 ✔
2894
            return false;
2 ✔
2895
        }
2896
        err = err.toLowerCase();
3 ✔
2897
        return err.startsWith("user authentication")
7 ✔
2898
            || err.contains("authorization violation")
4 ✔
2899
            || err.startsWith("account authentication expired");
5 ✔
2900
    }
2901

2902
    /**
2903
     * Immediately flushes the underlying connection buffer if the connection is valid.
2904
     * @throws IOException if the connection flush fails
2905
     */
2906
    public void flushBuffer() throws IOException {
2907
        if (!isConnected()) {
×
2908
            throw new IllegalStateException("NatsConnection is not active.");
×
2909
        }
2910
        writer.flushBuffer();
×
2911
    }
×
2912

2913
    /**
2914
     * Get the number of messages in the outgoing queue for this connection.
2915
     * This value is volatile in the sense that it changes often and may be adjusted by more than one message.
2916
     * It changes every time a message is published (put in the outgoing queue)
2917
     * and every time a message is removed from the queue to be written over the socket
2918
     * @return the number of messages in the outgoing queue
2919
     */
2920
    public long outgoingPendingMessageCount() {
2921
        return writer == null ? -1 : writer.outgoingPendingMessageCount();
×
2922
    }
2923

2924
    /**
2925
     * Get the number of bytes based to be written calculated from the messages in the outgoing queue for this connection.
2926
     * This value is volatile in the sense that it changes often and may be adjusted by more than one message's bytes.
2927
     * It changes every time a message is published (put in the outgoing queue)
2928
     * and every time a message is removed from the queue to be written over the socket
2929
     * @return the number of messages in the outgoing queue
2930
     */
2931
    public long outgoingPendingBytes() {
2932
        return writer == null ? -1 : writer.outgoingPendingBytes();
×
2933
    }
2934

2935
    /**
2936
     * The default this connection applies to the flushImmediatelyAfterPublish parameter of the request methods,
2937
     * taken from the options at construction. Flushing trades throughput for latency on a single request.
2938
     * @return true if requests flush the outgoing buffer immediately
2939
     */
2940
    public boolean isForceFlushOnRequest() {
2941
        return forceFlushOnRequest;
×
2942
    }
2943
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc