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

nats-io / nats.java / #2059

15 Jul 2025 06:40PM UTC coverage: 95.593% (-0.06%) from 95.655%
#2059

push

github

web-flow
Merge pull request #1357 from nats-io/shutdown-internal-connection-executors

[FIX] Shutdown internal executors on connection close.

19 of 19 new or added lines in 2 files covered. (100.0%)

16 existing lines in 7 files now uncovered.

11844 of 12390 relevant lines covered (95.59%)

0.96 hits per line

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

94.29
/src/main/java/io/nats/client/impl/NatsConnection.java
1
// Copyright 2015-2018 The NATS Authors
2
// Licensed under the Apache License, Version 2.0 (the "License");
3
// you may not use this file except in compliance with the License.
4
// You may obtain a copy of the License at:
5
//
6
// http://www.apache.org/licenses/LICENSE-2.0
7
//
8
// Unless required by applicable law or agreed to in writing, software
9
// distributed under the License is distributed on an "AS IS" BASIS,
10
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
// See the License for the specific language governing permissions and
12
// limitations under the License.
13

14
package io.nats.client.impl;
15

16
import io.nats.client.*;
17
import io.nats.client.ConnectionListener.Events;
18
import io.nats.client.api.ServerInfo;
19
import io.nats.client.support.*;
20

21
import java.io.IOException;
22
import java.net.InetAddress;
23
import java.net.URISyntaxException;
24
import java.nio.ByteBuffer;
25
import java.nio.CharBuffer;
26
import java.time.Duration;
27
import java.time.Instant;
28
import java.util.*;
29
import java.util.concurrent.*;
30
import java.util.concurrent.atomic.AtomicBoolean;
31
import java.util.concurrent.atomic.AtomicLong;
32
import java.util.concurrent.atomic.AtomicReference;
33
import java.util.concurrent.locks.Condition;
34
import java.util.concurrent.locks.ReentrantLock;
35
import java.util.function.Predicate;
36

37
import static io.nats.client.support.NatsConstants.*;
38
import static io.nats.client.support.NatsRequestCompletableFuture.CancelAction;
39
import static io.nats.client.support.Validator.*;
40
import static java.nio.charset.StandardCharsets.UTF_8;
41

42
class NatsConnection implements Connection {
43

44
    public static final double NANOS_PER_SECOND = 1_000_000_000.0;
45

46
    private final Options options;
47
    final boolean forceFlushOnRequest;
48

49
    private final StatisticsCollector statistics;
50

51
    private boolean connecting; // you can only connect in one thread
52
    private boolean disconnecting; // you can only disconnect in one thread
53
    private boolean closing; // respect a close call regardless
54
    private Exception exceptionDuringConnectChange; // exception occurred in another thread while dis/connecting
55
    final ReentrantLock closeSocketLock;
56

57
    private Status status;
58
    private final ReentrantLock statusLock;
59
    private final Condition statusChanged;
60

61
    private CompletableFuture<DataPort> dataPortFuture;
62
    private DataPort dataPort;
63
    private NatsUri currentServer;
64
    private CompletableFuture<Boolean> reconnectWaiter;
65
    private final HashMap<NatsUri, String> serverAuthErrors;
66

67
    private NatsConnectionReader reader;
68
    private NatsConnectionWriter writer;
69

70
    private final AtomicReference<ServerInfo> serverInfo;
71

72
    private final Map<String, NatsSubscription> subscribers;
73
    private final Map<String, NatsDispatcher> dispatchers; // use a concurrent map so we get more consistent iteration behavior
74
    private final Collection<ConnectionListener> connectionListeners;
75
    private final Map<String, NatsRequestCompletableFuture> responsesAwaiting;
76
    private final Map<String, NatsRequestCompletableFuture> responsesRespondedTo;
77
    private final ConcurrentLinkedDeque<CompletableFuture<Boolean>> pongQueue;
78

79
    private final String mainInbox;
80
    private final AtomicReference<NatsDispatcher> inboxDispatcher;
81
    private final ReentrantLock inboxDispatcherLock;
82
    private ScheduledTask pingTask;
83
    private ScheduledTask cleanupTask;
84

85
    private final AtomicBoolean needPing;
86

87
    private final AtomicLong nextSid;
88
    private final NUID nuid;
89

90
    private final AtomicReference<String> connectError;
91
    private final AtomicReference<String> lastError;
92
    private final AtomicReference<CompletableFuture<Boolean>> draining;
93
    private final AtomicBoolean blockPublishForDrain;
94
    private final AtomicBoolean tryingToConnect;
95

96
    private final ExecutorService callbackRunner;
97
    private final ExecutorService executor;
98
    private final ExecutorService connectExecutor;
99
    private final ScheduledExecutorService scheduledExecutor;
100
    private final boolean advancedTracking;
101

102
    private final ServerPool serverPool;
103
    private final DispatcherFactory dispatcherFactory;
104
    final CancelAction cancelAction;
105

106
    private final boolean trace;
107
    private final TimeTraceLogger timeTraceLogger;
108

109
    NatsConnection(Options options) {
1✔
110
        trace = options.isTraceConnection();
1✔
111
        timeTraceLogger = options.getTimeTraceLogger();
1✔
112
        timeTraceLogger.trace("creating connection object");
1✔
113

114
        this.options = options;
1✔
115
        forceFlushOnRequest = options.forceFlushOnRequest();
1✔
116

117
        advancedTracking = options.isTrackAdvancedStats();
1✔
118
        this.statistics = options.getStatisticsCollector() == null ? new NatsStatistics() : options.getStatisticsCollector();
1✔
119
        this.statistics.setAdvancedTracking(advancedTracking);
1✔
120

121
        this.closeSocketLock = new ReentrantLock();
1✔
122

123
        this.statusLock = new ReentrantLock();
1✔
124
        this.statusChanged = this.statusLock.newCondition();
1✔
125
        this.status = Status.DISCONNECTED;
1✔
126
        this.reconnectWaiter = new CompletableFuture<>();
1✔
127
        this.reconnectWaiter.complete(Boolean.TRUE);
1✔
128

129
        this.connectionListeners = ConcurrentHashMap.newKeySet();
1✔
130
        if (options.getConnectionListener() != null) {
1✔
131
            addConnectionListener(options.getConnectionListener());
1✔
132
        }
133

134
        this.dispatchers = new ConcurrentHashMap<>();
1✔
135
        this.subscribers = new ConcurrentHashMap<>();
1✔
136
        this.responsesAwaiting = new ConcurrentHashMap<>();
1✔
137
        this.responsesRespondedTo = new ConcurrentHashMap<>();
1✔
138

139
        this.serverAuthErrors = new HashMap<>();
1✔
140

141
        this.nextSid = new AtomicLong(1);
1✔
142
        timeTraceLogger.trace("creating NUID");
1✔
143
        this.nuid = new NUID();
1✔
144
        this.mainInbox = createInbox() + ".*";
1✔
145

146
        this.lastError = new AtomicReference<>();
1✔
147
        this.connectError = new AtomicReference<>();
1✔
148

149
        this.serverInfo = new AtomicReference<>();
1✔
150
        this.inboxDispatcher = new AtomicReference<>();
1✔
151
        this.inboxDispatcherLock = new ReentrantLock();
1✔
152
        this.pongQueue = new ConcurrentLinkedDeque<>();
1✔
153
        this.draining = new AtomicReference<>();
1✔
154
        this.blockPublishForDrain = new AtomicBoolean();
1✔
155
        this.tryingToConnect = new AtomicBoolean();
1✔
156

157
        timeTraceLogger.trace("creating executors");
1✔
158
        this.executor = options.getExecutor();
1✔
159
        this.callbackRunner = options.getCallbackExecutor();
1✔
160
        this.connectExecutor = options.getConnectExecutor();
1✔
161
        this.scheduledExecutor = options.getScheduledExecutor();
1✔
162

163
        timeTraceLogger.trace("creating reader and writer");
1✔
164
        this.reader = new NatsConnectionReader(this);
1✔
165
        this.writer = new NatsConnectionWriter(this, null);
1✔
166

167
        this.needPing = new AtomicBoolean(true);
1✔
168

169
        serverPool = options.getServerPool() == null ? new NatsServerPool() : options.getServerPool();
1✔
170
        serverPool.initialize(options);
1✔
171
        dispatcherFactory = options.getDispatcherFactory() == null ? new DispatcherFactory() : options.getDispatcherFactory();
1✔
172

173
        cancelAction = options.isReportNoResponders() ? CancelAction.REPORT : CancelAction.CANCEL;
1✔
174

175
        timeTraceLogger.trace("connection object created");
1✔
176
    }
1✔
177

178
    // Connect is only called after creation
179
    void connect(boolean reconnectOnConnect) throws InterruptedException, IOException {
180
        if (!tryingToConnect.get()) {
1✔
181
            try {
182
                tryingToConnect.set(true);
1✔
183
                connectImpl(reconnectOnConnect);
1✔
184
            }
185
            finally {
186
                tryingToConnect.set(false);
1✔
187
            }
188
        }
189
    }
1✔
190

191
    void connectImpl(boolean reconnectOnConnect) throws InterruptedException, IOException {
192
        if (options.getServers().isEmpty()) {
1✔
193
            throw new IllegalArgumentException("No servers provided in options");
×
194
        }
195

196
        boolean trace = options.isTraceConnection();
1✔
197
        long start = NatsSystemClock.nanoTime();
1✔
198

199
        this.lastError.set("");
1✔
200

201
        timeTraceLogger.trace("starting connect loop");
1✔
202

203
        Set<NatsUri> failList = new HashSet<>();
1✔
204
        boolean keepGoing = true;
1✔
205
        NatsUri first = null;
1✔
206
        NatsUri cur;
207
        while (keepGoing && (cur = serverPool.peekNextServer()) != null) {
1✔
208
            if (first == null) {
1✔
209
                first = cur;
1✔
210
            }
211
            else if (cur.equals(first)) {
1✔
212
                break;  // connect only goes through loop once
1✔
213
            }
214
            serverPool.nextServer(); // b/c we only peeked.
1✔
215

216
            // let server pool resolve hostnames, then loop through resolved
217
            List<NatsUri> resolvedList = resolveHost(cur);
1✔
218
            for (NatsUri resolved : resolvedList) {
1✔
219
                if (isClosed()) {
1✔
220
                    keepGoing = false;
1✔
221
                    break;
1✔
222
                }
223
                connectError.set(""); // new on each attempt
1✔
224

225
                timeTraceLogger.trace("setting status to connecting");
1✔
226
                updateStatus(Status.CONNECTING);
1✔
227

228
                timeTraceLogger.trace("trying to connect to %s", cur);
1✔
229
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
1✔
230

231
                if (isConnected()) {
1✔
232
                    serverPool.connectSucceeded(cur);
1✔
233
                    keepGoing = false;
1✔
234
                    break;
1✔
235
                }
236

237
                timeTraceLogger.trace("setting status to disconnected");
1✔
238
                updateStatus(Status.DISCONNECTED);
1✔
239

240
                failList.add(cur);
1✔
241
                serverPool.connectFailed(cur);
1✔
242

243
                String err = connectError.get();
1✔
244

245
                if (this.isAuthenticationError(err)) {
1✔
246
                    this.serverAuthErrors.put(resolved, err);
1✔
247
                }
248
            }
1✔
249
        }
1✔
250

251
        if (!isConnected() && !isClosed()) {
1✔
252
            if (reconnectOnConnect) {
1✔
253
                timeTraceLogger.trace("trying to reconnect on connect");
1✔
254
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
1✔
255
            }
256
            else {
257
                timeTraceLogger.trace("connection failed, closing to cleanup");
1✔
258
                close();
1✔
259

260
                String err = connectError.get();
1✔
261
                if (this.isAuthenticationError(err)) {
1✔
262
                    throw new AuthenticationException("Authentication error connecting to NATS server: " + err);
1✔
263
                }
264
                throw new IOException("Unable to connect to NATS servers: " + failList);
1✔
265
            }
266
        }
267
        else if (trace) {
1✔
268
            long end = NatsSystemClock.nanoTime();
1✔
269
            double seconds = ((double) (end - start)) / NANOS_PER_SECOND;
1✔
270
            timeTraceLogger.trace("connect complete in %.3f seconds", seconds);
1✔
271
        }
272
    }
1✔
273

274
    @Override
275
    public void forceReconnect() throws IOException, InterruptedException {
276
        forceReconnect(null);
1✔
277
    }
1✔
278

279
    @Override
280
    public void forceReconnect(ForceReconnectOptions options) throws IOException, InterruptedException {
281
        if (!tryingToConnect.get()) {
1✔
282
            try {
283
                tryingToConnect.set(true);
1✔
284
                forceReconnectImpl(options);
1✔
285
            }
286
            finally {
287
                tryingToConnect.set(false);
1✔
288
            }
289
        }
290
    }
1✔
291

292
    void forceReconnectImpl(ForceReconnectOptions options) throws InterruptedException {
293
        if (options != null && options.getFlushWait() != null) {
1✔
294
            try {
295
                flush(options.getFlushWait());
×
296
            }
297
            catch (TimeoutException e) {
×
298
                // ignore, don't care, too bad;
299
            }
×
300
        }
301

302
        closeSocketLock.lock();
1✔
303
        try {
304
            updateStatus(Status.DISCONNECTED);
1✔
305

306
            // Close and reset the current data port and future
307
            if (dataPortFuture != null) {
1✔
308
                dataPortFuture.cancel(true);
1✔
309
                dataPortFuture = null;
1✔
310
            }
311

312
            // close the data port as a task so as not to block reconnect
313
            if (dataPort != null) {
1✔
314
                final DataPort closeMe = dataPort;
1✔
315
                dataPort = null;
1✔
316
                executor.submit(() -> {
1✔
317
                    try {
318
                        if (options != null && options.isForceClose()) {
1✔
UNCOV
319
                            closeMe.forceClose();
×
320
                        }
321
                        else {
322
                            closeMe.close();
1✔
323
                        }
324
                    }
325
                    catch (IOException ignore) {
×
326
                    }
1✔
327
                });
1✔
328
            }
329

330
            // stop i/o
331
            try {
332
                this.reader.stop(false).get(100, TimeUnit.MILLISECONDS);
1✔
333
            }
334
            catch (Exception ex) {
×
335
                processException(ex);
×
336
            }
1✔
337
            try {
338
                this.writer.stop().get(100, TimeUnit.MILLISECONDS);
1✔
339
            }
340
            catch (Exception ex) {
×
341
                processException(ex);
×
342
            }
1✔
343

344
            // new reader/writer
345
            reader = new NatsConnectionReader(this);
1✔
346
            writer = new NatsConnectionWriter(this, writer);
1✔
347
        }
348
        finally {
349
            closeSocketLock.unlock();
1✔
350
        }
351

352
        // calling connect just starts like a new connection versus reconnect
353
        // but we have to manually resubscribe like reconnect once it is connected
354
        reconnectImpl();
1✔
355
        writer.setReconnectMode(false);
1✔
356
    }
1✔
357

358
    void reconnect() throws InterruptedException {
359
        if (!tryingToConnect.get()) {
1✔
360
            try {
361
                tryingToConnect.set(true);
1✔
362
                reconnectImpl();
1✔
363
            }
364
            finally {
365
                tryingToConnect.set(false);
1✔
366
            }
367
        }
368
    }
1✔
369

370
    // Reconnect can only be called when the connection is disconnected
371
    void reconnectImpl() throws InterruptedException {
372
        if (isClosed()) {
1✔
373
            return;
1✔
374
        }
375

376
        if (options.getMaxReconnect() == 0) {
1✔
377
            this.close();
1✔
378
            return;
1✔
379
        }
380

381
        writer.setReconnectMode(true);
1✔
382

383
        if (!isConnected() && !isClosed() && !this.isClosing()) {
1✔
384
            boolean keepGoing = true;
1✔
385
            int totalRounds = 0;
1✔
386
            NatsUri first = null;
1✔
387
            NatsUri cur;
388
            while (keepGoing && (cur = serverPool.nextServer()) != null) {
1✔
389
                if (first == null) {
1✔
390
                    first = cur;
1✔
391
                }
392
                else if (first.equals(cur)) {
1✔
393
                    // went around the pool an entire time
394
                    invokeReconnectDelayHandler(++totalRounds);
1✔
395
                }
396

397
                // let server list provider resolve hostnames
398
                // then loop through resolved
399
                List<NatsUri> resolvedList = resolveHost(cur);
1✔
400
                for (NatsUri resolved : resolvedList) {
1✔
401
                    if (isClosed()) {
1✔
402
                        keepGoing = false;
1✔
403
                        break;
1✔
404
                    }
405
                    connectError.set(""); // reset on each loop
1✔
406
                    if (isDisconnectingOrClosed() || this.isClosing()) {
1✔
407
                        keepGoing = false;
1✔
408
                        break;
1✔
409
                    }
410
                    updateStatus(Status.RECONNECTING);
1✔
411

412
                    timeTraceLogger.trace("reconnecting to server %s", cur);
1✔
413
                    tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
1✔
414

415
                    if (isConnected()) {
1✔
416
                        serverPool.connectSucceeded(cur);
1✔
417
                        statistics.incrementReconnects();
1✔
418
                        keepGoing = false;
1✔
419
                        break;
1✔
420
                    }
421

422
                    serverPool.connectFailed(cur);
1✔
423
                    String err = connectError.get();
1✔
424
                    if (this.isAuthenticationError(err)) {
1✔
425
                        if (err.equals(this.serverAuthErrors.get(resolved))) {
1✔
426
                            keepGoing = false; // double auth error
1✔
427
                            break;
1✔
428
                        }
429
                        serverAuthErrors.put(resolved, err);
1✔
430
                    }
431
                }
1✔
432
            }
1✔
433
        } // end-main-loop
434

435
        if (!isConnected()) {
1✔
436
            this.close();
1✔
437
            return;
1✔
438
        }
439

440
        this.subscribers.forEach((sid, sub) -> {
1✔
441
            if (sub.getDispatcher() == null && !sub.isDraining()) {
1✔
442
                sendSubscriptionMessage(sub.getSID(), sub.getSubject(), sub.getQueueName(), true);
1✔
443
            }
444
        });
1✔
445

446
        this.dispatchers.forEach((nuid, d) -> {
1✔
447
            if (!d.isDraining()) {
1✔
448
                d.resendSubscriptions();
1✔
449
            }
450
        });
1✔
451

452
        try {
453
            this.flush(this.options.getConnectionTimeout());
1✔
454
        } catch (Exception exp) {
1✔
455
            this.processException(exp);
1✔
456
        }
1✔
457

458
        processConnectionEvent(Events.RESUBSCRIBED);
1✔
459

460
        // When the flush returns we are done sending internal messages,
461
        // so we can switch to the non-reconnect queue
462
        this.writer.setReconnectMode(false);
1✔
463
    }
1✔
464

465
    long timeCheck(long endNanos, String message) throws TimeoutException {
466
        long remaining = endNanos - NatsSystemClock.nanoTime();
1✔
467
        if (trace) {
1✔
468
            traceTimeCheck(message, remaining);
1✔
469
        }
470
        if (remaining < 0) {
1✔
471
            throw new TimeoutException("connection timed out");
1✔
472
        }
473
        return remaining;
1✔
474
    }
475

476
    void traceTimeCheck(String message, long remaining) {
477
        if (remaining < 0) {
1✔
478
            if (remaining > -1_000_000) { // less than -1 ms
1✔
479
                timeTraceLogger.trace(message + String.format(", %d (ns) beyond timeout", -remaining));
1✔
480
            }
481
            else if (remaining > -1_000_000_000) { // less than -1 second
1✔
482
                long ms = -remaining / 1_000_000;
1✔
483
                timeTraceLogger.trace(message + String.format(", %d (ms) beyond timeout", ms));
1✔
484
            }
1✔
485
            else {
486
                double seconds = ((double)-remaining) / 1_000_000_000.0;
1✔
487
                timeTraceLogger.trace(message + String.format(", %.3f (s) beyond timeout", seconds));
1✔
488
            }
1✔
489
        }
490
        else if (remaining < 1_000_000) {
1✔
491
            timeTraceLogger.trace(message + String.format(", %d (ns) remaining", remaining));
1✔
492
        }
493
        else if (remaining < 1_000_000_000) {
1✔
494
            long ms = remaining / 1_000_000;
1✔
495
            timeTraceLogger.trace(message + String.format(", %d (ms) remaining", ms));
1✔
496
        }
1✔
497
        else {
498
            double seconds = ((double) remaining) / 1_000_000_000.0;
1✔
499
            timeTraceLogger.trace(message + String.format(", %.3f (s) remaining", seconds));
1✔
500
        }
501
    }
1✔
502

503
    // is called from reconnect and connect
504
    // will wait for any previous attempt to complete, using the reader.stop and
505
    // writer.stop
506
    void tryToConnect(NatsUri cur, NatsUri resolved, long now) {
507
        currentServer = null;
1✔
508

509
        try {
510
            Duration connectTimeout = options.getConnectionTimeout();
1✔
511
            boolean trace = options.isTraceConnection();
1✔
512
            long end = now + connectTimeout.toNanos();
1✔
513
            timeCheck(end, "starting connection attempt");
1✔
514

515
            statusLock.lock();
1✔
516
            try {
517
                if (this.connecting) {
1✔
518
                    return;
×
519
                }
520
                this.connecting = true;
1✔
521
                statusChanged.signalAll();
1✔
522
            } finally {
523
                statusLock.unlock();
1✔
524
            }
525

526
            // Create a new future for the dataport, the reader/writer will use this
527
            // to wait for the connect/failure.
528
            this.dataPortFuture = new CompletableFuture<>();
1✔
529

530
            // Make sure the reader and writer are stopped
531
            long timeoutNanos = timeCheck(end, "waiting for reader");
1✔
532
            if (reader.isRunning()) {
1✔
533
                this.reader.stop().get(timeoutNanos, TimeUnit.NANOSECONDS);
×
534
            }
535
            timeoutNanos = timeCheck(end, "waiting for writer");
1✔
536
            if (writer.isRunning()) {
1✔
537
                this.writer.stop().get(timeoutNanos, TimeUnit.NANOSECONDS);
×
538
            }
539

540
            timeCheck(end, "cleaning pong queue");
1✔
541
            cleanUpPongQueue();
1✔
542

543
            timeoutNanos = timeCheck(end, "connecting data port");
1✔
544
            DataPort newDataPort = this.options.buildDataPort();
1✔
545
            newDataPort.connect(resolved.toString(), this, timeoutNanos);
1✔
546

547
            // Notify any threads waiting on the sockets
548
            this.dataPort = newDataPort;
1✔
549
            this.dataPortFuture.complete(this.dataPort);
1✔
550

551
            // Wait for the INFO message manually
552
            // all other traffic will use the reader and writer
553
            // TLS First, don't read info until after upgrade
554
            Callable<Object> connectTask = () -> {
1✔
555
                if (!options.isTlsFirst()) {
1✔
556
                    readInitialInfo();
1✔
557
                    checkVersionRequirements();
1✔
558
                }
559
                long start = NatsSystemClock.nanoTime();
1✔
560
                upgradeToSecureIfNeeded(resolved);
1✔
561
                if (trace && options.isTLSRequired()) {
1✔
562
                    // If the time appears too long it might be related to
563
                    // https://github.com/nats-io/nats.java#linux-platform-note
564
                    timeTraceLogger.trace("TLS upgrade took: %.3f (s)",
×
565
                            ((double) (NatsSystemClock.nanoTime() - start)) / NANOS_PER_SECOND);
×
566
                }
567
                if (options.isTlsFirst()) {
1✔
568
                    readInitialInfo();
1✔
569
                    checkVersionRequirements();
1✔
570
                }
571
                return null;
1✔
572
            };
573

574
            timeoutNanos = timeCheck(end, "reading info, version and upgrading to secure if necessary");
1✔
575
            Future<Object> future = this.connectExecutor.submit(connectTask);
1✔
576
            try {
577
                future.get(timeoutNanos, TimeUnit.NANOSECONDS);
1✔
578
            } finally {
579
                future.cancel(true);
1✔
580
            }
581

582
            // start the reader and writer after we secured the connection, if necessary
583
            timeCheck(end, "starting reader");
1✔
584
            this.reader.start(this.dataPortFuture);
1✔
585
            timeCheck(end, "starting writer");
1✔
586
            this.writer.start(this.dataPortFuture);
1✔
587

588
            timeCheck(end, "sending connect message");
1✔
589
            this.sendConnect(resolved);
1✔
590

591
            timeoutNanos = timeCheck(end, "sending initial ping");
1✔
592
            Future<Boolean> pongFuture = sendPing();
1✔
593

594
            if (pongFuture != null) {
1✔
595
                pongFuture.get(timeoutNanos, TimeUnit.NANOSECONDS);
1✔
596
            }
597

598
            if (pingTask == null) {
1✔
599
                timeCheck(end, "starting ping and cleanup timers");
1✔
600
                long pingMillis = this.options.getPingInterval().toMillis();
1✔
601

602
                if (pingMillis > 0) {
1✔
603
                    pingTask = new ScheduledTask(scheduledExecutor, pingMillis, () -> {
1✔
604
                        if (isConnected() && !isClosing()) {
1✔
605
                            try {
606
                                softPing(); // The timer always uses the standard queue
1✔
607
                            }
608
                            catch (Exception e) {
1✔
609
                                // it's running in a thread, there is no point throwing here
610
                            }
1✔
611
                        }
612
                    });
1✔
613
                }
614

615
                long cleanMillis = this.options.getRequestCleanupInterval().toMillis();
1✔
616

617
                if (cleanMillis > 0) {
1✔
618
                    cleanupTask = new ScheduledTask(scheduledExecutor, cleanMillis, () -> cleanResponses(false));
1✔
619
                }
620
            }
621

622
            // Set connected status
623
            timeCheck(end, "updating status to connected");
1✔
624
            statusLock.lock();
1✔
625
            try {
626
                this.connecting = false;
1✔
627

628
                if (this.exceptionDuringConnectChange != null) {
1✔
629
                    throw this.exceptionDuringConnectChange;
×
630
                }
631

632
                this.currentServer = cur;
1✔
633
                this.serverAuthErrors.clear(); // reset on successful connection
1✔
634
                updateStatus(Status.CONNECTED); // will signal status change, we also signal in finally
1✔
635
            } finally {
636
                statusLock.unlock();
1✔
637
            }
638
            timeTraceLogger.trace("status updated");
1✔
639
        } catch (Exception exp) {
1✔
640
            processException(exp);
1✔
641
            try {
642
                // allow force reconnect since this is pretty exceptional,
643
                // a connection failure while trying to connect
644
                this.closeSocket(false, true);
1✔
645
            } catch (InterruptedException e) {
×
646
                processException(e);
×
647
                Thread.currentThread().interrupt();
×
648
            }
1✔
649
        } finally {
650
            statusLock.lock();
1✔
651
            try {
652
                this.connecting = false;
1✔
653
                statusChanged.signalAll();
1✔
654
            } finally {
655
                statusLock.unlock();
1✔
656
            }
657
        }
658
    }
1✔
659

660
    void checkVersionRequirements() throws IOException {
661
        Options opts = getOptions();
1✔
662
        ServerInfo info = getInfo();
1✔
663

664
        if (opts.isNoEcho() && info.getProtocolVersion() < 1) {
1✔
665
            throw new IOException("Server does not support no echo.");
1✔
666
        }
667
    }
1✔
668

669
    void upgradeToSecureIfNeeded(NatsUri nuri) throws IOException {
670
        // When already communicating over "https" websocket, do NOT try to upgrade to secure.
671
        if (!nuri.isWebsocket()) {
1✔
672
            if (options.isTlsFirst()) {
1✔
673
                dataPort.upgradeToSecure();
1✔
674
            }
675
            else {
676
                // server    | client options      | result
677
                // --------- | ------------------- | --------
678
                // required  | not isTLSRequired() | mismatch
679
                // available | not isTLSRequired() | ok
680
                // neither   | not isTLSRequired() | ok
681
                // required  | isTLSRequired()     | ok
682
                // available | isTLSRequired()     | ok
683
                // neither   | isTLSRequired()     | mismatch
684
                ServerInfo serverInfo = getInfo();
1✔
685
                if (options.isTLSRequired()) {
1✔
686
                    if (!serverInfo.isTLSRequired() && !serverInfo.isTLSAvailable()) {
1✔
687
                        throw new IOException("SSL connection wanted by client.");
1✔
688
                    }
689
                    dataPort.upgradeToSecure();
1✔
690
                }
691
                else if (serverInfo.isTLSRequired()) {
1✔
692
                    throw new IOException("SSL required by server.");
1✔
693
                }
694
            }
695
        }
696
    }
1✔
697
    // Called from reader/writer thread
698
    void handleCommunicationIssue(Exception io) {
699
        // If we are connecting or disconnecting, note exception and leave
700
        statusLock.lock();
1✔
701
        try {
702
            if (this.connecting || this.disconnecting || this.status == Status.CLOSED || this.isDraining()) {
1✔
703
                this.exceptionDuringConnectChange = io;
1✔
704
                return;
1✔
705
            }
706
        } finally {
707
            statusLock.unlock();
1✔
708
        }
709

710
        processException(io);
1✔
711

712
        // Spawn a thread so we don't have timing issues with
713
        // waiting on read/write threads
714
        executor.submit(() -> {
1✔
715
            if (!tryingToConnect.get()) {
1✔
716
                try {
717
                    tryingToConnect.set(true);
1✔
718

719
                    // any issue that brings us here is pretty serious
720
                    // so we are comfortable forcing the close
721
                    this.closeSocket(true, true);
1✔
722
                } catch (InterruptedException e) {
×
723
                    processException(e);
×
724
                    Thread.currentThread().interrupt();
×
725
                } finally {
726
                    tryingToConnect.set(false);
1✔
727
                }
728
            }
729
        });
1✔
730
    }
1✔
731

732
    // Close socket is called when another connect attempt is possible
733
    // Close is called when the connection should shut down, period
734
    void closeSocket(boolean tryReconnectIfConnected, boolean forceClose) throws InterruptedException {
735
        // Ensure we close the socket exclusively within one thread.
736
        closeSocketLock.lock();
1✔
737
        try {
738
            boolean wasConnected;
739
            statusLock.lock();
1✔
740
            try {
741
                if (isDisconnectingOrClosed()) {
1✔
742
                    waitForDisconnectOrClose(this.options.getConnectionTimeout());
1✔
743
                    return;
1✔
744
                }
745
                this.disconnecting = true;
1✔
746
                this.exceptionDuringConnectChange = null;
1✔
747
                wasConnected = (this.status == Status.CONNECTED);
1✔
748
                statusChanged.signalAll();
1✔
749
            } finally {
750
                statusLock.unlock();
1✔
751
            }
752

753
            closeSocketImpl(forceClose);
1✔
754

755
            statusLock.lock();
1✔
756
            try {
757
                updateStatus(Status.DISCONNECTED);
1✔
758
                this.exceptionDuringConnectChange = null; // Ignore IOExceptions during closeSocketImpl()
1✔
759
                this.disconnecting = false;
1✔
760
                statusChanged.signalAll();
1✔
761
            } finally {
762
                statusLock.unlock();
1✔
763
            }
764

765
            if (isClosing()) { // isClosing() means we are in the close method or were asked to be
1✔
766
                close();
1✔
767
            } else if (wasConnected && tryReconnectIfConnected) {
1✔
768
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
1✔
769
            }
770
        } finally {
771
            closeSocketLock.unlock();
1✔
772
        }
773
    }
1✔
774

775
    // Close socket is called when another connect attempt is possible
776
    // Close is called when the connection should shut down, period
777
    /**
778
     * {@inheritDoc}
779
     */
780
    @Override
781
    public void close() throws InterruptedException {
782
        this.close(true, false);
1✔
783
    }
1✔
784

785
    void close(boolean checkDrainStatus, boolean forceClose) throws InterruptedException {
786
        statusLock.lock();
1✔
787
        try {
788
            if (checkDrainStatus && this.isDraining()) {
1✔
789
                waitForDisconnectOrClose(this.options.getConnectionTimeout());
1✔
790
                return;
1✔
791
            }
792

793
            this.closing = true;// We were asked to close, so do it
1✔
794
            if (isDisconnectingOrClosed()) {
1✔
795
                waitForDisconnectOrClose(this.options.getConnectionTimeout());
1✔
796
                return;
1✔
797
            } else {
798
                this.disconnecting = true;
1✔
799
                this.exceptionDuringConnectChange = null;
1✔
800
                statusChanged.signalAll();
1✔
801
            }
802
        } finally {
803
            statusLock.unlock();
1✔
804
        }
805

806
        // Stop the reconnect wait timer after we stop the writer/reader (only if we are
807
        // really closing, not on errors)
808
        if (this.reconnectWaiter != null) {
1✔
809
            this.reconnectWaiter.cancel(true);
1✔
810
        }
811

812
        closeSocketImpl(forceClose);
1✔
813

814
        this.dispatchers.forEach((nuid, d) -> d.stop(false));
1✔
815

816
        this.subscribers.forEach((sid, sub) -> sub.invalidate());
1✔
817

818
        this.dispatchers.clear();
1✔
819
        this.subscribers.clear();
1✔
820

821
        if (pingTask != null) {
1✔
822
            pingTask.shutdown();
1✔
823
            pingTask = null;
1✔
824
        }
825
        if (cleanupTask != null) {
1✔
826
            cleanupTask.shutdown();
1✔
827
            cleanupTask = null;
1✔
828
        }
829

830
        cleanResponses(true);
1✔
831

832
        cleanUpPongQueue();
1✔
833

834
        statusLock.lock();
1✔
835
        try {
836
            updateStatus(Status.CLOSED); // will signal, we also signal when we stop disconnecting
1✔
837

838
            /*
839
             * if (exceptionDuringConnectChange != null) {
840
             * processException(exceptionDuringConnectChange); exceptionDuringConnectChange
841
             * = null; }
842
             */
843
        } finally {
844
            statusLock.unlock();
1✔
845
        }
846

847
        // Stop the error handling and connect executors
848
        callbackRunner.shutdown();
1✔
849
        try {
850
            callbackRunner.awaitTermination(this.options.getConnectionTimeout().toNanos(), TimeUnit.NANOSECONDS);
1✔
851
        } finally {
852
            callbackRunner.shutdownNow();
1✔
853
        }
854

855
        // There's no need to wait for running tasks since we're told to close
856
        connectExecutor.shutdownNow();
1✔
857

858
        // The callbackRunner and connectExecutor always come from a factory
859
        // so we always shut them down.
860
        // The executor and scheduledExecutor come from a factory iff
861
        // the user does not supply them, so we shut them down in that case.
862
        if (options.executorIsInternal()) {
1✔
863
            executor.shutdownNow();
1✔
864
        }
865
        if (options.scheduledExecutorIsInternal()) {
1✔
866
            scheduledExecutor.shutdownNow();
1✔
867
        }
868

869
        statusLock.lock();
1✔
870
        try {
871
            this.disconnecting = false;
1✔
872
            statusChanged.signalAll();
1✔
873
        } finally {
874
            statusLock.unlock();
1✔
875
        }
876
    }
1✔
877

878
    boolean callbackRunnerIsShutdown() {
879
        return callbackRunner == null || callbackRunner.isShutdown();
1✔
880
    }
881

882
    boolean executorIsShutdown() {
883
        return executor == null || executor.isShutdown();
1✔
884
    }
885

886
    boolean connectExecutorIsShutdown() {
887
        return connectExecutor == null || connectExecutor.isShutdown();
1✔
888
    }
889

890
    boolean scheduledExecutorIsShutdown() {
891
        return scheduledExecutor == null || scheduledExecutor.isShutdown();
1✔
892
    }
893

894
    // Should only be called from closeSocket or close
895
    void closeSocketImpl(boolean forceClose) {
896
        this.currentServer = null;
1✔
897

898
        // Signal both to stop.
899
        final Future<Boolean> readStop = this.reader.stop();
1✔
900
        final Future<Boolean> writeStop = this.writer.stop();
1✔
901

902
        // Now wait until they both stop before closing the socket.
903
        try {
904
            readStop.get(1, TimeUnit.SECONDS);
1✔
905
        } catch (Exception ex) {
1✔
906
            //
907
        }
1✔
908
        try {
909
            writeStop.get(1, TimeUnit.SECONDS);
1✔
910
        } catch (Exception ex) {
1✔
911
            //
912
        }
1✔
913

914
        // Close and reset the current data port and future
915
        if (dataPortFuture != null) {
1✔
916
            dataPortFuture.cancel(true);
1✔
917
            dataPortFuture = null;
1✔
918
        }
919

920
        // Close the current socket and cancel anyone waiting for it
921
        try {
922
            if (dataPort != null) {
1✔
923
                if (forceClose) {
1✔
924
                    dataPort.forceClose();
1✔
925
                }
926
                else {
927
                    dataPort.close();
1✔
928
                }
929
            }
930

931
        } catch (IOException ex) {
×
932
            processException(ex);
×
933
        }
1✔
934
        cleanUpPongQueue();
1✔
935

936
        try {
937
            this.reader.stop().get(10, TimeUnit.SECONDS);
1✔
938
        } catch (Exception ex) {
×
939
            processException(ex);
×
940
        }
1✔
941
        try {
942
            this.writer.stop().get(10, TimeUnit.SECONDS);
1✔
943
        } catch (Exception ex) {
1✔
944
            processException(ex);
1✔
945
        }
1✔
946
    }
1✔
947

948
    void cleanUpPongQueue() {
949
        Future<Boolean> b;
950
        while ((b = pongQueue.poll()) != null) {
1✔
951
            b.cancel(true);
1✔
952
        }
953
    }
1✔
954

955
    /**
956
     * {@inheritDoc}
957
     */
958
    @Override
959
    public void publish(String subject, byte[] body) {
960
        publishInternal(subject, null, null, body, true, false);
1✔
961
    }
1✔
962

963
    /**
964
     * {@inheritDoc}
965
     */
966
    @Override
967
    public void publish(String subject, Headers headers, byte[] body) {
968
        publishInternal(subject, null, headers, body, true, false);
1✔
969
    }
1✔
970

971
    /**
972
     * {@inheritDoc}
973
     */
974
    @Override
975
    public void publish(String subject, String replyTo, byte[] body) {
976
        publishInternal(subject, replyTo, null, body, true, false);
1✔
977
    }
1✔
978

979
    /**
980
     * {@inheritDoc}
981
     */
982
    @Override
983
    public void publish(String subject, String replyTo, Headers headers, byte[] body) {
984
        publishInternal(subject, replyTo, headers, body, true, false);
1✔
985
    }
1✔
986

987
    /**
988
     * {@inheritDoc}
989
     */
990
    @Override
991
    public void publish(Message message) {
992
        validateNotNull(message, "Message");
1✔
993
        publishInternal(message.getSubject(), message.getReplyTo(), message.getHeaders(), message.getData(), false, false);
1✔
994
    }
1✔
995

996
    void publishInternal(String subject, String replyTo, Headers headers, byte[] data, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) {
997
        checkPayloadSize(data);
1✔
998
        NatsPublishableMessage npm = new NatsPublishableMessage(subject, replyTo, headers, data, validateSubjectAndReplyTo, flushImmediatelyAfterPublish);
1✔
999
        if (npm.hasHeaders && !serverInfo.get().isHeadersSupported()) {
1✔
1000
            throw new IllegalArgumentException("Headers are not supported by the server, version: " + serverInfo.get().getVersion());
1✔
1001
        }
1002

1003
        if (isClosed()) {
1✔
1004
            throw new IllegalStateException("Connection is Closed");
1✔
1005
        } else if (blockPublishForDrain.get()) {
1✔
1006
            throw new IllegalStateException("Connection is Draining"); // Ok to publish while waiting on subs
×
1007
        }
1008

1009
        if ((status == Status.RECONNECTING || status == Status.DISCONNECTED)
1✔
1010
                && !this.writer.canQueueDuringReconnect(npm)) {
1✔
1011
            throw new IllegalStateException(
1✔
1012
                    "Unable to queue any more messages during reconnect, max buffer is " + options.getReconnectBufferSize());
1✔
1013
        }
1014

1015
        queueOutgoing(npm);
1✔
1016
    }
1✔
1017

1018
    private void checkPayloadSize(byte[] body) {
1019
        if (options.clientSideLimitChecks() && body != null && body.length > this.getMaxPayload() && this.getMaxPayload() > 0) {
1✔
1020
            throw new IllegalArgumentException(
1✔
1021
                "Message payload size exceed server configuration " + body.length + " vs " + this.getMaxPayload());
1✔
1022
        }
1023
    }
1✔
1024
    /**
1025
     * {@inheritDoc}
1026
     */
1027
    @Override
1028
    public Subscription subscribe(String subject) {
1029
        validateSubject(subject, true);
1✔
1030
        return createSubscription(subject, null, null, null);
1✔
1031
    }
1032

1033
    /**
1034
     * {@inheritDoc}
1035
     */
1036
    @Override
1037
    public Subscription subscribe(String subject, String queueName) {
1038
        validateSubject(subject, true);
1✔
1039
        validateQueueName(queueName, true);
1✔
1040
        return createSubscription(subject, queueName, null, null);
1✔
1041
    }
1042

1043
    void invalidate(NatsSubscription sub) {
1044
        remove(sub);
1✔
1045
        sub.invalidate();
1✔
1046
    }
1✔
1047

1048
    void remove(NatsSubscription sub) {
1049
        CharSequence sid = sub.getSID();
1✔
1050
        subscribers.remove(sid);
1✔
1051

1052
        if (sub.getNatsDispatcher() != null) {
1✔
1053
            sub.getNatsDispatcher().remove(sub);
1✔
1054
        }
1055
    }
1✔
1056

1057
    void unsubscribe(NatsSubscription sub, int after) {
1058
        if (isClosed()) { // last chance, usually sub will catch this
1✔
1059
            throw new IllegalStateException("Connection is Closed");
×
1060
        }
1061

1062
        if (after <= 0) {
1✔
1063
            this.invalidate(sub); // Will clean it up
1✔
1064
        } else {
1065
            sub.setUnsubLimit(after);
1✔
1066

1067
            if (sub.reachedUnsubLimit()) {
1✔
1068
                sub.invalidate();
1✔
1069
            }
1070
        }
1071

1072
        if (!isConnected()) {
1✔
1073
            return; // We will set up sub on reconnect or ignore
1✔
1074
        }
1075

1076
        sendUnsub(sub, after);
1✔
1077
    }
1✔
1078

1079
    void sendUnsub(NatsSubscription sub, int after) {
1080
        ByteArrayBuilder bab =
1✔
1081
            new ByteArrayBuilder().append(UNSUB_SP_BYTES).append(sub.getSID());
1✔
1082
        if (after > 0) {
1✔
1083
            bab.append(SP).append(after);
1✔
1084
        }
1085
        queueOutgoing(new ProtocolMessage(bab, true));
1✔
1086
    }
1✔
1087

1088
    // Assumes the null/empty checks were handled elsewhere
1089
    NatsSubscription createSubscription(String subject, String queueName, NatsDispatcher dispatcher, NatsSubscriptionFactory factory) {
1090
        if (isClosed()) {
1✔
1091
            throw new IllegalStateException("Connection is Closed");
1✔
1092
        } else if (isDraining() && (dispatcher == null || dispatcher != this.inboxDispatcher.get())) {
1✔
1093
            throw new IllegalStateException("Connection is Draining");
1✔
1094
        }
1095

1096
        NatsSubscription sub;
1097
        String sid = getNextSid();
1✔
1098

1099
        if (factory == null) {
1✔
1100
            sub = new NatsSubscription(sid, subject, queueName, this, dispatcher);
1✔
1101
        }
1102
        else {
1103
            sub = factory.createNatsSubscription(sid, subject, queueName, this, dispatcher);
1✔
1104
        }
1105
        subscribers.put(sid, sub);
1✔
1106

1107
        sendSubscriptionMessage(sid, subject, queueName, false);
1✔
1108
        return sub;
1✔
1109
    }
1110

1111
    String getNextSid() {
1112
        return Long.toString(nextSid.getAndIncrement());
1✔
1113
    }
1114

1115
    String reSubscribe(NatsSubscription sub, String subject, String queueName) {
1116
        String sid = getNextSid();
1✔
1117
        sendSubscriptionMessage(sid, subject, queueName, false);
1✔
1118
        subscribers.put(sid, sub);
1✔
1119
        return sid;
1✔
1120
    }
1121

1122
    void sendSubscriptionMessage(String sid, String subject, String queueName, boolean treatAsInternal) {
1123
        if (!isConnected()) {
1✔
1124
            return; // We will set up sub on reconnect or ignore
1✔
1125
        }
1126

1127
        ByteArrayBuilder bab = new ByteArrayBuilder(UTF_8).append(SUB_SP_BYTES).append(subject);
1✔
1128
        if (queueName != null) {
1✔
1129
            bab.append(SP).append(queueName);
1✔
1130
        }
1131
        bab.append(SP).append(sid);
1✔
1132

1133
        // setting this to filter on stop.
1134
        // if it's an "internal" message, it won't be filtered
1135
        // if it's a normal message, the subscription will already be registered
1136
        // and therefore will be re-subscribed after a stop anyway
1137
        ProtocolMessage subMsg = new ProtocolMessage(bab, true);
1✔
1138
        if (treatAsInternal) {
1✔
1139
            queueInternalOutgoing(subMsg);
1✔
1140
        } else {
1141
            queueOutgoing(subMsg);
1✔
1142
        }
1143
    }
1✔
1144

1145
    /**
1146
     * {@inheritDoc}
1147
     */
1148
    @Override
1149
    public String createInbox() {
1150
        return options.getInboxPrefix() + nuid.next();
1✔
1151
    }
1152

1153
    int getRespInboxLength() {
1154
        return options.getInboxPrefix().length() + 22 + 1; // 22 for nuid, 1 for .
1✔
1155
    }
1156

1157
    String createResponseInbox(String inbox) {
1158
        // Substring gets rid of the * [trailing]
1159
        return inbox.substring(0, getRespInboxLength()) + nuid.next();
1✔
1160
    }
1161

1162
    // If the inbox is long enough, pull out the end part, otherwise, just use the
1163
    // full thing
1164
    String getResponseToken(String responseInbox) {
1165
        int len = getRespInboxLength();
1✔
1166
        if (responseInbox.length() <= len) {
1✔
1167
            return responseInbox;
1✔
1168
        }
1169
        return responseInbox.substring(len);
1✔
1170
    }
1171

1172
    void cleanResponses(boolean closing) {
1173
        ArrayList<String> toRemove = new ArrayList<>();
1✔
1174
        boolean wasInterrupted = false;
1✔
1175

1176
        for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesAwaiting.entrySet()) {
1✔
1177
            boolean remove = false;
1✔
1178
            NatsRequestCompletableFuture future = entry.getValue();
1✔
1179
            if (future.hasExceededTimeout()) {
1✔
1180
                remove = true;
1✔
1181
                future.cancelTimedOut();
1✔
1182
            }
1183
            else if (closing) {
1✔
1184
                remove = true;
1✔
1185
                future.cancelClosing();
1✔
1186
            }
1187
            else if (future.isDone()) {
1✔
1188
                // done should have already been removed, not sure if
1189
                // this even needs checking, but it won't hurt
1190
                remove = true;
1✔
1191
                try {
UNCOV
1192
                    future.get();
×
1193
                }
1194
                catch (InterruptedException e) {
×
1195
                    Thread.currentThread().interrupt();
×
1196
                    // we might have collected some entries already, but were interrupted
1197
                    // break out so we finish as quick as possible
1198
                    // cleanResponses will be called again anyway
1199
                    wasInterrupted = true;
×
1200
                    break;
×
1201
                }
1202
                catch (Throwable ignore) {}
1✔
1203
            }
1204

1205
            if (remove) {
1✔
1206
                toRemove.add(entry.getKey());
1✔
1207
                statistics.decrementOutstandingRequests();
1✔
1208
            }
1209
        }
1✔
1210

1211
        for (String key : toRemove) {
1✔
1212
            responsesAwaiting.remove(key);
1✔
1213
        }
1✔
1214

1215
        if (advancedTracking && !wasInterrupted) {
1✔
1216
            toRemove.clear(); // just reuse this
1✔
1217
            for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesRespondedTo.entrySet()) {
1✔
1218
                NatsRequestCompletableFuture future = entry.getValue();
1✔
1219
                if (future.hasExceededTimeout()) {
1✔
1220
                    toRemove.add(entry.getKey());
1✔
1221
                    future.cancelTimedOut();
1✔
1222
                }
1223
            }
1✔
1224

1225
            for (String token : toRemove) {
1✔
1226
                responsesRespondedTo.remove(token);
1✔
1227
            }
1✔
1228
        }
1229
    }
1✔
1230

1231
    /**
1232
     * {@inheritDoc}
1233
     */
1234
    @Override
1235
    public Message request(String subject, byte[] body, Duration timeout) throws InterruptedException {
1236
        return requestInternal(subject, null, body, timeout, cancelAction, true, forceFlushOnRequest);
1✔
1237
    }
1238

1239
    /**
1240
     * {@inheritDoc}
1241
     */
1242
    @Override
1243
    public Message request(String subject, Headers headers, byte[] body, Duration timeout) throws InterruptedException {
1244
        return requestInternal(subject, headers, body, timeout, cancelAction, true, forceFlushOnRequest);
1✔
1245
    }
1246

1247
    /**
1248
     * {@inheritDoc}
1249
     */
1250
    @Override
1251
    public Message request(Message message, Duration timeout) throws InterruptedException {
1252
        validateNotNull(message, "Message");
1✔
1253
        return requestInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false, forceFlushOnRequest);
1✔
1254
    }
1255

1256
    Message requestInternal(String subject, Headers headers, byte[] data, Duration timeout,
1257
                            CancelAction cancelAction, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) throws InterruptedException {
1258
        CompletableFuture<Message> incoming = requestFutureInternal(subject, headers, data, timeout, cancelAction, validateSubjectAndReplyTo, flushImmediatelyAfterPublish);
1✔
1259
        try {
1260
            return incoming.get(timeout.toNanos(), TimeUnit.NANOSECONDS);
1✔
1261
        } catch (TimeoutException | ExecutionException | CancellationException e) {
1✔
1262
            return null;
1✔
1263
        }
1264
    }
1265

1266
    /**
1267
     * {@inheritDoc}
1268
     */
1269
    @Override
1270
    public CompletableFuture<Message> request(String subject, byte[] body) {
1271
        return requestFutureInternal(subject, null, body, null, cancelAction, true, forceFlushOnRequest);
1✔
1272
    }
1273

1274
    /**
1275
     * {@inheritDoc}
1276
     */
1277
    @Override
1278
    public CompletableFuture<Message> request(String subject, Headers headers, byte[] body) {
1279
        return requestFutureInternal(subject, headers, body, null, cancelAction, true, forceFlushOnRequest);
1✔
1280
    }
1281

1282
    /**
1283
     * {@inheritDoc}
1284
     */
1285
    @Override
1286
    public CompletableFuture<Message> requestWithTimeout(String subject, byte[] body, Duration timeout) {
1287
        return requestFutureInternal(subject, null, body, timeout, cancelAction, true, forceFlushOnRequest);
1✔
1288
    }
1289

1290
    /**
1291
     * {@inheritDoc}
1292
     */
1293
    @Override
1294
    public CompletableFuture<Message> requestWithTimeout(String subject, Headers headers, byte[] body, Duration timeout) {
1295
        return requestFutureInternal(subject, headers, body, timeout, cancelAction, true, forceFlushOnRequest);
1✔
1296
    }
1297

1298
    /**
1299
     * {@inheritDoc}
1300
     */
1301
    @Override
1302
    public CompletableFuture<Message> requestWithTimeout(Message message, Duration timeout) {
1303
        validateNotNull(message, "Message");
1✔
1304
        return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false, forceFlushOnRequest);
1✔
1305
    }
1306

1307
    /**
1308
     * {@inheritDoc}
1309
     */
1310
    @Override
1311
    public CompletableFuture<Message> request(Message message) {
1312
        validateNotNull(message, "Message");
1✔
1313
        return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), null, cancelAction, false, forceFlushOnRequest);
1✔
1314
    }
1315

1316
    CompletableFuture<Message> requestFutureInternal(String subject, Headers headers, byte[] data, Duration futureTimeout,
1317
                                                     CancelAction cancelAction, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) {
1318
        checkPayloadSize(data);
1✔
1319

1320
        if (isClosed()) {
1✔
1321
            throw new IllegalStateException("Connection is Closed");
1✔
1322
        } else if (isDraining()) {
1✔
1323
            throw new IllegalStateException("Connection is Draining");
1✔
1324
        }
1325

1326
        if (inboxDispatcher.get() == null) {
1✔
1327
            inboxDispatcherLock.lock();
1✔
1328
            try {
1329
                if (inboxDispatcher.get() == null) {
1✔
1330
                    NatsDispatcher d = dispatcherFactory.createDispatcher(this, this::deliverReply);
1✔
1331

1332
                    // Ensure the dispatcher is started before publishing messages
1333
                    String id = this.nuid.next();
1✔
1334
                    this.dispatchers.put(id, d);
1✔
1335
                    d.start(id);
1✔
1336
                    d.subscribe(this.mainInbox);
1✔
1337
                    inboxDispatcher.set(d);
1✔
1338
                }
1339
            } finally {
1340
                inboxDispatcherLock.unlock();
1✔
1341
            }
1342
        }
1343

1344
        boolean oldStyle = options.isOldRequestStyle();
1✔
1345
        String responseInbox = oldStyle ? createInbox() : createResponseInbox(this.mainInbox);
1✔
1346
        String responseToken = getResponseToken(responseInbox);
1✔
1347
        NatsRequestCompletableFuture future =
1✔
1348
            new NatsRequestCompletableFuture(cancelAction,
1349
                futureTimeout == null ? options.getRequestCleanupInterval() : futureTimeout, options.useTimeoutException());
1✔
1350

1351
        if (!oldStyle) {
1✔
1352
            responsesAwaiting.put(responseToken, future);
1✔
1353
        }
1354
        statistics.incrementOutstandingRequests();
1✔
1355

1356
        if (oldStyle) {
1✔
1357
            NatsDispatcher dispatcher = this.inboxDispatcher.get();
1✔
1358
            NatsSubscription sub = dispatcher.subscribeReturningSubscription(responseInbox);
1✔
1359
            dispatcher.unsubscribe(responseInbox, 1);
1✔
1360
            // Unsubscribe when future is cancelled:
1361
            future.whenComplete((msg, exception) -> {
1✔
1362
                if (exception instanceof CancellationException) {
1✔
1363
                    dispatcher.unsubscribe(responseInbox);
×
1364
                }
1365
            });
1✔
1366
            responsesAwaiting.put(sub.getSID(), future);
1✔
1367
        }
1368

1369
        publishInternal(subject, responseInbox, headers, data, validateSubjectAndReplyTo, flushImmediatelyAfterPublish);
1✔
1370
        statistics.incrementRequestsSent();
1✔
1371

1372
        return future;
1✔
1373
    }
1374

1375
    void deliverReply(Message msg) {
1376
        boolean oldStyle = options.isOldRequestStyle();
1✔
1377
        String subject = msg.getSubject();
1✔
1378
        String token = getResponseToken(subject);
1✔
1379
        String key = oldStyle ? msg.getSID() : token;
1✔
1380
        NatsRequestCompletableFuture f = responsesAwaiting.remove(key);
1✔
1381
        if (f != null) {
1✔
1382
            if (advancedTracking) {
1✔
1383
                responsesRespondedTo.put(key, f);
1✔
1384
            }
1385
            statistics.decrementOutstandingRequests();
1✔
1386
            if (msg.isStatusMessage() && msg.getStatus().getCode() == 503) {
1✔
1387
                switch (f.getCancelAction()) {
1✔
1388
                    case COMPLETE:
1389
                        f.complete(msg);
1✔
1390
                        break;
1✔
1391
                    case REPORT:
1392
                        f.completeExceptionally(new JetStreamStatusException(msg.getStatus()));
1✔
1393
                        break;
1✔
1394
                    case CANCEL:
1395
                    default:
1396
                        f.cancel(true);
1✔
1397
                }
1398
            }
1399
            else {
1400
                f.complete(msg);
1✔
1401
            }
1402
            statistics.incrementRepliesReceived();
1✔
1403
        }
1404
        else if (!oldStyle && !subject.startsWith(mainInbox)) {
1✔
1405
            if (advancedTracking) {
1✔
1406
                if (responsesRespondedTo.get(key) != null) {
1✔
1407
                    statistics.incrementDuplicateRepliesReceived();
1✔
1408
                } else {
1409
                    statistics.incrementOrphanRepliesReceived();
1✔
1410
                }
1411
            }
1412
        }
1413
    }
1✔
1414

1415
    public Dispatcher createDispatcher() {
1416
        return createDispatcher(null);
1✔
1417
    }
1418

1419
    public Dispatcher createDispatcher(MessageHandler handler) {
1420
        if (isClosed()) {
1✔
1421
            throw new IllegalStateException("Connection is Closed");
1✔
1422
        } else if (isDraining()) {
1✔
1423
            throw new IllegalStateException("Connection is Draining");
1✔
1424
        }
1425

1426
        NatsDispatcher dispatcher = dispatcherFactory.createDispatcher(this, handler);
1✔
1427
        String id = this.nuid.next();
1✔
1428
        this.dispatchers.put(id, dispatcher);
1✔
1429
        dispatcher.start(id);
1✔
1430
        return dispatcher;
1✔
1431
    }
1432

1433
    public void closeDispatcher(Dispatcher d) {
1434
        if (isClosed()) {
1✔
1435
            throw new IllegalStateException("Connection is Closed");
1✔
1436
        } else if (!(d instanceof NatsDispatcher)) {
1✔
1437
            throw new IllegalArgumentException("Connection can only manage its own dispatchers");
×
1438
        }
1439

1440
        NatsDispatcher nd = ((NatsDispatcher) d);
1✔
1441

1442
        if (nd.isDraining()) {
1✔
1443
            return; // No op while draining
1✔
1444
        }
1445

1446
        if (!this.dispatchers.containsKey(nd.getId())) {
1✔
1447
            throw new IllegalArgumentException("Dispatcher is already closed.");
1✔
1448
        }
1449

1450
        cleanupDispatcher(nd);
1✔
1451
    }
1✔
1452

1453
    void cleanupDispatcher(NatsDispatcher nd) {
1454
        nd.stop(true);
1✔
1455
        this.dispatchers.remove(nd.getId());
1✔
1456
    }
1✔
1457

1458
    Map<String, Dispatcher> getDispatchers() {
1459
        return Collections.unmodifiableMap(dispatchers);
1✔
1460
    }
1461

1462
    public void addConnectionListener(ConnectionListener connectionListener) {
1463
        connectionListeners.add(connectionListener);
1✔
1464
    }
1✔
1465

1466
    public void removeConnectionListener(ConnectionListener connectionListener) {
1467
        connectionListeners.remove(connectionListener);
1✔
1468
    }
1✔
1469

1470
    public void flush(Duration timeout) throws TimeoutException, InterruptedException {
1471

1472
        Instant start = Instant.now();
1✔
1473
        waitForConnectOrClose(timeout);
1✔
1474

1475
        if (isClosed()) {
1✔
1476
            throw new TimeoutException("Attempted to flush while closed");
1✔
1477
        }
1478

1479
        if (timeout == null) {
1✔
1480
            timeout = Duration.ZERO;
1✔
1481
        }
1482

1483
        Instant now = Instant.now();
1✔
1484
        Duration waitTime = Duration.between(start, now);
1✔
1485

1486
        if (!timeout.equals(Duration.ZERO) && waitTime.compareTo(timeout) >= 0) {
1✔
1487
            throw new TimeoutException("Timeout out waiting for connection before flush.");
1✔
1488
        }
1489

1490
        try {
1491
            Future<Boolean> waitForIt = sendPing();
1✔
1492

1493
            if (waitForIt == null) { // error in the send ping code
1✔
1494
                return;
×
1495
            }
1496

1497
            long nanos = timeout.toNanos();
1✔
1498

1499
            if (nanos > 0) {
1✔
1500

1501
                nanos -= waitTime.toNanos();
1✔
1502

1503
                if (nanos <= 0) {
1✔
1504
                    nanos = 1; // let the future timeout if it isn't resolved
×
1505
                }
1506

1507
                waitForIt.get(nanos, TimeUnit.NANOSECONDS);
1✔
1508
            } else {
1509
                waitForIt.get();
1✔
1510
            }
1511

1512
            this.statistics.incrementFlushCounter();
1✔
1513
        } catch (ExecutionException | CancellationException e) {
1✔
1514
            throw new TimeoutException(e.toString());
1✔
1515
        }
1✔
1516
    }
1✔
1517

1518
    void sendConnect(NatsUri nuri) throws IOException {
1519
        try {
1520
            ServerInfo info = this.serverInfo.get();
1✔
1521
            // This is changed - we used to use info.isAuthRequired(), but are changing it to
1522
            // better match older versions of the server. It may change again in the future.
1523
            CharBuffer connectOptions = options.buildProtocolConnectOptionsString(
1✔
1524
                nuri.toString(), true, info.getNonce());
1✔
1525
            ByteArrayBuilder bab =
1✔
1526
                new ByteArrayBuilder(OP_CONNECT_SP_LEN + connectOptions.limit(), UTF_8)
1✔
1527
                    .append(CONNECT_SP_BYTES).append(connectOptions);
1✔
1528
            queueInternalOutgoing(new ProtocolMessage(bab, false));
1✔
1529
        } catch (Exception exp) {
1✔
1530
            throw new IOException("Error sending connect string", exp);
1✔
1531
        }
1✔
1532
    }
1✔
1533

1534
    CompletableFuture<Boolean> sendPing() {
1535
        return this.sendPing(true);
1✔
1536
    }
1537

1538
    CompletableFuture<Boolean> softPing() {
1539
        return this.sendPing(false);
1✔
1540
    }
1541

1542
    /**
1543
     * {@inheritDoc}
1544
     */
1545
    @Override
1546
    public Duration RTT() throws IOException {
1547
        if (!isConnectedOrConnecting()) {
1✔
1548
            throw new IOException("Must be connected to do RTT.");
1✔
1549
        }
1550

1551
        long timeout = options.getConnectionTimeout().toMillis();
1✔
1552
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
1✔
1553
        pongQueue.add(pongFuture);
1✔
1554
        try {
1555
            long time = NatsSystemClock.nanoTime();
1✔
1556
            writer.queueInternalMessage(new ProtocolMessage(PING_PROTO));
1✔
1557
            pongFuture.get(timeout, TimeUnit.MILLISECONDS);
1✔
1558
            return Duration.ofNanos(NatsSystemClock.nanoTime() - time);
1✔
1559
        }
1560
        catch (ExecutionException e) {
×
1561
            throw new IOException(e.getCause());
×
1562
        }
1563
        catch (TimeoutException e) {
×
1564
            throw new IOException(e);
×
1565
        }
1566
        catch (InterruptedException e) {
×
1567
            Thread.currentThread().interrupt();
×
1568
            throw new IOException(e);
×
1569
        }
1570
    }
1571

1572
    // Send a ping request and push a pong future on the queue.
1573
    // futures are completed in order, keep this one if a thread wants to wait
1574
    // for a specific pong. Note, if no pong returns the wait will not return
1575
    // without setting a timeout.
1576
    CompletableFuture<Boolean> sendPing(boolean treatAsInternal) {
1577
        if (!isConnectedOrConnecting()) {
1✔
1578
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
1✔
1579
            retVal.complete(Boolean.FALSE);
1✔
1580
            return retVal;
1✔
1581
        }
1582

1583
        if (!treatAsInternal && !this.needPing.get()) {
1✔
1584
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
1✔
1585
            retVal.complete(Boolean.TRUE);
1✔
1586
            this.needPing.set(true);
1✔
1587
            return retVal;
1✔
1588
        }
1589

1590
        int max = options.getMaxPingsOut();
1✔
1591
        if (max > 0 && pongQueue.size() + 1 > max) {
1✔
1592
            handleCommunicationIssue(new IllegalStateException("Max outgoing Ping count exceeded."));
1✔
1593
            return null;
1✔
1594
        }
1595

1596
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
1✔
1597
        pongQueue.add(pongFuture);
1✔
1598

1599
        if (treatAsInternal) {
1✔
1600
            queueInternalOutgoing(new ProtocolMessage(PING_PROTO));
1✔
1601
        } else {
1602
            queueOutgoing(new ProtocolMessage(PING_PROTO));
1✔
1603
        }
1604

1605
        this.needPing.set(true);
1✔
1606
        this.statistics.incrementPingCount();
1✔
1607
        return pongFuture;
1✔
1608
    }
1609

1610
    // This is a minor speed / memory enhancement.
1611
    // We can't reuse the same instance of any NatsMessage b/c of the "NatsMessage next" state
1612
    // But it is safe to share the data bytes and the size since those fields are just being read
1613
    // This constructor "ProtocolMessage(ProtocolMessage pm)" shares the data and size
1614
    // reducing allocation of data for something that is often created and used
1615
    // These static instances are the once that are used for copying, sendPing and sendPong
1616
    private static final ProtocolMessage PING_PROTO = new ProtocolMessage(OP_PING_BYTES);
1✔
1617
    private static final ProtocolMessage PONG_PROTO = new ProtocolMessage(OP_PONG_BYTES);
1✔
1618

1619
    void sendPong() {
1620
        queueInternalOutgoing(new ProtocolMessage(PONG_PROTO));
1✔
1621
    }
1✔
1622

1623
    // Called by the reader
1624
    void handlePong() {
1625
        CompletableFuture<Boolean> pongFuture = pongQueue.pollFirst();
1✔
1626
        if (pongFuture != null) {
1✔
1627
            pongFuture.complete(Boolean.TRUE);
1✔
1628
        }
1629
    }
1✔
1630

1631
    void readInitialInfo() throws IOException {
1632
        byte[] readBuffer = new byte[options.getBufferSize()];
1✔
1633
        ByteBuffer protocolBuffer = ByteBuffer.allocate(options.getBufferSize());
1✔
1634
        boolean gotCRLF = false;
1✔
1635
        boolean gotCR = false;
1✔
1636

1637
        while (!gotCRLF) {
1✔
1638
            int read = this.dataPort.read(readBuffer, 0, readBuffer.length);
1✔
1639

1640
            if (read < 0) {
1✔
1641
                break;
1✔
1642
            }
1643

1644
            int i = 0;
1✔
1645
            while (i < read) {
1✔
1646
                byte b = readBuffer[i++];
1✔
1647

1648
                if (gotCR) {
1✔
1649
                    if (b != LF) {
1✔
1650
                        throw new IOException("Missed LF after CR waiting for INFO.");
1✔
1651
                    } else if (i < read) {
1✔
1652
                        throw new IOException("Read past initial info message.");
1✔
1653
                    }
1654

1655
                    gotCRLF = true;
1✔
1656
                    break;
1✔
1657
                }
1658

1659
                if (b == CR) {
1✔
1660
                    gotCR = true;
1✔
1661
                } else {
1662
                    if (!protocolBuffer.hasRemaining()) {
1✔
1663
                        protocolBuffer = enlargeBuffer(protocolBuffer); // just double it
1✔
1664
                    }
1665
                    protocolBuffer.put(b);
1✔
1666
                }
1667
            }
1✔
1668
        }
1✔
1669

1670
        if (!gotCRLF) {
1✔
1671
            throw new IOException("Failed to read initial info message.");
1✔
1672
        }
1673

1674
        protocolBuffer.flip();
1✔
1675

1676
        String infoJson = UTF_8.decode(protocolBuffer).toString();
1✔
1677
        infoJson = infoJson.trim();
1✔
1678
        String[] msg = infoJson.split("\\s");
1✔
1679
        String op = msg[0].toUpperCase();
1✔
1680

1681
        if (!OP_INFO.equals(op)) {
1✔
1682
            throw new IOException("Received non-info initial message.");
1✔
1683
        }
1684

1685
        handleInfo(infoJson);
1✔
1686
    }
1✔
1687

1688
    void handleInfo(String infoJson) {
1689
        ServerInfo serverInfo = new ServerInfo(infoJson);
1✔
1690
        this.serverInfo.set(serverInfo);
1✔
1691

1692
        List<String> urls = this.serverInfo.get().getConnectURLs();
1✔
1693
        if (urls != null && !urls.isEmpty()) {
1✔
1694
            if (serverPool.acceptDiscoveredUrls(urls)) {
1✔
1695
                processConnectionEvent(Events.DISCOVERED_SERVERS);
1✔
1696
            }
1697
        }
1698

1699
        if (serverInfo.isLameDuckMode()) {
1✔
1700
            processConnectionEvent(Events.LAME_DUCK);
1✔
1701
        }
1702
    }
1✔
1703

1704
    void queueOutgoing(NatsMessage msg) {
1705
        if (msg.getControlLineLength() > this.options.getMaxControlLine()) {
1✔
1706
            throw new IllegalArgumentException("Control line is too long");
1✔
1707
        }
1708
        if (!writer.queue(msg)) {
1✔
1709
            options.getErrorListener().messageDiscarded(this, msg);
1✔
1710
        }
1711
    }
1✔
1712

1713
    void queueInternalOutgoing(NatsMessage msg) {
1714
        if (msg.getControlLineLength() > this.options.getMaxControlLine()) {
1✔
1715
            throw new IllegalArgumentException("Control line is too long");
×
1716
        }
1717
        this.writer.queueInternalMessage(msg);
1✔
1718
    }
1✔
1719

1720
    void deliverMessage(NatsMessage msg) {
1721
        this.needPing.set(false);
1✔
1722
        this.statistics.incrementInMsgs();
1✔
1723
        this.statistics.incrementInBytes(msg.getSizeInBytes());
1✔
1724

1725
        NatsSubscription sub = subscribers.get(msg.getSID());
1✔
1726

1727
        if (sub != null) {
1✔
1728
            msg.setSubscription(sub);
1✔
1729

1730
            NatsDispatcher d = sub.getNatsDispatcher();
1✔
1731
            NatsConsumer c = (d == null) ? sub : d;
1✔
1732
            MessageQueue q = ((d == null) ? sub.getMessageQueue() : d.getMessageQueue());
1✔
1733

1734
            if (c.hasReachedPendingLimits()) {
1✔
1735
                // Drop the message and count it
1736
                this.statistics.incrementDroppedCount();
1✔
1737
                c.incrementDroppedCount();
1✔
1738

1739
                // Notify the first time
1740
                if (!c.isMarkedSlow()) {
1✔
1741
                    c.markSlow();
1✔
1742
                    processSlowConsumer(c);
1✔
1743
                }
1744
            } else if (q != null) {
1✔
1745
                c.markNotSlow();
1✔
1746

1747
                // beforeQueueProcessor returns true if the message is allowed to be queued
1748
                if (sub.getBeforeQueueProcessor().apply(msg)) {
1✔
1749
                    q.push(msg);
1✔
1750
                }
1751
            }
1752

1753
        }
1754
//        else {
1755
//            // Drop messages we don't have a subscriber for (could be extras on an
1756
//            // auto-unsub for example)
1757
//        }
1758
    }
1✔
1759

1760
    void processOK() {
1761
        this.statistics.incrementOkCount();
1✔
1762
    }
1✔
1763

1764
    void processSlowConsumer(Consumer consumer) {
1765
        if (!this.callbackRunner.isShutdown()) {
1✔
1766
            try {
1767
                this.callbackRunner.execute(() -> {
1✔
1768
                    try {
1769
                        options.getErrorListener().slowConsumerDetected(this, consumer);
1✔
1770
                    } catch (Exception ex) {
1✔
1771
                        this.statistics.incrementExceptionCount();
1✔
1772
                    }
1✔
1773
                });
1✔
1774
            } catch (RejectedExecutionException re) {
×
1775
                // Timing with shutdown, let it go
1776
            }
1✔
1777
        }
1778
    }
1✔
1779

1780
    void processException(Exception exp) {
1781
        this.statistics.incrementExceptionCount();
1✔
1782

1783
        if (!this.callbackRunner.isShutdown()) {
1✔
1784
            try {
1785
                this.callbackRunner.execute(() -> {
1✔
1786
                    try {
1787
                        options.getErrorListener().exceptionOccurred(this, exp);
1✔
1788
                    } catch (Exception ex) {
1✔
1789
                        this.statistics.incrementExceptionCount();
1✔
1790
                    }
1✔
1791
                });
1✔
UNCOV
1792
            } catch (RejectedExecutionException re) {
×
1793
                // Timing with shutdown, let it go
1794
            }
1✔
1795
        }
1796
    }
1✔
1797

1798
    void processError(String errorText) {
1799
        this.statistics.incrementErrCount();
1✔
1800

1801
        this.lastError.set(errorText);
1✔
1802
        this.connectError.set(errorText); // even if this isn't during connection, save it just in case
1✔
1803

1804
        // If we are connected && we get an authentication error, save it
1805
        if (this.isConnected() && this.isAuthenticationError(errorText) && currentServer != null) {
1✔
1806
            this.serverAuthErrors.put(currentServer, errorText);
1✔
1807
        }
1808

1809
        if (!this.callbackRunner.isShutdown()) {
1✔
1810
            try {
1811
                this.callbackRunner.execute(() -> {
1✔
1812
                    try {
1813
                        options.getErrorListener().errorOccurred(this, errorText);
1✔
1814
                    } catch (Exception ex) {
1✔
1815
                        this.statistics.incrementExceptionCount();
1✔
1816
                    }
1✔
1817
                });
1✔
1818
            } catch (RejectedExecutionException re) {
×
1819
                // Timing with shutdown, let it go
1820
            }
1✔
1821
        }
1822
    }
1✔
1823

1824
    interface ErrorListenerCaller {
1825
        void call(Connection conn, ErrorListener el);
1826
    }
1827

1828
    void executeCallback(ErrorListenerCaller elc) {
1829
        if (!this.callbackRunner.isShutdown()) {
1✔
1830
            try {
1831
                this.callbackRunner.execute(() -> elc.call(this, options.getErrorListener()));
1✔
1832
            } catch (RejectedExecutionException re) {
×
1833
                // Timing with shutdown, let it go
1834
            }
1✔
1835
        }
1836
    }
1✔
1837

1838
    void processConnectionEvent(Events type) {
1839
        if (!this.callbackRunner.isShutdown()) {
1✔
1840
            try {
1841
                for (ConnectionListener listener : connectionListeners) {
1✔
1842
                    this.callbackRunner.execute(() -> {
1✔
1843
                        try {
1844
                            listener.connectionEvent(this, type);
1✔
1845
                        } catch (Exception ex) {
1✔
1846
                            this.statistics.incrementExceptionCount();
1✔
1847
                        }
1✔
1848
                    });
1✔
1849
                }
1✔
1850
            } catch (RejectedExecutionException re) {
1✔
1851
                // Timing with shutdown, let it go
1852
            }
1✔
1853
        }
1854
    }
1✔
1855

1856
    /**
1857
     * {@inheritDoc}
1858
     */
1859
    @Override
1860
    public ServerInfo getServerInfo() {
1861
        return getInfo();
1✔
1862
    }
1863

1864
    /**
1865
     * {@inheritDoc}
1866
     */
1867
    @Override
1868
    public InetAddress getClientInetAddress() {
1869
        try {
1870
            return InetAddress.getByName(getInfo().getClientIp());
1✔
1871
        }
1872
        catch (Exception e) {
×
1873
            return null;
×
1874
        }
1875
    }
1876

1877
    ServerInfo getInfo() {
1878
        return this.serverInfo.get();
1✔
1879
    }
1880

1881
    /**
1882
     * {@inheritDoc}
1883
     */
1884
    @Override
1885
    public Options getOptions() {
1886
        return this.options;
1✔
1887
    }
1888

1889
    /**
1890
     * {@inheritDoc}
1891
     */
1892
    @Override
1893
    public Statistics getStatistics() {
1894
        return this.statistics;
1✔
1895
    }
1896

1897
    StatisticsCollector getNatsStatistics() {
1898
        return this.statistics;
1✔
1899
    }
1900

1901
    DataPort getDataPort() {
1902
        return this.dataPort;
1✔
1903
    }
1904

1905
    // Used for testing
1906
    int getConsumerCount() {
1907
        return this.subscribers.size() + this.dispatchers.size();
1✔
1908
    }
1909

1910
    public long getMaxPayload() {
1911
        ServerInfo info = this.serverInfo.get();
1✔
1912

1913
        if (info == null) {
1✔
1914
            return -1;
×
1915
        }
1916

1917
        return info.getMaxPayload();
1✔
1918
    }
1919

1920
    /**
1921
     * Return the list of known server urls, including additional servers discovered
1922
     * after a connection has been established.
1923
     * @return this connection's list of known server URLs
1924
     */
1925
    public Collection<String> getServers() {
1926
        return serverPool.getServerList();
1✔
1927
    }
1928

1929
    protected List<NatsUri> resolveHost(NatsUri nuri) {
1930
        // 1. If the nuri host is not already an ip address or the nuri is not for websocket or fast fallback is disabled,
1931
        //    let the pool resolve it.
1932
        List<NatsUri> results = new ArrayList<>();
1✔
1933
        if (!nuri.hostIsIpAddress() && !nuri.isWebsocket() && !options.isEnableFastFallback()) {
1✔
1934
            List<String> ips = serverPool.resolveHostToIps(nuri.getHost());
1✔
1935
            if (ips != null) {
1✔
1936
                for (String ip : ips) {
1✔
1937
                    try {
1938
                        results.add(nuri.reHost(ip));
1✔
1939
                    }
1940
                    catch (URISyntaxException u) {
1✔
1941
                        // ??? should never happen
1942
                    }
1✔
1943
                }
1✔
1944
            }
1945
        }
1946

1947
        // 2. If there were no results,
1948
        //    - host was already an ip address or
1949
        //    - host was for websocket or
1950
        //    - fast fallback is enabled
1951
        //    - pool returned nothing or
1952
        //    - resolving failed...
1953
        //    so the list just becomes the original host.
1954
        if (results.isEmpty()) {
1✔
1955
            results.add(nuri);
1✔
1956
        }
1957
        return results;
1✔
1958
    }
1959

1960
    /**
1961
     * {@inheritDoc}
1962
     */
1963
    @Override
1964
    public String getConnectedUrl() {
1965
        return currentServer == null ? null : currentServer.toString();
1✔
1966
    }
1967

1968
    /**
1969
     * {@inheritDoc}
1970
     */
1971
    @Override
1972
    public Status getStatus() {
1973
        return this.status;
1✔
1974
    }
1975

1976
    /**
1977
     * {@inheritDoc}
1978
     */
1979
    @Override
1980
    public String getLastError() {
1981
        return this.lastError.get();
1✔
1982
    }
1983

1984
    /**
1985
     * {@inheritDoc}
1986
     */
1987
    @Override
1988
    public void clearLastError() {
1989
        this.lastError.set("");
1✔
1990
    }
1✔
1991

1992
    ExecutorService getExecutor() {
1993
        return executor;
1✔
1994
    }
1995

1996
    ScheduledExecutorService getScheduledExecutor() {
1997
        return scheduledExecutor;
1✔
1998
    }
1999

2000
    void updateStatus(Status newStatus) {
2001
        Status oldStatus = this.status;
1✔
2002

2003
        statusLock.lock();
1✔
2004
        try {
2005
            if (oldStatus == Status.CLOSED || newStatus == oldStatus) {
1✔
2006
                return;
1✔
2007
            }
2008
            this.status = newStatus;
1✔
2009
        } finally {
2010
            statusChanged.signalAll();
1✔
2011
            statusLock.unlock();
1✔
2012
        }
2013

2014
        if (this.status == Status.DISCONNECTED) {
1✔
2015
            processConnectionEvent(Events.DISCONNECTED);
1✔
2016
        } else if (this.status == Status.CLOSED) {
1✔
2017
            processConnectionEvent(Events.CLOSED);
1✔
2018
        } else if (oldStatus == Status.RECONNECTING && this.status == Status.CONNECTED) {
1✔
2019
            processConnectionEvent(Events.RECONNECTED);
1✔
2020
        } else if (this.status == Status.CONNECTED) {
1✔
2021
            processConnectionEvent(Events.CONNECTED);
1✔
2022
        }
2023
    }
1✔
2024

2025
    boolean isClosing() {
2026
        return this.closing;
1✔
2027
    }
2028

2029
    boolean isClosed() {
2030
        return this.status == Status.CLOSED;
1✔
2031
    }
2032

2033
    boolean isConnected() {
2034
        return this.status == Status.CONNECTED;
1✔
2035
    }
2036

2037
    boolean isDisconnected() {
2038
        return this.status == Status.DISCONNECTED;
×
2039
    }
2040

2041
    boolean isConnectedOrConnecting() {
2042
        statusLock.lock();
1✔
2043
        try {
2044
            return this.status == Status.CONNECTED || this.connecting;
1✔
2045
        } finally {
2046
            statusLock.unlock();
1✔
2047
        }
2048
    }
2049

2050
    boolean isDisconnectingOrClosed() {
2051
        statusLock.lock();
1✔
2052
        try {
2053
            return this.status == Status.CLOSED || this.disconnecting;
1✔
2054
        } finally {
2055
            statusLock.unlock();
1✔
2056
        }
2057
    }
2058

2059
    boolean isDisconnecting() {
2060
        statusLock.lock();
1✔
2061
        try {
2062
            return this.disconnecting;
1✔
2063
        } finally {
2064
            statusLock.unlock();
1✔
2065
        }
2066
    }
2067

2068
    void waitForDisconnectOrClose(Duration timeout) throws InterruptedException {
2069
        waitFor(timeout, (Void) -> this.isDisconnecting() && !this.isClosed() );
1✔
2070
    }
1✔
2071

2072
    void waitForConnectOrClose(Duration timeout) throws InterruptedException {
2073
        waitFor(timeout, (Void) -> !this.isConnected() && !this.isClosed());
1✔
2074
    }
1✔
2075

2076
    void waitFor(Duration timeout, Predicate<Void> test) throws InterruptedException {
2077
        statusLock.lock();
1✔
2078
        try {
2079
            long currentWaitNanos = (timeout != null) ? timeout.toNanos() : -1;
1✔
2080
            long start = NatsSystemClock.nanoTime();
1✔
2081
            while (currentWaitNanos >= 0 && test.test(null)) {
1✔
2082
                if (currentWaitNanos > 0) {
1✔
2083
                    statusChanged.await(currentWaitNanos, TimeUnit.NANOSECONDS);
1✔
2084
                    long now = NatsSystemClock.nanoTime();
1✔
2085
                    currentWaitNanos = currentWaitNanos - (now - start);
1✔
2086
                    start = now;
1✔
2087

2088
                    if (currentWaitNanos <= 0) {
1✔
2089
                        break;
1✔
2090
                    }
2091
                } else {
1✔
2092
                    statusChanged.await();
×
2093
                }
2094
            }
2095
        } finally {
2096
            statusLock.unlock();
1✔
2097
        }
2098
    }
1✔
2099

2100
    void invokeReconnectDelayHandler(long totalRounds) {
2101
        long currentWaitNanos = 0;
1✔
2102

2103
        ReconnectDelayHandler handler = options.getReconnectDelayHandler();
1✔
2104
        if (handler == null) {
1✔
2105
            Duration dur = options.getReconnectWait();
1✔
2106
            if (dur != null) {
1✔
2107
                currentWaitNanos = dur.toNanos();
1✔
2108
                dur = serverPool.hasSecureServer() ? options.getReconnectJitterTls() : options.getReconnectJitter();
1✔
2109
                if (dur != null) {
1✔
2110
                    currentWaitNanos += ThreadLocalRandom.current().nextLong(dur.toNanos());
1✔
2111
                }
2112
            }
2113
        }
1✔
2114
        else {
2115
            Duration waitTime = handler.getWaitTime(totalRounds);
1✔
2116
            if (waitTime != null) {
1✔
2117
                currentWaitNanos = waitTime.toNanos();
1✔
2118
            }
2119
        }
2120

2121
        this.reconnectWaiter = new CompletableFuture<>();
1✔
2122

2123
        long start = NatsSystemClock.nanoTime();
1✔
2124
        while (currentWaitNanos > 0 && !isDisconnectingOrClosed() && !isConnected() && !this.reconnectWaiter.isDone()) {
1✔
2125
            try {
2126
                this.reconnectWaiter.get(currentWaitNanos, TimeUnit.NANOSECONDS);
×
2127
            } catch (Exception exp) {
1✔
2128
                // ignore, try to loop again
2129
            }
×
2130
            long now = NatsSystemClock.nanoTime();
1✔
2131
            currentWaitNanos = currentWaitNanos - (now - start);
1✔
2132
            start = now;
1✔
2133
        }
1✔
2134

2135
        this.reconnectWaiter.complete(Boolean.TRUE);
1✔
2136
    }
1✔
2137

2138
    ByteBuffer enlargeBuffer(ByteBuffer buffer) {
2139
        int current = buffer.capacity();
1✔
2140
        int newSize = current * 2;
1✔
2141
        ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
1✔
2142
        buffer.flip();
1✔
2143
        newBuffer.put(buffer);
1✔
2144
        return newBuffer;
1✔
2145
    }
2146

2147
    // For testing
2148
    NatsConnectionReader getReader() {
2149
        return this.reader;
1✔
2150
    }
2151

2152
    // For testing
2153
    NatsConnectionWriter getWriter() {
2154
        return this.writer;
1✔
2155
    }
2156

2157
    // For testing
2158
    Future<DataPort> getDataPortFuture() {
2159
        return this.dataPortFuture;
1✔
2160
    }
2161

2162
    boolean isDraining() {
2163
        return this.draining.get() != null;
1✔
2164
    }
2165

2166
    boolean isDrained() {
2167
        CompletableFuture<Boolean> tracker = this.draining.get();
1✔
2168

2169
        try {
2170
            if (tracker != null && tracker.getNow(false)) {
1✔
2171
                return true;
1✔
2172
            }
2173
        } catch (Exception e) {
×
2174
            // These indicate the tracker was cancelled/timed out
2175
        }
1✔
2176

2177
        return false;
1✔
2178
    }
2179

2180
    /**
2181
     * {@inheritDoc}
2182
     */
2183
    @Override
2184
    public CompletableFuture<Boolean> drain(Duration timeout) throws TimeoutException, InterruptedException {
2185

2186
        if (isClosing() || isClosed()) {
1✔
2187
            throw new IllegalStateException("A connection can't be drained during close.");
1✔
2188
        }
2189

2190
        this.statusLock.lock();
1✔
2191
        try {
2192
            if (isDraining()) {
1✔
2193
                return this.draining.get();
1✔
2194
            }
2195
            this.draining.set(new CompletableFuture<>());
1✔
2196
        } finally {
2197
            this.statusLock.unlock();
1✔
2198
        }
2199

2200
        final CompletableFuture<Boolean> tracker = this.draining.get();
1✔
2201
        Instant start = Instant.now();
1✔
2202

2203
        // Don't include subscribers with dispatchers
2204
        HashSet<NatsSubscription> pureSubscribers = new HashSet<>(this.subscribers.values());
1✔
2205
        pureSubscribers.removeIf((s) -> s.getDispatcher() != null);
1✔
2206

2207
        final HashSet<NatsConsumer> consumers = new HashSet<>();
1✔
2208
        consumers.addAll(pureSubscribers);
1✔
2209
        consumers.addAll(this.dispatchers.values());
1✔
2210

2211
        NatsDispatcher inboxer = this.inboxDispatcher.get();
1✔
2212

2213
        if (inboxer != null) {
1✔
2214
            consumers.add(inboxer);
1✔
2215
        }
2216

2217
        // Stop the consumers NOW so that when this method returns they are blocked
2218
        consumers.forEach((cons) -> {
1✔
2219
            cons.markDraining(tracker);
1✔
2220
            cons.sendUnsubForDrain();
1✔
2221
        });
1✔
2222

2223
        try {
2224
            this.flush(timeout); // Flush and wait up to the timeout, if this fails, let the caller know
1✔
2225
        } catch (Exception e) {
1✔
2226
            this.close(false, false);
1✔
2227
            throw e;
1✔
2228
        }
1✔
2229

2230
        consumers.forEach(NatsConsumer::markUnsubedForDrain);
1✔
2231

2232
        // Wait for the timeout or the pending count to go to 0
2233
        executor.submit(() -> {
1✔
2234
            try {
2235
                long stop = (timeout == null || timeout.equals(Duration.ZERO))
1✔
2236
                    ? Long.MAX_VALUE
2237
                    : NatsSystemClock.nanoTime() + timeout.toNanos();
1✔
2238
                while (NatsSystemClock.nanoTime() < stop && !Thread.interrupted())
1✔
2239
                {
2240
                    consumers.removeIf(NatsConsumer::isDrained);
1✔
2241
                    if (consumers.isEmpty()) {
1✔
2242
                        break;
1✔
2243
                    }
2244
                    //noinspection BusyWait
2245
                    Thread.sleep(1); // Sleep 1 milli
1✔
2246
                }
2247

2248
                // Stop publishing
2249
                this.blockPublishForDrain.set(true);
1✔
2250

2251
                // One last flush
2252
                if (timeout == null || timeout.equals(Duration.ZERO)) {
1✔
2253
                    this.flush(Duration.ZERO);
1✔
2254
                } else {
2255
                    Instant now = Instant.now();
1✔
2256
                    Duration passed = Duration.between(start, now);
1✔
2257
                    Duration newTimeout = timeout.minus(passed);
1✔
2258
                    if (newTimeout.toNanos() > 0) {
1✔
2259
                        this.flush(newTimeout);
1✔
2260
                    }
2261
                }
2262
                this.close(false, false); // close the connection after the last flush
1✔
2263
                tracker.complete(consumers.isEmpty());
1✔
2264
            } catch (TimeoutException e) {
×
2265
                this.processException(e);
×
2266
            } catch (InterruptedException e) {
×
2267
                this.processException(e);
×
2268
                Thread.currentThread().interrupt();
×
2269
            } finally {
2270
                try {
2271
                    this.close(false, false);// close the connection after the last flush
1✔
2272
                } catch (InterruptedException e) {
×
2273
                    processException(e);
×
2274
                    Thread.currentThread().interrupt();
×
2275
                }
1✔
2276
                tracker.complete(false);
1✔
2277
            }
2278
        });
1✔
2279

2280
        return tracker;
1✔
2281
    }
2282

2283
    boolean isAuthenticationError(String err) {
2284
        if (err == null) {
1✔
2285
            return false;
1✔
2286
        }
2287
        err = err.toLowerCase();
1✔
2288
        return err.startsWith("user authentication")
1✔
2289
            || err.contains("authorization violation")
1✔
2290
            || err.startsWith("account authentication expired");
1✔
2291
    }
2292

2293
    /**
2294
     * {@inheritDoc}
2295
     */
2296
    @Override
2297
    public void flushBuffer() throws IOException {
2298
        if (!isConnected()) {
1✔
2299
            throw new IllegalStateException("Connection is not active.");
1✔
2300
        }
2301
        writer.flushBuffer();
1✔
2302
    }
1✔
2303

2304
    /**
2305
     * {@inheritDoc}
2306
     */
2307
    @Override
2308
    public StreamContext getStreamContext(String streamName) throws IOException, JetStreamApiException {
2309
        Validator.validateStreamName(streamName, true);
1✔
2310
        ensureNotClosing();
1✔
2311
        return new NatsStreamContext(streamName, null, this, null);
1✔
2312
    }
2313

2314
    /**
2315
     * {@inheritDoc}
2316
     */
2317
    @Override
2318
    public StreamContext getStreamContext(String streamName, JetStreamOptions options) throws IOException, JetStreamApiException {
2319
        Validator.validateStreamName(streamName, true);
1✔
2320
        ensureNotClosing();
1✔
2321
        return new NatsStreamContext(streamName, null, this, options);
1✔
2322
    }
2323

2324
    /**
2325
     * {@inheritDoc}
2326
     */
2327
    @Override
2328
    public ConsumerContext getConsumerContext(String streamName, String consumerName) throws IOException, JetStreamApiException {
2329
        return getStreamContext(streamName).getConsumerContext(consumerName);
1✔
2330
    }
2331

2332
    /**
2333
     * {@inheritDoc}
2334
     */
2335
    @Override
2336
    public ConsumerContext getConsumerContext(String streamName, String consumerName, JetStreamOptions options) throws IOException, JetStreamApiException {
2337
        return getStreamContext(streamName, options).getConsumerContext(consumerName);
1✔
2338
    }
2339

2340
    /**
2341
     * {@inheritDoc}
2342
     */
2343
    @Override
2344
    public JetStream jetStream() throws IOException {
2345
        ensureNotClosing();
1✔
2346
        return new NatsJetStream(this, null);
1✔
2347
    }
2348

2349
    /**
2350
     * {@inheritDoc}
2351
     */
2352
    @Override
2353
    public JetStream jetStream(JetStreamOptions options) throws IOException {
2354
        ensureNotClosing();
1✔
2355
        return new NatsJetStream(this, options);
1✔
2356
    }
2357

2358
    /**
2359
     * {@inheritDoc}
2360
     */
2361
    @Override
2362
    public JetStreamManagement jetStreamManagement() throws IOException {
2363
        ensureNotClosing();
1✔
2364
        return new NatsJetStreamManagement(this, null);
1✔
2365
    }
2366

2367
    /**
2368
     * {@inheritDoc}
2369
     */
2370
    @Override
2371
    public JetStreamManagement jetStreamManagement(JetStreamOptions options) throws IOException {
2372
        ensureNotClosing();
1✔
2373
        return new NatsJetStreamManagement(this, options);
1✔
2374
    }
2375

2376
    /**
2377
     * {@inheritDoc}
2378
     */
2379
    @Override
2380
    public KeyValue keyValue(String bucketName) throws IOException {
2381
        Validator.validateBucketName(bucketName, true);
1✔
2382
        ensureNotClosing();
1✔
2383
        return new NatsKeyValue(this, bucketName, null);
1✔
2384
    }
2385

2386
    /**
2387
     * {@inheritDoc}
2388
     */
2389
    @Override
2390
    public KeyValue keyValue(String bucketName, KeyValueOptions options) throws IOException {
2391
        Validator.validateBucketName(bucketName, true);
1✔
2392
        ensureNotClosing();
1✔
2393
        return new NatsKeyValue(this, bucketName, options);
1✔
2394
    }
2395

2396
    /**
2397
     * {@inheritDoc}
2398
     */
2399
    @Override
2400
    public KeyValueManagement keyValueManagement() throws IOException {
2401
        ensureNotClosing();
1✔
2402
        return new NatsKeyValueManagement(this, null);
1✔
2403
    }
2404

2405
    /**
2406
     * {@inheritDoc}
2407
     */
2408
    @Override
2409
    public KeyValueManagement keyValueManagement(KeyValueOptions options) throws IOException {
2410
        ensureNotClosing();
1✔
2411
        return new NatsKeyValueManagement(this, options);
1✔
2412
    }
2413

2414
    /**
2415
     * {@inheritDoc}
2416
     */
2417
    @Override
2418
    public ObjectStore objectStore(String bucketName) throws IOException {
2419
        Validator.validateBucketName(bucketName, true);
1✔
2420
        ensureNotClosing();
1✔
2421
        return new NatsObjectStore(this, bucketName, null);
1✔
2422
    }
2423

2424
    /**
2425
     * {@inheritDoc}
2426
     */
2427
    @Override
2428
    public ObjectStore objectStore(String bucketName, ObjectStoreOptions options) throws IOException {
2429
        Validator.validateBucketName(bucketName, true);
1✔
2430
        ensureNotClosing();
1✔
2431
        return new NatsObjectStore(this, bucketName, options);
1✔
2432
    }
2433

2434
    /**
2435
     * {@inheritDoc}
2436
     */
2437
    @Override
2438
    public ObjectStoreManagement objectStoreManagement() throws IOException {
2439
        ensureNotClosing();
1✔
2440
        return new NatsObjectStoreManagement(this, null);
1✔
2441
    }
2442

2443
    /**
2444
     * {@inheritDoc}
2445
     */
2446
    @Override
2447
    public ObjectStoreManagement objectStoreManagement(ObjectStoreOptions options) throws IOException {
2448
        ensureNotClosing();
1✔
2449
        return new NatsObjectStoreManagement(this, options);
1✔
2450
    }
2451

2452
    private void ensureNotClosing() throws IOException {
2453
        if (isClosing() || isClosed()) {
1✔
2454
            throw new IOException("A JetStream context can't be established during close.");
1✔
2455
        }
2456
    }
1✔
2457
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc