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

synadia-io / nats.java.v3 / 37648054681

07 Oct 2026 03:55PM UTC coverage: 89.174% (-2.1%) from 91.29%
37648054681

push

github

scottf
NatsConnectionV3 (default) with OutboundBuffer, classic fixes, build-variants CI

- NatsConnectionV3 extends NatsConnection, selected by ConnectionImplementation (V3 default, Classic available via option, property or -PconnectionImplementation)
- OutboundBuffer: control lane + bounded data blocks; SUB control line limit check; kept-block limits from the first server's max payload
- Classic fixes: protocol buffer sizing, tryingToConnect CAS, lock-free pending getters, sendPing pong future cleanup, per-run reader/writer flag
- build-variants.yml replaces build-windows.yml
- Plans: connection V3, active/passive V3, testing.java overview, V2 bugs from review

4596 of 5421 branches covered (84.78%)

Branch coverage included in aggregate %.

1027 of 1185 new or added lines in 12 files covered. (86.67%)

161 existing lines in 4 files now uncovered.

11532 of 12665 relevant lines covered (91.05%)

4.03 hits per line

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

76.0
/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.compareAndSet(false, true)) {
6 !
221
            try {
222
                connectImpl(reconnectOnConnect);
3 ✔
223
            }
224
            finally {
225
                tryingToConnect.set(false);
4 ✔
226
            }
227
        }
228
    }
1 ✔
229

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

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

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

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

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

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

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

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

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

273
                if (isAuthenticationError(err)) {
4 !
UNCOV
274
                    serverAuthErrors.put(resolved, err);
×
275
                }
276
            }
1 ✔
277

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

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

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

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

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

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

UNCOV
343
        closeSocketLock.lock();
×
344
        try {
UNCOV
345
            updateStatus(DISCONNECTED);
×
346

347
            // Close and reset the current data port and future
UNCOV
348
            if (dataPortFuture != null) {
×
UNCOV
349
                dataPortFuture.cancel(true);
×
UNCOV
350
                dataPortFuture = null;
×
351
            }
352

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

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

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

UNCOV
402
        reconnectImpl();
×
UNCOV
403
    }
×
404

405
    protected void reconnect() throws InterruptedException {
NEW
406
        if (tryingToConnect.compareAndSet(false, true)) {
×
407
            try {
UNCOV
408
                reconnectImpl();
×
409
            }
410
            finally {
UNCOV
411
                tryingToConnect.set(false);
×
412
            }
413
        }
UNCOV
414
    }
×
415

416
    // Reconnect can only be called when the connection is disconnected
417
    protected void reconnectImpl() throws InterruptedException {
418
        if (isClosed()) {
3 !
UNCOV
419
            return;
×
420
        }
421

422
        if (options.getMaxReconnects() == 0) {
4 !
UNCOV
423
            close(true, false);
×
UNCOV
424
            return;
×
425
        }
426

427
        writer.enterReconnectMode();
3 ✔
428

429
        if (!isConnected() && !isClosed() && !isClosing()) {
9 !
430
            reconnectImplConnect();
2 ✔
431
        }
432

433
        if (!isConnected()) {
3 !
UNCOV
434
            close(true, false);
×
UNCOV
435
            return;
×
436
        }
437

438
        subscriptions.forEach((sid, subscription) -> {
5 ✔
439
            if (subscription.dispatcher == null && !subscription.sub.isDraining()) {
3 !
UNCOV
440
                sendSubscriptionMessage(sid, subscription.sub.getSubject(), subscription.sub.getQueueName(), true);
×
441
            }
442
        });
1 ✔
443

444
        dispatchers.forEach(d -> {
4 ✔
445
            if (!d.isDraining()) {
3 !
446
                d.resendSubscriptions();
2 ✔
447
            }
448
        });
1 ✔
449

450
        writer.enterWaitingForEndReconnectMode();
3 ✔
451

452
        processConnectionEvent(ConnectionEvent.RESUBSCRIBED, uriDetail(currentServer));
7 ✔
453
    }
1 ✔
454

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

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

489
                tryToConnect(cur, resolved, NatsSystemClock.nanoTime());
5 ✔
490

491
                if (isConnected()) {
3 ✔
492
                    serverPool.connectSucceeded(cur);
4 ✔
493
                    statistics.incrementReconnects();
3 ✔
494
                    return;
1 ✔
495
                }
496

497
                String err = connectError.get();
5 ✔
498
                if (isAuthenticationError(err)) {
4 !
UNCOV
499
                    if (err.equals(serverAuthErrors.get(resolved))) {
×
UNCOV
500
                        return; // double auth error
×
501
                    }
UNCOV
502
                    serverAuthErrors.put(resolved, err);
×
503
                }
504
            }
1 ✔
505

506
            if (!isConnected() && !isClosed()) {
6 !
507
                serverPool.connectFailed(cur);
4 ✔
508
            }
509
        }
1 ✔
UNCOV
510
    }
×
511

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

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

526
        try {
527
            long end = nowNanos + (options.getConnectionTimeout() * NANOS_PER_MILLI);
8 ✔
528
            timeCheck(end);
4 ✔
529

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

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

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

556
            timeCheck(end);
4 ✔
557
            cleanUpPongQueue();
2 ✔
558

559
            timeLeftNanos = timeCheck(end);
4 ✔
560
            DataPort newDataPort = options.createDataPort();
4 ✔
561
            newDataPort.connect(this, resolved, timeLeftNanos);
5 ✔
562

563
            // Notify any threads waiting on the sockets
564
            this.dataPort = newDataPort;
3 ✔
565
            dataPortFuture.complete(dataPort);
6 ✔
566

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

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

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

603
            timeCheck(end);
4 ✔
604
            sendConnect(resolved);
3 ✔
605

606
            timeLeftNanos = timeCheck(end);
4 ✔
607
            Future<Boolean> pongFuture = sendPing();
3 ✔
608

609
            if (pongFuture != null) {
2 !
610
                pongFuture.get(timeLeftNanos, TimeUnit.NANOSECONDS);
5 ✔
611
            }
612

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

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

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

641
                if (exceptionDuringConnectChange != null) {
3 !
UNCOV
642
                    throw exceptionDuringConnectChange;
×
643
                }
644

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

677
    protected void clearCurrentServer() {
678
        if (currentServer != null) {
3 ✔
679
            lastServer = currentServer;
4 ✔
680
        }
681
        currentServer = null;
3 ✔
682
    }
1 ✔
683

684
    protected void checkVersionRequirements() throws IOException {
685
        Options opts = getOptions();
3 ✔
686
        ServerInfo info = getServerInfo();
3 ✔
687

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

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

722
    /**
723
     * Called by a reader that hit an I/O error. The reader passes itself so an implementation that uses
724
     * more than one reader over the connection's life can ignore a report from one that is no longer current.
725
     * @param source the reader reporting
726
     * @param io the error
727
     */
728
    protected void handleCommunicationIssue(NatsConnectionReader source, Exception io) {
729
        handleCommunicationIssue(io);
3 ✔
730
    }
1 ✔
731

732
    // Called from reader/writer thread
733
    protected void handleCommunicationIssue(Exception io) {
734
        // If we are connecting or disconnecting, note exception and leave
735
        statusLock.lock();
3 ✔
736
        try {
737
            if (connecting || disconnecting || status == CLOSED || isDraining()) {
13 !
UNCOV
738
                this.exceptionDuringConnectChange = io;
×
UNCOV
739
                return;
×
740
            }
741
        }
742
        finally {
743
            statusLock.unlock();
3 ✔
744
        }
745

746
        processException(io);
3 ✔
747
        if (currentServer != null) {
3 !
748
            serverPool.connectFailed(currentServer);
5 ✔
749
        }
750

751
        // Spawn a thread so we don't have timing issues with
752
        // waiting on read/write threads
753
        executor.submit(() -> {
6 ✔
754
            if (tryingToConnect.compareAndSet(false, true)) {
6 !
755
                try {
756
                    // any issue that brings us here is pretty serious
757
                    // so we are comfortable forcing the close
758
                    closeSocket(true, true);
4 ✔
759
                }
760
                catch (InterruptedException e) {
×
761
                    processException(e);
×
762
                    Thread.currentThread().interrupt();
×
763
                }
764
                finally {
765
                    tryingToConnect.set(false);
4 ✔
766
                }
767
            }
768
        });
1 ✔
769
    }
1 ✔
770

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

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

795
                statusChanged.signalAll();
3 ✔
796
            }
797
            finally {
798
                statusLock.unlock();
3 ✔
799
            }
800

801
            closeSocketImpl(forceClose);
3 ✔
802

803
            statusLock.lock();
3 ✔
804
            try {
805
                this.exceptionDuringConnectChange = null; // Ignore IOExceptions during closeSocketImpl()
3 ✔
806
                this.disconnecting = false;
3 ✔
807
                statusChanged.signalAll();
3 ✔
808
            }
809
            finally {
810
                statusLock.unlock();
3 ✔
811
            }
812

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

825
    // Close socket is called when another connect attempt is possible
826
    // Close is called when the connection should shut down, period
827

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

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

857
            this.closing = true;// We were asked to close, so do it
3 ✔
858
            if (isDisconnectingOrClosed()) {
3 !
UNCOV
859
                waitForDisconnectOrClose(options.getConnectionTimeout());
×
UNCOV
860
                return;
×
861
            }
862
            else {
863
                this.disconnecting = true;
3 ✔
864
                this.exceptionDuringConnectChange = null;
3 ✔
865
                statusChanged.signalAll();
3 ✔
866
            }
867
        }
868
        finally {
869
            statusLock.unlock();
3 ✔
870
        }
871

872
        // Stop the reconnect wait timer after we stop the writer/reader (only if we are
873
        // really closing, not on errors)
874
        if (reconnectWaiter != null) {
3 !
875
            reconnectWaiter.cancel(true);
5 ✔
876
        }
877

878
        closeSocketImpl(forceClose);
3 ✔
879

880
        dispatchers.forEach(d -> d.stop(false));
4 ✔
881

882
        subscriptions.forEach((sid, subscription) -> subscription.sub.invalidate());
4 ✔
883

884
        dispatchers.clear();
3 ✔
885
        subscriptions.clear();
3 ✔
886

887
        if (pingTask != null) {
3 ✔
888
            pingTask.shutdown();
3 ✔
889
            pingTask = null;
3 ✔
890
        }
891
        if (cleanupTask != null) {
3 ✔
892
            cleanupTask.shutdown();
3 ✔
893
            cleanupTask = null;
3 ✔
894
        }
895

896
        cleanResponses(true);
3 ✔
897

898
        cleanUpPongQueue();
2 ✔
899

900
        statusLock.lock();
3 ✔
901
        try {
902
            updateStatus(CLOSED); // will signal, we also signal when we stop disconnecting
3 ✔
903

904
            /*
905
             * if (exceptionDuringConnectChange != null) {
906
             * processException(exceptionDuringConnectChange); exceptionDuringConnectChange
907
             * = null; }
908
             */
909
        }
910
        finally {
911
            statusLock.unlock();
3 ✔
912
        }
913

914
        callbackExecutor = null;
3 ✔
915
        executor = null;
3 ✔
916
        connectExecutor = null;
3 ✔
917
        readerExecutor = null;
3 ✔
918
        writerExecutor = null;
3 ✔
919
        scheduledExecutor = null;
3 ✔
920
        options.shutdownExecutors();
3 ✔
921

922
        statusLock.lock();
3 ✔
923
        try {
924
            this.disconnecting = false;
3 ✔
925
            statusChanged.signalAll();
3 ✔
926
        }
927
        finally {
928
            statusLock.unlock();
3 ✔
929
        }
930
    }
1 ✔
931

932
    // these *ExecutorIsClosed() are only used for tests
933
    protected boolean callbackExecutorIsClosed() { return callbackExecutor == null; }
7 ✔
934
    protected boolean executorIsClosed() { return executor == null; }
7 ✔
935
    protected boolean connectExecutorIsClosed() { return connectExecutor == null; }
7 ✔
936
    protected boolean readerExecutorIsClosed() { return readerExecutor == null; }
7 ✔
937
    protected boolean writerExecutorIsClosed() { return writerExecutor == null; }
7 ✔
938
    protected boolean scheduledExecutorIsClosed() { return scheduledExecutor == null; }
7 ✔
939

940
    // Should only be called from closeSocket or close
941
    protected void closeSocketImpl(boolean forceClose) {
942
        clearCurrentServer();
2 ✔
943

944
        // Signal both to stop.
945
        final Future<Boolean> readStop = reader.stop();
4 ✔
946
        final Future<Boolean> writeStop = writer.stop();
4 ✔
947

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

962
        // Close and reset the current data port and future
963
        if (dataPortFuture != null) {
3 ✔
964
            dataPortFuture.cancel(true);
5 ✔
965
            dataPortFuture = null;
3 ✔
966
        }
967

968
        // Close the current socket and cancel anyone waiting for it
969
        try {
970
            if (dataPort != null) {
3 ✔
971
                if (forceClose) {
2 ✔
972
                    dataPort.forceClose();
4 ✔
973
                }
974
                else {
975
                    dataPort.close();
3 ✔
976
                }
977
            }
978

979
        }
980
        catch (IOException ex) {
×
981
            processException(ex);
×
982
        }
1 ✔
983
        cleanUpPongQueue();
2 ✔
984

985
        try {
986
            reader.stop().get(10, TimeUnit.SECONDS);
7 ✔
987
        }
988
        catch (Exception ex) {
×
989
            processException(ex);
×
990
        }
1 ✔
991
        try {
992
            writer.stop().get(10, TimeUnit.SECONDS);
7 ✔
993
        }
994
        catch (Exception ex) {
×
995
            processException(ex);
×
996
        }
1 ✔
997
    }
1 ✔
998

999
    protected void cleanUpPongQueue() {
1000
        Future<Boolean> b;
1001
        while ((b = pongQueue.poll()) != null) {
7 ✔
1002
            b.cancel(true);
5 ✔
1003
        }
1004
    }
1 ✔
1005

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

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

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

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

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

1101
    /**
1102
     * Send a message.
1103
     * @param message the message to send
1104
     * @throws IllegalArgumentException if the message is null, the subject or reply subject is invalid, or the headers are not supported by the connected server
1105
     */
1106
    public void publish(@NonNull NatsMessage message) {
UNCOV
1107
        Validator.validateNotNull(message, "Message");
×
UNCOV
1108
        subjectValidate(message.getSubject());
×
UNCOV
1109
        replyValidate(message.getReplyTo());
×
UNCOV
1110
        validateNotClosed();
×
UNCOV
1111
        _publish(new InternalPublishableMessage(message, false));
×
UNCOV
1112
    }
×
1113

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

1128
    // MUST CALL validateNotClosed(); BEFORE CALLING THIS
1129
    private void _publish(InternalPublishableMessage ipm) {
1130
        if (ipm.hasHeaders && !serverInfo.get().isHeadersSupported()) {
3 !
UNCOV
1131
            throw new IllegalArgumentException("Headers are not supported by the server, version: " + serverInfo.get().getVersion());
×
1132
        }
1133

1134
        if ((status == RECONNECTING || status == DISCONNECTED)
8 !
UNCOV
1135
            && !writer.canQueueDuringReconnect(ipm)) {
×
UNCOV
1136
            throw new IllegalStateException(
×
UNCOV
1137
                "Unable to queue any more messages during reconnect, max buffer is " + options.getReconnectBufferSize());
×
1138
        }
1139

1140
        queueOutgoing(ipm);
3 ✔
1141
    }
1 ✔
1142

1143
    private void validateNotClosed() {
1144
        if (isClosed()) {
3 !
UNCOV
1145
            throw new IllegalStateException("NatsConnection is Closed");
×
1146
        }
1147
        else if (blockPublishForDrain.get()) {
4 !
1148
            throw new IllegalStateException("NatsConnection is Draining"); // Ok to publish while waiting on subs
×
1149
        }
1150
    }
1 ✔
1151

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

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

1196
    protected void invalidate(NatsSubscription sub) {
1197
        remove(sub);
3 ✔
1198
        sub.invalidate();
2 ✔
1199
    }
1 ✔
1200

1201
    protected void remove(NatsSubscription sub) {
1202
        SubscriptionInfo subscription = subscriptions.remove(sub.getSID());
7 ✔
1203

1204
        if (subscription != null && subscription.dispatcher != null) {
5 ✔
1205
            subscription.dispatcher.remove(sub);
4 ✔
1206
        }
1207
    }
1 ✔
1208

1209
    protected void unsubscribe(NatsSubscription sub, int after) {
1210
        if (isClosed()) { // last chance, usually sub will catch this
3 !
1211
            throw new IllegalStateException("NatsConnection is Closed");
×
1212
        }
1213

1214
        if (after <= 0) {
2 ✔
1215
            invalidate(sub); // Will clean it up
4 ✔
1216
        }
1217
        else {
1218
            sub.setUnsubLimit(after);
4 ✔
1219

1220
            if (sub.reachedUnsubLimit()) {
3 ✔
1221
                invalidate(sub); // takes the sid out of subscriptions, not just the sub out of service
3 ✔
1222
            }
1223
        }
1224

1225
        if (!isConnected()) {
3 !
1226
            return; // We will set up sub on reconnect or ignore
×
1227
        }
1228

1229
        sendUnsub(sub, after);
4 ✔
1230
    }
1 ✔
1231

1232
    protected void sendUnsub(@NonNull NatsSubscription sub, int after) {
UNCOV
1233
        ByteArrayBuilder bab =
×
UNCOV
1234
            new ByteArrayBuilder().append(UNSUB_SP_BYTES).append(sub.getSID());
×
UNCOV
1235
        if (after > 0) {
×
UNCOV
1236
            bab.append(SP).append(after);
×
1237
        }
UNCOV
1238
        queueOutgoing(new ProtocolMessage(bab, true));
×
UNCOV
1239
    }
×
1240

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

1250
        protected SubscriptionInfo(NatsSubscription sub, @Nullable NatsDispatcher dispatcher) {
2 ✔
1251
            this.sub = sub;
3 ✔
1252
            this.dispatcher = dispatcher;
3 ✔
1253
        }
1 ✔
1254
    }
1255

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

1269
        NatsSubscription sub;
1270
        String sid = getNextSid();
3 ✔
1271

1272
        if (factory == null) {
2 ✔
1273
            sub = new NatsSubscription(sid, subject, queueGroup, this, dispatcher);
10 ✔
1274
        }
1275
        else {
1276
            sub = factory.createNatsSubscription(sid, subject, queueGroup, this, dispatcher);
8 ✔
1277
        }
1278
        subscriptions.put(sid, new SubscriptionInfo(sub, dispatcher));
10 ✔
1279

1280
        sendSubscriptionMessage(sid, subject, queueGroup, false);
6 ✔
1281
        return sub;
2 ✔
1282
    }
1283

1284
    protected String getNextSid() {
1285
        return Long.toString(nextSid.getAndIncrement());
5 ✔
1286
    }
1287

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

1296
    protected void sendSubscriptionMessage(String sid, String subject, String queueGroup, boolean treatAsInternal) {
1297
        if (!isConnected()) {
3 !
UNCOV
1298
            return; // We will set up sub on reconnect or ignore
×
1299
        }
1300

1301
        ByteArrayBuilder bab = new ByteArrayBuilder(UTF_8).append(SUB_SP_BYTES).append(subject);
9 ✔
1302
        if (queueGroup != null) {
2 !
1303
            bab.append(SP).append(queueGroup);
6 ✔
1304
        }
1305
        bab.append(SP).append(sid);
6 ✔
1306

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

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

1331
    protected int getRespInboxLength() {
1332
        return options.getInboxPrefix().length() + 22 + 1; // 22 for nuid, 1 for .
9 ✔
1333
    }
1334

1335
    protected String createResponseInbox(String inbox) {
1336
        // Substring gets rid of the * [trailing]
1337
        return inbox.substring(0, getRespInboxLength()) + nuid.next();
10 ✔
1338
    }
1339

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

1350
    protected void cleanResponses(boolean closing) {
1351
        ArrayList<String> toRemove = new ArrayList<>();
4 ✔
1352
        boolean wasInterrupted = false;
2 ✔
1353

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

1384
            if (remove) {
2 ✔
1385
                toRemove.add(entry.getKey());
6 ✔
1386
                statistics.decrementOutstandingRequests();
3 ✔
1387
            }
1388
        }
1 ✔
1389

1390
        for (String key : toRemove) {
10 ✔
1391
            responsesAwaiting.remove(key);
5 ✔
1392
        }
1 ✔
1393

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

1404
            for (String token : toRemove) {
10 ✔
1405
                responsesRespondedTo.remove(token);
5 ✔
1406
            }
1 ✔
1407
        }
1408
    }
1 ✔
1409

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

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

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

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

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

1516
    /**
1517
     * Send a request. The returned future will be completed when the
1518
     * response comes back.
1519
     *
1520
     * @param subject the subject for the service that will handle the request
1521
     * @param data the content of the message
1522
     * @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.
1523
     * @throws IllegalArgumentException if the subject is invalid
1524
     */
1525
    @NonNull
1526
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data) {
1527
        return requestAsync(subject, null, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1528
    }
1529

1530
    /**
1531
     * Send a request. The returned future will be completed when the
1532
     * response comes back.
1533
     *
1534
     * @param subject the subject for the service that will handle the request
1535
     * @param headers Optional headers to publish with the message.
1536
     * @param data the content of the message
1537
     * @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.
1538
     * @throws IllegalArgumentException if the subject is invalid
1539
     */
1540
    @NonNull
1541
    public CompletableFuture<Message> requestAsync(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data) {
1542
        return requestAsync(subject, headers, data, -1, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1543
    }
1544

1545
    /**
1546
     * Send a request. The returned future will be completed when the
1547
     * response comes back.
1548
     *
1549
     * @param subject the subject for the service that will handle the request
1550
     * @param data the content of the message
1551
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1552
     * @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.
1553
     * @throws IllegalArgumentException if the subject is invalid
1554
     */
1555
    @NonNull
1556
    public CompletableFuture<Message> requestAsync(@NonNull String subject, byte @Nullable [] data, long timeoutMillis) {
1557
        return requestAsync(subject, null, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1558
    }
1559

1560
    /**
1561
     * Send a request. The returned future will be completed when the
1562
     * response comes back.
1563
     *
1564
     * @param subject the subject for the service that will handle the request
1565
     * @param data the content of the message
1566
     * @param headers Optional headers to publish with the message.
1567
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1568
     * @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.
1569
     * @throws IllegalArgumentException if the subject is invalid
1570
     */
1571
    @NonNull
1572
    public CompletableFuture<Message> requestAsync(@NonNull String subject, @Nullable Headers headers, byte @Nullable [] data, long timeoutMillis) {
1573
        return requestAsync(subject, headers, data, timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
10 ✔
1574
    }
1575

1576
    /**
1577
     * Send a request. The returned future will be completed when the
1578
     * response comes back.
1579
     *
1580
     * <p>The Message object allows you to set a replyTo, but in requests,
1581
     * the replyTo is reserved for internal use as the address for the
1582
     * server to respond to the client with the consumer's reply.</p>
1583
     *
1584
     * @param message the message
1585
     * @param timeoutMillis the time in milliseconds to wait for a response; a value less than 1 uses the default
1586
     * @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.
1587
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1588
     */
1589
    @NonNull
1590
    public CompletableFuture<Message> requestAsync(@NonNull Message message, long timeoutMillis) {
1591
        Validator.validateNotNull(message, "Message");
×
1592
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), timeoutMillis, CancelAction.REPORT, forceFlushOnRequest);
×
1593
    }
1594

1595
    /**
1596
     * Send a request. The returned future will be completed when the
1597
     * response comes back.
1598
     *
1599
     * <p>The Message object allows you to set a replyTo, but in requests,
1600
     * the replyTo is reserved for internal use as the address for the
1601
     * server to respond to the client with the consumer's reply.</p>
1602
     *
1603
     * @param message the message
1604
     * @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.
1605
     * @throws IllegalArgumentException if the message is null, or the subject is invalid
1606
     */
1607
    @NonNull
1608
    public CompletableFuture<Message> request(@NonNull Message message) {
1609
        Validator.validateNotNull(message, "Message");
4 ✔
1610
        return requestAsync(message.getSubject(), message.getHeaders(), message.getData(), -1, CancelAction.REPORT, forceFlushOnRequest);
13 ✔
1611
    }
1612

1613
    /**
1614
     * Send a request, returning a future for the response. This is the core request-send used by all the
1615
     * other request / requestAsync methods.
1616
     *
1617
     * @param subject the subject for the service that will handle the request
1618
     * @param headers optional headers to publish with the message
1619
     * @param data the content of the message
1620
     * @param timeoutMillis the time in milliseconds before the outstanding request is cleaned up; a value
1621
     *                       less than 0 uses the request cleanup interval default
1622
     * @param cancelAction what to do with the future if the request is cancelled (cancel, report, or complete)
1623
     * @param flushImmediatelyAfterPublish whether to flush the outgoing buffer immediately after publishing the request
1624
     * @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.
1625
     * @throws IllegalArgumentException if the subject is invalid
1626
     */
1627
    @NonNull
1628
    public CompletableFuture<Message> requestAsync(@NonNull String subject,
1629
                                                   @Nullable Headers headers,
1630
                                                   byte @Nullable [] data,
1631
                                                   long timeoutMillis,
1632
                                                   @NonNull CancelAction cancelAction,
1633
                                                   boolean flushImmediatelyAfterPublish)
1634
    {
UNCOV
1635
        validateNotClosed();
×
UNCOV
1636
        subjectValidate(subject);
×
1637

UNCOV
1638
        if (inboxDispatcher.get() == null) {
×
UNCOV
1639
            inboxDispatcherLock.lock();
×
1640
            try {
UNCOV
1641
                if (inboxDispatcher.get() == null) {
×
UNCOV
1642
                    NatsDispatcher d = dispatcherFactory.createDispatcher(this, this::deliverReply);
×
1643

1644
                    // Ensure the dispatcher is started before publishing messages
UNCOV
1645
                    dispatchers.add(d);
×
UNCOV
1646
                    d.start();
×
UNCOV
1647
                    d.subscribe(mainInbox);
×
UNCOV
1648
                    inboxDispatcher.set(d);
×
1649
                }
1650
            }
1651
            finally {
UNCOV
1652
                inboxDispatcherLock.unlock();
×
1653
            }
1654
        }
1655

UNCOV
1656
        String responseInbox = createResponseInbox(mainInbox);
×
UNCOV
1657
        String responseToken = getResponseToken(responseInbox);
×
UNCOV
1658
        NatsRequestCompletableFuture future =
×
1659
            new NatsRequestCompletableFuture(cancelAction,
UNCOV
1660
                timeoutMillis <= 0 ? options.getRequestCleanupInterval() : timeoutMillis, options.useTimeoutException());
×
1661

UNCOV
1662
        responsesAwaiting.put(responseToken, future);
×
UNCOV
1663
        statistics.incrementOutstandingRequests();
×
1664

1665
        try {
UNCOV
1666
            _publish(new InternalPublishableMessage(data, subject, responseInbox, headers, flushImmediatelyAfterPublish));
×
1667
        }
UNCOV
1668
        catch (RuntimeException e) {
×
1669
            // The publish never made it onto the queue (queue full/busy, or the calling thread was
1670
            // interrupted). Undo the registration so we don't leak a never-completable future waiting
1671
            // for the cleanup timer, then let the caller see the failure.
UNCOV
1672
            responsesAwaiting.remove(responseToken);
×
UNCOV
1673
            statistics.decrementOutstandingRequests();
×
UNCOV
1674
            throw e;
×
UNCOV
1675
        }
×
1676

UNCOV
1677
        statistics.incrementRequestsSent();
×
1678

UNCOV
1679
        return future;
×
1680
    }
1681

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

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

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

1758
        NatsDispatcher dispatcher = dispatcherFactory.createDispatcher(this, handler);
6 ✔
1759
        dispatchers.add(dispatcher);
5 ✔
1760
        dispatcher.start();
2 ✔
1761
        return dispatcher;
2 ✔
1762
    }
1763

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

1780
        NatsDispatcher nd = (NatsDispatcher) d;
3 ✔
1781

1782
        if (nd.isDraining()) {
3 !
1783
            return; // No op while draining
×
1784
        }
1785

1786
        if (!dispatchers.contains(nd)) {
5 ✔
1787
            throw new IllegalArgumentException("Dispatcher is already closed.");
5 ✔
1788
        }
1789

1790
        cleanupDispatcher(nd);
3 ✔
1791
    }
1 ✔
1792

1793
    protected void cleanupDispatcher(NatsDispatcher nd) {
1794
        nd.stop(true);
3 ✔
1795
        dispatchers.remove(nd);
5 ✔
1796
    }
1 ✔
1797

1798
    protected Set<Dispatcher> getDispatchers() {
1799
        return Collections.unmodifiableSet(dispatchers);
×
1800
    }
1801

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

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

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

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

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

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

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

1893
        if (isClosed()) {
3 ✔
1894
            throw new TimeoutException("Attempted to flush while closed");
5 ✔
1895
        }
1896

1897
        if (!forever && NatsSystemClock.nanoTime() - startNanos >= timeoutNanos) {
8 ✔
1898
            throw new TimeoutException("Timeout out waiting for connection before flush.");
5 ✔
1899
        }
1900

1901
        try {
1902
            Future<Boolean> waitForIt = sendPing();
3 ✔
1903

1904
            if (waitForIt == null) { // error in the send ping code
2 !
1905
                return;
×
1906
            }
1907

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

1920
            statistics.incrementFlushCounter();
3 ✔
1921
        }
1922
        catch (ExecutionException | CancellationException e) {
1 ✔
1923
            throw new TimeoutException(e.toString());
6 ✔
1924
        }
1 ✔
1925
    }
1 ✔
1926

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

1944
    protected CompletableFuture<Boolean> sendPing() {
1945
        return sendPing(true);
4 ✔
1946
    }
1947

1948
    protected void softPing() {
1949
        sendPing(false);
4 ✔
1950
    }
1 ✔
1951

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

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

1982
        int max = options.getMaxPingsOut();
4 ✔
1983
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
1984
            throw new IllegalStateException("Max outgoing Ping count exceeded.");
×
1985
        }
1986

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

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

2026
        if (!treatAsInternal && !needPing.get()) {
2 !
UNCOV
2027
            CompletableFuture<Boolean> retVal = new CompletableFuture<>();
×
UNCOV
2028
            retVal.complete(Boolean.TRUE);
×
UNCOV
2029
            needPing.set(true);
×
UNCOV
2030
            return retVal;
×
2031
        }
2032

2033
        int max = options.getMaxPingsOut();
4 ✔
2034
        if (max > 0 && pongQueue.size() + 1 > max) {
9 !
UNCOV
2035
            handleCommunicationIssue(new IllegalStateException("Max outgoing Ping count exceeded."));
×
UNCOV
2036
            return null;
×
2037
        }
2038

2039
        CompletableFuture<Boolean> pongFuture = new CompletableFuture<>();
4 ✔
2040
        pongQueue.add(pongFuture);
5 ✔
2041

2042
        try {
2043
            if (treatAsInternal) {
2 !
2044
                queueInternalOutgoing(new ProtocolMessage(PING_PROTO));
7 ✔
2045
            }
2046
            else {
NEW
2047
                queueOutgoing(new ProtocolMessage(PING_PROTO));
×
2048
            }
2049
        }
NEW
2050
        catch (RuntimeException e) {
×
2051
            // a future left in the queue would be completed by the next PONG,
2052
            // consuming the one a live waiter (a flush) is owed
NEW
2053
            pongQueue.remove(pongFuture);
×
NEW
2054
            throw e;
×
2055
        }
1 ✔
2056

2057
        needPing.set(true);
4 ✔
2058
        statistics.incrementPingCount();
3 ✔
2059
        return pongFuture;
2 ✔
2060
    }
2061

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

2071
    protected void sendPong() {
2072
        queueInternalOutgoing(new ProtocolMessage(PONG_PROTO));
6 ✔
2073
    }
1 ✔
2074

2075
    // Called by the reader
2076
    protected void handlePong() {
2077
        CompletableFuture<Boolean> pongFuture = pongQueue.pollFirst();
5 ✔
2078
        if (pongFuture != null) {
2 !
2079
            pongFuture.complete(Boolean.TRUE);
4 ✔
2080
        }
2081
    }
1 ✔
2082

2083
    protected void readInitialInfo() throws IOException {
2084
        byte[] readBuffer = new byte[options.getBufferSize()];
5 ✔
2085
        ByteBuffer protocolBuffer = ByteBuffer.allocate(options.getBufferSize());
5 ✔
2086
        boolean gotCRLF = false;
2 ✔
2087
        boolean gotCR = false;
2 ✔
2088

2089
        while (!gotCRLF) {
2 ✔
2090
            int read = dataPort.read(readBuffer, 0, readBuffer.length);
8 ✔
2091

2092
            if (read < 0) {
2 ✔
2093
                break;
1 ✔
2094
            }
2095

2096
            int i = 0;
2 ✔
2097
            while (i < read) {
3 ✔
2098
                byte b = readBuffer[i++];
5 ✔
2099

2100
                if (gotCR) {
2 ✔
2101
                    if (b != LF) {
3 ✔
2102
                        throw new IOException("Missed LF after CR waiting for INFO.");
5 ✔
2103
                    }
2104
                    else if (i < read) {
3 ✔
2105
                        throw new IOException("Read past initial info message.");
5 ✔
2106
                    }
2107

2108
                    gotCRLF = true;
2 ✔
2109
                    break;
1 ✔
2110
                }
2111

2112
                if (b == CR) {
3 ✔
2113
                    gotCR = true;
3 ✔
2114
                }
2115
                else {
2116
                    if (!protocolBuffer.hasRemaining()) {
3 ✔
2117
                        protocolBuffer = enlargeBuffer(protocolBuffer); // just double it
4 ✔
2118
                    }
2119
                    protocolBuffer.put(b);
4 ✔
2120
                }
2121
            }
1 ✔
2122
        }
1 ✔
2123

2124
        if (!gotCRLF) {
2 ✔
2125
            throw new IOException("Failed to read initial info message.");
5 ✔
2126
        }
2127

2128
        protocolBuffer.flip();
3 ✔
2129

2130
        String infoJson = UTF_8.decode(protocolBuffer).toString();
5 ✔
2131
        infoJson = infoJson.trim();
3 ✔
2132
        String[] msg = infoJson.split("\\s");
4 ✔
2133
        String op = msg[0].toUpperCase();
5 ✔
2134

2135
        if (!OP_INFO.equals(op)) {
4 ✔
2136
            throw new IOException("Received non-info initial message.");
5 ✔
2137
        }
2138

2139
        handleInfo(infoJson);
3 ✔
2140
    }
1 ✔
2141

2142
    protected void handleInfo(String infoJson) {
2143
        ServerInfo newServerInfo = new ServerInfo(infoJson);
5 ✔
2144
        serverInfo.set(newServerInfo);
4 ✔
2145

2146
        List<String> urls = newServerInfo.getConnectURLs();
3 ✔
2147
        if (!urls.isEmpty()) {
3 ✔
2148
            if (serverPool.acceptDiscoveredUrls(urls)) {
5 ✔
2149
                processConnectionEvent(ConnectionEvent.DISCOVERED_SERVERS, urls.toString());
5 ✔
2150
            }
2151
        }
2152

2153
        if (newServerInfo.isLameDuckMode()) {
3 ✔
2154
            processConnectionEvent(ConnectionEvent.LAME_DUCK, uriDetail(currentServer));
7 ✔
2155
            this.lameDuckTriggered = true;
3 ✔
2156
        }
2157
    }
1 ✔
2158

2159
    protected void validatePayloadAndControlLineSizes(NatsMessage msg) {
2160
        if (options.clientSideLimitChecks()) {
4 !
2161
            if (getMaxPayload() > 0 && msg.getPayloadSize() > getMaxPayload()) {
12 !
UNCOV
2162
                throw new IllegalArgumentException(
×
UNCOV
2163
                    "Message payload size exceed server configuration " + msg.getPayloadSize() + " vs " + getMaxPayload());
×
2164
            }
2165
            if (msg.getControlLineLength() > options.getMaxControlLine()) {
6 !
UNCOV
2166
                throw new IllegalArgumentException("Control line is too long");
×
2167
            }
2168
        }
2169
    }
1 ✔
2170

2171
    protected void queueOutgoing(NatsMessage msg) {
2172
        validatePayloadAndControlLineSizes(msg);
3 ✔
2173
        if (!writer.queue(msg)) {
5 !
UNCOV
2174
            notifyErrorListener((c, el) -> el.messageDiscarded(c, msg));
×
2175
        }
2176
    }
1 ✔
2177

2178
    protected void queueInternalOutgoing(NatsMessage msg) {
2179
        validatePayloadAndControlLineSizes(msg);
3 ✔
2180
        writer.queueInternalMessage(msg);
4 ✔
2181
    }
1 ✔
2182

2183
    protected void deliverMessage(NatsMessage msg) {
2184
        needPing.set(false);
4 ✔
2185
        statistics.incrementIn(msg.getSizeInBytes());
5 ✔
2186

2187
        SubscriptionInfo subscription = subscriptions.get(msg.getSID());
7 ✔
2188

2189
        if (subscription != null) {
2 !
2190
            NatsSubscription sub = subscription.sub;
3 ✔
2191
            msg.setSubscription(sub);
3 ✔
2192

2193
            NatsDispatcher d = subscription.dispatcher;
3 ✔
2194
            NatsMessageSink s = (d == null) ? sub : d;
6 ✔
2195
            // The only read of the queue reference on this path: q is what is judged below and q is
2196
            // what is pushed to, so an invalidate() on another thread cannot change it underneath
2197
            // the decision.
2198
            ConsumerMessageQueue q = ((d == null) ? sub.getMessageQueue() : d.getMessageQueue());
8 ✔
2199

2200
            switch (s.getDeliverabilityState(q)) {
7 !
2201
                case AVAILABLE -> {
2202
                    s.markNotSlow();
2 ✔
2203
                    // the before queue processor contract is to return true if the message is allowed to be queued
2204
                    if (sub.getBeforeQueueProcessor().apply(msg)) {
7 !
2205
                        q.push(msg);
4 ✔
2206
                    }
2207
                }
2208
                case FULL -> {
2209
                    // Drop the message and count it
2210
                    statistics.incrementDroppedCount();
3 ✔
2211
                    s.incrementDroppedCount();
2 ✔
2212

2213
                    // Notify the first time
2214
                    if (!s.isMarkedSlow()) {
3 ✔
2215
                        s.markSlow();
2 ✔
2216
                        processSlowConsumer(sub);
4 ✔
2217
                    }
2218
                }
2219
                case NOT_AVAILABLE -> {
2220
                    // The subscription went away between registration and delivery. Count the drop,
2221
                    // but do not mark slow - a subscription that is gone is not a slow consumer.
2222
                    statistics.incrementDroppedCount();
3 ✔
2223
                    s.incrementDroppedCount();
2 ✔
2224
                }
2225
            }
2226
        }
2227
//      else Drop messages we don't have a subscriber for (could be extras on an auto-unsub for example)
2228
    }
1 ✔
2229

2230
    protected void processOK() {
2231
        statistics.incrementOkCount();
3 ✔
2232
    }
1 ✔
2233

2234
    protected void makeCallback(Runnable callback) {
2235
        if (callbackExecutor != null) {
3 ✔
2236
            try {
2237
                callbackExecutor.execute(() -> {
6 ✔
2238
                    try {
2239
                        callback.run();
2 ✔
2240
                    }
2241
                    catch (Exception ex) {
1 ✔
2242
                        statistics.incrementExceptionCount();
3 ✔
2243
                    }
1 ✔
2244
                });
1 ✔
2245
            }
2246
            catch (RejectedExecutionException re) {
×
2247
                // Timing with shutdown probably, let it go
2248
            }
1 ✔
2249
        }
2250
    }
1 ✔
2251

2252
    /**
2253
     * Report that a subscription or dispatcher has fallen behind its pending limits. Only the first
2254
     * detection is reported until the consumer catches up again.
2255
     * @param subscription the subscription or dispatcher that is behind
2256
     */
2257
    public void processSlowConsumer(Subscription subscription) {
2258
        notifyErrorListener((c, el) -> el.slowConsumerDetected(c, subscription));
9 ✔
2259
    }
1 ✔
2260

2261
    /**
2262
     * Report an exception the client caught and handled, incrementing the exception statistic.
2263
     * @param exp the exception
2264
     */
2265
    public void processException(Exception exp) {
2266
        statistics.incrementExceptionCount();
3 ✔
2267
        notifyErrorListener((c, el) -> el.exceptionOccurred(c, exp));
9 ✔
2268
    }
1 ✔
2269

2270
    /**
2271
     * Handle a -ERR protocol message from the server. The text becomes the last error, and an
2272
     * authentication error is also remembered against the current server so the connect logic can
2273
     * stop retrying a server that will never accept these credentials.
2274
     * @param errorText the error text as sent by the server
2275
     */
2276
    public void processError(String errorText) {
2277
        statistics.incrementErrCount();
3 ✔
2278

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

2282
        // If we get an authentication error, save it
2283
        if (isAuthenticationError(errorText) && currentServer != null) {
7 ✔
2284
            serverAuthErrors.put(currentServer, errorText);
7 ✔
2285
        }
2286

2287
        notifyErrorListener((c, el) -> el.errorOccurred(c, errorText));
9 ✔
2288
    }
1 ✔
2289

2290
    /**
2291
     * Picks which {@link ErrorListener} method to invoke, so {@link #notifyErrorListener(ErrorListenerCaller)}
2292
     * can do the fan-out once for every event.
2293
     */
2294
    public interface ErrorListenerCaller {
2295
        /**
2296
         * Invoke the event method on one listener.
2297
         * @param conn the connection the event is for
2298
         * @param el the listener to call
2299
         */
2300
        void call(NatsConnection conn, ErrorListener el);
2301
    }
2302

2303
    /**
2304
     * The single fan-out point for every ErrorListener event. One callback per listener, so a slow or
2305
     * throwing listener cannot block the others.
2306
     * @param elc the call to make on each listener
2307
     */
2308
    public void notifyErrorListener(ErrorListenerCaller elc) {
2309
        for (ErrorListener listener : errorListeners.values()) {
12 ✔
2310
            makeCallback(() -> elc.call(this, listener));
11 ✔
2311
        }
1 ✔
2312
    }
1 ✔
2313

2314
    protected String uriDetail(NatsUri uri) {
2315
        return uri == null ? null : uri.toString();
7 ✔
2316
    }
2317

2318
    protected String uriDetail(NatsUri uri, NatsUri hostOrlast) {
2319
        if (uri != null) {
2 !
2320
            if (hostOrlast == null || uri.equals(hostOrlast)) {
6 !
2321
                return uri.toString();
3 ✔
2322
            }
2323
            return uri + " [" + hostOrlast + "]";
6 ✔
2324
        }
2325
        return hostOrlast == null ? null : hostOrlast.toString();
×
2326
    }
2327

2328
    protected void processConnectionEvent(ConnectionEvent type, String uriDetails) {
2329
        long time = System.currentTimeMillis();
2 ✔
2330
        for (ConnectionListener listener : connectionListeners.values()) {
12 ✔
2331
            makeCallback(() -> listener.connectionEvent(this, type, time, uriDetails));
16 ✔
2332
        }
1 ✔
2333
    }
1 ✔
2334

2335
    /**
2336
     * Return the server info object. Will never be null, but will be an instance of {@link ServerInfo#EMPTY_INFO}
2337
     * before a connection is made, and will represent the last connected server once connected and while disconnected
2338
     * until a new connection is made.
2339
     * @return the server information such as id, client info, etc.
2340
     */
2341
    @NonNull
2342
    public ServerInfo getServerInfo() {
2343
        return serverInfo.get();
5 ✔
2344
    }
2345

2346
    /**
2347
     * the InetAddress of client as known by the NATS server, otherwise null.
2348
     * @return the InetAddress
2349
     */
2350
    @Nullable
2351
    public InetAddress getClientInetAddress() {
2352
        try {
2353
            ServerInfo si = getServerInfo();
3 ✔
2354
            return si == ServerInfo.EMPTY_INFO ? null : NatsInetAddress.getByName(si.getClientIp());
7 !
2355
        }
2356
        catch (Exception e) {
×
2357
            return null;
×
2358
        }
2359
    }
2360

2361
    /**
2362
     * the read-only options used to create this connection
2363
     * @return the Options
2364
     */
2365
    @NonNull
2366
    public Options getOptions() {
2367
        return options;
3 ✔
2368
    }
2369

2370
    /**
2371
     * a wrapper for useful statistics about the connection
2372
     * @return the Statistics implementation
2373
     */
2374
    @NonNull
2375
    public Statistics getStatistics() {
2376
        return statistics.getStatistics();
4 ✔
2377
    }
2378

2379
    protected StatisticsCollector getStatisticsCollector() {
2380
        return statistics;
3 ✔
2381
    }
2382

2383
    protected DataPort getDataPort() {
2384
        return dataPort;
3 ✔
2385
    }
2386

2387
    // Used for testing
2388
    protected int getSinkCount() {
2389
        return subscriptions.size() + dispatchers.size();
8 ✔
2390
    }
2391

2392
    /**
2393
     * MaxPayload returns the size limit that a message payload can have. This is
2394
     * set by the server configuration and delivered to the client upon connect.
2395
     *
2396
     * @return the maximum size of a message payload
2397
     */
2398
    public long getMaxPayload() {
2399
        ServerInfo info = serverInfo.get();
5 ✔
2400

2401
        if (info == null) {
2 !
2402
            return -1;
×
2403
        }
2404

2405
        return info.getMaxPayload();
3 ✔
2406
    }
2407

2408
    /**
2409
     * Return the list of known server urls, including additional servers discovered
2410
     * after a connection has been established.
2411
     * Will be empty (but not null) before a connection is made and will represent the last connected server while disconnected
2412
     * @return this connection's list of known server URLs
2413
     */
2414
    @NonNull
2415
    public Collection<String> getServers() {
2416
        return serverPool.getServerList();
4 ✔
2417
    }
2418

2419
    protected List<NatsUri> resolveHost(NatsUri nuri) {
2420
        // 1. If the nuri host is not already an ip address
2421
        //      -and- the nuri is not for websocket
2422
        //      -and- the HostnameResolveMode is Resolve
2423
        //    let the pool resolve it.
2424
        HostnameResolveMode resolveMode = options.hostnameResolveMode();
4 ✔
2425
        List<NatsUri> results = new ArrayList<>();
4 ✔
2426
        if (!nuri.hostIsIpAddress()
4 ✔
2427
            && !nuri.isWebsocket()
5 !
2428
            && resolveMode.resolve)
2429
        {
2430
            List<String> ips = serverPool.resolveHostToIps(
5 ✔
2431
                nuri.getHost(), resolveMode.maxOneResult, resolveMode.includeIPV6);
5 ✔
2432
            if (ips != null) {
2 !
2433
                for (String ip : ips) {
10 ✔
2434
                    try {
2435
                        results.add(nuri.reHost(ip));
6 ✔
2436
                    }
2437
                    catch (URISyntaxException u) {
×
2438
                        // ??? should never happen
2439
                        throw new RuntimeException(u);
×
2440
                    }
1 ✔
2441
                }
1 ✔
2442
            }
2443
        }
2444

2445
        // 2. If there were no results,
2446
        //    - host was already an ip address
2447
        //    - host was for websocket
2448
        //    - hostnameResolveMode did not want to be resolved
2449
        //    - pool returned nothing
2450
        //    - resolving failed...
2451
        //    so the list just becomes the original host.
2452
        if (results.isEmpty()) {
3 ✔
2453
            results.add(nuri);
4 ✔
2454
        }
2455
        return results;
2 ✔
2456
    }
2457

2458
    /**
2459
     * the url used for the current connection, or null if disconnected
2460
     * @return the url string
2461
     */
2462
    @Nullable
2463
    public String getConnectedUrl() {
2464
        return currentServer == null ? null : currentServer.toString();
9 ✔
2465
    }
2466

2467
    /**
2468
     * Returns the connection's current status.
2469
     *
2470
     * @return the connection's status
2471
     */
2472
    @NonNull
2473
    public ConnectionStatus getStatus() {
2474
        return status;
3 ✔
2475
    }
2476

2477
    /**
2478
     * the error text from the last error sent by the server to this client
2479
     * @return the last error text
2480
     */
2481
    @Nullable
2482
    public String getLastError() {
2483
        return lastError.get();
5 ✔
2484
    }
2485

2486
    /**
2487
     * Clear the last error from the server
2488
     */
2489
    public void clearLastError() {
2490
        lastError.set(null);
4 ✔
2491
    }
1 ✔
2492

2493
    protected ExecutorService getExecutor() {
2494
        return executor;
3 ✔
2495
    }
2496

2497
    protected ExecutorService getReaderExecutor() {
2498
        return readerExecutor;
3 ✔
2499
    }
2500

2501
    protected ExecutorService getWriterExecutor() {
2502
        return writerExecutor;
3 ✔
2503
    }
2504

2505
    protected ScheduledExecutorService getScheduledExecutor() {
2506
        return scheduledExecutor;
3 ✔
2507
    }
2508

2509
    protected void updateStatus(ConnectionStatus newStatus) {
2510
        updateStatus(newStatus, uriDetail(currentServer == null ? lastServer : currentServer));
13 ✔
2511
    }
1 ✔
2512

2513
    protected void updateStatus(ConnectionStatus newStatus, NatsUri resolvedUri, NatsUri hostUri) {
2514
        updateStatus(newStatus, uriDetail(resolvedUri, hostUri));
7 ✔
2515
    }
1 ✔
2516

2517
    protected void updateStatus(ConnectionStatus newStatus, String uriDetail) {
2518
        // The status is read under the lock and the event is chosen from the transition this
2519
        // call actually made, not from re-reading the field afterwards. Re-reading raises the
2520
        // event for whatever status another thread has since installed - or none at all, which
2521
        // is how a DISCONNECTED event goes missing when a reconnect follows closely behind.
2522
        ConnectionStatus oldStatus;
2523
        statusLock.lock();
3 ✔
2524
        try {
2525
            oldStatus = status;
3 ✔
2526
            if (oldStatus == CLOSED || newStatus == oldStatus) {
6 !
2527
                return;
1 ✔
2528
            }
2529
            this.status = newStatus;
3 ✔
2530
            statusChanged.signalAll();
3 ✔
2531
        }
2532
        finally {
2533
            statusLock.unlock();
3 ✔
2534
        }
2535

2536
        if (newStatus == DISCONNECTED) {
3 ✔
2537
            processConnectionEvent(ConnectionEvent.DISCONNECTED, uriDetail);
5 ✔
2538
        }
2539
        else if (newStatus == CLOSED) {
3 ✔
2540
            processConnectionEvent(ConnectionEvent.CLOSED, uriDetail);
5 ✔
2541
        }
2542
        else if (oldStatus == RECONNECTING && newStatus == CONNECTED) {
6 !
2543
            processConnectionEvent(ConnectionEvent.RECONNECTED, uriDetail);
5 ✔
2544
        }
2545
        else if (newStatus == CONNECTED) {
3 ✔
2546
            processConnectionEvent(ConnectionEvent.CONNECTED, uriDetail);
4 ✔
2547
        }
2548
    }
1 ✔
2549

2550
    protected boolean isClosing() {
2551
        return closing;
3 ✔
2552
    }
2553

2554
    protected boolean isClosed() {
2555
        return status == CLOSED;
8 ✔
2556
    }
2557

2558
    protected boolean isConnected() {
2559
        return status == CONNECTED;
8 ✔
2560
    }
2561

2562
    protected boolean isDisconnected() {
2563
        return status == DISCONNECTED;
×
2564
    }
2565

2566
    protected boolean isConnectedOrConnecting() {
2567
        statusLock.lock();
3 ✔
2568
        try {
2569
            return status == CONNECTED || connecting;
12 !
2570
        } finally {
2571
            statusLock.unlock();
3 ✔
2572
        }
2573
    }
2574

2575
    protected boolean isDisconnectingOrClosed() {
2576
        statusLock.lock();
3 ✔
2577
        try {
2578
            return status == CLOSED || disconnecting;
11 !
2579
        } finally {
2580
            statusLock.unlock();
3 ✔
2581
        }
2582
    }
2583

2584
    protected boolean isDisconnecting() {
2585
        statusLock.lock();
3 ✔
2586
        try {
2587
            return disconnecting;
5 ✔
2588
        } finally {
2589
            statusLock.unlock();
3 ✔
2590
        }
2591
    }
2592

2593
    protected void waitForDisconnectOrClose(long timeoutMillis) throws InterruptedException {
2594
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> isDisconnecting() && !isClosed() );
16 !
2595
    }
1 ✔
2596

2597
    protected void waitForConnectOrClose(long timeoutMillis) throws InterruptedException {
2598
        waitWhile(timeoutMillis < 0 ? -1 : timeoutMillis * NANOS_PER_MILLI, (Void) -> !isConnected() && !isClosed());
21 !
2599
    }
1 ✔
2600

2601
    // operates purely in nanoseconds (precise, monotonic); the millis-facing wrappers above convert once at the edge.
2602
    // < 0 returns immediately, 0 waits forever, > 0 waits up to that many nanos.
2603
    protected void waitWhile(long timeoutNanos, Predicate<Void> waitWhileTrue) throws InterruptedException {
2604
        statusLock.lock();
3 ✔
2605
        try {
2606
            long currentWaitNanos = timeoutNanos;
2 ✔
2607
            long start = NatsSystemClock.nanoTime();
2 ✔
2608
            while (currentWaitNanos >= 0 && waitWhileTrue.test(null)) {
8 !
2609
                if (currentWaitNanos > 0) {
4 !
2610
                    if (statusChanged.await(currentWaitNanos, TimeUnit.NANOSECONDS) && !waitWhileTrue.test(null)) {
10 ✔
2611
                        break;
1 ✔
2612
                    }
2613
                    long now = NatsSystemClock.nanoTime();
2 ✔
2614
                    currentWaitNanos = currentWaitNanos - (now - start);
6 ✔
2615
                    start = now;
2 ✔
2616

2617
                    if (currentWaitNanos <= 0) {
4 ✔
2618
                        break;
1 ✔
2619
                    }
2620
                }
1 ✔
2621
                else {
2622
                    statusChanged.await();
×
2623
                }
2624
            }
2625
        }
2626
        finally {
2627
            statusLock.unlock();
3 ✔
2628
        }
2629
    }
1 ✔
2630

2631
    /**
2632
     * Whether the reconnect delay handler is invoked before the first round of a reconnect campaign.
2633
     * Rounds after the first always invoke it. This is the only place {@link ReconnectDelayBehavior}
2634
     * is read, so the setting applies to every handler, including a custom one.
2635
     * @param lameDuck whether the campaign was triggered by a server lame duck signal
2636
     * @return true to invoke the handler before round 1
2637
     */
2638
    private boolean delayBeforeFirstRound(boolean lameDuck) {
2639
        return switch (options.reconnectDelayBehavior()) {
8 !
UNCOV
2640
            case BeforeAllRounds -> true;
×
UNCOV
2641
            case LameDuckAware -> lameDuck;
×
2642
            default -> false;   // BeforeSubsequentRounds
1 ✔
2643
        };
2644
    }
2645

2646
    protected void invokeReconnectDelayHandler(long round, boolean lameDuckTriggered) {
2647
        long currentWaitMillis = options.getReconnectDelayHandler()
8 ✔
2648
            .getWaitTimeMillis(round, options, serverPool.hasSecureServer(), lameDuckTriggered);
4 ✔
2649

2650
        this.reconnectWaiter = new CompletableFuture<>();
5 ✔
2651

2652
        long start = NatsSystemClock.nanoTime();
2 ✔
2653
        while (currentWaitMillis > 0 && !isDisconnectingOrClosed() && !isConnected() && !reconnectWaiter.isDone()) {
14 !
2654
            try {
2655
                reconnectWaiter.get(currentWaitMillis, TimeUnit.MILLISECONDS);
×
2656
            } catch (Exception exp) {
1 ✔
2657
                // ignore, try to loop again
2658
            }
×
2659
            long elapsedMillis = (NatsSystemClock.nanoTime() - start) / 1_000_000L;
6 ✔
2660
            currentWaitMillis -= elapsedMillis;
4 ✔
2661
            start = NatsSystemClock.nanoTime();
2 ✔
2662
        }
1 ✔
2663

2664
        reconnectWaiter.complete(Boolean.TRUE);
5 ✔
2665
    }
1 ✔
2666

2667
    protected ByteBuffer enlargeBuffer(ByteBuffer buffer) {
2668
        int current = buffer.capacity();
3 ✔
2669
        int newSize = current * 2;
4 ✔
2670
        ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
3 ✔
2671
        buffer.flip();
3 ✔
2672
        newBuffer.put(buffer);
4 ✔
2673
        return newBuffer;
2 ✔
2674
    }
2675

2676
    // For testing
2677
    protected NatsConnectionReader getReader() {
2678
        return reader;
3 ✔
2679
    }
2680

2681
    // For testing
2682
    protected NatsConnectionWriter getWriter() {
UNCOV
2683
        return writer;
×
2684
    }
2685

2686
    // For testing
2687
    protected Future<DataPort> getDataPortFuture() {
UNCOV
2688
        return dataPortFuture;
×
2689
    }
2690

2691
    protected boolean isDraining() {
2692
        return draining.get() != null;
8 ✔
2693
    }
2694

2695
    protected boolean isDrained() {
2696
        CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2697

2698
        try {
2699
            if (tracker != null && tracker.getNow(false)) {
9 !
2700
                return true;
2 ✔
2701
            }
2702
        } catch (Exception e) {
×
2703
            // These indicate the tracker was cancelled/timed out
2704
        }
1 ✔
2705

2706
        return false;
2 ✔
2707
    }
2708

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

2738
        if (isClosing() || isClosed()) {
6 !
2739
            throw new IllegalStateException("A connection can't be drained during close.");
5 ✔
2740
        }
2741

2742
        statusLock.lock();
3 ✔
2743
        try {
2744
            if (isDraining()) {
3 ✔
2745
                return draining.get();
7 ✔
2746
            }
2747
            draining.set(new CompletableFuture<>());
6 ✔
2748
        } finally {
2749
            statusLock.unlock();
3 ✔
2750
        }
2751

2752
        final CompletableFuture<Boolean> tracker = draining.get();
5 ✔
2753
        long startNanos = NatsSystemClock.nanoTime();
2 ✔
2754

2755
        // Don't include subscriptions with dispatchers
2756
        HashSet<NatsSubscription> pureSubscriptions = new HashSet<>();
4 ✔
2757
        for (SubscriptionInfo subscription : subscriptions.values()) {
12 ✔
2758
            if (subscription.dispatcher == null) {
3 ✔
2759
                pureSubscriptions.add(subscription.sub);
5 ✔
2760
            }
2761
        }
1 ✔
2762

2763
        final HashSet<NatsMessageSink> sinks = new HashSet<>();
4 ✔
2764
        sinks.addAll(pureSubscriptions);
4 ✔
2765
        sinks.addAll(dispatchers);
5 ✔
2766

2767
        NatsDispatcher inboxer = inboxDispatcher.get();
5 ✔
2768

2769
        if (inboxer != null) {
2 ✔
2770
            sinks.add(inboxer);
4 ✔
2771
        }
2772

2773
        // Stop the sinks NOW so that when this method returns they are blocked
2774
        sinks.forEach((sink) -> {
4 ✔
2775
            sink.markDraining(tracker);
3 ✔
2776
            sink.sendUnsubForDrain();
2 ✔
2777
        });
1 ✔
2778

2779
        try {
2780
            flush(timeoutMillis); // Flush and wait up to the timeout, if this fails, let the caller know
3 ✔
2781
        } catch (Exception e) {
1 ✔
2782
            close(false, false);
4 ✔
2783
            throw e;
2 ✔
2784
        }
1 ✔
2785

2786
        sinks.forEach(NatsMessageSink::markUnsubedForDrain);
3 ✔
2787

2788
        // Wait for the timeout or all sinks are drained
2789
        executor.submit(() -> {
10 ✔
2790
            try {
2791
                long timeoutNanos = timeoutMillis <= 0 ? Long.MAX_VALUE : timeoutMillis * NANOS_PER_MILLI;
10 ✔
2792
                long startTime = System.nanoTime();
2 ✔
2793
                while (NatsSystemClock.nanoTime() - startTime < timeoutNanos && !Thread.interrupted()) {
8 !
2794
                    sinks.removeIf(NatsMessageSink::isDrained);
4 ✔
2795
                    if (sinks.isEmpty()) {
3 ✔
2796
                        break;
1 ✔
2797
                    }
2798
                    //noinspection BusyWait
2799
                    Thread.sleep(1); // Sleep 1 milli
3 ✔
2800
                }
2801

2802
                // Stop publishing
2803
                blockPublishForDrain.set(true);
4 ✔
2804

2805
                // One last flush
2806
                if (timeoutMillis <= 0) {
4 ✔
2807
                    flush(0);
4 ✔
2808
                } else {
2809
                    long remainingMillis = timeoutMillis - (NatsSystemClock.nanoTime() - startNanos) / NANOS_PER_MILLI;
8 ✔
2810
                    if (remainingMillis > 0) {
4 ✔
2811
                        flush(remainingMillis);
3 ✔
2812
                    }
2813
                }
2814
                close(false, false); // close the connection after the last flush
4 ✔
2815
                tracker.complete(sinks.isEmpty());
6 ✔
2816
            } catch (TimeoutException e) {
×
2817
                processException(e);
×
2818
            } catch (InterruptedException e) {
×
2819
                processException(e);
×
2820
                Thread.currentThread().interrupt();
×
2821
            } finally {
2822
                try {
2823
                    close(false, false);// close the connection after the last flush
4 ✔
2824
                } catch (InterruptedException e) {
×
2825
                    processException(e);
×
2826
                    Thread.currentThread().interrupt();
×
2827
                }
1 ✔
2828
                tracker.complete(false);
5 ✔
2829
            }
2830
        });
1 ✔
2831

2832
        return tracker;
2 ✔
2833
    }
2834

2835
    protected boolean isAuthenticationError(String err) {
2836
        if (err == null) {
2 ✔
2837
            return false;
2 ✔
2838
        }
2839
        err = err.toLowerCase();
3 ✔
2840
        return err.startsWith("user authentication")
7 ✔
2841
            || err.contains("authorization violation")
4 ✔
2842
            || err.startsWith("account authentication expired");
5 ✔
2843
    }
2844

2845
    /**
2846
     * Immediately flushes the underlying connection buffer if the connection is valid.
2847
     * @throws IOException if the connection flush fails
2848
     */
2849
    public void flushBuffer() throws IOException {
UNCOV
2850
        if (!isConnected()) {
×
UNCOV
2851
            throw new IllegalStateException("NatsConnection is not active.");
×
2852
        }
UNCOV
2853
        writer.flushBuffer();
×
UNCOV
2854
    }
×
2855

2856
    /**
2857
     * Get the number of messages in the outgoing queue for this connection.
2858
     * This value is volatile in the sense that it changes often and may be adjusted by more than one message.
2859
     * It changes every time a message is published (put in the outgoing queue)
2860
     * and every time a message is removed from the queue to be written over the socket
2861
     * @return the number of messages in the outgoing queue
2862
     */
2863
    public long outgoingPendingMessageCount() {
NEW
2864
        return writer == null ? -1 : writer.outgoingPendingMessageCount();
×
2865
    }
2866

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

2878
    /**
2879
     * The default this connection applies to the flushImmediatelyAfterPublish parameter of the request methods,
2880
     * taken from the options at construction. Flushing trades throughput for latency on a single request.
2881
     * @return true if requests flush the outgoing buffer immediately
2882
     */
2883
    public boolean isForceFlushOnRequest() {
2884
        return forceFlushOnRequest;
×
2885
    }
2886
}
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