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

synadia-io / nats.java.v3 / 36787514223

30 Sep 2026 10:29PM UTC coverage: 90.827% (-0.04%) from 90.863%
36787514223

push

github

web-flow
Merge pull request #7 from synadia-io/pr-test-core

Nats javadoc: an optional JWT

4186 of 4830 branches covered (86.67%)

Branch coverage included in aggregate %.

10646 of 11500 relevant lines covered (92.57%)

4.12 hits per line

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

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

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

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

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

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

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

40
    protected final StatisticsCollector statistics;
41

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

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

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

62
    protected NatsConnectionReader reader;
63
    protected NatsConnectionWriter writer;
64

65
    protected final AtomicReference<ServerInfo> serverInfo;
66

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

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

81
    protected final AtomicBoolean needPing;
82

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

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

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

100
    protected final boolean advancedTracking;
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

238
        Set<NatsUri> failList = new HashSet<>();
4 ✔
239
        boolean keepGoing = true;
2 ✔
240
        NatsUri first = null;
2 ✔
241
        NatsUri cur;
242
        while (keepGoing && (cur = serverPool.peekNextServer()) != null) {
8 ✔
243
            if (first == null) {
2 ✔
244
                first = cur;
3 ✔
245
            }
246
            else if (cur.equals(first)) {
4 ✔
247
                break;  // connect only goes through loop once
1 ✔
248
            }
249
            serverPool.nextServer(); // b/c we only peeked.
4 ✔
250

251
            // let server pool resolve hostnames, then loop through resolved
252
            List<NatsUri> resolvedList = resolveHost(cur);
4 ✔
253
            for (NatsUri resolved : resolvedList) {
10 ✔
254
                if (isClosed()) {
3 ✔
255
                    keepGoing = false;
2 ✔
256
                    break;
1 ✔
257
                }
258
                connectError.set(""); // new on each attempt
4 ✔
259

260
                updateStatus(CONNECTING, resolved, cur);
5 ✔
261

262
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
5 ✔
263

264
                if (isConnected()) {
3 ✔
265
                    serverPool.connectSucceeded(cur);
4 ✔
266
                    keepGoing = false;
2 ✔
267
                    break;
1 ✔
268
                }
269

270
                updateStatus(DISCONNECTED, resolved, cur);
5 ✔
271

272
                String err = connectError.get();
5 ✔
273

274
                if (isAuthenticationError(err)) {
4 ✔
275
                    serverAuthErrors.put(resolved, err);
6 ✔
276
                }
277
            }
1 ✔
278

279
            if (!isConnected() && !isClosed()) {
6 ✔
280
                failList.add(cur);
4 ✔
281
                serverPool.connectFailed(cur);
4 ✔
282
            }
283
        }
1 ✔
284

285
        if (!isConnected() && !isClosed()) {
6 ✔
286
            if (reconnectOnConnect) {
2 ✔
287
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
3 ✔
288
            }
289
            else {
290
                close(true, false);
4 ✔
291

292
                String err = connectError.get();
5 ✔
293
                if (isAuthenticationError(err)) {
4 ✔
294
                    throw new AuthenticationException("Authentication error connecting to NATS server: " + err);
6 ✔
295
                }
296
                throw new IOException("Unable to connect to NATS servers: " + failList);
7 ✔
297
            }
298
        }
299
    }
1 ✔
300

301
    /**
302
     * Forces reconnect behavior. Stops the current connection including the reading and writing,
303
     * copies already queued outgoing messages, and then begins the reconnect logic.
304
     * Does not flush. Does not force close the connection. See {@link ForceReconnectOptions}.
305
     * @throws IOException the forceReconnect fails
306
     * @throws InterruptedException the connection is not connected
307
     */
308
    public void forceReconnect() throws IOException, InterruptedException {
309
        forceReconnect(ForceReconnectOptions.DEFAULT_INSTANCE);
3 ✔
310
    }
1 ✔
311

312
    /**
313
     * Forces reconnect behavior. Stops the current connection including the reading and writing,
314
     * copies already queued outgoing messages, and then begins the reconnect logic.
315
     * If options are not provided, the default options are used meaning Does not flush and Does not force close the connection.
316
     * See {@link ForceReconnectOptions}.
317
     * @param options options for how the forceReconnect works.
318
     * @throws IOException the forceReconnect fails
319
     * @throws InterruptedException the connection is not connected
320
     */
321
    public void forceReconnect(ForceReconnectOptions options) throws IOException, InterruptedException {
322
        if (!tryingToConnect.get()) {
4 ✔
323
            try {
324
                tryingToConnect.set(true);
4 ✔
325
                forceReconnectImpl(options == null ? ForceReconnectOptions.DEFAULT_INSTANCE : options);
5 !
326
            }
327
            finally {
328
                tryingToConnect.set(false);
4 ✔
329
            }
330
        }
331
    }
1 ✔
332

333
    protected void forceReconnectImpl(@NonNull ForceReconnectOptions frOpts) throws InterruptedException {
334
        if (frOpts.isFlush()) {
3 ✔
335
            try {
336
                flush(frOpts.getFlushWait());
4 ✔
337
            }
338
            catch (TimeoutException e) {
×
339
                // Ignored. Manual test demonstrates that if the connection is dropped
340
                // in the middle of the flush, the most likely reason for a TimeoutException,
341
                // the socket is closed.
342
            }
1 ✔
343
        }
344

345
        closeSocketLock.lock();
3 ✔
346
        try {
347
            updateStatus(DISCONNECTED);
3 ✔
348

349
            // Close and reset the current data port and future
350
            if (dataPortFuture != null) {
3 !
351
                dataPortFuture.cancel(true);
5 ✔
352
                dataPortFuture = null;
3 ✔
353
            }
354

355
            // Stop i/o BEFORE closing the port. stop(false) clears the reader's running flag without
356
            // shutting down input, so when the close below unblocks the pending read, the reader sees the
357
            // resulting IOException as an expected shutdown rather than a communication issue. Both stop
358
            // methods return a future completed when the thread actually exits; both are initialized to an
359
            // already-completed future at construction, so joining one that never started returns at once.
360
            Future<Boolean> readerStopped = reader.stop(false);
5 ✔
361
            Future<Boolean> writerStopped = writer.stop();
4 ✔
362

363
            // close the data port as a task so as not to block reconnecting
364
            if (dataPort != null) {
3 !
365
                final DataPort dataPortToClose = dataPort;
3 ✔
366
                dataPort = null;
3 ✔
367
                executor.submit(() -> {
7 ✔
368
                    try {
369
                        if (frOpts.isForceClose()) {
3 ✔
370
                            dataPortToClose.forceClose();
3 ✔
371
                        }
372
                        else {
373
                            dataPortToClose.close();
2 ✔
374
                        }
375
                    }
376
                    catch (IOException ignore) {
×
377
                        // ignored since running as a task and nothing we can do.
378
                    }
1 ✔
379
                });
1 ✔
380
            }
381

382
            // Join the i/o threads with the full connection timeout, not a token wait. These reader and
383
            // writer instances are reused by the reconnect, so a thread that outlives this point can still
384
            // fire handleCommunicationIssue on the healthy connection and stomp the shared running flag.
385
            // The close above normally lands sub-millisecond, so these return immediately in the common case.
386
            long timeoutMillis = options.getConnectionTimeout();
4 ✔
387
            try {
388
                readerStopped.get(timeoutMillis, TimeUnit.MILLISECONDS);
5 ✔
389
            }
390
            catch (Exception ex) {
×
391
                processException(ex);
×
392
            }
1 ✔
393
            try {
394
                writerStopped.get(timeoutMillis, TimeUnit.MILLISECONDS);
5 ✔
395
            }
396
            catch (Exception ex) {
×
397
                processException(ex);
×
398
            }
1 ✔
399
        }
400
        finally {
401
            closeSocketLock.unlock();
3 ✔
402
        }
403

404
        reconnectImpl();
2 ✔
405
    }
1 ✔
406

407
    protected void reconnect() throws InterruptedException {
408
        if (!tryingToConnect.get()) {
4 !
409
            try {
410
                tryingToConnect.set(true);
4 ✔
411
                reconnectImpl();
2 ✔
412
            }
413
            finally {
414
                tryingToConnect.set(false);
4 ✔
415
            }
416
        }
417
    }
1 ✔
418

419
    // Reconnect can only be called when the connection is disconnected
420
    protected void reconnectImpl() throws InterruptedException {
421
        if (isClosed()) {
3 ✔
422
            return;
1 ✔
423
        }
424

425
        if (options.getMaxReconnects() == 0) {
4 ✔
426
            close(true, false);
4 ✔
427
            return;
1 ✔
428
        }
429

430
        writer.enterReconnectMode();
3 ✔
431

432
        if (!isConnected() && !isClosed() && !isClosing()) {
9 !
433
            reconnectImplConnect();
2 ✔
434
        }
435

436
        if (!isConnected()) {
3 ✔
437
            close(true, false);
4 ✔
438
            return;
1 ✔
439
        }
440

441
        subscriptions.forEach((sid, subscription) -> {
5 ✔
442
            if (subscription.dispatcher == null && !subscription.sub.isDraining()) {
7 !
443
                sendSubscriptionMessage(sid, subscription.sub.getSubject(), subscription.sub.getQueueName(), true);
10 ✔
444
            }
445
        });
1 ✔
446

447
        dispatchers.forEach(d -> {
4 ✔
448
            if (!d.isDraining()) {
3 !
449
                d.resendSubscriptions();
2 ✔
450
            }
451
        });
1 ✔
452

453
        writer.enterWaitingForEndReconnectMode();
3 ✔
454

455
        processConnectionEvent(ConnectionEvents.RESUBSCRIBED, uriDetail(currentServer));
7 ✔
456
    }
1 ✔
457

458
    protected void reconnectImplConnect() {
459
        long round = 0;
2 ✔
460
        NatsUri first = null;
2 ✔
461
        NatsUri cur;
462
        while ((cur = serverPool.nextServer()) != null) {
6 ✔
463
            if (first == null) {
2 ✔
464
                first = cur;
2 ✔
465
                ++round;   // round becomes 1
4 ✔
466
                // Consume the lame duck signal here whatever the behavior does with it, otherwise a
467
                // signal received under a behavior that ignores it survives into a later campaign.
468
                boolean lameDuck = lameDuckTriggered;
3 ✔
469
                lameDuckTriggered = false;
3 ✔
470
                if (delayBeforeFirstRound(lameDuck)) {
4 ✔
471
                    invokeReconnectDelayHandler(round, lameDuck);
4 ✔
472
                }
473
            }
1 ✔
474
            else if (first.equals(cur)) {
4 ✔
475
                // went around the pool an entire time
476
                invokeReconnectDelayHandler(++round, false);
8 ✔
477
            }
478

479
            // let server list provider resolve hostnames
480
            // then loop through resolved
481
            List<NatsUri> resolvedList = resolveHost(cur);
4 ✔
482
            for (NatsUri resolved : resolvedList) {
10 ✔
483
                if (isClosed()) {
3 ✔
484
                    return;
1 ✔
485
                }
486
                connectError.set(""); // reset on each loop
4 ✔
487
                if (isDisconnectingOrClosed() || isClosing()) {
6 !
488
                    return;
1 ✔
489
                }
490
                updateStatus(RECONNECTING, resolved, cur);
5 ✔
491

492
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
5 ✔
493

494
                if (isConnected()) {
3 ✔
495
                    serverPool.connectSucceeded(cur);
4 ✔
496
                    statistics.incrementReconnects();
3 ✔
497
                    return;
1 ✔
498
                }
499

500
                String err = connectError.get();
5 ✔
501
                if (isAuthenticationError(err)) {
4 ✔
502
                    if (err.equals(serverAuthErrors.get(resolved))) {
7 ✔
503
                        return; // double auth error
1 ✔
504
                    }
505
                    serverAuthErrors.put(resolved, err);
6 ✔
506
                }
507
            }
1 ✔
508

509
            if (!isConnected() && !isClosed()) {
6 !
510
                serverPool.connectFailed(cur);
4 ✔
511
            }
512
        }
1 ✔
513
    }
1 ✔
514

515
    protected long timeCheck(long endNanos) throws TimeoutException {
516
        long remainingNanos = endNanos - NatsSystemClock.nanoTime();
4 ✔
517
        if (remainingNanos < 0) {
4 !
518
            throw new TimeoutException("connection timed out");
×
519
        }
520
        return remainingNanos;
2 ✔
521
    }
522

523
    // is called from reconnect and connect
524
    // will wait for any previous attempt to complete, using the reader.stop and
525
    // writer.stop
526
    protected void tryToConnect(NatsUri cur, NatsUri resolved, long nowNanos) {
527
        clearCurrentServer();
2 ✔
528

529
        try {
530
            long end = nowNanos + (options.getConnectionTimeout() * NANOS_PER_MILLI);
8 ✔
531
            timeCheck(end);
4 ✔
532

533
            statusLock.lock();
3 ✔
534
            try {
535
                if (connecting) {
3 !
536
                    return;
×
537
                }
538
                this.connecting = true;
3 ✔
539
                statusChanged.signalAll();
3 ✔
540
            }
541
            finally {
542
                statusLock.unlock();
3 ✔
543
            }
544

545
            // Create a new future for the DataPort, the reader/writer will use this
546
            // to wait for the connect/failure.
547
            this.dataPortFuture = new CompletableFuture<>();
5 ✔
548

549
            // Make sure the reader and writer are stopped
550
            long timeLeftNanos = timeCheck(end);
4 ✔
551
            if (reader.isRunning()) {
4 !
552
                reader.stop().get(timeLeftNanos, TimeUnit.NANOSECONDS);
×
553
            }
554
            timeLeftNanos = timeCheck(end);
4 ✔
555
            if (writer.isRunning()) {
4 !
556
                writer.stop().get(timeLeftNanos, TimeUnit.NANOSECONDS);
×
557
            }
558

559
            timeCheck(end);
4 ✔
560
            cleanUpPongQueue();
2 ✔
561

562
            timeLeftNanos = timeCheck(end);
4 ✔
563
            DataPort newDataPort = options.createDataPort();
4 ✔
564
            newDataPort.connect(this, resolved, timeLeftNanos);
5 ✔
565

566
            // Notify any threads waiting on the sockets
567
            this.dataPort = newDataPort;
3 ✔
568
            dataPortFuture.complete(dataPort);
6 ✔
569

570
            // Wait for the INFO message manually.
571
            // All other traffic will use the reader and writer
572
            // TLS First, don't read info until after upgrade
573
            // ---
574
            // Also this task does not have any exception catching
575
            // Since it is submitted as an async task, the future
576
            // will be aware of any exception thrown, and the future.get()
577
            // will throw an ExecutionException which is handled futher down
578
            Callable<Object> connectTask = () -> {
4 ✔
579
                if (!options.isTlsFirst()) {
4 ✔
580
                    readInitialInfo();
2 ✔
581
                    checkVersionRequirements();
2 ✔
582
                }
583
                upgradeToSecureIfNeeded(resolved);
3 ✔
584
                if (options.isTlsFirst()) {
4 ✔
585
                    readInitialInfo();
2 ✔
586
                    checkVersionRequirements();
2 ✔
587
                }
588
                return null;
2 ✔
589
            };
590

591
            timeLeftNanos = timeCheck(end);
4 ✔
592
            Future<Object> future = connectExecutor.submit(connectTask);
5 ✔
593
            try {
594
                future.get(timeLeftNanos, TimeUnit.NANOSECONDS);
5 ✔
595
            }
596
            finally {
597
                future.cancel(true);
4 ✔
598
            }
599

600
            // start the reader and writer after we secured the connection, if necessary
601
            timeCheck(end);
4 ✔
602
            reader.start(dataPortFuture);
5 ✔
603
            timeCheck(end);
4 ✔
604
            writer.start(dataPortFuture);
5 ✔
605

606
            timeCheck(end);
4 ✔
607
            sendConnect(resolved);
3 ✔
608

609
            timeLeftNanos = timeCheck(end);
4 ✔
610
            Future<Boolean> pongFuture = sendPing();
3 ✔
611

612
            if (pongFuture != null) {
2 !
613
                pongFuture.get(timeLeftNanos, TimeUnit.NANOSECONDS);
5 ✔
614
            }
615

616
            if (pingTask == null) {
3 ✔
617
                timeCheck(end);
4 ✔
618
                long pingMillis = options.getPingInterval();
4 ✔
619
                if (pingMillis > 0) {
4 !
620
                    pingTask = new ScheduledTask(scheduledExecutor, pingMillis, () -> {
10 ✔
621
                        if (isConnected() && !isClosing()) {
6 !
622
                            try {
623
                                softPing(); // The timer always uses the standard queue
2 ✔
624
                            }
625
                            catch (Exception e) {
×
626
                                // it's running in a thread, there is no point throwing here
627
                            }
1 ✔
628
                        }
629
                    });
1 ✔
630
                }
631

632
                long cleanMillis = options.getRequestCleanupInterval();
4 ✔
633
                if (cleanMillis > 0) {
4 !
634
                    cleanupTask = new ScheduledTask(scheduledExecutor, cleanMillis, () -> cleanResponses(false));
14 ✔
635
                }
636
            }
637

638
            // Set connected status
639
            timeCheck(end);
4 ✔
640
            statusLock.lock();
3 ✔
641
            try {
642
                this.connecting = false;
3 ✔
643

644
                if (exceptionDuringConnectChange != null) {
3 !
645
                    throw exceptionDuringConnectChange;
×
646
                }
647

648
                this.currentServer = cur;
3 ✔
649
                serverAuthErrors.clear(); // reset on successful connection
3 ✔
650
                updateStatus(CONNECTED); // will signal status change, we also signal in finally
3 ✔
651
            }
652
            finally {
653
                statusLock.unlock();
3 ✔
654
            }
655
        }
656
        catch (Exception exp) {
1 ✔
657
            processException(exp);
3 ✔
658
            try {
659
                // allow force reconnect since this is pretty exceptional,
660
                // a connection failure while trying to connect
661
                closeSocket(false, true);
4 ✔
662
            }
663
            catch (InterruptedException e) {
×
664
                processException(e);
×
665
                Thread.currentThread().interrupt();
×
666
            }
1 ✔
667
        }
668
        finally {
669
            statusLock.lock();
3 ✔
670
            try {
671
                this.connecting = false;
3 ✔
672
                statusChanged.signalAll();
3 ✔
673
            }
674
            finally {
675
                statusLock.unlock();
3 ✔
676
            }
677
        }
678
    }
1 ✔
679

680
    protected void clearCurrentServer() {
681
        if (currentServer != null) {
3 ✔
682
            lastServer = currentServer;
4 ✔
683
        }
684
        currentServer = null;
3 ✔
685
    }
1 ✔
686

687
    protected void checkVersionRequirements() throws IOException {
688
        Options opts = getOptions();
3 ✔
689
        ServerInfo info = getServerInfo();
3 ✔
690

691
        if (opts.isNoEcho() && info.getProtocolVersion() < 1) {
7 ✔
692
            throw new IOException("Server does not support no echo.");
5 ✔
693
        }
694
    }
1 ✔
695

696
    protected void upgradeToSecureIfNeeded(NatsUri nuri) throws IOException {
697
        // When already communicating over "https" websocket, do NOT try to upgrade to secure.
698
        if (!nuri.isWebsocket()) {
3 ✔
699
            if (options.isTlsFirst()) {
4 ✔
700
                dataPort.upgradeToSecure();
4 ✔
701
            }
702
            else {
703
                // server    | client options      | result
704
                // --------- | ------------------- | --------
705
                // required  | not isTLSRequired() | mismatch
706
                // available | not isTLSRequired() | ok
707
                // neither   | not isTLSRequired() | ok
708
                // required  | isTLSRequired()     | ok
709
                // available | isTLSRequired()     | ok
710
                // neither   | isTLSRequired()     | mismatch
711
                ServerInfo serverInfo = getServerInfo();
3 ✔
712
                if (options.isTLSRequired()) {
4 ✔
713
                    if (!serverInfo.isTLSRequired() && !serverInfo.isTLSAvailable()) {
6 ✔
714
                        throw new IOException("SSL connection wanted by client.");
5 ✔
715
                    }
716
                    dataPort.upgradeToSecure();
4 ✔
717
                }
718
                else if (serverInfo.isTLSRequired()) {
3 ✔
719
                    throw new IOException("SSL required by server.");
5 ✔
720
                }
721
            }
722
        }
723
    }
1 ✔
724

725
    // Called from reader/writer thread
726
    protected void handleCommunicationIssue(Exception io) {
727
        // If we are connecting or disconnecting, note exception and leave
728
        statusLock.lock();
3 ✔
729
        try {
730
            if (connecting || disconnecting || status == CLOSED || isDraining()) {
13 !
731
                this.exceptionDuringConnectChange = io;
3 ✔
732
                return;
1 ✔
733
            }
734
        }
735
        finally {
736
            statusLock.unlock();
3 ✔
737
        }
738

739
        processException(io);
3 ✔
740
        if (currentServer != null) {
3 ✔
741
            serverPool.connectFailed(currentServer);
5 ✔
742
        }
743

744
        // Spawn a thread so we don't have timing issues with
745
        // waiting on read/write threads
746
        executor.submit(() -> {
6 ✔
747
            if (!tryingToConnect.get()) {
4 ✔
748
                try {
749
                    tryingToConnect.set(true);
4 ✔
750

751
                    // any issue that brings us here is pretty serious
752
                    // so we are comfortable forcing the close
753
                    closeSocket(true, true);
4 ✔
754
                }
755
                catch (InterruptedException e) {
×
756
                    processException(e);
×
757
                    Thread.currentThread().interrupt();
×
758
                }
759
                finally {
760
                    tryingToConnect.set(false);
4 ✔
761
                }
762
            }
763
        });
1 ✔
764
    }
1 ✔
765

766
    // Close socket is called when another connect attempt is possible
767
    // Close is called when the connection should shut down, period
768
    protected void closeSocket(boolean tryReconnectIfConnected, boolean forceClose) throws InterruptedException {
769
        // Ensure we close the socket exclusively within one thread.
770
        closeSocketLock.lock();
3 ✔
771
        try {
772
            boolean wasConnected;
773
            statusLock.lock();
3 ✔
774
            try {
775
                if (isDisconnectingOrClosed()) {
3 ✔
776
                    waitForDisconnectOrClose(options.getConnectionTimeout());
5 ✔
777
                    return;
1 ✔
778
                }
779
                this.disconnecting = true;
3 ✔
780
                this.exceptionDuringConnectChange = null;
3 ✔
781
                wasConnected = (status == CONNECTED);
8 ✔
782

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

790
                statusChanged.signalAll();
3 ✔
791
            }
792
            finally {
793
                statusLock.unlock();
3 ✔
794
            }
795

796
            closeSocketImpl(forceClose);
3 ✔
797

798
            statusLock.lock();
3 ✔
799
            try {
800
                this.exceptionDuringConnectChange = null; // Ignore IOExceptions during closeSocketImpl()
3 ✔
801
                this.disconnecting = false;
3 ✔
802
                statusChanged.signalAll();
3 ✔
803
            }
804
            finally {
805
                statusLock.unlock();
3 ✔
806
            }
807

808
            if (isClosing()) { // isClosing() means we are in the close method or were asked to be
3 ✔
809
                close(true, false);
5 ✔
810
            }
811
            else if (wasConnected && tryReconnectIfConnected) {
4 !
812
                reconnectImpl(); // call the impl here otherwise the tryingToConnect guard will block the behavior
2 ✔
813
            }
814
        }
815
        finally {
816
            closeSocketLock.unlock();
3 ✔
817
        }
818
    }
1 ✔
819

820
    // Close socket is called when another connect attempt is possible
821
    // Close is called when the connection should shut down, period
822

823
    /**
824
     * Close the connection and release all blocking calls like {@link #flush flush}
825
     * and {@code Subscription.nextMessage}.
826
     * If close() is called after {@link #drain(long) drain} it will wait up to the connection timeout
827
     * to return, but it will not initiate a close. The drain takes precedence and will initiate the close.
828
     * <p>If the calling thread is interrupted while close is waiting, the close stops waiting,
829
     * the thread's interrupt status is restored, and the method returns.
830
     */
831
    @Override
832
    public void close() {
833
        try {
834
            close(true, false);
4 ✔
835
        }
836
        catch (InterruptedException e) {
×
837
            Thread.currentThread().interrupt(); // restore interrupt status; do not propagate from close()
×
838
        }
1 ✔
839
    }
1 ✔
840

841
    // Several paths reach this (public close(), connect/reconnect failure, closeSocket, drain, final flush),
842
    // but every caller passes forceClose=false, hence the SameParameterValue suppression.
843
    @SuppressWarnings("SameParameterValue")
844
    protected void close(boolean checkDrainStatus, boolean forceClose) throws InterruptedException {
845
        statusLock.lock();
3 ✔
846
        try {
847
            if (checkDrainStatus && isDraining()) {
5 ✔
848
                waitForDisconnectOrClose(options.getConnectionTimeout());
5 ✔
849
                return;
1 ✔
850
            }
851

852
            this.closing = true;// We were asked to close, so do it
3 ✔
853
            if (isDisconnectingOrClosed()) {
3 ✔
854
                waitForDisconnectOrClose(options.getConnectionTimeout());
5 ✔
855
                return;
1 ✔
856
            }
857
            else {
858
                this.disconnecting = true;
3 ✔
859
                this.exceptionDuringConnectChange = null;
3 ✔
860
                statusChanged.signalAll();
3 ✔
861
            }
862
        }
863
        finally {
864
            statusLock.unlock();
3 ✔
865
        }
866

867
        // Stop the reconnect wait timer after we stop the writer/reader (only if we are
868
        // really closing, not on errors)
869
        if (reconnectWaiter != null) {
3 !
870
            reconnectWaiter.cancel(true);
5 ✔
871
        }
872

873
        closeSocketImpl(forceClose);
3 ✔
874

875
        dispatchers.forEach(d -> d.stop(false));
8 ✔
876

877
        subscriptions.forEach((sid, subscription) -> subscription.sub.invalidate());
8 ✔
878

879
        dispatchers.clear();
3 ✔
880
        subscriptions.clear();
3 ✔
881

882
        if (pingTask != null) {
3 ✔
883
            pingTask.shutdown();
3 ✔
884
            pingTask = null;
3 ✔
885
        }
886
        if (cleanupTask != null) {
3 ✔
887
            cleanupTask.shutdown();
3 ✔
888
            cleanupTask = null;
3 ✔
889
        }
890

891
        cleanResponses(true);
3 ✔
892

893
        cleanUpPongQueue();
2 ✔
894

895
        statusLock.lock();
3 ✔
896
        try {
897
            updateStatus(CLOSED); // will signal, we also signal when we stop disconnecting
3 ✔
898

899
            /*
900
             * if (exceptionDuringConnectChange != null) {
901
             * processException(exceptionDuringConnectChange); exceptionDuringConnectChange
902
             * = null; }
903
             */
904
        }
905
        finally {
906
            statusLock.unlock();
3 ✔
907
        }
908

909
        callbackExecutor = null;
3 ✔
910
        executor = null;
3 ✔
911
        connectExecutor = null;
3 ✔
912
        readerExecutor = null;
3 ✔
913
        writerExecutor = null;
3 ✔
914
        scheduledExecutor = null;
3 ✔
915
        options.shutdownExecutors();
3 ✔
916

917
        statusLock.lock();
3 ✔
918
        try {
919
            this.disconnecting = false;
3 ✔
920
            statusChanged.signalAll();
3 ✔
921
        }
922
        finally {
923
            statusLock.unlock();
3 ✔
924
        }
925
    }
1 ✔
926

927
    // these *ExecutorIsClosed() are only used for tests
928
    protected boolean callbackExecutorIsClosed() { return callbackExecutor == null; }
7 ✔
929
    protected boolean executorIsClosed() { return executor == null; }
7 ✔
930
    protected boolean connectExecutorIsClosed() { return connectExecutor == null; }
7 ✔
931
    protected boolean readerExecutorIsClosed() { return readerExecutor == null; }
7 ✔
932
    protected boolean writerExecutorIsClosed() { return writerExecutor == null; }
7 ✔
933
    protected boolean scheduledExecutorIsClosed() { return scheduledExecutor == null; }
7 ✔
934

935
    // Should only be called from closeSocket or close
936
    protected void closeSocketImpl(boolean forceClose) {
937
        clearCurrentServer();
2 ✔
938

939
        // Signal both to stop.
940
        final Future<Boolean> readStop = reader.stop();
4 ✔
941
        final Future<Boolean> writeStop = writer.stop();
4 ✔
942

943
        // Now wait until they both stop before closing the socket.
944
        try {
945
            readStop.get(1, TimeUnit.SECONDS);
5 ✔
946
        }
947
        catch (Exception ex) {
1 ✔
948
            //
949
        }
1 ✔
950
        try {
951
            writeStop.get(1, TimeUnit.SECONDS);
5 ✔
952
        }
953
        catch (Exception ex) {
×
954
            //
955
        }
1 ✔
956

957
        // Close and reset the current data port and future
958
        if (dataPortFuture != null) {
3 ✔
959
            dataPortFuture.cancel(true);
5 ✔
960
            dataPortFuture = null;
3 ✔
961
        }
962

963
        // Close the current socket and cancel anyone waiting for it
964
        try {
965
            if (dataPort != null) {
3 ✔
966
                if (forceClose) {
2 ✔
967
                    dataPort.forceClose();
4 ✔
968
                }
969
                else {
970
                    dataPort.close();
3 ✔
971
                }
972
            }
973

974
        }
975
        catch (IOException ex) {
×
976
            processException(ex);
×
977
        }
1 ✔
978
        cleanUpPongQueue();
2 ✔
979

980
        try {
981
            reader.stop().get(10, TimeUnit.SECONDS);
7 ✔
982
        }
983
        catch (Exception ex) {
×
984
            processException(ex);
×
985
        }
1 ✔
986
        try {
987
            writer.stop().get(10, TimeUnit.SECONDS);
7 ✔
988
        }
989
        catch (Exception ex) {
×
990
            processException(ex);
×
991
        }
1 ✔
992
    }
1 ✔
993

994
    protected void cleanUpPongQueue() {
995
        Future<Boolean> b;
996
        while ((b = pongQueue.poll()) != null) {
7 ✔
997
            b.cancel(true);
5 ✔
998
        }
999
    }
1 ✔
1000

1001
    /**
1002
     * Send a message to the specified subject.
1003
     * @param subject the subject to send the message to
1004
     * @param data the message data
1005
     * @throws IllegalArgumentException if the subject is invalid
1006
     */
1007
    public void publish(@NonNull String subject, byte @Nullable [] data) {
1008
        publish(subject, null, null, data, false);
7 ✔
1009
    }
1 ✔
1010

1011
    /**
1012
     * Send a message to the specified subject. The message data <strong>will
1013
     * not</strong> be copied. The expected usage with string content is something
1014
     * like:
1015
     *
1016
     * <pre>
1017
     * nc = Nats.connect()
1018
     * Headers h = new Headers().put("key", "value");
1019
     * nc.publish("destination", h, "message".getBytes("UTF-8"))
1020
     * </pre>
1021
     *
1022
     * where the sender creates a byte array immediately before calling publish.
1023
     * See {@link #publish(String, String, byte[]) publish()} for more details on
1024
     * publish during reconnect.
1025
     *
1026
     * @param subject the subject to send the message to
1027
     * @param headers Optional headers to publish with the message.
1028
     * @param data the message data
1029
     * @throws IllegalStateException if the reconnect buffer is exceeded
1030
     * @throws IllegalArgumentException if the subject is invalid, or the headers are not supported by the connected server
1031
     */
1032
    public void publish(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data) {
1033
        publish(subject, null, headers, data, false);
7 ✔
1034
    }
1 ✔
1035

1036
    /**
1037
     * Send a request to the specified subject, providing a replyTo subject. The
1038
     * message data <strong>will not</strong> be copied. The expected usage with
1039
     * string content is something like:
1040
     *
1041
     * <pre>
1042
     * nc = Nats.connect()
1043
     * nc.publish("destination", "reply-to", "message".getBytes("UTF-8"))
1044
     * </pre>
1045
     *
1046
     * where the sender creates a byte array immediately before calling publish.
1047
     * <p>
1048
     * During reconnect the client will try to buffer messages. The buffer size is set
1049
     * in the connect options, see {@link OptionsBuilder#reconnectBufferSize(long) reconnectBufferSize()}
1050
     * with a default value of {@link OptionsConstants#DEFAULT_RECONNECT_BUF_SIZE 8 * 1024 * 1024} bytes.
1051
     * If the buffer is exceeded an IllegalStateException is thrown. Applications should use
1052
     * this exception as a signal to wait for reconnect before continuing.
1053
     * </p>
1054
     * @param subject the subject to send the message to
1055
     * @param replyTo the subject the receiver should send any response to
1056
     * @param data the message data
1057
     * @throws IllegalStateException if the reconnect buffer is exceeded
1058
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid
1059
     */
1060
    public void publish(@NonNull String subject, @Nullable String replyTo, byte @Nullable [] data) {
1061
        publish(subject, replyTo, null, data, false);
7 ✔
1062
    }
1 ✔
1063

1064
    /**
1065
     * Send a message to the specified subject.
1066
     * @param subject the subject to send the message to
1067
     * @param replyTo the subject the receiver should send any response to
1068
     * @param headers Optional headers to publish with the message.
1069
     * @param data the message data
1070
     * @throws IllegalStateException if the reconnect buffer is exceeded
1071
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid, or the headers are not supported by the connected server
1072
     */
1073
    public void publish(@NonNull String subject, @Nullable String replyTo, @Nullable Headers headers, byte @Nullable [] data) {
1074
        subject = subjectValidate(subject);
4 ✔
1075
        replyTo = replyValidate(replyTo);
4 ✔
1076
        validateNotClosed();
2 ✔
1077
        _publish(new InternalPublishableMessage(data, subject, replyTo, headers, false));
10 ✔
1078
    }
1 ✔
1079

1080
    /**
1081
     * Send a message to the specified subject.
1082
     * @param subject the subject to send the message to
1083
     * @param replyTo the subject the receiver should send any response to
1084
     * @param headers Optional headers to publish with the message.
1085
     * @param data the message data
1086
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing
1087
     * @throws IllegalArgumentException if the subject is invalid, or the reply subject is invalid, or the headers are not supported by the connected server
1088
     */
1089
    public void publish(@NonNull String subject, @Nullable String replyTo, @Nullable Headers headers, byte @Nullable [] data, boolean flushImmediatelyAfterPublish) {
1090
        subject = subjectValidate(subject);
4 ✔
1091
        replyTo = replyValidate(replyTo);
4 ✔
1092
        validateNotClosed();
2 ✔
1093
        _publish(new InternalPublishableMessage(data, subject, replyTo, headers, flushImmediatelyAfterPublish));
10 ✔
1094
    }
1 ✔
1095

1096
    /**
1097
     * Send a message.
1098
     * @param message the message to send
1099
     * @throws IllegalArgumentException if the message is null, the subject or reply subject is invalid, or the headers are not supported by the connected server
1100
     */
1101
    public void publish(@NonNull NatsMessage message) {
1102
        Validator.validateNotNull(message, "Message");
4 ✔
1103
        subjectValidate(message.getSubject());
5 ✔
1104
        replyValidate(message.getReplyTo());
5 ✔
1105
        validateNotClosed();
2 ✔
1106
        _publish(new InternalPublishableMessage(message, false));
7 ✔
1107
    }
1 ✔
1108

1109
    /**
1110
     * Send a message.
1111
     * @param message the message to send
1112
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing
1113
     * @throws IllegalArgumentException if the message is null, the subject or reply subject is invalid, or the headers are not supported by the connected server
1114
     */
1115
    public void publish(@NonNull NatsMessage message, boolean flushImmediatelyAfterPublish) {
1116
        Validator.validateNotNull(message, "Message");
×
1117
        subjectValidate(message.getSubject());
×
1118
        replyValidate(message.getReplyTo());
×
1119
        validateNotClosed();
×
1120
        _publish(new InternalPublishableMessage(message, flushImmediatelyAfterPublish));
×
1121
    }
×
1122

1123
    // MUST CALL validateNotClosed(); BEFORE CALLING THIS
1124
    private void _publish(InternalPublishableMessage ipm) {
1125
        if (ipm.hasHeaders && !serverInfo.get().isHeadersSupported()) {
9 ✔
1126
            throw new IllegalArgumentException("Headers are not supported by the server, version: " + serverInfo.get().getVersion());
10 ✔
1127
        }
1128

1129
        if ((status == RECONNECTING || status == DISCONNECTED)
11 ✔
1130
            && !writer.canQueueDuringReconnect(ipm)) {
2 ✔
1131
            throw new IllegalStateException(
4 ✔
1132
                "Unable to queue any more messages during reconnect, max buffer is " + options.getReconnectBufferSize());
4 ✔
1133
        }
1134

1135
        queueOutgoing(ipm);
3 ✔
1136
    }
1 ✔
1137

1138
    private void validateNotClosed() {
1139
        if (isClosed()) {
3 ✔
1140
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1141
        }
1142
        else if (blockPublishForDrain.get()) {
4 !
1143
            throw new IllegalStateException("NatsConnection is Draining"); // Ok to publish while waiting on subs
×
1144
        }
1145
    }
1 ✔
1146

1147
    /**
1148
     * Create a synchronous subscription to the specified subject.
1149
     *
1150
     * <p>Use the {@code Subscription.nextMessage}
1151
     * method to read messages for this subscription.
1152
     *
1153
     * <p>See {@link #createDispatcher(MessageHandler) createDispatcher} for
1154
     * information about creating an asynchronous subscription with callbacks.
1155
     *
1156
     * <p>This method will throw an IllegalArgumentException if the subject is invalid.
1157
     *
1158
     * @param subject the subject to subscribe to
1159
     * @return an object representing the subscription
1160
     * @throws IllegalArgumentException if the subject is invalid
1161
     */
1162
    @NonNull
1163
    public NatsSubscription subscribe(@NonNull String subject) {
1164
        subjectValidate(subject);
4 ✔
1165
        return _createSubscriptionByFactory(subject, null, null, null);
7 ✔
1166
    }
1167

1168
    /**
1169
     * Create a synchronous subscription to the specified subject and queue.
1170
     *
1171
     * <p>Use the {@code Subscription.nextMessage} method to read
1172
     * messages for this subscription.
1173
     *
1174
     * <p>See {@link #createDispatcher(MessageHandler) createDispatcher} for
1175
     * information about creating an asynchronous subscription with callbacks.
1176
     *
1177
     * <p>This method will throw an IllegalArgumentException if the subject is invalid.
1178
     *
1179
     * @param subject the subject to subscribe to
1180
     * @param queueName the queue group to join
1181
     * @return an object representing the subscription
1182
     * @throws IllegalArgumentException if the subject or the queue name is invalid
1183
     */
1184
    @NonNull
1185
    public NatsSubscription subscribe(@NonNull String subject, @NonNull String queueName) {
1186
        subjectValidate(subject);
4 ✔
1187
        Validator.validateQueueName(queueName, true);
4 ✔
1188
        return _createSubscriptionByFactory(subject, queueName, null, null);
7 ✔
1189
    }
1190

1191
    protected void invalidate(NatsSubscription sub) {
1192
        remove(sub);
3 ✔
1193
        sub.invalidate();
2 ✔
1194
    }
1 ✔
1195

1196
    protected void remove(NatsSubscription sub) {
1197
        SubscriptionInfo subscription = subscriptions.remove(sub.getSID());
7 ✔
1198

1199
        if (subscription != null && subscription.dispatcher != null) {
5 ✔
1200
            subscription.dispatcher.remove(sub);
4 ✔
1201
        }
1202
    }
1 ✔
1203

1204
    protected void unsubscribe(NatsSubscription sub, int after) {
1205
        if (isClosed()) { // last chance, usually sub will catch this
3 !
1206
            throw new IllegalStateException("NatsConnection is Closed");
×
1207
        }
1208

1209
        if (after <= 0) {
2 ✔
1210
            invalidate(sub); // Will clean it up
4 ✔
1211
        }
1212
        else {
1213
            sub.setUnsubLimit(after);
4 ✔
1214

1215
            if (sub.reachedUnsubLimit()) {
3 ✔
1216
                invalidate(sub); // takes the sid out of subscriptions, not just the sub out of service
3 ✔
1217
            }
1218
        }
1219

1220
        if (!isConnected()) {
3 !
1221
            return; // We will set up sub on reconnect or ignore
×
1222
        }
1223

1224
        sendUnsub(sub, after);
4 ✔
1225
    }
1 ✔
1226

1227
    protected void sendUnsub(@NonNull NatsSubscription sub, int after) {
1228
        ByteArrayBuilder bab =
4 ✔
1229
            new ByteArrayBuilder().append(UNSUB_SP_BYTES).append(sub.getSID());
5 ✔
1230
        if (after > 0) {
2 ✔
1231
            bab.append(SP).append(after);
6 ✔
1232
        }
1233
        queueOutgoing(new ProtocolMessage(bab, true));
7 ✔
1234
    }
1 ✔
1235

1236
    /**
1237
     * What the connection knows about one sid: the subscription, and the dispatcher delivering to it
1238
     * if there is one. The connection owns this link, so delivery, reconnect and drain never have to
1239
     * ask a subscription what it belongs to.
1240
     */
1241
    protected static final class SubscriptionInfo {
1242
        protected final NatsSubscription sub;
1243
        protected final @Nullable NatsDispatcher dispatcher;
1244

1245
        protected SubscriptionInfo(NatsSubscription sub, @Nullable NatsDispatcher dispatcher) {
2 ✔
1246
            this.sub = sub;
3 ✔
1247
            this.dispatcher = dispatcher;
3 ✔
1248
        }
1 ✔
1249
    }
1250

1251
    // Assumes the null/empty checks were handled elsewhere
1252
    @NonNull
1253
    NatsSubscription _createSubscriptionByFactory(@NonNull String subject,
1254
                                                  @Nullable String queueGroup,
1255
                                                  @Nullable NatsDispatcher dispatcher,
1256
                                                  @Nullable NatsSubscriptionFactory factory) {
1257
        if (isClosed()) {
3 ✔
1258
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1259
        }
1260
        else if (isDraining() && (dispatcher == null || dispatcher != inboxDispatcher.get())) {
10 !
1261
            throw new IllegalStateException("NatsConnection is Draining");
5 ✔
1262
        }
1263

1264
        NatsSubscription sub;
1265
        String sid = getNextSid();
3 ✔
1266

1267
        if (factory == null) {
2 ✔
1268
            sub = new NatsSubscription(sid, subject, queueGroup, this, dispatcher);
10 ✔
1269
        }
1270
        else {
1271
            sub = factory.createNatsSubscription(sid, subject, queueGroup, this, dispatcher);
8 ✔
1272
        }
1273
        subscriptions.put(sid, new SubscriptionInfo(sub, dispatcher));
10 ✔
1274

1275
        sendSubscriptionMessage(sid, subject, queueGroup, false);
6 ✔
1276
        return sub;
2 ✔
1277
    }
1278

1279
    protected String getNextSid() {
1280
        return Long.toString(nextSid.getAndIncrement());
5 ✔
1281
    }
1282

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

1291
    protected void sendSubscriptionMessage(String sid, String subject, String queueGroup, boolean treatAsInternal) {
1292
        if (!isConnected()) {
3 ✔
1293
            return; // We will set up sub on reconnect or ignore
1 ✔
1294
        }
1295

1296
        ByteArrayBuilder bab = new ByteArrayBuilder(UTF_8).append(SUB_SP_BYTES).append(subject);
9 ✔
1297
        if (queueGroup != null) {
2 ✔
1298
            bab.append(SP).append(queueGroup);
6 ✔
1299
        }
1300
        bab.append(SP).append(sid);
6 ✔
1301

1302
        // setting this to filter on stop.
1303
        // if it's an "internal" message, it won't be filtered
1304
        // if it's a normal message, the subscription will already be registered
1305
        // and therefore will be re-subscribed after a stop anyway
1306
        ProtocolMessage subMsg = new ProtocolMessage(bab, true);
6 ✔
1307
        if (treatAsInternal) {
2 ✔
1308
            queueInternalOutgoing(subMsg);
4 ✔
1309
        }
1310
        else {
1311
            queueOutgoing(subMsg);
3 ✔
1312
        }
1313
    }
1 ✔
1314

1315
    /**
1316
     * Create a new inbox subject, can be used for directed replies from
1317
     * subscribers. These are guaranteed to be unique, but can be shared and subscribed
1318
     * to by others.
1319
     * @return the inbox
1320
     */
1321
    @NonNull
1322
    public String createInbox() {
1323
        return options.getInboxPrefix() + nuid.next();
8 ✔
1324
    }
1325

1326
    protected int getRespInboxLength() {
1327
        return options.getInboxPrefix().length() + 22 + 1; // 22 for nuid, 1 for .
9 ✔
1328
    }
1329

1330
    protected String createResponseInbox(String inbox) {
1331
        // Substring gets rid of the * [trailing]
1332
        return inbox.substring(0, getRespInboxLength()) + nuid.next();
10 ✔
1333
    }
1334

1335
    // If the inbox is long enough, pull out the end part, otherwise, just use the
1336
    // full thing
1337
    protected String getResponseToken(String responseInbox) {
1338
        int len = getRespInboxLength();
3 ✔
1339
        if (responseInbox.length() <= len) {
4 !
1340
            return responseInbox;
×
1341
        }
1342
        return responseInbox.substring(len);
4 ✔
1343
    }
1344

1345
    protected void cleanResponses(boolean closing) {
1346
        ArrayList<String> toRemove = new ArrayList<>();
4 ✔
1347
        boolean wasInterrupted = false;
2 ✔
1348

1349
        for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesAwaiting.entrySet()) {
12 ✔
1350
            boolean remove = false;
2 ✔
1351
            NatsRequestCompletableFuture future = entry.getValue();
4 ✔
1352
            if (future.hasExceededTimeout()) {
3 ✔
1353
                remove = true;
2 ✔
1354
                future.cancelTimedOut();
3 ✔
1355
            }
1356
            else if (closing) {
2 ✔
1357
                remove = true;
2 ✔
1358
                future.cancelClosing();
3 ✔
1359
            }
1360
            else if (future.isDone()) {
3 ✔
1361
                // done should have already been removed, not sure if
1362
                // this even needs checking, but it won't hurt
1363
                remove = true;
2 ✔
1364
                try {
1365
                    future.get();
×
1366
                }
1367
                catch (InterruptedException e) {
×
1368
                    Thread.currentThread().interrupt();
×
1369
                    // We might have collected some entries already, but were interrupted.
1370
                    // Break out so we finish as quick as possible,
1371
                    // cleanResponses will be called again anyway
1372
                    wasInterrupted = true;
×
1373
                    break;
×
1374
                }
1375
                catch (Throwable ignore) {
1 ✔
1376
                }
×
1377
            }
1378

1379
            if (remove) {
2 ✔
1380
                toRemove.add(entry.getKey());
6 ✔
1381
                statistics.decrementOutstandingRequests();
3 ✔
1382
            }
1383
        }
1 ✔
1384

1385
        for (String key : toRemove) {
10 ✔
1386
            responsesAwaiting.remove(key);
5 ✔
1387
        }
1 ✔
1388

1389
        if (advancedTracking && !wasInterrupted) {
5 !
1390
            toRemove.clear(); // we can reuse this but it needs to be cleared
2 ✔
1391
            for (Map.Entry<String, NatsRequestCompletableFuture> entry : responsesRespondedTo.entrySet()) {
12 ✔
1392
                NatsRequestCompletableFuture future = entry.getValue();
4 ✔
1393
                if (future.hasExceededTimeout()) {
3 ✔
1394
                    toRemove.add(entry.getKey());
6 ✔
1395
                    future.cancelTimedOut();
2 ✔
1396
                }
1397
            }
1 ✔
1398

1399
            for (String token : toRemove) {
10 ✔
1400
                responsesRespondedTo.remove(token);
5 ✔
1401
            }
1 ✔
1402
        }
1403
    }
1 ✔
1404

1405
    /**
1406
     * Send a request and returns the reply or null. This version of request is equivalent
1407
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1408
     * the timeout and handling the ExecutionException and TimeoutException.
1409
     *
1410
     * @param subject the subject for the service that will handle the request
1411
     * @param data the content of the message
1412
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1413
     * @return the reply message or null if the timeout is reached
1414
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1415
     * @throws IllegalArgumentException if the subject is invalid
1416
     */
1417
    @Nullable
1418
    public Message request(@NonNull String subject, byte @Nullable [] data, long timeoutMillis) throws InterruptedException {
1419
        return request(subject, null, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1420
    }
1421

1422
    /**
1423
     * Send a request and returns the reply or null. This version of request is equivalent
1424
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1425
     * the timeout and handling the ExecutionException and TimeoutException.
1426
     *
1427
     * @param subject the subject for the service that will handle the request
1428
     * @param headers Optional headers to publish with the message.
1429
     * @param data the content of the message
1430
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1431
     * @return the reply message or null if the timeout is reached
1432
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1433
     * @throws IllegalArgumentException if the subject is invalid
1434
     */
1435
    @Nullable
1436
    public Message request(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis) throws InterruptedException {
1437
        return request(subject, headers, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1438
    }
1439

1440
    /**
1441
     * Send a request and returns the reply or null. This version of request is equivalent
1442
     * to calling get on the future returned from {@link #requestAsync(String, byte[]) request()} with
1443
     * the timeout and handling the ExecutionException and TimeoutException.
1444
     *
1445
     * <p>The Message object allows you to set a replyTo, but in requests,
1446
     * the replyTo is reserved for internal use as the address for the
1447
     * server to respond to the client with the consumer's reply.</p>
1448
     *
1449
     * @param message the message
1450
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1451
     * @return the reply message or null if the timeout is reached
1452
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1453
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1454
     */
1455
    @Nullable
1456
    public Message request(@NonNull Message message, long timeoutMillis) throws InterruptedException {
1457
        Validator.validateNotNull(message, "Message");
4 ✔
1458
        return request(message.getSubject(), message.getHeaders(), message.getData(), timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
13 ✔
1459
    }
1460

1461
    /**
1462
     * Send a request and returns the reply or null. Same as the other request methods except that it lets
1463
     * the caller choose what happens to the outstanding request if no reply arrives in time.
1464
     *
1465
     * @param subject the subject for the service that will handle the request
1466
     * @param headers optional headers to publish with the message
1467
     * @param data the content of the message
1468
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1469
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1470
     * @return the reply message or null if the timeout is reached
1471
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1472
     * @throws IllegalArgumentException if the subject is invalid
1473
     */
1474
    @Nullable
1475
    public Message request(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis, @NonNull CancelAction cancelAction) throws InterruptedException {
1476
        return request(subject, headers, data, timeoutMillis, cancelAction, forceFlushOnRequest);
×
1477
    }
1478

1479
    /**
1480
     * Send a request and returns the reply or null. This is the core blocking request used by all the
1481
     * other request methods; it publishes, then waits on the future for the reply.
1482
     *
1483
     * @param subject the subject for the service that will handle the request
1484
     * @param headers optional headers to publish with the message
1485
     * @param data the content of the message
1486
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default connection timeout
1487
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1488
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing the request
1489
     * @return the reply message or null if the timeout is reached
1490
     * @throws InterruptedException if one is thrown while waiting, in order to propagate it up
1491
     * @throws IllegalArgumentException if the subject is invalid
1492
     */
1493
    @Nullable
1494
    public Message request(@NonNull String subject,
1495
                           @Nullable Headers headers,
1496
                           byte @Nullable [] data,
1497
                           long timeoutMillis,
1498
                           @NonNull CancelAction cancelAction,
1499
                           boolean flushImmediatelyAfterPublish) throws InterruptedException
1500
    {
1501
        CompletableFuture<Message> incoming = requestAsync(subject, headers, data, timeoutMillis, cancelAction, flushImmediatelyAfterPublish);
9 ✔
1502
        try {
1503
            long getMillis = timeoutMillis <= 0 ? getOptions().getConnectionTimeout() : timeoutMillis;
6 !
1504
            return incoming.get(getMillis, TimeUnit.MILLISECONDS);
6 ✔
1505
        }
1506
        catch (TimeoutException | ExecutionException | CancellationException e) {
×
1507
            return null;
×
1508
        }
1509
    }
1510

1511
    /**
1512
     * Send a request. The returned future will be completed when the
1513
     * response comes back.
1514
     *
1515
     * @param subject the subject for the service that will handle the request
1516
     * @param data the content of the message
1517
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1518
     * @throws IllegalArgumentException if the subject is invalid
1519
     */
1520
    @NonNull
1521
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data) {
1522
        return requestAsync(subject, null, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1523
    }
1524

1525
    /**
1526
     * Send a request. The returned future will be completed when the
1527
     * response comes back.
1528
     *
1529
     * @param subject the subject for the service that will handle the request
1530
     * @param headers Optional headers to publish with the message.
1531
     * @param data the content of the message
1532
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1533
     * @throws IllegalArgumentException if the subject is invalid
1534
     */
1535
    @NonNull
1536
    public CompletableFuture<Message> requestAsync(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data) {
1537
        return requestAsync(subject, headers, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1538
    }
1539

1540
    /**
1541
     * Send a request. The returned future will be completed when the
1542
     * response comes back.
1543
     *
1544
     * @param subject the subject for the service that will handle the request
1545
     * @param data the content of the message
1546
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1547
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1548
     * @throws IllegalArgumentException if the subject is invalid
1549
     */
1550
    @NonNull
1551
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data, long timeoutMillis) {
1552
        return requestAsync(subject, null, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1553
    }
1554

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

1571
    /**
1572
     * Send a request. The returned future will be completed when the
1573
     * response comes back.
1574
     *
1575
     * <p>The Message object allows you to set a replyTo, but in requests,
1576
     * the replyTo is reserved for internal use as the address for the
1577
     * server to respond to the client with the consumer's reply.</p>
1578
     *
1579
     * @param message the message
1580
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1581
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1582
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1583
     */
1584
    @NonNull
1585
    public CompletableFuture<Message> requestAsync(@NonNull Message message, long timeoutMillis) {
1586
        Validator.validateNotNull(message, "Message");
×
1587
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
×
1588
    }
1589

1590
    /**
1591
     * Send a request. The returned future will be completed when the
1592
     * response comes back.
1593
     *
1594
     * <p>The Message object allows you to set a replyTo, but in requests,
1595
     * the replyTo is reserved for internal use as the address for the
1596
     * server to respond to the client with the consumer's reply.</p>
1597
     *
1598
     * @param message the message
1599
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1600
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1601
     */
1602
    @NonNull
1603
    public CompletableFuture<Message> request(@NonNull Message message) {
1604
        Validator.validateNotNull(message, "Message");
4 ✔
1605
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), -1, CancelAction.REPORT, forceFlushOnRequest);
13 ✔
1606
    }
1607

1608
    /**
1609
     * Send a request, returning a future for the response. This is the core request-send used by all the
1610
     * other request / requestAsync methods.
1611
     *
1612
     * @param subject the subject for the service that will handle the request
1613
     * @param headers optional headers to publish with the message
1614
     * @param data the content of the message
1615
     * @param timeoutMillis the time in milliseconds before the outstanding request is cleaned up; a value
1616
     *                       less than 0 uses the request cleanup interval default
1617
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1618
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing the request
1619
     * @return a Future for the response, which may be cancelled on error or timed out The future completes exceptionally with a StatusException when the reply is a 503 no responders status and the cancel action is REPORT, which is the default.
1620
     * @throws IllegalArgumentException if the subject is invalid
1621
     */
1622
    @NonNull
1623
    public CompletableFuture<Message> requestAsync(@NonNull String subject,
1624
                                                   @Nullable Headers headers,
1625
                                                   byte @Nullable [] data,
1626
                                                   long timeoutMillis,
1627
                                                   @NonNull CancelAction cancelAction,
1628
                                                   boolean flushImmediatelyAfterPublish)
1629
    {
1630
        validateNotClosed();
2 ✔
1631
        subjectValidate(subject);
4 ✔
1632

1633
        if (inboxDispatcher.get() == null) {
4 ✔
1634
            inboxDispatcherLock.lock();
3 ✔
1635
            try {
1636
                if (inboxDispatcher.get() == null) {
4 !
1637
                    NatsDispatcher d = dispatcherFactory.createDispatcher(this, this::deliverReply);
7 ✔
1638

1639
                    // Ensure the dispatcher is started before publishing messages
1640
                    dispatchers.add(d);
5 ✔
1641
                    d.start();
2 ✔
1642
                    d.subscribe(mainInbox);
5 ✔
1643
                    inboxDispatcher.set(d);
4 ✔
1644
                }
1645
            }
1646
            finally {
1647
                inboxDispatcherLock.unlock();
3 ✔
1648
            }
1649
        }
1650

1651
        String responseInbox = createResponseInbox(mainInbox);
5 ✔
1652
        String responseToken = getResponseToken(responseInbox);
4 ✔
1653
        NatsRequestCompletableFuture future =
3 ✔
1654
            new NatsRequestCompletableFuture(cancelAction,
1655
                timeoutMillis <= 0 ? options.getRequestCleanupInterval() : timeoutMillis, options.useTimeoutException());
14 ✔
1656

1657
        responsesAwaiting.put(responseToken, future);
6 ✔
1658
        statistics.incrementOutstandingRequests();
3 ✔
1659

1660
        try {
1661
            _publish(new InternalPublishableMessage(data, subject, responseInbox, headers, flushImmediatelyAfterPublish));
10 ✔
1662
        }
1663
        catch (RuntimeException e) {
1 ✔
1664
            // The publish never made it onto the queue (queue full/busy, or the calling thread was
1665
            // interrupted). Undo the registration so we don't leak a never-completable future waiting
1666
            // for the cleanup timer, then let the caller see the failure.
1667
            responsesAwaiting.remove(responseToken);
5 ✔
1668
            statistics.decrementOutstandingRequests();
3 ✔
1669
            throw e;
2 ✔
1670
        }
1 ✔
1671

1672
        statistics.incrementRequestsSent();
3 ✔
1673

1674
        return future;
2 ✔
1675
    }
1676

1677
    protected void deliverReply(Message msg) {
1678
        String subject = msg.getSubject();
3 ✔
1679
        String key = getResponseToken(subject);
4 ✔
1680
        NatsRequestCompletableFuture f = responsesAwaiting.remove(key);
6 ✔
1681
        if (f != null) {
2 ✔
1682
            if (advancedTracking) {
3 ✔
1683
                responsesRespondedTo.put(key, f);
6 ✔
1684
            }
1685
            statistics.decrementOutstandingRequests();
3 ✔
1686
            if (msg.isStatusMessage() && msg.getStatus().getCode() == 503) {
8 !
1687
                switch (f.getCancelAction()) {
6 !
1688
                    case COMPLETE:
1689
                        f.complete(msg);
×
1690
                        break;
×
1691
                    case REPORT:
1692
                        f.completeExceptionally(new StatusException(msg.getStatus()));
8 ✔
1693
                        break;
1 ✔
1694
                    case CANCEL:
1695
                    default:
1696
                        f.cancel(true);
×
1697
                }
1698
            }
1699
            else {
1700
                f.complete(msg);
4 ✔
1701
            }
1702
            statistics.incrementRepliesReceived();
4 ✔
1703
        }
1704
        else if (!subject.startsWith(mainInbox)) {
5 !
1705
            if (advancedTracking) {
3 ✔
1706
                if (responsesRespondedTo.get(key) != null) {
5 ✔
1707
                    statistics.incrementDuplicateRepliesReceived();
4 ✔
1708
                }
1709
                else {
1710
                    statistics.incrementOrphanRepliesReceived();
3 ✔
1711
                }
1712
            }
1713
        }
1714
    }
1 ✔
1715

1716
    /**
1717
     * Convenience method to create a dispatcher with no default handler. Only used
1718
     * with JetStream subscriptions that require specific handlers per subscription.
1719
     *
1720
     * @return a new Dispatcher
1721
     */
1722
    @NonNull
1723
    public NatsDispatcher createDispatcher() {
1724
        return createDispatcher(null);
4 ✔
1725
    }
1726

1727
    /**
1728
     * Create a {@code Dispatcher} for this connection. The dispatcher can group one
1729
     * or more subscriptions into a single callback thread. All messages go to the
1730
     * same {@code MessageHandler}.
1731
     *
1732
     * <p>Use the Dispatcher's {@link Dispatcher#subscribe(String)} and
1733
     * {@link Dispatcher#subscribe(String, String)} methods to add subscriptions.
1734
     *
1735
     * <pre>
1736
     * nc = Nats.connect()
1737
     * d = nc.createDispatcher((m) -&gt; System.out.println(m)).subscribe("hello");
1738
     * </pre>
1739
     *
1740
     * @param handler The target for the messages. If the handler is null, subscribing without
1741
     *                using its API that accepts a handler will discard messages.
1742
     * @return a new Dispatcher
1743
     */
1744
    @NonNull
1745
    public NatsDispatcher createDispatcher(@Nullable MessageHandler handler) {
1746
        if (isClosed()) {
3 ✔
1747
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1748
        }
1749
        else if (isDraining()) {
3 ✔
1750
            throw new IllegalStateException("NatsConnection is Draining");
5 ✔
1751
        }
1752

1753
        NatsDispatcher dispatcher = dispatcherFactory.createDispatcher(this, handler);
6 ✔
1754
        dispatchers.add(dispatcher);
5 ✔
1755
        dispatcher.start();
2 ✔
1756
        return dispatcher;
2 ✔
1757
    }
1758

1759
    /**
1760
     * Close a dispatcher. This will unsubscribe any subscriptions and stop the delivery thread.
1761
     *
1762
     * <p>Once closed the dispatcher will throw an exception on subsequent subscribe or unsubscribe calls.
1763
     *
1764
     * @param d the dispatcher to close
1765
     * @throws IllegalArgumentException if the dispatcher was not created by this connection, or has already been closed
1766
     */
1767
    public void closeDispatcher(@NonNull Dispatcher d) {
1768
        if (isClosed()) {
3 ✔
1769
            throw new IllegalStateException("NatsConnection is Closed");
5 ✔
1770
        }
1771
        else if (!(d instanceof NatsDispatcher)) {
3 !
1772
            throw new IllegalArgumentException("NatsConnection can only manage its own dispatchers");
×
1773
        }
1774

1775
        NatsDispatcher nd = (NatsDispatcher) d;
3 ✔
1776

1777
        if (nd.isDraining()) {
3 !
1778
            return; // No op while draining
×
1779
        }
1780

1781
        if (!dispatchers.contains(nd)) {
5 ✔
1782
            throw new IllegalArgumentException("Dispatcher is already closed.");
5 ✔
1783
        }
1784

1785
        cleanupDispatcher(nd);
3 ✔
1786
    }
1 ✔
1787

1788
    protected void cleanupDispatcher(NatsDispatcher nd) {
1789
        nd.stop(true);
3 ✔
1790
        dispatchers.remove(nd);
5 ✔
1791
    }
1 ✔
1792

1793
    protected Set<Dispatcher> getDispatchers() {
1794
        return Collections.unmodifiableSet(dispatchers);
×
1795
    }
1796

1797
    /**
1798
     * Attach another ConnectionListener. Adding a listener whose id is already registered replaces it.
1799
     *
1800
     * <p>The ConnectionListener will only receive NatsConnection events arriving after it has been attached.  When
1801
     * a NatsConnection event is raised, the invocation order and parallelism of multiple ConnectionListeners is not
1802
     * specified.
1803
     *
1804
     * <p>Listeners attached here are not written back to the Options - another connection built from the same
1805
     * Options starts with only the listeners the Options carries.
1806
     *
1807
     * @param connectionListener the ConnectionListener to attach
1808
     */
1809
    public void addConnectionListener(@NonNull ConnectionListener connectionListener) {
1810
        connectionListeners.put(connectionListener.getConnectionListenerId(), connectionListener);
7 ✔
1811
    }
1 ✔
1812

1813
    /**
1814
     * Detach a ConnectionListener. This will cease delivery of any further NatsConnection events to this instance.
1815
     *
1816
     * @param connectionListener the ConnectionListener to detach
1817
     */
1818
    public void removeConnectionListener(@NonNull ConnectionListener connectionListener) {
1819
        connectionListeners.remove(connectionListener.getConnectionListenerId());
6 ✔
1820
    }
1 ✔
1821

1822
    /**
1823
     * Detach the ConnectionListener registered under the given id. See {@link ConnectionListener#getConnectionListenerId()}.
1824
     * Removing an id that is not registered is a no-op.
1825
     *
1826
     * @param id the id of the ConnectionListener to detach
1827
     */
1828
    public void removeConnectionListenerById(@NonNull String id) {
1829
        connectionListeners.remove(id);
5 ✔
1830
    }
1 ✔
1831

1832
    /**
1833
     * Attach an ErrorListener, which will receive error events for this connection in addition to any
1834
     * supplied in the {@link Options Options}. Adding a listener whose id is already registered replaces it.
1835
     *
1836
     * <p>The ErrorListener will only receive events arriving after it has been attached. When an event is
1837
     * raised, the invocation order and parallelism of multiple ErrorListeners is not specified.
1838
     *
1839
     * <p>Listeners attached here are not written back to the Options - another connection built from the same
1840
     * Options starts with only the listeners the Options carries.
1841
     *
1842
     * @param errorListener the ErrorListener to attach
1843
     */
1844
    public void addErrorListener(@NonNull ErrorListener errorListener) {
1845
        errorListeners.put(errorListener.getErrorListenerId(), errorListener);
7 ✔
1846
    }
1 ✔
1847

1848
    /**
1849
     * Detach an ErrorListener. This will cease delivery of any further error events to this instance.
1850
     *
1851
     * @param errorListener the ErrorListener to detach
1852
     */
1853
    public void removeErrorListener(@NonNull ErrorListener errorListener) {
1854
        errorListeners.remove(errorListener.getErrorListenerId());
6 ✔
1855
    }
1 ✔
1856

1857
    /**
1858
     * Detach the ErrorListener registered under the given id. See {@link ErrorListener#getErrorListenerId()}.
1859
     * Removing an id that is not registered is a no-op.
1860
     *
1861
     * @param id the id of the ErrorListener to detach
1862
     */
1863
    public void removeErrorListenerById(@NonNull String id) {
1864
        errorListeners.remove(id);
5 ✔
1865
    }
1 ✔
1866

1867
    /**
1868
     * Flush the connection's buffer of outgoing messages, including sending a
1869
     * protocol message to and from the server.
1870
     * If called while the connection is closed, this method will immediately
1871
     * throw a TimeoutException, regardless of the timeout.
1872
     * If called while the connection is disconnected due to network issues this
1873
     * method will wait for up to the timeout for a reconnect or close.
1874
     *
1875
     * @param timeoutMillis the time in milliseconds to wait for the flush to succeed; pass 0 (or a negative value) to wait forever.
1876
     * @throws TimeoutException if the timeout is exceeded
1877
     * @throws InterruptedException if the underlying thread is interrupted
1878
     */
1879
    public void flush(long timeoutMillis) throws TimeoutException, InterruptedException {
1880
        // The timeout comes in as millis, but we operate in nanos: we do work below
1881
        // (waitForConnectOrClose, then sendPing) before blocking on the future, so we
1882
        // recompute the remaining time right at the get() call for a precise wait.
1883
        long startNanos = NatsSystemClock.nanoTime();
2 ✔
1884
        boolean forever = timeoutMillis <= 0; // 0 (or negative) waits forever
8 ✔
1885
        long timeoutNanos = forever ? 0 : timeoutMillis * NANOS_PER_MILLI;
8 ✔
1886
        waitForConnectOrClose(forever ? 0 : timeoutMillis);
7 ✔
1887

1888
        if (isClosed()) {
3 ✔
1889
            throw new TimeoutException("Attempted to flush while closed");
5 ✔
1890
        }
1891

1892
        if (!forever && NatsSystemClock.nanoTime() - startNanos >= timeoutNanos) {
8 ✔
1893
            throw new TimeoutException("Timeout out waiting for connection before flush.");
5 ✔
1894
        }
1895

1896
        try {
1897
            Future<Boolean> waitForIt = sendPing();
3 ✔
1898

1899
            if (waitForIt == null) { // error in the send ping code
2 !
1900
                return;
×
1901
            }
1902

1903
            if (forever) {
2 ✔
1904
                waitForIt.get();
4 ✔
1905
            }
1906
            else {
1907
                // recompute the time left here, after the work above, so the future waits for a precise remainder
1908
                long remainingNanos = timeoutNanos - (NatsSystemClock.nanoTime() - startNanos);
6 ✔
1909
                if (remainingNanos <= 0) {
4 !
1910
                    remainingNanos = 1; // let the future timeout if it isn't resolved
×
1911
                }
1912
                waitForIt.get(remainingNanos, TimeUnit.NANOSECONDS);
5 ✔
1913
            }
1914

1915
            statistics.incrementFlushCounter();
3 ✔
1916
        }
1917
        catch (ExecutionException | CancellationException e) {
1 ✔
1918
            throw new TimeoutException(e.toString());
6 ✔
1919
        }
1 ✔
1920
    }
1 ✔
1921

1922
    protected void sendConnect(NatsUri nuri) throws IOException {
1923
        try {
1924
            ServerInfo info = serverInfo.get();
5 ✔
1925
            // This is changed - we used to use info.isAuthRequired(), but are changing it to
1926
            // better match older versions of the server. It may change again in the future.
1927
            CharBuffer connectOptions = options.buildProtocolConnectOptionsString(
5 ✔
1928
                nuri.toString(), true, info.getNonce());
4 ✔
1929
            ByteArrayBuilder bab =
4 ✔
1930
                new ByteArrayBuilder(OP_CONNECT_SP_LEN + connectOptions.limit(), UTF_8)
5 ✔
1931
                    .append(CONNECT_SP_BYTES).append(connectOptions);
4 ✔
1932
            queueInternalOutgoing(new ProtocolMessage(bab, false));
7 ✔
1933
        }
1934
        catch (Exception exp) {
1 ✔
1935
            throw new IOException("Error sending connect string", exp);
6 ✔
1936
        }
1 ✔
1937
    }
1 ✔
1938

1939
    protected CompletableFuture<Boolean> sendPing() {
1940
        return sendPing(true);
4 ✔
1941
    }
1942

1943
    protected void softPing() {
1944
        sendPing(false);
4 ✔
1945
    }
1 ✔
1946

1947
    /**
1948
     * Calculates the round trip time between this client and the server,
1949
     * waiting up to the connection timeout for the response.
1950
     * @return the round trip time in nanoseconds
1951
     * @throws IllegalStateException if the connection is not connected, or the max outgoing ping count would be exceeded
1952
     * @throws IOException if the connection is closed while waiting for the response
1953
     * @throws TimeoutException if the server does not respond in time
1954
     * @throws InterruptedException if the thread is interrupted
1955
     */
1956
    public long RTT() throws IOException, TimeoutException, InterruptedException {
1957
        return RTT(options.getConnectionTimeout());
6 ✔
1958
    }
1959

1960
    /**
1961
     * Calculates the round trip time between this client and the server.
1962
     * <p>A completed round trip also confirms the server has processed everything this
1963
     * connection sent before it, since the server reads one connection's stream in order.
1964
     * @param timeoutMillis the maximum time to wait, in milliseconds; less than 1 uses the connection timeout
1965
     * @return the round trip time in nanoseconds. Note the wait is bounded in millis but the
1966
     *         measurement is returned in nanos, since a round trip is routinely sub-millisecond
1967
     * @throws IllegalStateException if the connection is not connected, or the max outgoing ping count would be exceeded
1968
     * @throws IOException if the connection is closed while waiting for the response
1969
     * @throws TimeoutException if the server does not respond in time
1970
     * @throws InterruptedException if the thread is interrupted
1971
     */
1972
    public long RTT(long timeoutMillis) throws IOException, TimeoutException, InterruptedException {
1973
        if (!isConnected()) {
3 ✔
1974
            throw new IllegalStateException("Must be connected to do RTT.");
5 ✔
1975
        }
1976

1977
        int max = options.getMaxPingsOut();
4 ✔
1978
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
1979
            throw new IllegalStateException("Max outgoing Ping count exceeded.");
×
1980
        }
1981

1982
        long timeout = timeoutMillis < 1 ? options.getConnectionTimeout() : timeoutMillis;
10 ✔
1983
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
4 ✔
1984
        pongQueue.add(pongFuture);
5 ✔
1985
        boolean ponged = false;
2 ✔
1986
        try {
1987
            long startNanos = NatsSystemClock.nanoTime();
2 ✔
1988
            queueOutgoing(new ProtocolMessage(PING_PROTO));
6 ✔
1989
            pongFuture.get(timeout, TimeUnit.MILLISECONDS);
5 ✔
1990
            ponged = true;
2 ✔
1991
            return NatsSystemClock.nanoTime() - startNanos;
6 ✔
1992
        }
1993
        catch (CancellationException e) {
×
1994
            // cleanUpPongQueue cancels every waiter when the connection drops
1995
            throw new IOException("Connection closed while waiting for RTT.", e);
×
1996
        }
1997
        catch (ExecutionException e) {
×
1998
            throw new IOException(e.getCause());
×
1999
        }
2000
        finally {
2001
            // a future left in the queue would be completed by the next PONG,
2002
            // consuming the one a live waiter (a flush) is owed
2003
            if (!ponged) {
2 !
2004
                pongQueue.remove(pongFuture);
×
2005
            }
2006
        }
2007
    }
2008

2009
    // Send a ping request and push a pong future on the queue.
2010
    // Futures are completed in order, keep this one if a thread wants to wait
2011
    // for a specific pong. Note, if no pong returns, the wait will not return
2012
    // without setting a timeout.
2013
    @Nullable
2014
    protected CompletableFuture<Boolean> sendPing(boolean treatAsInternal) {
2015
        if (!isConnectedOrConnecting()) {
3 !
2016
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
×
2017
            retVal.complete(Boolean.FALSE);
×
2018
            return retVal;
×
2019
        }
2020

2021
        if (!treatAsInternal && !needPing.get()) {
6 ✔
2022
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
4 ✔
2023
            retVal.complete(Boolean.TRUE);
4 ✔
2024
            needPing.set(true);
4 ✔
2025
            return retVal;
2 ✔
2026
        }
2027

2028
        int max = options.getMaxPingsOut();
4 ✔
2029
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
2030
            handleCommunicationIssue(new IllegalStateException("Max outgoing Ping count exceeded."));
6 ✔
2031
            return null;
2 ✔
2032
        }
2033

2034
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
4 ✔
2035
        pongQueue.add(pongFuture);
5 ✔
2036

2037
        if (treatAsInternal) {
2 ✔
2038
            queueInternalOutgoing(new ProtocolMessage(PING_PROTO));
7 ✔
2039
        }
2040
        else {
2041
            queueOutgoing(new ProtocolMessage(PING_PROTO));
6 ✔
2042
        }
2043

2044
        needPing.set(true);
4 ✔
2045
        statistics.incrementPingCount();
3 ✔
2046
        return pongFuture;
2 ✔
2047
    }
2048

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

2058
    protected void sendPong() {
2059
        queueInternalOutgoing(new ProtocolMessage(PONG_PROTO));
6 ✔
2060
    }
1 ✔
2061

2062
    // Called by the reader
2063
    protected void handlePong() {
2064
        CompletableFuture<Boolean> pongFuture = pongQueue.pollFirst();
5 ✔
2065
        if (pongFuture != null) {
2 !
2066
            pongFuture.complete(Boolean.TRUE);
4 ✔
2067
        }
2068
    }
1 ✔
2069

2070
    protected void readInitialInfo() throws IOException {
2071
        byte[] readBuffer = new byte[options.getBufferSize()];
5 ✔
2072
        ByteBuffer protocolBuffer = ByteBuffer.allocate(options.getBufferSize());
5 ✔
2073
        boolean gotCRLF = false;
2 ✔
2074
        boolean gotCR = false;
2 ✔
2075

2076
        while (!gotCRLF) {
2 ✔
2077
            int read = dataPort.read(readBuffer, 0, readBuffer.length);
8 ✔
2078

2079
            if (read < 0) {
2 ✔
2080
                break;
1 ✔
2081
            }
2082

2083
            int i = 0;
2 ✔
2084
            while (i < read) {
3 ✔
2085
                byte b = readBuffer[i++];
5 ✔
2086

2087
                if (gotCR) {
2 ✔
2088
                    if (b != LF) {
3 ✔
2089
                        throw new IOException("Missed LF after CR waiting for INFO.");
5 ✔
2090
                    }
2091
                    else if (i < read) {
3 ✔
2092
                        throw new IOException("Read past initial info message.");
5 ✔
2093
                    }
2094

2095
                    gotCRLF = true;
2 ✔
2096
                    break;
1 ✔
2097
                }
2098

2099
                if (b == CR) {
3 ✔
2100
                    gotCR = true;
3 ✔
2101
                }
2102
                else {
2103
                    if (!protocolBuffer.hasRemaining()) {
3 ✔
2104
                        protocolBuffer = enlargeBuffer(protocolBuffer); // just double it
4 ✔
2105
                    }
2106
                    protocolBuffer.put(b);
4 ✔
2107
                }
2108
            }
1 ✔
2109
        }
1 ✔
2110

2111
        if (!gotCRLF) {
2 ✔
2112
            throw new IOException("Failed to read initial info message.");
5 ✔
2113
        }
2114

2115
        protocolBuffer.flip();
3 ✔
2116

2117
        String infoJson = UTF_8.decode(protocolBuffer).toString();
5 ✔
2118
        infoJson = infoJson.trim();
3 ✔
2119
        String[] msg = infoJson.split("\\s");
4 ✔
2120
        String op = msg[0].toUpperCase();
5 ✔
2121

2122
        if (!OP_INFO.equals(op)) {
4 ✔
2123
            throw new IOException("Received non-info initial message.");
5 ✔
2124
        }
2125

2126
        handleInfo(infoJson);
3 ✔
2127
    }
1 ✔
2128

2129
    protected void handleInfo(String infoJson) {
2130
        ServerInfo newServerInfo = new ServerInfo(infoJson);
5 ✔
2131
        serverInfo.set(newServerInfo);
4 ✔
2132

2133
        List<String> urls = newServerInfo.getConnectURLs();
3 ✔
2134
        if (!urls.isEmpty()) {
3 ✔
2135
            if (serverPool.acceptDiscoveredUrls(urls)) {
5 ✔
2136
                processConnectionEvent(ConnectionEvents.DISCOVERED_SERVERS, urls.toString());
5 ✔
2137
            }
2138
        }
2139

2140
        if (newServerInfo.isLameDuckMode()) {
3 ✔
2141
            processConnectionEvent(ConnectionEvents.LAME_DUCK, uriDetail(currentServer));
7 ✔
2142
            this.lameDuckTriggered = true;
3 ✔
2143
        }
2144
    }
1 ✔
2145

2146
    protected void validatePayloadAndControlLineSizes(NatsMessage msg) {
2147
        if (options.clientSideLimitChecks()) {
4 ✔
2148
            if (getMaxPayload() > 0 && msg.getPayloadSize() > getMaxPayload()) {
12 ✔
2149
                throw new IllegalArgumentException(
3 ✔
2150
                    "Message payload size exceed server configuration " + msg.getPayloadSize() + " vs " + getMaxPayload());
6 ✔
2151
            }
2152
            if (msg.getControlLineLength() > options.getMaxControlLine()) {
6 ✔
2153
                throw new IllegalArgumentException("Control line is too long");
5 ✔
2154
            }
2155
        }
2156
    }
1 ✔
2157

2158
    protected void queueOutgoing(NatsMessage msg) {
2159
        validatePayloadAndControlLineSizes(msg);
3 ✔
2160
        if (!writer.queue(msg)) {
5 ✔
2161
            notifyErrorListener((c, el) -> el.messageDiscarded(c, msg));
9 ✔
2162
        }
2163
    }
1 ✔
2164

2165
    protected void queueInternalOutgoing(NatsMessage msg) {
2166
        validatePayloadAndControlLineSizes(msg);
3 ✔
2167
        writer.queueInternalMessage(msg);
4 ✔
2168
    }
1 ✔
2169

2170
    protected void deliverMessage(NatsMessage msg) {
2171
        needPing.set(false);
4 ✔
2172
        statistics.incrementIn(msg.getSizeInBytes());
5 ✔
2173

2174
        SubscriptionInfo subscription = subscriptions.get(msg.getSID());
7 ✔
2175

2176
        if (subscription != null) {
2 !
2177
            NatsSubscription sub = subscription.sub;
3 ✔
2178
            msg.setSubscription(sub);
3 ✔
2179

2180
            NatsDispatcher d = subscription.dispatcher;
3 ✔
2181
            NatsMessageSink s = (d == null) ? sub : d;
6 ✔
2182
            // The only read of the queue reference on this path: q is what is judged below and q is
2183
            // what is pushed to, so an invalidate() on another thread cannot change it underneath
2184
            // the decision.
2185
            ConsumerMessageQueue q = ((d == null) ? sub.getMessageQueue() : d.getMessageQueue());
8 ✔
2186

2187
            switch (s.getDeliverabilityState(q)) {
7 !
2188
                case AVAILABLE -> {
2189
                    s.markNotSlow();
2 ✔
2190
                    // the before queue processor contract is to return true if the message is allowed to be queued
2191
                    if (sub.getBeforeQueueProcessor().apply(msg)) {
7 !
2192
                        q.push(msg);
4 ✔
2193
                    }
2194
                }
2195
                case FULL -> {
2196
                    // Drop the message and count it
2197
                    statistics.incrementDroppedCount();
3 ✔
2198
                    s.incrementDroppedCount();
2 ✔
2199

2200
                    // Notify the first time
2201
                    if (!s.isMarkedSlow()) {
3 ✔
2202
                        s.markSlow();
2 ✔
2203
                        processSlowConsumer(sub);
4 ✔
2204
                    }
2205
                }
2206
                case NOT_AVAILABLE -> {
2207
                    // The subscription went away between registration and delivery. Count the drop,
2208
                    // but do not mark slow - a subscription that is gone is not a slow consumer.
2209
                    statistics.incrementDroppedCount();
3 ✔
2210
                    s.incrementDroppedCount();
2 ✔
2211
                }
2212
            }
2213
        }
2214
//      else Drop messages we don't have a subscriber for (could be extras on an auto-unsub for example)
2215
    }
1 ✔
2216

2217
    protected void processOK() {
2218
        statistics.incrementOkCount();
3 ✔
2219
    }
1 ✔
2220

2221
    protected void makeCallback(Runnable callback) {
2222
        if (callbackExecutor != null) {
3 ✔
2223
            try {
2224
                callbackExecutor.execute(() -> {
6 ✔
2225
                    try {
2226
                        callback.run();
2 ✔
2227
                    }
2228
                    catch (Exception ex) {
1 ✔
2229
                        statistics.incrementExceptionCount();
3 ✔
2230
                    }
1 ✔
2231
                });
1 ✔
2232
            }
2233
            catch (RejectedExecutionException re) {
×
2234
                // Timing with shutdown probably, let it go
2235
            }
1 ✔
2236
        }
2237
    }
1 ✔
2238

2239
    /**
2240
     * Report that a subscription or dispatcher has fallen behind its pending limits. Only the first
2241
     * detection is reported until the consumer catches up again.
2242
     * @param subscription the subscription or dispatcher that is behind
2243
     */
2244
    public void processSlowConsumer(Subscription subscription) {
2245
        notifyErrorListener((c, el) -> el.slowConsumerDetected(c, subscription));
9 ✔
2246
    }
1 ✔
2247

2248
    /**
2249
     * Report an exception the client caught and handled, incrementing the exception statistic.
2250
     * @param exp the exception
2251
     */
2252
    public void processException(Exception exp) {
2253
        statistics.incrementExceptionCount();
3 ✔
2254
        notifyErrorListener((c, el) -> el.exceptionOccurred(c, exp));
9 ✔
2255
    }
1 ✔
2256

2257
    /**
2258
     * Handle a -ERR protocol message from the server. The text becomes the last error, and an
2259
     * authentication error is also remembered against the current server so the connect logic can
2260
     * stop retrying a server that will never accept these credentials.
2261
     * @param errorText the error text as sent by the server
2262
     */
2263
    public void processError(String errorText) {
2264
        statistics.incrementErrCount();
3 ✔
2265

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

2269
        // If we get an authentication error, save it
2270
        if (isAuthenticationError(errorText) && currentServer != null) {
7 ✔
2271
            serverAuthErrors.put(currentServer, errorText);
7 ✔
2272
        }
2273

2274
        notifyErrorListener((c, el) -> el.errorOccurred(c, errorText));
9 ✔
2275
    }
1 ✔
2276

2277
    /**
2278
     * Picks which {@link ErrorListener} method to invoke, so {@link #notifyErrorListener(ErrorListenerCaller)}
2279
     * can do the fan-out once for every event.
2280
     */
2281
    public interface ErrorListenerCaller {
2282
        /**
2283
         * Invoke the event method on one listener.
2284
         * @param conn the connection the event is for
2285
         * @param el the listener to call
2286
         */
2287
        void call(NatsConnection conn, ErrorListener el);
2288
    }
2289

2290
    /**
2291
     * The single fan-out point for every ErrorListener event. One callback per listener, so a slow or
2292
     * throwing listener cannot block the others.
2293
     * @param elc the call to make on each listener
2294
     */
2295
    public void notifyErrorListener(ErrorListenerCaller elc) {
2296
        for (ErrorListener listener : errorListeners.values()) {
12 ✔
2297
            makeCallback(() -> elc.call(this, listener));
11 ✔
2298
        }
1 ✔
2299
    }
1 ✔
2300

2301
    protected String uriDetail(NatsUri uri) {
2302
        return uri == null ? null : uri.toString();
7 ✔
2303
    }
2304

2305
    protected String uriDetail(NatsUri uri, NatsUri hostOrlast) {
2306
        if (uri != null) {
2 !
2307
            if (hostOrlast == null || uri.equals(hostOrlast)) {
6 !
2308
                return uri.toString();
3 ✔
2309
            }
2310
            return uri + " [" + hostOrlast + "]";
6 ✔
2311
        }
2312
        return hostOrlast == null ? null : hostOrlast.toString();
×
2313
    }
2314

2315
    protected void processConnectionEvent(ConnectionEvents type, String uriDetails) {
2316
        long time = System.currentTimeMillis();
2 ✔
2317
        for (ConnectionListener listener : connectionListeners.values()) {
12 ✔
2318
            makeCallback(() -> listener.connectionEvent(this, type, time, uriDetails));
16 ✔
2319
        }
1 ✔
2320
    }
1 ✔
2321

2322
    /**
2323
     * Return the server info object. Will never be null, but will be an instance of {@link ServerInfo#EMPTY_INFO}
2324
     * before a connection is made, and will represent the last connected server once connected and while disconnected
2325
     * until a new connection is made.
2326
     * @return the server information such as id, client info, etc.
2327
     */
2328
    @NonNull
2329
    public ServerInfo getServerInfo() {
2330
        return serverInfo.get();
5 ✔
2331
    }
2332

2333
    /**
2334
     * the InetAddress of client as known by the NATS server, otherwise null.
2335
     * @return the InetAddress
2336
     */
2337
    @Nullable
2338
    public InetAddress getClientInetAddress() {
2339
        try {
2340
            ServerInfo si = getServerInfo();
3 ✔
2341
            return si == ServerInfo.EMPTY_INFO ? null : NatsInetAddress.getByName(si.getClientIp());
7 !
2342
        }
2343
        catch (Exception e) {
×
2344
            return null;
×
2345
        }
2346
    }
2347

2348
    /**
2349
     * the read-only options used to create this connection
2350
     * @return the Options
2351
     */
2352
    @NonNull
2353
    public Options getOptions() {
2354
        return options;
3 ✔
2355
    }
2356

2357
    /**
2358
     * a wrapper for useful statistics about the connection
2359
     * @return the Statistics implementation
2360
     */
2361
    @NonNull
2362
    public Statistics getStatistics() {
2363
        return statistics.getStatistics();
4 ✔
2364
    }
2365

2366
    protected StatisticsCollector getStatisticsCollector() {
2367
        return statistics;
3 ✔
2368
    }
2369

2370
    protected DataPort getDataPort() {
2371
        return dataPort;
3 ✔
2372
    }
2373

2374
    // Used for testing
2375
    protected int getSinkCount() {
2376
        return subscriptions.size() + dispatchers.size();
8 ✔
2377
    }
2378

2379
    /**
2380
     * MaxPayload returns the size limit that a message payload can have. This is
2381
     * set by the server configuration and delivered to the client upon connect.
2382
     *
2383
     * @return the maximum size of a message payload
2384
     */
2385
    public long getMaxPayload() {
2386
        ServerInfo info = serverInfo.get();
5 ✔
2387

2388
        if (info == null) {
2 !
2389
            return -1;
×
2390
        }
2391

2392
        return info.getMaxPayload();
3 ✔
2393
    }
2394

2395
    /**
2396
     * Return the list of known server urls, including additional servers discovered
2397
     * after a connection has been established.
2398
     * Will be empty (but not null) before a connection is made and will represent the last connected server while disconnected
2399
     * @return this connection's list of known server URLs
2400
     */
2401
    @NonNull
2402
    public Collection<String> getServers() {
2403
        return serverPool.getServerList();
4 ✔
2404
    }
2405

2406
    protected List<NatsUri> resolveHost(NatsUri nuri) {
2407
        // 1. If the nuri host is not already an ip address
2408
        //      -and- the nuri is not for websocket
2409
        //      -and- the HostnameResolveMode is Resolve
2410
        //    let the pool resolve it.
2411
        HostnameResolveMode resolveMode = options.hostnameResolveMode();
4 ✔
2412
        List<NatsUri> results = new ArrayList<>();
4 ✔
2413
        if (!nuri.hostIsIpAddress()
4 ✔
2414
            && !nuri.isWebsocket()
5 !
2415
            && resolveMode.resolve)
2416
        {
2417
            List<String> ips = serverPool.resolveHostToIps(
5 ✔
2418
                nuri.getHost(), resolveMode.maxOneResult, resolveMode.includeIPV6);
5 ✔
2419
            if (ips != null) {
2 !
2420
                for (String ip : ips) {
10 ✔
2421
                    try {
2422
                        results.add(nuri.reHost(ip));
6 ✔
2423
                    }
2424
                    catch (URISyntaxException u) {
×
2425
                        // ??? should never happen
2426
                        throw new RuntimeException(u);
×
2427
                    }
1 ✔
2428
                }
1 ✔
2429
            }
2430
        }
2431

2432
        // 2. If there were no results,
2433
        //    - host was already an ip address
2434
        //    - host was for websocket
2435
        //    - hostnameResolveMode did not want to be resolved
2436
        //    - pool returned nothing
2437
        //    - resolving failed...
2438
        //    so the list just becomes the original host.
2439
        if (results.isEmpty()) {
3 ✔
2440
            results.add(nuri);
4 ✔
2441
        }
2442
        return results;
2 ✔
2443
    }
2444

2445
    /**
2446
     * the url used for the current connection, or null if disconnected
2447
     * @return the url string
2448
     */
2449
    @Nullable
2450
    public String getConnectedUrl() {
2451
        return currentServer == null ? null : currentServer.toString();
9 ✔
2452
    }
2453

2454
    /**
2455
     * Returns the connection's current status.
2456
     *
2457
     * @return the connection's status
2458
     */
2459
    @NonNull
2460
    public ConnectionStatus getStatus() {
2461
        return status;
3 ✔
2462
    }
2463

2464
    /**
2465
     * the error text from the last error sent by the server to this client
2466
     * @return the last error text
2467
     */
2468
    @Nullable
2469
    public String getLastError() {
2470
        return lastError.get();
5 ✔
2471
    }
2472

2473
    /**
2474
     * Clear the last error from the server
2475
     */
2476
    public void clearLastError() {
2477
        lastError.set(null);
4 ✔
2478
    }
1 ✔
2479

2480
    protected ExecutorService getExecutor() {
2481
        return executor;
3 ✔
2482
    }
2483

2484
    protected ExecutorService getReaderExecutor() {
2485
        return readerExecutor;
3 ✔
2486
    }
2487

2488
    protected ExecutorService getWriterExecutor() {
2489
        return writerExecutor;
3 ✔
2490
    }
2491

2492
    protected ScheduledExecutorService getScheduledExecutor() {
2493
        return scheduledExecutor;
3 ✔
2494
    }
2495

2496
    protected void updateStatus(ConnectionStatus newStatus) {
2497
        updateStatus(newStatus, uriDetail(currentServer == null ? lastServer : currentServer));
13 ✔
2498
    }
1 ✔
2499

2500
    protected void updateStatus(ConnectionStatus newStatus, NatsUri resolvedUri, NatsUri hostUri) {
2501
        updateStatus(newStatus, uriDetail(resolvedUri, hostUri));
7 ✔
2502
    }
1 ✔
2503

2504
    protected void updateStatus(ConnectionStatus newStatus, String uriDetail) {
2505
        // The status is read under the lock and the event is chosen from the transition this
2506
        // call actually made, not from re-reading the field afterwards. Re-reading raises the
2507
        // event for whatever status another thread has since installed - or none at all, which
2508
        // is how a DISCONNECTED event goes missing when a reconnect follows closely behind.
2509
        ConnectionStatus oldStatus;
2510
        statusLock.lock();
3 ✔
2511
        try {
2512
            oldStatus = status;
3 ✔
2513
            if (oldStatus == CLOSED || newStatus == oldStatus) {
6 !
2514
                return;
1 ✔
2515
            }
2516
            this.status = newStatus;
3 ✔
2517
            statusChanged.signalAll();
3 ✔
2518
        }
2519
        finally {
2520
            statusLock.unlock();
3 ✔
2521
        }
2522

2523
        if (newStatus == DISCONNECTED) {
3 ✔
2524
            processConnectionEvent(ConnectionEvents.DISCONNECTED, uriDetail);
5 ✔
2525
        }
2526
        else if (newStatus == CLOSED) {
3 ✔
2527
            processConnectionEvent(ConnectionEvents.CLOSED, uriDetail);
5 ✔
2528
        }
2529
        else if (oldStatus == RECONNECTING && newStatus == CONNECTED) {
6 !
2530
            processConnectionEvent(ConnectionEvents.RECONNECTED, uriDetail);
5 ✔
2531
        }
2532
        else if (newStatus == CONNECTED) {
3 ✔
2533
            processConnectionEvent(ConnectionEvents.CONNECTED, uriDetail);
4 ✔
2534
        }
2535
    }
1 ✔
2536

2537
    protected boolean isClosing() {
2538
        return closing;
3 ✔
2539
    }
2540

2541
    protected boolean isClosed() {
2542
        return status == CLOSED;
8 ✔
2543
    }
2544

2545
    protected boolean isConnected() {
2546
        return status == CONNECTED;
8 ✔
2547
    }
2548

2549
    protected boolean isDisconnected() {
2550
        return status == DISCONNECTED;
×
2551
    }
2552

2553
    protected boolean isConnectedOrConnecting() {
2554
        statusLock.lock();
3 ✔
2555
        try {
2556
            return status == CONNECTED || connecting;
12 !
2557
        } finally {
2558
            statusLock.unlock();
3 ✔
2559
        }
2560
    }
2561

2562
    protected boolean isDisconnectingOrClosed() {
2563
        statusLock.lock();
3 ✔
2564
        try {
2565
            return status == CLOSED || disconnecting;
13 ✔
2566
        } finally {
2567
            statusLock.unlock();
3 ✔
2568
        }
2569
    }
2570

2571
    protected boolean isDisconnecting() {
2572
        statusLock.lock();
3 ✔
2573
        try {
2574
            return disconnecting;
5 ✔
2575
        } finally {
2576
            statusLock.unlock();
3 ✔
2577
        }
2578
    }
2579

2580
    protected void waitForDisconnectOrClose(long timeoutMillis) throws InterruptedException {
2581
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> isDisconnecting() && !isClosed() );
21 !
2582
    }
1 ✔
2583

2584
    protected void waitForConnectOrClose(long timeoutMillis) throws InterruptedException {
2585
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> !isConnected() && !isClosed());
21 !
2586
    }
1 ✔
2587

2588
    // operates purely in nanoseconds (precise, monotonic); the millis-facing wrappers above convert once at the edge.
2589
    // < 0 returns immediately, 0 waits forever, > 0 waits up to that many nanos.
2590
    protected void waitWhile(long timeoutNanos, Predicate<Void> waitWhileTrue) throws InterruptedException {
2591
        statusLock.lock();
3 ✔
2592
        try {
2593
            long currentWaitNanos = timeoutNanos;
2 ✔
2594
            long start = NatsSystemClock.nanoTime();
2 ✔
2595
            while (currentWaitNanos >= 0 && waitWhileTrue.test(null)) {
8 !
2596
                if (currentWaitNanos > 0) {
4 !
2597
                    if (statusChanged.await(currentWaitNanos, TimeUnit.NANOSECONDS) && !waitWhileTrue.test(null)) {
10 ✔
2598
                        break;
1 ✔
2599
                    }
2600
                    long now = NatsSystemClock.nanoTime();
2 ✔
2601
                    currentWaitNanos = currentWaitNanos - (now - start);
6 ✔
2602
                    start = now;
2 ✔
2603

2604
                    if (currentWaitNanos <= 0) {
4 ✔
2605
                        break;
1 ✔
2606
                    }
2607
                }
1 ✔
2608
                else {
2609
                    statusChanged.await();
×
2610
                }
2611
            }
2612
        }
2613
        finally {
2614
            statusLock.unlock();
3 ✔
2615
        }
2616
    }
1 ✔
2617

2618
    /**
2619
     * Whether the reconnect delay handler is invoked before the first round of a reconnect campaign.
2620
     * Rounds after the first always invoke it. This is the only place {@link ReconnectDelayBehavior}
2621
     * is read, so the setting applies to every handler, including a custom one.
2622
     * @param lameDuck whether the campaign was triggered by a server lame duck signal
2623
     * @return true to invoke the handler before round 1
2624
     */
2625
    private boolean delayBeforeFirstRound(boolean lameDuck) {
2626
        return switch (options.reconnectDelayBehavior()) {
8 ✔
2627
            case BeforeAllRounds -> true;
2 ✔
2628
            case LameDuckAware -> lameDuck;
2 ✔
2629
            default -> false;   // BeforeSubsequentRounds
1 ✔
2630
        };
2631
    }
2632

2633
    protected void invokeReconnectDelayHandler(long round, boolean lameDuckTriggered) {
2634
        long currentWaitMillis = options.getReconnectDelayHandler()
8 ✔
2635
            .getWaitTimeMillis(round, options, serverPool.hasSecureServer(), lameDuckTriggered);
4 ✔
2636

2637
        this.reconnectWaiter = new CompletableFuture<>();
5 ✔
2638

2639
        long start = NatsSystemClock.nanoTime();
2 ✔
2640
        while (currentWaitMillis > 0 && !isDisconnectingOrClosed() && !isConnected() && !reconnectWaiter.isDone()) {
14 !
2641
            try {
2642
                reconnectWaiter.get(currentWaitMillis, TimeUnit.MILLISECONDS);
×
2643
            } catch (Exception exp) {
1 ✔
2644
                // ignore, try to loop again
2645
            }
×
2646
            long elapsedMillis = (NatsSystemClock.nanoTime() - start) / 1_000_000L;
6 ✔
2647
            currentWaitMillis -= elapsedMillis;
4 ✔
2648
            start = NatsSystemClock.nanoTime();
2 ✔
2649
        }
1 ✔
2650

2651
        reconnectWaiter.complete(Boolean.TRUE);
5 ✔
2652
    }
1 ✔
2653

2654
    protected ByteBuffer enlargeBuffer(ByteBuffer buffer) {
2655
        int current = buffer.capacity();
3 ✔
2656
        int newSize = current * 2;
4 ✔
2657
        ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
3 ✔
2658
        buffer.flip();
3 ✔
2659
        newBuffer.put(buffer);
4 ✔
2660
        return newBuffer;
2 ✔
2661
    }
2662

2663
    // For testing
2664
    protected NatsConnectionReader getReader() {
2665
        return reader;
3 ✔
2666
    }
2667

2668
    // For testing
2669
    protected NatsConnectionWriter getWriter() {
2670
        return writer;
3 ✔
2671
    }
2672

2673
    // For testing
2674
    protected Future<DataPort> getDataPortFuture() {
2675
        return dataPortFuture;
3 ✔
2676
    }
2677

2678
    protected boolean isDraining() {
2679
        return draining.get() != null;
8 ✔
2680
    }
2681

2682
    protected boolean isDrained() {
2683
        CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2684

2685
        try {
2686
            if (tracker != null && tracker.getNow(false)) {
9 !
2687
                return true;
2 ✔
2688
            }
2689
        } catch (Exception e) {
×
2690
            // These indicate the tracker was cancelled/timed out
2691
        }
1 ✔
2692

2693
        return false;
2 ✔
2694
    }
2695

2696
    /**
2697
     * Drain tells the connection to process in flight messages before closing.
2698
     * Drain initially drains all the sinks, stopping incoming messages.
2699
     * Next, publishing is halted and a flush call is used to insure all published
2700
     * messages have reached the server.
2701
     * Finally, the connection is closed.
2702
     * In order to drain subscribers, an unsub protocol message is sent to the server followed by a flush.
2703
     * These two steps occur before drain returns. The remaining steps occur in a background thread.
2704
     * This method tries to manage the timeout properly, so that if the timeout is 1 second, and the flush
2705
     * takes 100ms, the remaining steps have 900ms in the background thread.
2706
     * The connection will try to let all messages be drained, but when the timeout is reached
2707
     * the connection is closed and any outstanding dispatcher threads are interrupted.
2708
     * A future allows this call to be treated as synchronous or asynchronous as
2709
     * needed by the application. The value of the future will be true if all the subscriptions
2710
     * were drained in the timeout, and false otherwise. The future completes after the connection
2711
     * is closed, so any connection handler notifications will happen before the future completes.
2712
     *
2713
     * @param timeoutMillis The time in milliseconds to wait for the drain to succeed, pass 0 or less to wait
2714
     *                    forever. Drain involves moving messages to and from the server
2715
     *                    so a very short timeout is not recommended. If the timeout is reached before
2716
     *                    the drain completes, the connection is simply closed, which can result in message
2717
     *                    loss.
2718
     * @return A future that can be used to check if the drain has completed
2719
     * @throws InterruptedException if the thread is interrupted
2720
     * @throws TimeoutException if the initial flush times out
2721
     */
2722
    @NonNull
2723
    public CompletableFuture<Boolean> drain(long timeoutMillis) throws TimeoutException, InterruptedException {
2724

2725
        if (isClosing() || isClosed()) {
6 !
2726
            throw new IllegalStateException("A connection can't be drained during close.");
5 ✔
2727
        }
2728

2729
        statusLock.lock();
3 ✔
2730
        try {
2731
            if (isDraining()) {
3 ✔
2732
                return draining.get();
7 ✔
2733
            }
2734
            draining.set(new CompletableFuture<>());
6 ✔
2735
        } finally {
2736
            statusLock.unlock();
3 ✔
2737
        }
2738

2739
        final CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2740
        long startNanos = NatsSystemClock.nanoTime();
2 ✔
2741

2742
        // Don't include subscriptions with dispatchers
2743
        HashSet<NatsSubscription> pureSubscriptions = new HashSet<>();
4 ✔
2744
        for (SubscriptionInfo subscription : subscriptions.values()) {
12 ✔
2745
            if (subscription.dispatcher == null) {
3 ✔
2746
                pureSubscriptions.add(subscription.sub);
5 ✔
2747
            }
2748
        }
1 ✔
2749

2750
        final HashSet<NatsMessageSink> sinks = new HashSet<>();
4 ✔
2751
        sinks.addAll(pureSubscriptions);
4 ✔
2752
        sinks.addAll(dispatchers);
5 ✔
2753

2754
        NatsDispatcher inboxer = inboxDispatcher.get();
5 ✔
2755

2756
        if (inboxer != null) {
2 ✔
2757
            sinks.add(inboxer);
4 ✔
2758
        }
2759

2760
        // Stop the sinks NOW so that when this method returns they are blocked
2761
        sinks.forEach((sink) -> {
4 ✔
2762
            sink.markDraining(tracker);
3 ✔
2763
            sink.sendUnsubForDrain();
2 ✔
2764
        });
1 ✔
2765

2766
        try {
2767
            flush(timeoutMillis); // Flush and wait up to the timeout, if this fails, let the caller know
3 ✔
2768
        } catch (Exception e) {
1 ✔
2769
            close(false, false);
4 ✔
2770
            throw e;
2 ✔
2771
        }
1 ✔
2772

2773
        sinks.forEach(NatsMessageSink::markUnsubedForDrain);
3 ✔
2774

2775
        // Wait for the timeout or all sinks are drained
2776
        executor.submit(() -> {
10 ✔
2777
            try {
2778
                long timeoutNanos = timeoutMillis <= 0 ? Long.MAX_VALUE : timeoutMillis * NANOS_PER_MILLI;
10 ✔
2779
                long startTime = System.nanoTime();
2 ✔
2780
                while (NatsSystemClock.nanoTime() - startTime < timeoutNanos && !Thread.interrupted()) {
8 !
2781
                    sinks.removeIf(NatsMessageSink::isDrained);
4 ✔
2782
                    if (sinks.isEmpty()) {
3 ✔
2783
                        break;
1 ✔
2784
                    }
2785
                    //noinspection BusyWait
2786
                    Thread.sleep(1); // Sleep 1 milli
3 ✔
2787
                }
2788

2789
                // Stop publishing
2790
                blockPublishForDrain.set(true);
4 ✔
2791

2792
                // One last flush
2793
                if (timeoutMillis <= 0) {
4 ✔
2794
                    flush(0);
4 ✔
2795
                } else {
2796
                    long remainingMillis = timeoutMillis - (NatsSystemClock.nanoTime() - startNanos) / NANOS_PER_MILLI;
8 ✔
2797
                    if (remainingMillis > 0) {
4 ✔
2798
                        flush(remainingMillis);
3 ✔
2799
                    }
2800
                }
2801
                close(false, false); // close the connection after the last flush
4 ✔
2802
                tracker.complete(sinks.isEmpty());
6 ✔
2803
            } catch (TimeoutException e) {
×
2804
                processException(e);
×
2805
            } catch (InterruptedException e) {
×
2806
                processException(e);
×
2807
                Thread.currentThread().interrupt();
×
2808
            } finally {
2809
                try {
2810
                    close(false, false);// close the connection after the last flush
4 ✔
2811
                } catch (InterruptedException e) {
×
2812
                    processException(e);
×
2813
                    Thread.currentThread().interrupt();
×
2814
                }
1 ✔
2815
                tracker.complete(false);
5 ✔
2816
            }
2817
        });
1 ✔
2818

2819
        return tracker;
2 ✔
2820
    }
2821

2822
    protected boolean isAuthenticationError(String err) {
2823
        if (err == null) {
2 ✔
2824
            return false;
2 ✔
2825
        }
2826
        err = err.toLowerCase();
3 ✔
2827
        return err.startsWith("user authentication")
7 ✔
2828
            || err.contains("authorization violation")
4 ✔
2829
            || err.startsWith("account authentication expired");
5 ✔
2830
    }
2831

2832
    /**
2833
     * Immediately flushes the underlying connection buffer if the connection is valid.
2834
     * @throws IOException if the connection flush fails
2835
     */
2836
    public void flushBuffer() throws IOException {
2837
        if (!isConnected()) {
3 ✔
2838
            throw new IllegalStateException("NatsConnection is not active.");
5 ✔
2839
        }
2840
        writer.flushBuffer();
3 ✔
2841
    }
1 ✔
2842

2843
    /**
2844
     * Get the number of messages in the outgoing queue for this connection.
2845
     * This value is volatile in the sense that it changes often and may be adjusted by more than one message.
2846
     * It changes every time a message is published (put in the outgoing queue)
2847
     * and every time a message is removed from the queue to be written over the socket
2848
     * @return the number of messages in the outgoing queue
2849
     */
2850
    public long outgoingPendingMessageCount() {
2851
        closeSocketLock.lock();
3 ✔
2852
        try {
2853
            return writer == null ? -1 : writer.outgoingPendingMessageCount();
9 !
2854
        }
2855
        finally {
2856
            closeSocketLock.unlock();
3 ✔
2857
        }
2858
    }
2859

2860
    /**
2861
     * Get the number of bytes based to be written calculated from the messages in the outgoing queue for this connection.
2862
     * This value is volatile in the sense that it changes often and may be adjusted by more than one message's bytes.
2863
     * It changes every time a message is published (put in the outgoing queue)
2864
     * and every time a message is removed from the queue to be written over the socket
2865
     * @return the number of messages in the outgoing queue
2866
     */
2867
    public long outgoingPendingBytes() {
2868
        closeSocketLock.lock();
3 ✔
2869
        try {
2870
            return writer == null ? -1 : writer.outgoingPendingBytes();
9 !
2871
        }
2872
        finally {
2873
            closeSocketLock.unlock();
3 ✔
2874
        }
2875
    }
2876

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

© 2026 Coveralls, Inc