From 73c6039faa957405a62da77baa17867ab608ca74 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Mon, 20 Mar 2023 11:20:29 -0400 Subject: [PATCH] pull status handling (#819) --- build.gradle | 2 +- .../java/io/nats/examples/ExampleUtils.java | 15 +- .../nats/examples/jetstream/NatsJsUtils.java | 56 +- .../java/io/nats/client/ErrorListener.java | 23 +- .../nats/client/JetStreamStatusException.java | 25 +- .../io/nats/client/JetStreamSubscription.java | 43 +- .../io/nats/client/PullRequestOptions.java | 2 - .../io/nats/client/PullSubscribeOptions.java | 1 + .../io/nats/client/PushSubscribeOptions.java | 1 + .../java/io/nats/client/SubscribeOptions.java | 1 - .../client/impl/ErrorListenerLoggerImpl.java | 16 + .../io/nats/client/impl/MessageManager.java | 153 +- .../io/nats/client/impl/MessageQueue.java | 1 - .../io/nats/client/impl/NatsConnection.java | 8 +- .../io/nats/client/impl/NatsJetStream.java | 85 +- .../client/impl/NatsJetStreamMetaData.java | 2 +- .../impl/NatsJetStreamPullSubscription.java | 10 + .../impl/NatsJetStreamSubscription.java | 9 + .../io/nats/client/impl/NatsSubscription.java | 10 +- ...anager.java => OrderedMessageManager.java} | 65 +- .../nats/client/impl/PullMessageManager.java | 131 +- .../nats/client/impl/PushMessageManager.java | 225 +-- .../io/nats/client/support/JsonValue.java | 2 +- .../support/NatsJetStreamConstants.java | 14 + .../io/nats/client/support/PullStatus.java | 60 + .../java/io/nats/client/support/Status.java | 14 +- .../java/io/nats/client/OptionsTests.java | 1658 ++++++++--------- .../nats/client/impl/ErrorListenerTests.java | 17 +- ...Tests.java => JetStreamConsumerTests.java} | 161 +- .../nats/client/impl/JetStreamPullTests.java | 232 ++- .../nats/client/impl/JetStreamPushTests.java | 1 + .../nats/client/impl/JetStreamTestBase.java | 14 +- .../nats/client/impl/MessageManagerTests.java | 473 +++-- .../java/io/nats/client/impl/TestHandler.java | 138 +- .../java/io/nats/client/utils/TestBase.java | 21 + 35 files changed, 2291 insertions(+), 1398 deletions(-) rename src/main/java/io/nats/client/impl/{OrderedManager.java => OrderedMessageManager.java} (54%) create mode 100644 src/main/java/io/nats/client/support/PullStatus.java rename src/test/java/io/nats/client/impl/{JetStreamOrderedConsumerTests.java => JetStreamConsumerTests.java} (58%) diff --git a/build.gradle b/build.gradle index d8e2d2bcf..ac17724f8 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ repositories { dependencies { implementation 'net.i2p.crypto:eddsa:0.3.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' - testImplementation 'io.nats:jnats-server-runner:1.2.1' + testImplementation 'io.nats:jnats-server-runner:1.2.5' testImplementation 'nl.jqno.equalsverifier:equalsverifier:3.12.3' } diff --git a/src/examples/java/io/nats/examples/ExampleUtils.java b/src/examples/java/io/nats/examples/ExampleUtils.java index 95f3c11f7..2de50a309 100644 --- a/src/examples/java/io/nats/examples/ExampleUtils.java +++ b/src/examples/java/io/nats/examples/ExampleUtils.java @@ -14,6 +14,7 @@ package io.nats.examples; import io.nats.client.*; +import io.nats.client.impl.ErrorListenerLoggerImpl; import java.time.Duration; import java.util.concurrent.ThreadLocalRandom; @@ -31,19 +32,7 @@ public static String getServer(String[] args) { public static final ConnectionListener EXAMPLE_CONNECTION_LISTENER = (conn, type) -> System.out.println("Status change "+ type); - public static final ErrorListener EXAMPLE_ERROR_LISTENER = new ErrorListener() { - public void exceptionOccurred(Connection conn, Exception exp) { - System.out.println("Exception " + exp.getMessage()); - } - - public void errorOccurred(Connection conn, String type) { - System.out.println("Error " + type); - } - - public void slowConsumerDetected(Connection conn, Consumer consumer) { - System.out.println("Slow consumer"); - } - }; + public static final ErrorListener EXAMPLE_ERROR_LISTENER = new ErrorListenerLoggerImpl(); public static Options createExampleOptions(String[] args) throws Exception { String server = getServer(args); diff --git a/src/examples/java/io/nats/examples/jetstream/NatsJsUtils.java b/src/examples/java/io/nats/examples/jetstream/NatsJsUtils.java index fa57f779b..22ffcdeac 100644 --- a/src/examples/java/io/nats/examples/jetstream/NatsJsUtils.java +++ b/src/examples/java/io/nats/examples/jetstream/NatsJsUtils.java @@ -18,6 +18,7 @@ import io.nats.client.api.StorageType; import io.nats.client.api.StreamConfiguration; import io.nats.client.api.StreamInfo; +import io.nats.client.impl.NatsJetStreamMetaData; import io.nats.client.impl.NatsMessage; import java.io.IOException; @@ -178,8 +179,16 @@ public static void publish(JetStream js, String subject, int count) throws IOExc publish(js, subject, "data", count, -1, false); } + public static void publish(JetStream js, String subject, int count, int msgSize) throws IOException, JetStreamApiException { + publish(js, subject, "data", count, msgSize, false); + } + public static void publish(JetStream js, String subject, String prefix, int count) throws IOException, JetStreamApiException { - publish(js, subject, prefix, count, -1, true); + publish(js, subject, prefix, count, -1, false); + } + + public static void publish(JetStream js, String subject, String prefix, int count, int msgSize) throws IOException, JetStreamApiException { + publish(js, subject, prefix, count, msgSize, false); } public static void publish(JetStream js, String subject, String prefix, int count, boolean verbose) throws IOException, JetStreamApiException { @@ -205,7 +214,11 @@ public static void publish(JetStream js, String subject, String prefix, int coun } public static byte[] makeData(String prefix, int msgSize, boolean verbose, int x) { - String text = prefix + "#" + x + "#"; + if (msgSize == 0) { + return null; + } + + String text = prefix + "-" + x + "."; if (verbose) { System.out.print(" " + text); } @@ -342,6 +355,17 @@ public static void printObject(Object o, String... subObjectNames) { System.out.println(s); } + public static String metaString(NatsJetStreamMetaData meta) { + return "Meta{" + + "str='" + meta.getStream() + '\'' + + ", con='" + meta.getConsumer() + '\'' + + ", delivered=" + meta.deliveredCount() + + ", strSeq=" + meta.streamSequence() + + ", conSeq=" + meta.consumerSequence() + + ", pending=" + meta.pendingCount() + + '}'; + } + // ---------------------------------------------------------------------------------------------------- // REPORT // ---------------------------------------------------------------------------------------------------- @@ -385,4 +409,32 @@ public static int count408s(List messages) { } return count; } + + public static void createCleanMemStream(JetStreamManagement jsm, String stream, String... subs) throws IOException, JetStreamApiException { + try { + jsm.deleteStream(stream); + } + catch (Exception ignore) {} + + StreamConfiguration sc = StreamConfiguration.builder() + .name(stream) + .storageType(StorageType.Memory) + .subjects(subs) + .build(); + jsm.addStream(sc); + } + + public static void createCleanFileStream(JetStreamManagement jsm, String stream, String... subs) throws IOException, JetStreamApiException { + try { + jsm.deleteStream(stream); + } + catch (Exception ignore) {} + + StreamConfiguration sc = StreamConfiguration.builder() + .name(stream) + .storageType(StorageType.File) + .subjects(subs) + .build(); + jsm.addStream(sc); + } } diff --git a/src/main/java/io/nats/client/ErrorListener.java b/src/main/java/io/nats/client/ErrorListener.java index a535cfa3a..1617e814d 100644 --- a/src/main/java/io/nats/client/ErrorListener.java +++ b/src/main/java/io/nats/client/ErrorListener.java @@ -91,14 +91,33 @@ default void heartbeatAlarm(Connection conn, JetStreamSubscription sub, long lastStreamSequence, long lastConsumerSequence) {} /** - * Called by the connection when an unhandled status is received. - * + * Called when an unhandled status is received in a push subscription. * @param conn The connection that had the issue * @param sub the JetStreamSubscription that this occurred on * @param status the status */ default void unhandledStatus(Connection conn, JetStreamSubscription sub, Status status) {} + /** + * Called when a pull subscription receives a status message that indicates either + * the subscription or pull might be problematic + * + * @param conn The connection that had the issue + * @param sub the JetStreamSubscription that this occurred on + * @param status the status + */ + default void pullStatusWarning(Connection conn, JetStreamSubscription sub, Status status) {} + + /** + * Called when a pull subscription receives a status message that indicates either + * the subscription cannot continue or the pull request cannot be processed. + * + * @param conn The connection that had the issue + * @param sub the JetStreamSubscription that this occurred on + * @param status the status + */ + default void pullStatusError(Connection conn, JetStreamSubscription sub, Status status) {} + enum FlowControlSource { FLOW_CONTROL, HEARTBEAT } /** diff --git a/src/main/java/io/nats/client/JetStreamStatusException.java b/src/main/java/io/nats/client/JetStreamStatusException.java index d97f75080..b1b3ecac3 100644 --- a/src/main/java/io/nats/client/JetStreamStatusException.java +++ b/src/main/java/io/nats/client/JetStreamStatusException.java @@ -19,18 +19,31 @@ * JetStreamStatusException is used to indicate an unknown status message was received. */ public class JetStreamStatusException extends IllegalStateException { + public static final String DEFAULT_DESCRIPTION = "Unknown or unprocessed status message"; + private final JetStreamSubscription sub; + private final String description; private final Status status; /** * Construct an exception with a status message - * * @param sub the subscription * @param status the status */ public JetStreamStatusException(JetStreamSubscription sub, Status status) { - super("Unknown or unprocessed status message: " + status.getMessage()); + this(sub, DEFAULT_DESCRIPTION, status); + } + + /** + * Construct an exception with a status message + * @param sub the subscription + * @param description custom description + * @param status the status + */ + public JetStreamStatusException(JetStreamSubscription sub, String description, Status status) { + super(description + ": " + status.getMessage()); this.sub = sub; + this.description = description; this.status = status; } @@ -43,6 +56,14 @@ public JetStreamSubscription getSubscription() { return sub; } + /** + * Get the description + * @return the description + */ + public String getDescription() { + return description; + } + /** * Get the full status object * diff --git a/src/main/java/io/nats/client/JetStreamSubscription.java b/src/main/java/io/nats/client/JetStreamSubscription.java index c21952844..c1ea1a866 100644 --- a/src/main/java/io/nats/client/JetStreamSubscription.java +++ b/src/main/java/io/nats/client/JetStreamSubscription.java @@ -14,6 +14,7 @@ package io.nats.client; import io.nats.client.api.ConsumerInfo; +import io.nats.client.support.PullStatus; import java.io.IOException; import java.time.Duration; @@ -27,9 +28,8 @@ public interface JetStreamSubscription extends Subscription { /** * Initiate pull with the specified batch size. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @throws IllegalStateException if not a pull subscription. @@ -38,11 +38,8 @@ public interface JetStreamSubscription extends Subscription { /** * Initiate pull with the specified request options - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate - * - * IMPORTANT! PullRequestOptions ARE CURRENTLY EXPERIMENTAL AND SUBJECT TO CHANGE. + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param pullRequestOptions the options object * @throws IllegalStateException if not a pull subscription. @@ -53,7 +50,7 @@ public interface JetStreamSubscription extends Subscription { * Initiate pull in noWait mode with the specified batch size. * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @throws IllegalStateException if not a pull subscription. @@ -62,9 +59,8 @@ public interface JetStreamSubscription extends Subscription { /** * Initiate pull in noWait mode with the specified batch size. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @param expiresIn how long from now this request should be expired from the server wait list @@ -74,9 +70,8 @@ public interface JetStreamSubscription extends Subscription { /** * Initiate pull in noWait mode with the specified batch size. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @param expiresInMillis how long from now this request should be expired from the server wait list, in milliseconds @@ -89,13 +84,12 @@ public interface JetStreamSubscription extends Subscription { *

* sub.nextMessage(timeout) can return a: *

*

- * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @param expiresIn how long from now this request should be expired from the server wait list @@ -109,13 +103,12 @@ public interface JetStreamSubscription extends Subscription { *

* sub.nextMessage(timeout) can return a: *

*

- * * ! Pull subscriptions only. Push subscription will throw IllegalStateException - * ! Primitive API for Advanced use only. Prefer Fetch or Iterate + * ! Primitive API for ADVANCED use only, officially not supported. Prefer fetch, iterate or reader. * * @param batchSize the size of the batch * @param expiresInMillis how long from now this request should be expired from the server wait list, in milliseconds @@ -128,7 +121,6 @@ public interface JetStreamSubscription extends Subscription { * This uses pullExpiresIn under the covers, and manages all responses * from sub.nextMessage(...) to only return regular JetStream messages. * This can only be used when the subscription is pull based. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException * * @param batchSize the size of the batch @@ -144,7 +136,6 @@ public interface JetStreamSubscription extends Subscription { * This uses pullExpiresIn under the covers, and manages all responses * from sub.nextMessage(...) to only return regular JetStream messages. * This can only be used when the subscription is pull based. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException * * @param batchSize the size of the batch @@ -161,7 +152,6 @@ public interface JetStreamSubscription extends Subscription { * receive the first message within the max wait period. It will stop if the batch is * fulfilled or if there are fewer than batch size messages. 408 Status messages * are ignored and will not count toward the fulfilled batch size. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException * * @param batchSize the size of the batch @@ -178,7 +168,6 @@ public interface JetStreamSubscription extends Subscription { * receive the first message within the max wait period. It will stop if the batch is * fulfilled or if there are fewer than batch size messages. 408 Status messages * are ignored and will not count toward the fulfilled batch size. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException * * @param batchSize the size of the batch @@ -193,11 +182,8 @@ public interface JetStreamSubscription extends Subscription { * Prepares a reader. A reader looks like a push sync subscription, * meaning it is just an endless stream of messages to ask for by nextMessage, * but uses pull under the covers. - * * ! Pull subscriptions only. Push subscription will throw IllegalStateException * - * THIS API IS CONSIDERED EXPERIMENTAL AND SUBJECT TO CHANGE - * * @param batchSize the size of the batch * @param repullAt the point in the current batch to tell the server to start the next batch * @@ -214,4 +200,11 @@ public interface JetStreamSubscription extends Subscription { * @throws JetStreamApiException the request had an error related to the data */ ConsumerInfo getConsumerInfo() throws IOException, JetStreamApiException; -} \ No newline at end of file + + /** + * Get the current status of pull requests for this subscription + * ! Pull subscriptions only. Push subscription will throw IllegalStateException + * @return the PullStatus object + */ + PullStatus getPullStatus(); +} diff --git a/src/main/java/io/nats/client/PullRequestOptions.java b/src/main/java/io/nats/client/PullRequestOptions.java index accb296f6..fc46692b8 100644 --- a/src/main/java/io/nats/client/PullRequestOptions.java +++ b/src/main/java/io/nats/client/PullRequestOptions.java @@ -23,8 +23,6 @@ /** * The PullRequestOptions class specifies the options for pull requests - * - * IMPORTANT! PullRequestOptions ARE CURRENTLY EXPERIMENTAL AND SUBJECT TO CHANGE. */ public class PullRequestOptions implements JsonSerializable { diff --git a/src/main/java/io/nats/client/PullSubscribeOptions.java b/src/main/java/io/nats/client/PullSubscribeOptions.java index f38e9f1d4..9968a5d40 100644 --- a/src/main/java/io/nats/client/PullSubscribeOptions.java +++ b/src/main/java/io/nats/client/PullSubscribeOptions.java @@ -18,6 +18,7 @@ * Options are set using the {@link PullSubscribeOptions.Builder} or static helper methods. */ public class PullSubscribeOptions extends SubscribeOptions { + public static final PullSubscribeOptions DEFAULT_PULL_OPTS = PullSubscribeOptions.builder().build(); private PullSubscribeOptions(Builder builder) { super(builder, true, false, null, null, -1, -1); diff --git a/src/main/java/io/nats/client/PushSubscribeOptions.java b/src/main/java/io/nats/client/PushSubscribeOptions.java index 4e38840b7..9b1e3d04a 100644 --- a/src/main/java/io/nats/client/PushSubscribeOptions.java +++ b/src/main/java/io/nats/client/PushSubscribeOptions.java @@ -20,6 +20,7 @@ * Options are set using the {@link PushSubscribeOptions.Builder} or static helper methods. */ public class PushSubscribeOptions extends SubscribeOptions { + public static final PushSubscribeOptions DEFAULT_PUSH_OPTS = PushSubscribeOptions.builder().build(); private PushSubscribeOptions(Builder builder, boolean ordered, String deliverSubject, String deliverGroup, long pendingMessageLimit, long pendingByteLimit) { diff --git a/src/main/java/io/nats/client/SubscribeOptions.java b/src/main/java/io/nats/client/SubscribeOptions.java index 66ab893df..df3d1d65a 100644 --- a/src/main/java/io/nats/client/SubscribeOptions.java +++ b/src/main/java/io/nats/client/SubscribeOptions.java @@ -25,7 +25,6 @@ * The SubscribeOptions is the base class for PushSubscribeOptions and PullSubscribeOptions */ public abstract class SubscribeOptions { - public static final long DEFAULT_ORDERED_HEARTBEAT = 5000; protected final String stream; diff --git a/src/main/java/io/nats/client/impl/ErrorListenerLoggerImpl.java b/src/main/java/io/nats/client/impl/ErrorListenerLoggerImpl.java index 8cd068087..66558a1a9 100644 --- a/src/main/java/io/nats/client/impl/ErrorListenerLoggerImpl.java +++ b/src/main/java/io/nats/client/impl/ErrorListenerLoggerImpl.java @@ -92,6 +92,22 @@ public void unhandledStatus(final Connection conn, final JetStreamSubscription s LOGGER.warning(() -> supplyMessage("unhandledStatus", conn, null, sub, "Status:", status)); } + /** + * {@inheritDoc} + */ + @Override + public void pullStatusWarning(Connection conn, JetStreamSubscription sub, Status status) { + LOGGER.warning(() -> supplyMessage("pullStatusWarning", conn, null, sub, "Status:", status)); + } + + /** + * {@inheritDoc} + */ + @Override + public void pullStatusError(Connection conn, JetStreamSubscription sub, Status status) { + LOGGER.severe(() -> supplyMessage("pullStatusError", conn, null, sub, "Status:", status)); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/io/nats/client/impl/MessageManager.java b/src/main/java/io/nats/client/impl/MessageManager.java index 1351d08a6..94f21d20b 100644 --- a/src/main/java/io/nats/client/impl/MessageManager.java +++ b/src/main/java/io/nats/client/impl/MessageManager.java @@ -14,17 +14,162 @@ package io.nats.client.impl; import io.nats.client.Message; +import io.nats.client.PullRequestOptions; +import io.nats.client.support.PullStatus; + +import java.time.Duration; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicLong; abstract class MessageManager { + protected static final int THRESHOLD = 3; + + protected final NatsConnection conn; + protected final boolean syncMode; + protected NatsJetStreamSubscription sub; - boolean manage(Message msg) { - return false; + protected long lastStreamSeq; + protected long internalConsumerSeq; + + protected final AtomicLong lastMsgReceived; + + // heartbeat stuff + protected boolean hb; + protected long idleHeartbeatSetting; + protected long alarmPeriodSetting; + protected HeartbeatTimer heartbeatTimer; + + protected MessageManager(NatsConnection conn, boolean syncMode) { + this.conn = conn; + this.syncMode = syncMode; + lastStreamSeq = 0; + internalConsumerSeq = 0; + lastMsgReceived = new AtomicLong(System.currentTimeMillis()); + + hb = false; + idleHeartbeatSetting = 0; + alarmPeriodSetting = 0; } - void startup(NatsJetStreamSubscription sub) { + protected boolean isSyncMode() { return syncMode; } + protected long getLastStreamSequence() { return lastStreamSeq; } + protected long getInternalConsumerSequence() { return internalConsumerSeq; } + protected long getLastMsgReceived() { return lastMsgReceived.get(); } + protected boolean isHb() { return hb; } + protected long getIdleHeartbeatSetting() { return idleHeartbeatSetting; } + protected long getAlarmPeriodSetting() { return alarmPeriodSetting; } + + protected void startup(NatsJetStreamSubscription sub) { this.sub = sub; } - void shutdown() {} + protected void shutdown() { + shutdownHeartbeatTimer(); + } + + protected void startPullRequest(PullRequestOptions pullRequestOptions) { + // does nothing - only implemented for pulls, but in base class since instance is always referenced as MessageManager, not subclass + } + + protected PullStatus getPullStatus() { + return null; + } + + protected void messageReceived() { + lastMsgReceived.set(System.currentTimeMillis()); + } + + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + return true; + } + + abstract protected boolean manage(Message msg); + + protected void trackJsMessage(Message msg) { + NatsJetStreamMetaData meta = msg.metaData(); + lastStreamSeq = meta.streamSequence(); + internalConsumerSeq++; + } + + protected void initIdleHeartbeat(Duration configIdleHeartbeat, long configMessageAlarmTime) { + idleHeartbeatSetting = configIdleHeartbeat == null ? 0 : configIdleHeartbeat.toMillis(); + if (idleHeartbeatSetting <= 0) { + alarmPeriodSetting = 0; + hb = false; + } + else { + if (configMessageAlarmTime < idleHeartbeatSetting) { + alarmPeriodSetting = idleHeartbeatSetting * THRESHOLD; + } + else { + alarmPeriodSetting = configMessageAlarmTime; + } + hb = true; + } + } + + protected void initOrResetHeartbeatTimer() { + if (heartbeatTimer == null) { + heartbeatTimer = new HeartbeatTimer(alarmPeriodSetting); + } + else { + heartbeatTimer.restart(); + } + } + + protected void shutdownHeartbeatTimer() { + if (heartbeatTimer != null) { + heartbeatTimer.shutdown(); + heartbeatTimer = null; + } + } + + protected void handleHeartbeatError() { + conn.executeCallback((c, el) -> el.heartbeatAlarm(c, sub, lastStreamSeq, internalConsumerSeq)); + } + + protected class HeartbeatTimer { + protected Timer timer; + protected boolean alive = true; + protected long alarmPeriodSetting; + + protected class HeartbeatTimerTask extends TimerTask { + @Override + public void run() { + long sinceLast = System.currentTimeMillis() - lastMsgReceived.get(); + if (sinceLast > alarmPeriodSetting) { + handleHeartbeatError(); + } + restart(); + } + } + + protected HeartbeatTimer(long alarmPeriodSetting) { + this.alarmPeriodSetting = alarmPeriodSetting; + restart(); + } + + synchronized protected void restart() { + cancel(); + if (alive) { + timer = new Timer(); + timer.schedule(new HeartbeatTimerTask(), alarmPeriodSetting); + } + } + + synchronized protected void shutdown() { + alive = false; + cancel(); + } + + private void cancel() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + } + } } diff --git a/src/main/java/io/nats/client/impl/MessageQueue.java b/src/main/java/io/nats/client/impl/MessageQueue.java index a7cd92a6a..1fb95a4e2 100644 --- a/src/main/java/io/nats/client/impl/MessageQueue.java +++ b/src/main/java/io/nats/client/impl/MessageQueue.java @@ -111,7 +111,6 @@ boolean push(NatsMessage msg) { } boolean push(NatsMessage msg, boolean internal) { - this.filterLock.lock(); try { // If we aren't running, then we need to obey the filter lock diff --git a/src/main/java/io/nats/client/impl/NatsConnection.java b/src/main/java/io/nats/client/impl/NatsConnection.java index 613c24903..29db94e6c 100644 --- a/src/main/java/io/nats/client/impl/NatsConnection.java +++ b/src/main/java/io/nats/client/impl/NatsConnection.java @@ -1560,12 +1560,8 @@ void deliverMessage(NatsMessage msg) { } else if (q != null) { c.markNotSlow(); - // beforeQueueProcessor returns null if the message - // does not need to be queued, for instance heartbeats - // that are not flow control and are already seen by the - // auto status manager - msg = sub.getBeforeQueueProcessor().apply(msg); - if (msg != null) { + // beforeQueueProcessor returns true if the message is allowed to be queued + if (sub.getBeforeQueueProcessor().apply(msg)) { q.push(msg); } } diff --git a/src/main/java/io/nats/client/impl/NatsJetStream.java b/src/main/java/io/nats/client/impl/NatsJetStream.java index 0f3677a91..806b6dc4f 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStream.java +++ b/src/main/java/io/nats/client/impl/NatsJetStream.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import static io.nats.client.PushSubscribeOptions.DEFAULT_PUSH_OPTS; import static io.nats.client.support.NatsJetStreamClientError.*; import static io.nats.client.support.Validator.*; @@ -218,34 +219,24 @@ private Headers _mergeNum(Headers h, String key, String value) { // ---------------------------------------------------------------------------------------------------- // Subscribe // ---------------------------------------------------------------------------------------------------- - private static final PushSubscribeOptions DEFAULT_PUSH_OPTS = PushSubscribeOptions.builder().build(); - - // Push/PullMessageManagerFactory are internal and used for testing / providing a MessageManager mocks - interface PushMessageManagerFactory { - MessageManager createPushMessageManager( - NatsConnection conn, - NatsJetStream js, - String stream, - SubscribeOptions so, - ConsumerConfiguration serverCC, - boolean queueMode, - NatsDispatcher dispatcher); - } - - interface PullMessageManagerFactory { - MessageManager createPullMessageManager(); + interface MessageManagerFactory { + MessageManager createMessageManager( + NatsConnection conn, NatsJetStream js, String stream, + SubscribeOptions so, ConsumerConfiguration cc, boolean queueMode, boolean syncMode); } - PushMessageManagerFactory PUSH_MESSAGE_MANAGER_FACTORY = null; - PullMessageManagerFactory PULL_MESSAGE_MANAGER_FACTORY = PullMessageManager::new; + MessageManagerFactory _pushStandardMessageManagerFactory = PushMessageManager::new; + MessageManagerFactory _pushOrderedMessageManagerFactory = OrderedMessageManager::new; + MessageManagerFactory _pullMessageManagerFactory = + (mmConn, mmJs, mmStream, mmSo, mmCc, mmQueueMode, mmSyncMode) -> new PullMessageManager(mmConn, mmSyncMode); JetStreamSubscription createSubscription(String subject, - String queueName, - NatsDispatcher dispatcher, - MessageHandler userHandler, - boolean isAutoAck, - PushSubscribeOptions pushSubscribeOptions, - PullSubscribeOptions pullSubscribeOptions + String queueName, + NatsDispatcher dispatcher, + MessageHandler userHandler, + boolean isAutoAck, + PushSubscribeOptions pushSubscribeOptions, + PullSubscribeOptions pullSubscribeOptions ) throws IOException, JetStreamApiException { // 1. Prepare for all the validation @@ -257,7 +248,7 @@ JetStreamSubscription createSubscription(String subject, ConsumerConfiguration userCC; if (isPullMode) { - so = pullSubscribeOptions; // options must have already been checked to be non null + so = pullSubscribeOptions; // options must have already been checked to be non-null stream = pullSubscribeOptions.getStream(); userCC = so.getConsumerConfiguration(); @@ -328,7 +319,7 @@ JetStreamSubscription createSubscription(String subject, ConsumerConfigurationComparer userCCC = new ConsumerConfigurationComparer(userCC); List changes = userCCC.getChanges(serverCC); if (changes.size() > 0) { - throw JsSubExistingConsumerCannotBeModified.instance("Changed fields: " + changes.toString()); + throw JsSubExistingConsumerCannotBeModified.instance("Changed fields: " + changes); } // deliver subject must be null/empty for pull, defined for push @@ -407,41 +398,31 @@ else if (so.isBind()) { // 6. create the subscription. lambda needs final or effectively final vars NatsJetStreamSubscription sub; + final MessageManager mm; + final NatsSubscriptionFactory subFactory; if (isPullMode) { - final MessageManager manager = PULL_MESSAGE_MANAGER_FACTORY.createPullMessageManager(); - final NatsSubscriptionFactory factory = (sid, lSubject, lQgroup, lConn, lDispatcher) - -> new NatsJetStreamPullSubscription(sid, lSubject, lConn, this, fnlStream, settledConsumerName, manager); - sub = (NatsJetStreamSubscription) conn.createSubscription(fnlInboxDeliver, qgroup, null, factory); + mm = _pullMessageManagerFactory.createMessageManager(conn, this, fnlStream, so, settledServerCC, false, dispatcher == null); + subFactory = (sid, lSubject, lQgroup, lConn, lDispatcher) + -> new NatsJetStreamPullSubscription(sid, lSubject, lConn, this, fnlStream, settledConsumerName, mm); } else { - final MessageManager manager; - if (PUSH_MESSAGE_MANAGER_FACTORY != null) { - manager = PUSH_MESSAGE_MANAGER_FACTORY.createPushMessageManager(conn, this, fnlStream, so, settledServerCC, qgroup != null, dispatcher); - } - else if (so.isOrdered()) { - manager = new OrderedManager(conn, this, fnlStream, so, settledServerCC, qgroup != null, dispatcher); - } - else { - manager = new PushMessageManager(conn, this, fnlStream, so, settledServerCC, qgroup != null, dispatcher); - } - - final NatsSubscriptionFactory factory = (sid, lSubject, lQgroup, lConn, lDispatcher) - -> { + MessageManagerFactory mmFactory = so.isOrdered() ? _pushOrderedMessageManagerFactory : _pushStandardMessageManagerFactory; + mm = mmFactory.createMessageManager(conn, this, fnlStream, so, settledServerCC, false, dispatcher == null); + subFactory = (sid, lSubject, lQgroup, lConn, lDispatcher) -> { NatsJetStreamSubscription nsub = new NatsJetStreamSubscription(sid, lSubject, lQgroup, lConn, lDispatcher, - this, fnlStream, settledConsumerName, manager); + this, fnlStream, settledConsumerName, mm); if (lDispatcher == null) { nsub.setPendingLimits(so.getPendingMessageLimit(), so.getPendingByteLimit()); } return nsub; }; - - if (dispatcher == null) { - sub = (NatsJetStreamSubscription) conn.createSubscription(fnlInboxDeliver, qgroup, null, factory); - } - else { - AsyncMessageHandler handler = new AsyncMessageHandler(userHandler, isAutoAck, settledServerCC, manager); - sub = (NatsJetStreamSubscription) dispatcher.subscribeImplJetStream(fnlInboxDeliver, qgroup, handler, factory); - } + } + if (dispatcher == null) { + sub = (NatsJetStreamSubscription) conn.createSubscription(fnlInboxDeliver, qgroup, null, subFactory); + } + else { + AsyncMessageHandler handler = new AsyncMessageHandler(userHandler, isAutoAck, settledServerCC, mm); + sub = (NatsJetStreamSubscription) dispatcher.subscribeImplJetStream(fnlInboxDeliver, qgroup, handler, subFactory); } // 7. The consumer might need to be created, do it here diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java b/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java index c8ab23b7a..92a5dd0a7 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java @@ -40,7 +40,7 @@ public class NatsJetStreamMetaData { public String toString() { return "NatsJetStreamMetaData{" + "prefix='" + prefix + '\'' + - "domain='" + domain + '\'' + + ", domain='" + domain + '\'' + ", stream='" + stream + '\'' + ", consumer='" + consumer + '\'' + ", delivered=" + delivered + diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java b/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java index b2df25ed0..284e14efa 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java @@ -16,6 +16,7 @@ import io.nats.client.JetStreamReader; import io.nats.client.Message; import io.nats.client.PullRequestOptions; +import io.nats.client.support.PullStatus; import java.time.Duration; import java.util.ArrayList; @@ -51,6 +52,7 @@ public void pull(int batchSize) { @Override public void pull(PullRequestOptions pullRequestOptions) { String publishSubject = js.prependPrefix(String.format(JSAPI_CONSUMER_MSG_NEXT, stream, consumerName)); + manager.startPullRequest(pullRequestOptions); connection.publish(publishSubject, getSubject(), pullRequestOptions.serialize()); connection.lenientFlushBuffer(); } @@ -331,4 +333,12 @@ public void stop() { public JetStreamReader reader(final int batchSize, final int repullAt) { return new JetStreamReaderImpl(this, batchSize, repullAt); } + + /** + * {@inheritDoc} + */ + @Override + public PullStatus getPullStatus() { + return manager.getPullStatus(); + } } diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamSubscription.java b/src/main/java/io/nats/client/impl/NatsJetStreamSubscription.java index 991c16be5..b1b45835f 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamSubscription.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamSubscription.java @@ -16,6 +16,7 @@ import io.nats.client.*; import io.nats.client.api.ConsumerInfo; import io.nats.client.support.NatsJetStreamConstants; +import io.nats.client.support.PullStatus; import java.io.IOException; import java.time.Duration; @@ -233,6 +234,14 @@ public ConsumerInfo getConsumerInfo() throws IOException, JetStreamApiException return js.lookupConsumerInfo(stream, consumerName); } + /** + * {@inheritDoc} + */ + @Override + public PullStatus getPullStatus() { + throw new IllegalStateException(SUBSCRIPTION_TYPE_DOES_NOT_SUPPORT_PULL); + } + @Override public String toString() { return "NatsJetStreamSubscription{" + diff --git a/src/main/java/io/nats/client/impl/NatsSubscription.java b/src/main/java/io/nats/client/impl/NatsSubscription.java index b5de0cb04..ccae4d6fe 100644 --- a/src/main/java/io/nats/client/impl/NatsSubscription.java +++ b/src/main/java/io/nats/client/impl/NatsSubscription.java @@ -33,7 +33,7 @@ class NatsSubscription extends NatsConsumer implements Subscription { private final AtomicLong unSubMessageLimit; - private Function beforeQueueProcessor; + private Function beforeQueueProcessor; NatsSubscription(String sid, String subject, String queueName, NatsConnection connection, NatsDispatcher dispatcher) { super(connection); @@ -47,7 +47,7 @@ class NatsSubscription extends NatsConsumer implements Subscription { this.incoming = new MessageQueue(false); } - beforeQueueProcessor = m -> m; + setBeforeQueueProcessor(null); } void reSubscribe(String newDeliverSubject) { @@ -68,11 +68,11 @@ public boolean isActive() { return (this.dispatcher != null || this.incoming != null); } - void setBeforeQueueProcessor(Function beforeQueueProcessor) { - this.beforeQueueProcessor = beforeQueueProcessor; // better not be null if it's being set + void setBeforeQueueProcessor(Function beforeQueueProcessor) { + this.beforeQueueProcessor = beforeQueueProcessor == null ? m -> true : beforeQueueProcessor; } - public Function getBeforeQueueProcessor() { + public Function getBeforeQueueProcessor() { return beforeQueueProcessor; } diff --git a/src/main/java/io/nats/client/impl/OrderedManager.java b/src/main/java/io/nats/client/impl/OrderedMessageManager.java similarity index 54% rename from src/main/java/io/nats/client/impl/OrderedManager.java rename to src/main/java/io/nats/client/impl/OrderedMessageManager.java index b7d2b6bb3..1dc7b6545 100644 --- a/src/main/java/io/nats/client/impl/OrderedManager.java +++ b/src/main/java/io/nats/client/impl/OrderedMessageManager.java @@ -17,47 +17,72 @@ import io.nats.client.SubscribeOptions; import io.nats.client.api.ConsumerConfiguration; import io.nats.client.api.DeliverPolicy; +import io.nats.client.support.Status; -class OrderedManager extends PushMessageManager { +import java.util.concurrent.atomic.AtomicReference; - private long expectedConsumerSeq; +class OrderedMessageManager extends PushMessageManager { - OrderedManager(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, NatsDispatcher dispatcher) { - super(conn, js, stream, so, serverCC, queueMode, dispatcher); - expectedConsumerSeq = 1; // always starts at 1 + private long expectedExternalConsumerSeq; + private final AtomicReference targetSid; + + protected OrderedMessageManager(NatsConnection conn, + NatsJetStream js, + String stream, + SubscribeOptions so, + ConsumerConfiguration serverCC, + boolean queueMode, + boolean syncMode) { + super(conn, js, stream, so, serverCC, queueMode, syncMode); + expectedExternalConsumerSeq = 1; // always starts at 1 + targetSid = new AtomicReference<>(); } @Override - protected boolean subManage(Message msg) { - long receivedConsumerSeq = msg.metaData().consumerSequence(); - if (expectedConsumerSeq != receivedConsumerSeq) { - handleErrorCondition(); - return true; - } - expectedConsumerSeq++; - return false; + protected void startup(NatsJetStreamSubscription sub) { + targetSid.set(sub.getSID()); + super.startup(sub); } @Override - protected void handleHeartbeatError() { - handleErrorCondition(); + protected boolean manage(Message msg) { + if (!msg.getSID().equals(targetSid.get())) { + return true; + } + + Status status = msg.getStatus(); + if (status == null) { + long receivedConsumerSeq = msg.metaData().consumerSequence(); + if (expectedExternalConsumerSeq != receivedConsumerSeq) { + handleErrorCondition(); + return true; + } + trackJsMessage(msg); + expectedExternalConsumerSeq++; + return false; + } + + super.manageStatus(msg); + return true; // all status are managed } private void handleErrorCondition() { try { - expectedConsumerSeq = 1; // consumer always starts with consumer sequence 1 + targetSid.set(null); + expectedExternalConsumerSeq = 1; // consumer always starts with consumer sequence 1 // 1. shutdown the manager, for instance stops heartbeat timers - sub.manager.shutdown(); + shutdown(); // 2. re-subscribe. This means kill the sub then make a new one // New sub needs a new deliverSubject String newDeliverSubject = sub.connection.createInbox(); sub.reSubscribe(newDeliverSubject); + targetSid.set(sub.getSID()); // 3. make a new consumer using the same deliver subject but // with a new starting point - ConsumerConfiguration userCC = ConsumerConfiguration.builder(serverCC) + ConsumerConfiguration userCC = ConsumerConfiguration.builder(originalCc) .deliverPolicy(DeliverPolicy.ByStartSequence) .deliverSubject(newDeliverSubject) .startSequence(Math.max(1, lastStreamSeq + 1)) @@ -66,12 +91,12 @@ private void handleErrorCondition() { js._createConsumerUnsubscribeOnException(stream, userCC, sub); // 4. restart the manager. - sub.manager.startup(sub); + startup(sub); } catch (Exception e) { IllegalStateException ise = new IllegalStateException("Ordered subscription fatal error.", e); js.conn.processException(ise); - if (dispatcher == null) { // synchronous + if (syncMode) { throw ise; } } diff --git a/src/main/java/io/nats/client/impl/PullMessageManager.java b/src/main/java/io/nats/client/impl/PullMessageManager.java index 87c16ce07..50bbd55cd 100644 --- a/src/main/java/io/nats/client/impl/PullMessageManager.java +++ b/src/main/java/io/nats/client/impl/PullMessageManager.java @@ -15,21 +15,134 @@ import io.nats.client.JetStreamStatusException; import io.nats.client.Message; +import io.nats.client.PullRequestOptions; +import io.nats.client.support.PullStatus; +import io.nats.client.support.Status; -import java.util.Arrays; -import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import static io.nats.client.support.NatsJetStreamConstants.NATS_PENDING_BYTES; +import static io.nats.client.support.NatsJetStreamConstants.NATS_PENDING_MESSAGES; +import static io.nats.client.support.Status.*; class PullMessageManager extends MessageManager { - private static final List PULL_KNOWN_STATUS_CODES = Arrays.asList(404, 408, 409); + protected final AtomicLong pendingMessages; + protected final AtomicLong pendingBytes; + protected final AtomicBoolean trackingBytes; + + protected PullMessageManager(NatsConnection conn, boolean syncMode) { + super(conn, syncMode); + pendingMessages = new AtomicLong(0); + pendingBytes = new AtomicLong(0); + trackingBytes = new AtomicBoolean(false); + } + + @Override + protected void startup(NatsJetStreamSubscription sub) { + super.startup(sub); + sub.setBeforeQueueProcessor(this::beforeQueueProcessorImpl); + } + + @Override + protected void startPullRequest(PullRequestOptions pro) { + pendingMessages.addAndGet(pro.getBatchSize()); + pendingBytes.addAndGet(pro.getMaxBytes()); + trackingBytes.set(pendingBytes.get() > 0); + initIdleHeartbeat(pro.getIdleHeartbeat(), -1); + if (hb) { + initOrResetHeartbeatTimer(); + } + else { + shutdownHeartbeatTimer(); + } + } - boolean manage(Message msg) { - if (msg.isStatusMessage()) { - if ( !PULL_KNOWN_STATUS_CODES.contains(msg.getStatus().getCode()) ) { - throw new JetStreamStatusException(sub, msg.getStatus()); + @Override + protected PullStatus getPullStatus() { + return new PullStatus(pendingMessages.get(), pendingBytes.get(), hb); + } + + private void trackPending(long m, long b) { + boolean reachedEnd = false; + if (m > 0) { + if (pendingMessages.addAndGet(-m) < 1) { + reachedEnd = true; + } + } + if (trackingBytes.get() && b > 0) { + if (pendingBytes.addAndGet(-b) < 1) { + reachedEnd = true; } - return true; } - return false; + if (reachedEnd) { + pendingMessages.set(0); + pendingBytes.set(0); + trackingBytes.set(false); + if (hb) { + shutdownHeartbeatTimer(); + } + } + } + + @Override + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + if (hb) { + messageReceived(); + Status status = msg.getStatus(); + // only plain heartbeats do not get queued (return false == not queued) + // normal message || status but not hb + return status == null || !status.isHeartbeat(); + } + return true; + } + + @Override + protected boolean manage(Message msg) { + if (msg.getStatus() == null) { + trackJsMessage(msg); + trackPending(1, bytesInMessage(msg)); + return false; + } + + Status status = msg.getStatus(); + Headers h = msg.getHeaders(); + if (h != null) { + String s; + long m = ((s = h.getFirst(NATS_PENDING_MESSAGES)) == null) ? -1 : Long.parseLong(s); + long b = ((s = h.getFirst(NATS_PENDING_BYTES)) == null) ? -1 : Long.parseLong(s); + trackPending(m, b); + } + + int statusCode = status.getCode(); + if (statusCode == NOT_FOUND_CODE || statusCode == REQUEST_TIMEOUT_CODE) { + return true; // ignored + } + + if (statusCode == CONFLICT_CODE) { + // sometimes just a warning + if (status.getMessage().contains("Exceed")) { + conn.executeCallback((c, el) -> el.pullStatusWarning(c, sub, status)); + return true; + } + // fall through + } + + // all others are fatal + conn.executeCallback((c, el) -> el.pullStatusError(c, sub, status)); + if (syncMode) { + throw new JetStreamStatusException(sub, status); + } + + return true; // all status are managed + } + + private long bytesInMessage(Message msg) { + NatsMessage nm = (NatsMessage) msg; + return nm.subject.length() + + nm.headerLen + + nm.dataLen + + (nm.replyTo == null ? 0 : nm.replyTo.length()); } } diff --git a/src/main/java/io/nats/client/impl/PushMessageManager.java b/src/main/java/io/nats/client/impl/PushMessageManager.java index 1f59740f0..b977db5a6 100644 --- a/src/main/java/io/nats/client/impl/PushMessageManager.java +++ b/src/main/java/io/nats/client/impl/PushMessageManager.java @@ -13,233 +13,116 @@ package io.nats.client.impl; -import io.nats.client.ErrorListener; import io.nats.client.JetStreamStatusException; import io.nats.client.Message; import io.nats.client.SubscribeOptions; import io.nats.client.api.ConsumerConfiguration; import io.nats.client.support.Status; -import java.util.Collections; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.atomic.AtomicLong; - +import static io.nats.client.ErrorListener.FlowControlSource; +import static io.nats.client.ErrorListener.FlowControlSource.FLOW_CONTROL; +import static io.nats.client.ErrorListener.FlowControlSource.HEARTBEAT; import static io.nats.client.support.NatsJetStreamConstants.CONSUMER_STALLED_HDR; class PushMessageManager extends MessageManager { - protected static final List PUSH_KNOWN_STATUS_CODES = Collections.singletonList(409); - - protected static final int THRESHOLD = 3; - - protected final NatsConnection conn; - protected final NatsJetStream js; protected final String stream; - protected final ConsumerConfiguration serverCC; - protected final NatsDispatcher dispatcher; + protected final ConsumerConfiguration originalCc; - protected final boolean syncMode; protected final boolean queueMode; - protected final boolean hb; protected final boolean fc; - - protected final long idleHeartbeatSetting; - protected final long alarmPeriodSetting; - protected String lastFcSubject; - protected long lastStreamSeq; - protected long lastConsumerSeq; - protected final AtomicLong lastMsgReceived; - protected HeartbeatTimer heartbeatTimer; - - PushMessageManager(NatsConnection conn, + protected PushMessageManager(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, - ConsumerConfiguration serverCC, + ConsumerConfiguration originalCc, boolean queueMode, - NatsDispatcher dispatcher) + boolean syncMode) { - this.conn = conn; + super(conn, syncMode); this.js = js; this.stream = stream; - this.serverCC = serverCC; - this.dispatcher = dispatcher; - this.syncMode = dispatcher == null; + this.originalCc = originalCc; this.queueMode = queueMode; - lastStreamSeq = -1; - lastConsumerSeq = -1; - lastMsgReceived = new AtomicLong(System.currentTimeMillis()); if (queueMode) { - hb = false; fc = false; - idleHeartbeatSetting = 0; - alarmPeriodSetting = 0; } else { - idleHeartbeatSetting = serverCC.getIdleHeartbeat() == null ? 0 : serverCC.getIdleHeartbeat().toMillis(); - if (idleHeartbeatSetting <= 0) { - alarmPeriodSetting = 0; - hb = false; - } - else { - long mat = so.getMessageAlarmTime(); - if (mat < idleHeartbeatSetting) { - alarmPeriodSetting = idleHeartbeatSetting * THRESHOLD; - } - else { - alarmPeriodSetting = mat; - } - hb = true; - } - fc = hb && serverCC.isFlowControl(); // can't have fc w/o heartbeat + initIdleHeartbeat(originalCc.getIdleHeartbeat(), so.getMessageAlarmTime()); + fc = hb && originalCc.isFlowControl(); // can't have fc w/o heartbeat } } + protected boolean isQueueMode() { return queueMode; } + protected boolean isFc() { return fc; } + protected String getLastFcSubject() { return lastFcSubject; } + @Override - void startup(NatsJetStreamSubscription sub) { + protected void startup(NatsJetStreamSubscription sub) { super.startup(sub); + sub.setBeforeQueueProcessor(this::beforeQueueProcessorImpl); if (hb) { - sub.setBeforeQueueProcessor(this::beforeQueueProcessor); - heartbeatTimer = new HeartbeatTimer(); + initOrResetHeartbeatTimer(); } } @Override - void shutdown() { - if (heartbeatTimer != null) { - heartbeatTimer.shutdown(); - heartbeatTimer = null; - } - super.shutdown(); - } - - protected void handleHeartbeatError() { - conn.executeCallback((c, el) -> el.heartbeatAlarm(c, sub, lastStreamSeq, lastConsumerSeq)); - } - - class HeartbeatTimer { - Timer timer; - boolean alive = true; - - class HeartbeatTimerTask extends TimerTask { - @Override - public void run() { - long sinceLast = System.currentTimeMillis() - lastMsgReceived.get(); - if (sinceLast > alarmPeriodSetting) { - handleHeartbeatError(); + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + if (hb) { + messageReceived(); + Status status = msg.getStatus(); + if (status != null) { + // only plain heartbeats do not get queued + if (status.isHeartbeat()) { + return hasFcSubject(msg); // true if not a plain hb } - restart(); - } - } - - public HeartbeatTimer() { - restart(); - } - - synchronized void restart() { - cancel(); - if (alive) { - timer = new Timer(); - timer.schedule(new HeartbeatTimerTask(), alarmPeriodSetting); - } - } - - synchronized public void shutdown() { - alive = false; - cancel(); - } - - private void cancel() { - if (timer != null) { - timer.cancel(); - timer.purge(); - timer = null; } } + return true; } - boolean isSyncMode() { return syncMode; } - boolean isQueueMode() { return queueMode; } - boolean isFc() { return fc; } - boolean isHb() { return hb; } - - long getIdleHeartbeatSetting() { return idleHeartbeatSetting; } - long getAlarmPeriodSetting() { return alarmPeriodSetting; } - - String getLastFcSubject() { return lastFcSubject; } - long getLastStreamSequence() { return lastStreamSeq; } - long getLastConsumerSequence() { return lastConsumerSeq; } - long getLastMsgReceived() { return lastMsgReceived.get(); } - - NatsMessage beforeQueueProcessor(NatsMessage msg) { - lastMsgReceived.set(System.currentTimeMillis()); - if (msg.isStatusMessage() - && msg.getStatus().isHeartbeat() - && extractFcSubject(msg) == null) - { - return null; - } - return msg; + protected boolean hasFcSubject(Message msg) { + return msg.getHeaders() != null && msg.getHeaders().containsKey(CONSUMER_STALLED_HDR); } - protected boolean subManage(Message msg) { - return false; + protected String extractFcSubject(Message msg) { + return msg.getHeaders() == null ? null : msg.getHeaders().getFirst(CONSUMER_STALLED_HDR); } - boolean manage(Message msg) { - if (!sub.getSID().equals(msg.getSID())) { - return true; + @Override + protected boolean manage(Message msg) { + if (msg.getStatus() == null) { + trackJsMessage(msg); + return false; } + manageStatus(msg); + return true; // all status are managed + } - if (msg.isStatusMessage()) { - // this checks fc, hb and unknown - // only process fc and hb if those flags are set - // otherwise they are simply known statuses - Status status = msg.getStatus(); - if (status.isFlowControl()) { - if (fc) { - _processFlowControl(msg.getReplyTo(), ErrorListener.FlowControlSource.FLOW_CONTROL); - } - } - else if (status.isHeartbeat()) { - if (fc) { - // status flowControlSubject is set in the beforeQueueProcessor - _processFlowControl(extractFcSubject(msg), ErrorListener.FlowControlSource.HEARTBEAT); - } + protected void manageStatus(Message msg) { + // this checks fc, hb and unknown + // only process fc and hb if those flags are set + // otherwise they are simply known statuses + Status status = msg.getStatus(); + if (fc) { + boolean sfc = status.isFlowControl(); + String fcSubject = sfc ? msg.getReplyTo() : extractFcSubject(msg); + if (fcSubject != null) { + processFlowControl(fcSubject, sfc ? HEARTBEAT : FLOW_CONTROL); + return; } - else if (!PUSH_KNOWN_STATUS_CODES.contains(status.getCode())) { - // If this status is unknown to us, always use the error handler. - // If it's a sync call, also throw an exception - conn.executeCallback((c, el) -> el.unhandledStatus(c, sub, status)); - if (syncMode) { - throw new JetStreamStatusException(sub, status); - } - } - return true; } - - if (subManage(msg)) { - return true; + conn.executeCallback((c, el) -> el.unhandledStatus(c, sub, status)); + if (syncMode) { + throw new JetStreamStatusException(sub, status); } - - // JS Message - lastStreamSeq = msg.metaData().streamSequence(); - lastConsumerSeq = msg.metaData().consumerSequence(); - - return false; - } - - String extractFcSubject(Message msg) { - return msg.getHeaders() == null ? null : msg.getHeaders().getFirst(CONSUMER_STALLED_HDR); } - private void _processFlowControl(String fcSubject, ErrorListener.FlowControlSource source) { + private void processFlowControl(String fcSubject, FlowControlSource source) { // we may get multiple fc/hb messages with the same reply // only need to post to that subject once if (fcSubject != null && !fcSubject.equals(lastFcSubject)) { diff --git a/src/main/java/io/nats/client/support/JsonValue.java b/src/main/java/io/nats/client/support/JsonValue.java index b3902fc6e..20071692f 100644 --- a/src/main/java/io/nats/client/support/JsonValue.java +++ b/src/main/java/io/nats/client/support/JsonValue.java @@ -185,7 +185,7 @@ public String toJson() { switch (type) { case STRING: return valueString(string); case BOOL: return valueString(bool); - case MAP: return valueString(map); + case MAP: return valueString(map); case ARRAY: return valueString(array); case INTEGER: return i.toString(); case LONG: return l.toString(); diff --git a/src/main/java/io/nats/client/support/NatsJetStreamConstants.java b/src/main/java/io/nats/client/support/NatsJetStreamConstants.java index 9fac2ea5f..338b1dc03 100644 --- a/src/main/java/io/nats/client/support/NatsJetStreamConstants.java +++ b/src/main/java/io/nats/client/support/NatsJetStreamConstants.java @@ -98,7 +98,21 @@ public interface NatsJetStreamConstants { String NATS_SUBJECT = "Nats-Subject"; String NATS_LAST_SEQUENCE = "Nats-Last-Sequence"; + String NATS_PENDING_MESSAGES = "Nats-Pending-Messages"; + String NATS_PENDING_BYTES = "Nats-Pending-Bytes"; + int JS_CONSUMER_NOT_FOUND_ERR = 10014; int JS_NO_MESSAGE_FOUND_ERR = 10037; int JS_WRONG_LAST_SEQUENCE = 10071; + + String BAD_REQUEST = "Bad Request"; // 400 + String NO_MESSAGES = "No Messages"; // 404 + String CONSUMER_DELETED = "Consumer Deleted"; // 409 + String CONSUMER_IS_PUSH_BASED = "Consumer is push based"; // 409 + + String MESSAGE_SIZE_EXCEEDS_MAX_BYTES = "Message Size Exceeds MaxBytes"; // 409 + String EXCEEDED_MAX_WAITING = "Exceeded MaxWaiting"; // 409 + String EXCEEDED_MAX_REQUEST_BATCH = "Exceeded MaxRequestBatch"; // 409 + String EXCEEDED_MAX_REQUEST_EXPIRES = "Exceeded MaxRequestExpires"; // 409 + String EXCEEDED_MAX_REQUEST_MAX_BYTES = "Exceeded MaxRequestMaxBytes"; // 409 } diff --git a/src/main/java/io/nats/client/support/PullStatus.java b/src/main/java/io/nats/client/support/PullStatus.java new file mode 100644 index 000000000..fce089e71 --- /dev/null +++ b/src/main/java/io/nats/client/support/PullStatus.java @@ -0,0 +1,60 @@ +// Copyright 2022 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client.support; + +/** + * Class representing the current state of a pull subscription + */ +public class PullStatus { + private final long pendingMessages; + private final long pendingBytes; + private final boolean trackingHeartbeats; + + /** + * Construct a pull status object. This is really an internal object, + * and should not be constructed by users + * @param pendingMessages the number of pending messages + * @param pendingBytes the number of pending bytes + * @param trackingHeartbeats whether heartbeats are currently being tracked + */ + public PullStatus(long pendingMessages, long pendingBytes, boolean trackingHeartbeats) { + this.pendingMessages = pendingMessages; + this.pendingBytes = pendingBytes; + this.trackingHeartbeats = trackingHeartbeats; + } + + /** + * Get the number of pending messages for the pull + * @return the pending messages + */ + public long getPendingMessages() { + return pendingMessages; + } + + /** + * Get the number of pending bytes for the pull + * @return the pending bytes + */ + public long getPendingBytes() { + return pendingBytes; + } + + /** + * Get whether heartbeats are currently being tracked. + * @return true if heartbeats are currently being tracked + */ + public boolean isTrackingHeartbeats() { + return trackingHeartbeats; + } +} diff --git a/src/main/java/io/nats/client/support/Status.java b/src/main/java/io/nats/client/support/Status.java index 5a13d8c31..2a14b87ac 100644 --- a/src/main/java/io/nats/client/support/Status.java +++ b/src/main/java/io/nats/client/support/Status.java @@ -23,6 +23,10 @@ public class Status { public static final String NO_RESPONDERS_TEXT = "No Responders Available For Request"; public static final int FLOW_OR_HEARTBEAT_STATUS_CODE = 100; public static final int NO_RESPONDERS_CODE = 503; + public static final int BAD_REQUEST_CODE = 400; + public static final int NOT_FOUND_CODE = 404; + public static final int REQUEST_TIMEOUT_CODE = 408; + public static final int CONFLICT_CODE = 409; private final int code; private final String message; @@ -77,19 +81,15 @@ public String toString() { '}'; } - private boolean isStatus(int code, String text) { - return this.code == code && message.equals(text); - } - public boolean isFlowControl() { - return isStatus(FLOW_OR_HEARTBEAT_STATUS_CODE, FLOW_CONTROL_TEXT); + return code == FLOW_OR_HEARTBEAT_STATUS_CODE && message.equals(FLOW_CONTROL_TEXT); } public boolean isHeartbeat() { - return isStatus(FLOW_OR_HEARTBEAT_STATUS_CODE, HEARTBEAT_TEXT); + return code == FLOW_OR_HEARTBEAT_STATUS_CODE && message.equals(HEARTBEAT_TEXT); } public boolean isNoResponders() { - return isStatus(NO_RESPONDERS_CODE, NO_RESPONDERS_TEXT); + return code == NO_RESPONDERS_CODE && message.equals(NO_RESPONDERS_TEXT); } } diff --git a/src/test/java/io/nats/client/OptionsTests.java b/src/test/java/io/nats/client/OptionsTests.java index d359eee09..985291481 100644 --- a/src/test/java/io/nats/client/OptionsTests.java +++ b/src/test/java/io/nats/client/OptionsTests.java @@ -1,830 +1,830 @@ -// Copyright 2015-2018 The NATS Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package io.nats.client; - -import io.nats.client.ConnectionListener.Events; -import io.nats.client.impl.DataPort; -import io.nats.client.impl.ErrorListenerLoggerImpl; -import io.nats.client.impl.NatsServerPool; -import io.nats.client.impl.TestHandler; -import io.nats.client.support.HttpRequest; -import io.nats.client.support.NatsUri; -import io.nats.client.utils.CloseOnUpgradeAttempt; -import org.junit.jupiter.api.Test; - -import javax.net.ssl.SSLContext; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import static io.nats.client.support.NatsConstants.DEFAULT_PORT; -import static org.junit.jupiter.api.Assertions.*; - -public class OptionsTests { - - public static final String URL_PROTO_HOST_PORT_8080 = "nats://localhost:8080"; - public static final String URL_PROTO_HOST_PORT_8081 = "nats://localhost:8081"; - public static final String URL_HOST_PORT_8081 = "localhost:8081"; - - @Test - public void testClientVersion() { - assertTrue(Nats.CLIENT_VERSION.endsWith(".dev")); - } - - @Test - public void testDefaultOptions() { - Options o = new Options.Builder().build(); - - assertEquals(1, o.getServers().size(), "default one server"); - assertEquals(1, o.getUnprocessedServers().size(), "default one server"); - assertEquals(Options.DEFAULT_URL, o.getServers().toArray()[0].toString(), "default url"); - - assertEquals(Collections.emptyList(), o.getHttpRequestInterceptors(), "default http request interceptors"); - assertEquals(Options.DEFAULT_DATA_PORT_TYPE, o.getDataPortType(), "default data port type"); - - assertFalse(o.isVerbose(), "default verbose"); - assertFalse(o.isPedantic(), "default pedantic"); - assertFalse(o.isNoRandomize(), "default norandomize"); - assertFalse(o.isOldRequestStyle(), "default oldstyle"); - assertFalse(o.isNoEcho(), "default noEcho"); - assertFalse(o.supportUTF8Subjects(), "default UTF8 Support"); - assertFalse(o.isNoHeaders(), "default header support"); - assertFalse(o.isNoNoResponders(), "default no responders support"); - assertEquals(Options.DEFAULT_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL, o.isDiscardMessagesWhenOutgoingQueueFull(), - "default discard messages when outgoing queue full"); - - assertNull(o.getUsernameChars(), "default username"); - assertNull(o.getPasswordChars(), "default password"); - assertNull(o.getTokenChars(), "default token"); - assertNull(o.getConnectionName(), "default connection name"); - - assertNull(o.getSslContext(), "default ssl context"); - - assertEquals(Options.DEFAULT_MAX_RECONNECT, o.getMaxReconnect(), "default max reconnect"); - assertEquals(Options.DEFAULT_MAX_PINGS_OUT, o.getMaxPingsOut(), "default ping max"); - assertEquals(Options.DEFAULT_RECONNECT_BUF_SIZE, o.getReconnectBufferSize(), "default reconnect buffer size"); - assertEquals(Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE, o.getMaxMessagesInOutgoingQueue(), - "default max messages in outgoing queue"); - - assertEquals(Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait(), "default reconnect wait"); - assertEquals(Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout(), "default connection timeout"); - assertEquals(Options.DEFAULT_PING_INTERVAL, o.getPingInterval(), "default ping interval"); - assertEquals(Options.DEFAULT_REQUEST_CLEANUP_INTERVAL, o.getRequestCleanupInterval(), - "default cleanup interval"); - - assertTrue(o.getErrorListener() instanceof ErrorListenerLoggerImpl, "error handler"); - assertNull(o.getConnectionListener(), "disconnect handler"); - - // COVERAGE - o.setOldRequestStyle(true); - assertTrue(o.isOldRequestStyle(), "default oldstyle"); - } - - @Test - public void testChainedBooleanOptions() { - Options o = new Options.Builder().verbose().pedantic().noRandomize().supportUTF8Subjects() - .noEcho().oldRequestStyle().noHeaders().noNoResponders() - .discardMessagesWhenOutgoingQueueFull() - .build(); - assertNull(o.getUsernameChars(), "default username"); - assertTrue(o.isVerbose(), "chained verbose"); - assertTrue(o.isPedantic(), "chained pedantic"); - assertTrue(o.isNoRandomize(), "chained norandomize"); - assertTrue(o.isOldRequestStyle(), "chained oldstyle"); - assertTrue(o.isNoEcho(), "chained noecho"); - assertTrue(o.supportUTF8Subjects(), "chained utf8"); - assertTrue(o.isNoHeaders(), "chained no headers"); - assertTrue(o.isNoNoResponders(), "chained no noResponders"); - assertTrue(o.isDiscardMessagesWhenOutgoingQueueFull(), "chained discard messages when outgoing queue full"); - } - - @Test - public void testChainedStringOptions() { - Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).connectionName("name").build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertArrayEquals("hello".toCharArray(), o.getUsernameChars(), "chained username"); - assertArrayEquals("world".toCharArray(), o.getPasswordChars(), "chained password"); - assertEquals("name", o.getConnectionName(), "chained connection name"); - } - - @Test - public void testChainedSecure() throws Exception { - SSLContext ctx = TestSSLUtils.createTestSSLContext(); - SSLContext.setDefault(ctx); - Options o = new Options.Builder().secure().build(); - assertEquals(ctx, o.getSslContext(), "chained context"); - } - - @Test - public void testChainedSSLOptions() throws Exception { - SSLContext ctx = TestSSLUtils.createTestSSLContext(); - Options o = new Options.Builder().sslContext(ctx).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(ctx, o.getSslContext(), "chained context"); - } - - @Test - public void testChainedIntOptions() { - Options o = new Options.Builder().maxReconnects(100).maxPingsOut(200).reconnectBufferSize(300) - .maxControlLine(400) - .maxMessagesInOutgoingQueue(500) - .build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(100, o.getMaxReconnect(), "chained max reconnect"); - assertEquals(200, o.getMaxPingsOut(), "chained ping max"); - assertEquals(300, o.getReconnectBufferSize(), "chained reconnect buffer size"); - assertEquals(400, o.getMaxControlLine(), "chained max control line"); - assertEquals(500, o.getMaxMessagesInOutgoingQueue(), "chained max messages in outgoing queue"); - } - - @Test - public void testChainedDurationOptions() { - Options o = new Options.Builder().reconnectWait(Duration.ofMillis(101)) - .connectionTimeout(Duration.ofMillis(202)).pingInterval(Duration.ofMillis(303)) - .requestCleanupInterval(Duration.ofMillis(404)) - .reconnectJitter(Duration.ofMillis(505)) - .reconnectJitterTls(Duration.ofMillis(606)) - .build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(Duration.ofMillis(101), o.getReconnectWait(), "chained reconnect wait"); - assertEquals(Duration.ofMillis(202), o.getConnectionTimeout(), "chained connection timeout"); - assertEquals(Duration.ofMillis(303), o.getPingInterval(), "chained ping interval"); - assertEquals(Duration.ofMillis(404), o.getRequestCleanupInterval(), "chained cleanup interval"); - assertEquals(Duration.ofMillis(505), o.getReconnectJitter(), "chained reconnect jitter"); - assertEquals(Duration.ofMillis(606), o.getReconnectJitterTls(), "chained cleanup jitter tls"); - } - - @Test - public void testHttpRequestInterceptors() { - java.util.function.Consumer interceptor1 = req -> { - req.getHeaders().add("Test1", "Header"); - }; - java.util.function.Consumer interceptor2 = req -> { - req.getHeaders().add("Test2", "Header"); - }; - Options o = new Options.Builder() - .httpRequestInterceptor(interceptor1) - .httpRequestInterceptor(interceptor2) - .build(); - assertEquals(o.getHttpRequestInterceptors(), Arrays.asList(interceptor1, interceptor2)); - - o = new Options.Builder() - .httpRequestInterceptors(Arrays.asList(interceptor2, interceptor1)) - .build(); - assertEquals(o.getHttpRequestInterceptors(), Arrays.asList(interceptor2, interceptor1)); - } - - @Test - public void testChainedErrorHandler() { - TestHandler handler = new TestHandler(); - Options o = new Options.Builder().errorListener(handler).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(handler, o.getErrorListener(), "chained error handler"); - } - - @Test - public void testChainedConnectionListener() { - ConnectionListener cHandler = (c, e) -> System.out.println("connection event" + e); - Options o = new Options.Builder().connectionListener(cHandler).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertTrue(o.getErrorListener() instanceof ErrorListenerLoggerImpl, "error handler"); - assertSame(cHandler, o.getConnectionListener(), "chained connection handler"); - } - - @Test - public void testPropertiesBooleanBuilder() { - Properties props = new Properties(); - props.setProperty(Options.PROP_VERBOSE, "true"); - props.setProperty(Options.PROP_PEDANTIC, "true"); - props.setProperty(Options.PROP_NORANDOMIZE, "true"); - props.setProperty(Options.PROP_USE_OLD_REQUEST_STYLE, "true"); - props.setProperty(Options.PROP_OPENTLS, "true"); - props.setProperty(Options.PROP_NO_ECHO, "true"); - props.setProperty(Options.PROP_UTF8_SUBJECTS, "true"); - props.setProperty(Options.PROP_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL, "true"); - - Options o = new Options.Builder(props).build(); - assertNull(o.getUsernameChars(), "default username chars"); - assertTrue(o.isVerbose(), "property verbose"); - assertTrue(o.isPedantic(), "property pedantic"); - assertTrue(o.isNoRandomize(), "property norandomize"); - assertTrue(o.isOldRequestStyle(), "property oldstyle"); - assertTrue(o.isNoEcho(), "property noecho"); - assertTrue(o.supportUTF8Subjects(), "property utf8"); - assertTrue(o.isDiscardMessagesWhenOutgoingQueueFull(), "property discard messages when outgoing queue full"); - assertNotNull(o.getSslContext(), "property opentls"); - } - - @Test - public void testPropertiesStringOptions() { - Properties props = new Properties(); - props.setProperty(Options.PROP_USERNAME, "hello"); - props.setProperty(Options.PROP_PASSWORD, "world"); - props.setProperty(Options.PROP_CONNECTION_NAME, "name"); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertArrayEquals("hello".toCharArray(), o.getUsernameChars(), "property username"); - assertArrayEquals("world".toCharArray(), o.getPasswordChars(), "property password"); - assertEquals("name", o.getConnectionName(), "property connection name"); - - // COVERAGE - props.setProperty(Options.PROP_CONNECTION_NAME, ""); - new Options.Builder(props).build(); - - props.remove(Options.PROP_CONNECTION_NAME); - new Options.Builder(props).build(); - } - - @Test - public void testPropertiesSSLOptions() throws Exception { - // don't use default for tests, issues with forcing algorithm exception in other tests break it - SSLContext.setDefault(TestSSLUtils.createTestSSLContext()); - Properties props = new Properties(); - props.setProperty(Options.PROP_SECURE, "true"); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertNotNull(o.getSslContext(), "property context"); - } - - @Test - public void testBuilderCoverageOptions() { - Options o = new Options.Builder().build(); - assertTrue(o.clientSideLimitChecks()); - assertNull(o.getServerPool()); // there is a default provider - - o = new Options.Builder().clientSideLimitChecks(true).build(); - assertTrue(o.clientSideLimitChecks()); - - o = new Options.Builder() - .clientSideLimitChecks(false) - .serverPool(new NatsServerPool()) - .build(); - assertFalse(o.clientSideLimitChecks()); - assertNotNull(o.getServerPool()); - } - - @Test - public void testPropertiesCoverageOptions() throws Exception { - // don't use default for tests, issues with forcing algorithm exception in other tests break it - SSLContext.setDefault(TestSSLUtils.createTestSSLContext()); - Properties props = new Properties(); - props.setProperty(Options.PROP_SECURE, "false"); - props.setProperty(Options.PROP_OPENTLS, "false"); - props.setProperty(Options.PROP_NO_HEADERS, "true"); - props.setProperty(Options.PROP_NO_NORESPONDERS, "true"); - props.setProperty(Options.PROP_RECONNECT_JITTER, "1000"); - props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "2000"); - props.setProperty(Options.PROP_CLIENT_SIDE_LIMIT_CHECKS, "true"); - props.setProperty(Options.PROP_IGNORE_DISCOVERED_SERVERS, "true"); - props.setProperty(Options.PROP_SERVERS_POOL_IMPLEMENTATION_CLASS, "io.nats.client.utils.CoverageServerPool"); - props.setProperty(Options.PROP_NO_RESOLVE_HOSTNAMES, "true"); - - Options o = new Options.Builder(props).build(); - assertNull(o.getSslContext(), "property context"); - assertTrue(o.isNoHeaders()); - assertTrue(o.isNoNoResponders()); - assertTrue(o.clientSideLimitChecks()); - assertTrue(o.isIgnoreDiscoveredServers()); - assertNotNull(o.getServerPool()); - assertTrue(o.isNoResolveHostnames()); - } - - @Test - public void testPropertyIntOptions() { - Properties props = new Properties(); - props.setProperty(Options.PROP_MAX_RECONNECT, "100"); - props.setProperty(Options.PROP_MAX_PINGS, "200"); - props.setProperty(Options.PROP_RECONNECT_BUF_SIZE, "300"); - props.setProperty(Options.PROP_MAX_CONTROL_LINE, "400"); - props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "500"); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(100, o.getMaxReconnect(), "property max reconnect"); - assertEquals(200, o.getMaxPingsOut(), "property ping max"); - assertEquals(300, o.getReconnectBufferSize(), "property reconnect buffer size"); - assertEquals(400, o.getMaxControlLine(), "property max control line"); - assertEquals(500, o.getMaxMessagesInOutgoingQueue(), "property max messages in outgoing queue"); - } - - @Test - public void testDefaultPropertyIntOptions() { - Properties props = new Properties(); - props.setProperty(Options.PROP_RECONNECT_WAIT, "-1"); - props.setProperty(Options.PROP_RECONNECT_JITTER, "-1"); - props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "-1"); - props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "-1"); - props.setProperty(Options.PROP_PING_INTERVAL, "-1"); - props.setProperty(Options.PROP_CLEANUP_INTERVAL, "-1"); - props.setProperty(Options.PROP_MAX_CONTROL_LINE, "-1"); - props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "-1"); - - Options o = new Options.Builder(props).build(); - assertEquals(Options.DEFAULT_MAX_CONTROL_LINE, o.getMaxControlLine(), "default max control line"); - assertEquals(Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait(), "default reconnect wait"); - assertEquals(Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout(), "default connection timeout"); - assertEquals(Options.DEFAULT_PING_INTERVAL, o.getPingInterval(), "default ping interval"); - assertEquals(Options.DEFAULT_REQUEST_CLEANUP_INTERVAL, o.getRequestCleanupInterval(), - "default cleanup interval"); - assertEquals(Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE, o.getMaxMessagesInOutgoingQueue(), - "default max messages in outgoing queue"); - } - - @Test - public void testPropertyDurationOptions() { - Properties props = new Properties(); - props.setProperty(Options.PROP_RECONNECT_WAIT, "101"); - props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "202"); - props.setProperty(Options.PROP_PING_INTERVAL, "303"); - props.setProperty(Options.PROP_CLEANUP_INTERVAL, "404"); - props.setProperty(Options.PROP_RECONNECT_JITTER, "505"); - props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "606"); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertEquals(Duration.ofMillis(101), o.getReconnectWait(), "property reconnect wait"); - assertEquals(Duration.ofMillis(202), o.getConnectionTimeout(), "property connection timeout"); - assertEquals(Duration.ofMillis(303), o.getPingInterval(), "property ping interval"); - assertEquals(Duration.ofMillis(404), o.getRequestCleanupInterval(), "property cleanup interval"); - assertEquals(Duration.ofMillis(505), o.getReconnectJitter(), "property reconnect jitter"); - assertEquals(Duration.ofMillis(606), o.getReconnectJitterTls(), "property reconnect jitter tls"); - } - - @Test - public void testPropertyErrorHandler() { - Properties props = new Properties(); - props.setProperty(Options.PROP_ERROR_LISTENER, TestHandler.class.getCanonicalName()); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertNotNull(o.getErrorListener(), "property error handler"); - - o.getErrorListener().errorOccurred(null, "bad subject"); - assertEquals(((TestHandler) o.getErrorListener()).getCount(), 1, "property error handler class"); - } - - @Test - public void testPropertyConnectionListeners() { - Properties props = new Properties(); - props.setProperty(Options.PROP_CONNECTION_CB, TestHandler.class.getCanonicalName()); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertNotNull(o.getConnectionListener(), "property connection handler"); - - o.getConnectionListener().connectionEvent(null, Events.DISCONNECTED); - o.getConnectionListener().connectionEvent(null, Events.RECONNECTED); - o.getConnectionListener().connectionEvent(null, Events.CLOSED); - - assertEquals(((TestHandler) o.getConnectionListener()).getCount(), 3, "property connect handler class"); - } - - @Test - public void testChainOverridesProperties() { - Properties props = new Properties(); - props.setProperty(Options.PROP_TOKEN, "token"); - props.setProperty(Options.PROP_CONNECTION_NAME, "name"); - - Options o = new Options.Builder(props).connectionName("newname").build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - assertArrayEquals("token".toCharArray(), o.getTokenChars(), "property token"); - assertEquals("newname", o.getConnectionName(), "property connection name"); - } - - @Test - public void testDefaultConnectOptions() { - Options o = new Options.Builder().build(); - String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; - assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "default connect options"); - } - - @Test - public void testNonDefaultConnectOptions() { - Options o = new Options.Builder().noNoResponders().noHeaders().noEcho().pedantic().verbose().build(); - String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":true,\"pedantic\":true,\"tls_required\":false,\"echo\":false,\"headers\":false,\"no_responders\":false}"; - assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "non default connect options"); - } - - @Test - public void testConnectOptionsWithNameAndContext() throws Exception { - SSLContext ctx = TestSSLUtils.createTestSSLContext(); - Options o = new Options.Builder().sslContext(ctx).connectionName("c1").build(); - String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\",\"name\":\"c1\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":true,\"echo\":true,\"headers\":true,\"no_responders\":true}"; - assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "default connect options"); - } - - @Test - public void testAuthConnectOptions() { - Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).build(); - String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; - String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true" - + ",\"user\":\"hello\",\"pass\":\"world\"}"; - assertEquals(expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "no auth connect options"); - assertEquals(expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, null).toString(), "auth connect options"); - } - /* - expected: <{"lang":"java","version":"2.8.0","protocol":1,"verbose":false,"pedantic":false,"tls_required":false,"echo":true}> - but was: <{"lang":"java","version":"2.8.0","protocol":1,"verbose":false,"pedantic":false,"tls_required":false,"echo":true,"headers":true}> - */ - - @Test - public void testNKeyConnectOptions() throws Exception { - TestAuthHandler th = new TestAuthHandler(); - byte[] nonce = "abcdefg".getBytes(StandardCharsets.UTF_8); - String sig = Base64.getUrlEncoder().withoutPadding().encodeToString(th.sign(nonce)); - - Options o = new Options.Builder().authHandler(th).build(); - String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; - String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" - + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true" - + ",\"no_responders\":true,\"nkey\":\""+new String(th.getID())+"\",\"sig\":\""+sig+"\",\"jwt\":\"\"}"; - assertEquals(expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, nonce).toString(), "no auth connect options"); - assertEquals(expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, nonce).toString(), "auth connect options"); - } - - @Test - public void testDefaultDataPort() { - Options o = new Options.Builder().build(); - DataPort dataPort = o.buildDataPort(); - - assertNotNull(dataPort); - assertEquals(Options.DEFAULT_DATA_PORT_TYPE, dataPort.getClass().getCanonicalName(), "default dataPort"); - } - - @Test - public void testPropertyDataPortType() { - Properties props = new Properties(); - props.setProperty(Options.PROP_DATA_PORT_TYPE, CloseOnUpgradeAttempt.class.getCanonicalName()); - - Options o = new Options.Builder(props).build(); - assertFalse(o.isVerbose(), "default verbose"); // One from a different type - - assertEquals(CloseOnUpgradeAttempt.class.getCanonicalName(), o.buildDataPort().getClass().getCanonicalName(), - "property data port class"); - } - - @Test - public void testJetStreamProperties() { - Properties props = new Properties(); - props.setProperty(Options.PROP_INBOX_PREFIX, "custom-inbox-no-dot"); - Options o = new Options.Builder(props).build(); - assertEquals("custom-inbox-no-dot.", o.getInboxPrefix()); - - props.setProperty(Options.PROP_INBOX_PREFIX, "custom-inbox-ends-dot."); - o = new Options.Builder(props).build(); - assertEquals("custom-inbox-ends-dot.", o.getInboxPrefix()); - } - - @Test - public void testUserPassInURL() { - String serverURI = "nats://derek:password@localhost:2222"; - Options o = new Options.Builder().server(serverURI).build(); - - String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString(); - assertTrue(connectString.contains("\"user\":\"derek\"")); - assertTrue(connectString.contains("\"pass\":\"password\"")); - assertFalse(connectString.contains("\"token\":")); - } - - @Test - public void testTokenInURL() { - String serverURI = "nats://alberto@localhost:2222"; - Options o = new Options.Builder().server(serverURI).build(); - - String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString(); - assertTrue(connectString.contains("\"auth_token\":\"alberto\"")); - assertFalse(connectString.contains("\"user\":")); - assertFalse(connectString.contains("\"pass\":")); - } - - @Test - public void testThrowOnNoProps() { - assertThrows(IllegalArgumentException.class, () -> new Options.Builder(null)); - } - - @Test - public void testServerInProperties() { - Properties props = new Properties(); - props.setProperty(Options.PROP_URL, URL_PROTO_HOST_PORT_8080); - assertServersAndUnprocessed(false, new Options.Builder(props).build()); - } - - @Test - public void testServersInProperties() { - Properties props = new Properties(); - String urls = URL_PROTO_HOST_PORT_8080 + ", " + URL_HOST_PORT_8081; - props.setProperty(Options.PROP_SERVERS, urls); - assertServersAndUnprocessed(true, new Options.Builder(props).build()); - } - - @Test - public void testServers() { - String[] serverUrls = {URL_PROTO_HOST_PORT_8080, URL_HOST_PORT_8081}; - assertServersAndUnprocessed(true, new Options.Builder().servers(serverUrls).build()); - } - - @Test - public void testServersWithCommas() { - String serverURLs = URL_PROTO_HOST_PORT_8080 + "," + URL_HOST_PORT_8081; - assertServersAndUnprocessed(true, new Options.Builder().server(serverURLs).build()); - } - - @Test - public void testEmptyAndNullStringsInServers() { - String[] serverUrls = {"", null, URL_PROTO_HOST_PORT_8080, URL_HOST_PORT_8081}; - assertServersAndUnprocessed(true, new Options.Builder().servers(serverUrls).build()); - } - - private void assertServersAndUnprocessed(boolean two, Options o) { - Collection servers = o.getServers(); - URI[] serverArray = servers.toArray(new URI[0]); - List un = o.getUnprocessedServers(); - - int size = two ? 2 : 1; - assertEquals(size, serverArray.length); - assertEquals(size, un.size()); - - assertEquals(URL_PROTO_HOST_PORT_8080, serverArray[0].toString(), "property server"); - assertEquals(URL_PROTO_HOST_PORT_8080, un.get(0), "unprocessed server"); - - if (two) { - assertEquals(URL_PROTO_HOST_PORT_8081, serverArray[1].toString(), "property server"); - assertEquals(URL_HOST_PORT_8081, un.get(1), "unprocessed server"); - } - } - - @Test - public void testBadClassInPropertyConnectionListeners() { - assertThrows(IllegalArgumentException.class, () -> { - Properties props = new Properties(); - props.setProperty(Options.PROP_CONNECTION_CB, "foo"); - new Options.Builder(props); - }); - } - - @Test - public void testTokenAndUserThrows() { - assertThrows(IllegalStateException.class, - () -> new Options.Builder().token("foo".toCharArray()).userInfo("foo".toCharArray(), "bar".toCharArray()).build()); - } - - @Test - public void testThrowOnBadServerURI() { - assertThrows(IllegalArgumentException.class, - () -> new Options.Builder().server("foo:/bar\\:blammer").build()); - } - - @Test - public void testThrowOnEmptyServersProp() { - assertThrows(IllegalArgumentException.class, () -> { - Properties props = new Properties(); - props.setProperty(Options.PROP_SERVERS, ""); - new Options.Builder(props).build(); - }); - } - - @Test - public void testThrowOnBadServersURI() { - assertThrows(IllegalArgumentException.class, () -> { - String url1 = URL_PROTO_HOST_PORT_8080; - String url2 = "foo:/bar\\:blammer"; - String[] serverUrls = {url1, url2}; - new Options.Builder().servers(serverUrls).build(); - }); - } - - @Test - public void testSetExectuor() { - ExecutorService exec = Executors.newCachedThreadPool(); - Options options = new Options.Builder().executor(exec).build(); - assertEquals(exec, options.getExecutor()); - } - - @Test - public void testDefaultExecutor() throws Exception { - Options options = new Options.Builder().connectionName("test").build(); - Future future = options.getExecutor().submit(() -> Thread.currentThread().getName()); - String name = future.get(5, TimeUnit.SECONDS); - assertTrue(name.startsWith("test")); - - options = new Options.Builder().build(); - future = options.getExecutor().submit(() -> Thread.currentThread().getName()); - name = future.get(5, TimeUnit.SECONDS); - assertTrue(name.startsWith(Options.DEFAULT_THREAD_NAME_PREFIX)); - } - - String[] schemes = new String[] { "NATS", "unk", "tls", "opentls", "ws", "wss", "nats"}; - boolean[] secures = new boolean[] { false, false, true, true, false, true, false}; - boolean[] wses = new boolean[] { false, false, false, false, true, true, false}; - String[] hosts = new String[] { "host", "1.2.3.4", "[1:2:3:4::5]", null}; - boolean[] ips = new boolean[] { false, true, true, false}; - Integer[] ports = new Integer[] {1122, null}; - String[] userInfos = new String[] {null, "u:p"}; - - @Test - public void testNatsUri() throws URISyntaxException { - for (int e = 0; e < schemes.length; e++) { - _testNatsUri(e, null); - if (e > 1) { - _testNatsUri(-e, schemes[e]); - } - } - - // coverage - //noinspection SimplifiableAssertion,ConstantValue - assertFalse(new NatsUri(Options.DEFAULT_URL).equals(null)); - //noinspection SimplifiableAssertion - assertFalse(new NatsUri(Options.DEFAULT_URL).equals(new Object())); - } - - private void _testNatsUri(int e, String nullScheme) throws URISyntaxException { - String scheme = e < 0 ? null : schemes[e]; - e = Math.abs(e); - for (int h = 0; h < hosts.length; h++) { - String host = hosts[h]; - for (Integer port : ports) { - for (String userInfo : userInfos) { - StringBuilder sb = new StringBuilder(); - String expectedScheme; - if (scheme == null) { - expectedScheme = nullScheme; - } - else { - expectedScheme = scheme; - sb.append(scheme).append("://"); - } - if (userInfo != null) { - sb.append(userInfo).append("@"); - } - if (host != null) { - sb.append(host); - } - int expectedPort; - if (port == null) { - expectedPort = DEFAULT_PORT; - } - else { - expectedPort = port; - sb.append(":").append(port); - } - if (host == null || "unk".equals(scheme)) { - assertThrows(URISyntaxException.class, () -> new NatsUri(sb.toString())); - } - else { - NatsUri uri1 = scheme == null ? new NatsUri(sb.toString(), nullScheme) : new NatsUri(sb.toString()); - NatsUri uri2 = new NatsUri(uri1.getUri()); - assertEquals(uri1, uri2); - checkCreate(uri1, secures[e], wses[e], ips[h], expectedScheme, host, expectedPort, userInfo); - checkCreate(uri2, secures[e], wses[e], ips[h], expectedScheme, host, expectedPort, userInfo); - } - } - } - } - } - - private static void checkCreate(NatsUri uri, boolean secure, boolean ws, boolean ip, String scheme, String host, int port, String userInfo) throws URISyntaxException { - scheme = scheme.toLowerCase(); - assertEquals(secure, uri.isSecure()); - assertEquals(ws, uri.isWebsocket()); - assertEquals(scheme, uri.getScheme()); - assertEquals(host, uri.getHost()); - assertEquals(port, uri.getPort()); - assertEquals(userInfo, uri.getUserInfo()); - String expectedUri = userInfo == null - ? scheme + "://" + host + ":" + port - : scheme + "://" + userInfo + "@" + host + ":" + port; - assertEquals(expectedUri, uri.toString()); - assertEquals(expectedUri.replace(host, "rehost"), uri.reHost("rehost").toString()); - assertEquals(ip, uri.hostIsIpAddress()); - } - - @Test - public void testReconnectDelayHandler() { - ReconnectDelayHandler rdh = l -> Duration.ofSeconds(l * 2); - - Options o = new Options.Builder().reconnectDelayHandler(rdh).build(); - ReconnectDelayHandler rdhO = o.getReconnectDelayHandler(); - - assertNotNull(rdhO); - assertEquals(10, rdhO.getWaitTime(5).getSeconds()); - } - - @Test - public void testInboxPrefixCoverage() { - Options o = new Options.Builder().inboxPrefix("foo").build(); - assertEquals("foo.", o.getInboxPrefix()); - o = new Options.Builder().inboxPrefix("foo.").build(); - assertEquals("foo.", o.getInboxPrefix()); - } - - @Test - public void testSslContextIsProvided() { - Options o = new Options.Builder().server("nats://localhost").build(); - assertNull(o.getSslContext()); - o = new Options.Builder().server("ws://localhost").build(); - assertNull(o.getSslContext()); - o = new Options.Builder().server("localhost").build(); - assertNull(o.getSslContext()); - o = new Options.Builder().server("tls://localhost").build(); - assertNotNull(o.getSslContext()); - o = new Options.Builder().server("wss://localhost").build(); - assertNotNull(o.getSslContext()); - o = new Options.Builder().server("opentls://localhost").build(); - assertNotNull(o.getSslContext()); - o = new Options.Builder().server("nats://localhost,tls://localhost").build(); - assertNotNull(o.getSslContext()); - } - - @SuppressWarnings("deprecation") - @Test - public void coverageForDeprecated() { - Options o = new Options.Builder() - .token("deprecated") - .build(); - assertEquals("deprecated", o.getToken()); - assertNull(o.getUsername()); - assertNull(o.getPassword()); - - o = new Options.Builder() - .userInfo("user", "pass") - .build(); - assertEquals("user", o.getUsername()); - assertEquals("pass", o.getPassword()); - assertNull(o.getToken()); - } - -/* These next three require that no default is set anywhere, if another test - requires SSLContext.setDefault() and runs before these, they will fail. Commenting - out for now, this can be run manually. - - @Test(expected=NoSuchAlgorithmException.class) - public void testThrowOnBadContextForSecure() throws Exception { - try { - System.setProperty("javax.net.ssl.keyStore", "foo"); - System.setProperty("javax.net.ssl.trustStore", "bar"); - new Options.Builder().secure().build(); - assertFalse(true); - } - finally { - System.clearProperty("javax.net.ssl.keyStore"); - System.clearProperty("javax.net.ssl.trustStore"); - } - } - - @Test(expected=IllegalStateException.class) - public void testThrowOnBadContextForTLSUrl() throws Exception { - try { - System.setProperty("javax.net.ssl.keyStore", "foo"); - System.setProperty("javax.net.ssl.trustStore", "bar"); - new Options.Builder().server("tls://localhost:4242").build(); - assertFalse(true); - } - finally { - System.clearProperty("javax.net.ssl.keyStore"); - System.clearProperty("javax.net.ssl.trustStore"); - } - } - - @Test(expected=IllegalArgumentException.class) - public void testThrowOnBadContextSecureProp() { - try { - System.setProperty("javax.net.ssl.keyStore", "foo"); - System.setProperty("javax.net.ssl.trustStore", "bar"); - - Properties props = new Properties(); - props.setProperty(Options.PROP_SECURE, "true"); - new Options.Builder(props).build(); - assertFalse(true); - } - finally { - System.clearProperty("javax.net.ssl.keyStore"); - System.clearProperty("javax.net.ssl.trustStore"); - } - } - */ +// Copyright 2015-2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.nats.client; + +import io.nats.client.ConnectionListener.Events; +import io.nats.client.impl.DataPort; +import io.nats.client.impl.ErrorListenerLoggerImpl; +import io.nats.client.impl.NatsServerPool; +import io.nats.client.impl.TestHandler; +import io.nats.client.support.HttpRequest; +import io.nats.client.support.NatsUri; +import io.nats.client.utils.CloseOnUpgradeAttempt; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLContext; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static io.nats.client.support.NatsConstants.DEFAULT_PORT; +import static org.junit.jupiter.api.Assertions.*; + +public class OptionsTests { + + public static final String URL_PROTO_HOST_PORT_8080 = "nats://localhost:8080"; + public static final String URL_PROTO_HOST_PORT_8081 = "nats://localhost:8081"; + public static final String URL_HOST_PORT_8081 = "localhost:8081"; + + @Test + public void testClientVersion() { + assertTrue(Nats.CLIENT_VERSION.endsWith(".dev")); + } + + @Test + public void testDefaultOptions() { + Options o = new Options.Builder().build(); + + assertEquals(1, o.getServers().size(), "default one server"); + assertEquals(1, o.getUnprocessedServers().size(), "default one server"); + assertEquals(Options.DEFAULT_URL, o.getServers().toArray()[0].toString(), "default url"); + + assertEquals(Collections.emptyList(), o.getHttpRequestInterceptors(), "default http request interceptors"); + assertEquals(Options.DEFAULT_DATA_PORT_TYPE, o.getDataPortType(), "default data port type"); + + assertFalse(o.isVerbose(), "default verbose"); + assertFalse(o.isPedantic(), "default pedantic"); + assertFalse(o.isNoRandomize(), "default norandomize"); + assertFalse(o.isOldRequestStyle(), "default oldstyle"); + assertFalse(o.isNoEcho(), "default noEcho"); + assertFalse(o.supportUTF8Subjects(), "default UTF8 Support"); + assertFalse(o.isNoHeaders(), "default header support"); + assertFalse(o.isNoNoResponders(), "default no responders support"); + assertEquals(Options.DEFAULT_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL, o.isDiscardMessagesWhenOutgoingQueueFull(), + "default discard messages when outgoing queue full"); + + assertNull(o.getUsernameChars(), "default username"); + assertNull(o.getPasswordChars(), "default password"); + assertNull(o.getTokenChars(), "default token"); + assertNull(o.getConnectionName(), "default connection name"); + + assertNull(o.getSslContext(), "default ssl context"); + + assertEquals(Options.DEFAULT_MAX_RECONNECT, o.getMaxReconnect(), "default max reconnect"); + assertEquals(Options.DEFAULT_MAX_PINGS_OUT, o.getMaxPingsOut(), "default ping max"); + assertEquals(Options.DEFAULT_RECONNECT_BUF_SIZE, o.getReconnectBufferSize(), "default reconnect buffer size"); + assertEquals(Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE, o.getMaxMessagesInOutgoingQueue(), + "default max messages in outgoing queue"); + + assertEquals(Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait(), "default reconnect wait"); + assertEquals(Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout(), "default connection timeout"); + assertEquals(Options.DEFAULT_PING_INTERVAL, o.getPingInterval(), "default ping interval"); + assertEquals(Options.DEFAULT_REQUEST_CLEANUP_INTERVAL, o.getRequestCleanupInterval(), + "default cleanup interval"); + + assertTrue(o.getErrorListener() instanceof ErrorListenerLoggerImpl, "error handler"); + assertNull(o.getConnectionListener(), "disconnect handler"); + + // COVERAGE + o.setOldRequestStyle(true); + assertTrue(o.isOldRequestStyle(), "default oldstyle"); + } + + @Test + public void testChainedBooleanOptions() { + Options o = new Options.Builder().verbose().pedantic().noRandomize().supportUTF8Subjects() + .noEcho().oldRequestStyle().noHeaders().noNoResponders() + .discardMessagesWhenOutgoingQueueFull() + .build(); + assertNull(o.getUsernameChars(), "default username"); + assertTrue(o.isVerbose(), "chained verbose"); + assertTrue(o.isPedantic(), "chained pedantic"); + assertTrue(o.isNoRandomize(), "chained norandomize"); + assertTrue(o.isOldRequestStyle(), "chained oldstyle"); + assertTrue(o.isNoEcho(), "chained noecho"); + assertTrue(o.supportUTF8Subjects(), "chained utf8"); + assertTrue(o.isNoHeaders(), "chained no headers"); + assertTrue(o.isNoNoResponders(), "chained no noResponders"); + assertTrue(o.isDiscardMessagesWhenOutgoingQueueFull(), "chained discard messages when outgoing queue full"); + } + + @Test + public void testChainedStringOptions() { + Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).connectionName("name").build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertArrayEquals("hello".toCharArray(), o.getUsernameChars(), "chained username"); + assertArrayEquals("world".toCharArray(), o.getPasswordChars(), "chained password"); + assertEquals("name", o.getConnectionName(), "chained connection name"); + } + + @Test + public void testChainedSecure() throws Exception { + SSLContext ctx = TestSSLUtils.createTestSSLContext(); + SSLContext.setDefault(ctx); + Options o = new Options.Builder().secure().build(); + assertEquals(ctx, o.getSslContext(), "chained context"); + } + + @Test + public void testChainedSSLOptions() throws Exception { + SSLContext ctx = TestSSLUtils.createTestSSLContext(); + Options o = new Options.Builder().sslContext(ctx).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(ctx, o.getSslContext(), "chained context"); + } + + @Test + public void testChainedIntOptions() { + Options o = new Options.Builder().maxReconnects(100).maxPingsOut(200).reconnectBufferSize(300) + .maxControlLine(400) + .maxMessagesInOutgoingQueue(500) + .build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(100, o.getMaxReconnect(), "chained max reconnect"); + assertEquals(200, o.getMaxPingsOut(), "chained ping max"); + assertEquals(300, o.getReconnectBufferSize(), "chained reconnect buffer size"); + assertEquals(400, o.getMaxControlLine(), "chained max control line"); + assertEquals(500, o.getMaxMessagesInOutgoingQueue(), "chained max messages in outgoing queue"); + } + + @Test + public void testChainedDurationOptions() { + Options o = new Options.Builder().reconnectWait(Duration.ofMillis(101)) + .connectionTimeout(Duration.ofMillis(202)).pingInterval(Duration.ofMillis(303)) + .requestCleanupInterval(Duration.ofMillis(404)) + .reconnectJitter(Duration.ofMillis(505)) + .reconnectJitterTls(Duration.ofMillis(606)) + .build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(Duration.ofMillis(101), o.getReconnectWait(), "chained reconnect wait"); + assertEquals(Duration.ofMillis(202), o.getConnectionTimeout(), "chained connection timeout"); + assertEquals(Duration.ofMillis(303), o.getPingInterval(), "chained ping interval"); + assertEquals(Duration.ofMillis(404), o.getRequestCleanupInterval(), "chained cleanup interval"); + assertEquals(Duration.ofMillis(505), o.getReconnectJitter(), "chained reconnect jitter"); + assertEquals(Duration.ofMillis(606), o.getReconnectJitterTls(), "chained cleanup jitter tls"); + } + + @Test + public void testHttpRequestInterceptors() { + java.util.function.Consumer interceptor1 = req -> { + req.getHeaders().add("Test1", "Header"); + }; + java.util.function.Consumer interceptor2 = req -> { + req.getHeaders().add("Test2", "Header"); + }; + Options o = new Options.Builder() + .httpRequestInterceptor(interceptor1) + .httpRequestInterceptor(interceptor2) + .build(); + assertEquals(o.getHttpRequestInterceptors(), Arrays.asList(interceptor1, interceptor2)); + + o = new Options.Builder() + .httpRequestInterceptors(Arrays.asList(interceptor2, interceptor1)) + .build(); + assertEquals(o.getHttpRequestInterceptors(), Arrays.asList(interceptor2, interceptor1)); + } + + @Test + public void testChainedErrorHandler() { + TestHandler handler = new TestHandler(); + Options o = new Options.Builder().errorListener(handler).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(handler, o.getErrorListener(), "chained error handler"); + } + + @Test + public void testChainedConnectionListener() { + ConnectionListener cHandler = (c, e) -> System.out.println("connection event" + e); + Options o = new Options.Builder().connectionListener(cHandler).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertTrue(o.getErrorListener() instanceof ErrorListenerLoggerImpl, "error handler"); + assertSame(cHandler, o.getConnectionListener(), "chained connection handler"); + } + + @Test + public void testPropertiesBooleanBuilder() { + Properties props = new Properties(); + props.setProperty(Options.PROP_VERBOSE, "true"); + props.setProperty(Options.PROP_PEDANTIC, "true"); + props.setProperty(Options.PROP_NORANDOMIZE, "true"); + props.setProperty(Options.PROP_USE_OLD_REQUEST_STYLE, "true"); + props.setProperty(Options.PROP_OPENTLS, "true"); + props.setProperty(Options.PROP_NO_ECHO, "true"); + props.setProperty(Options.PROP_UTF8_SUBJECTS, "true"); + props.setProperty(Options.PROP_DISCARD_MESSAGES_WHEN_OUTGOING_QUEUE_FULL, "true"); + + Options o = new Options.Builder(props).build(); + assertNull(o.getUsernameChars(), "default username chars"); + assertTrue(o.isVerbose(), "property verbose"); + assertTrue(o.isPedantic(), "property pedantic"); + assertTrue(o.isNoRandomize(), "property norandomize"); + assertTrue(o.isOldRequestStyle(), "property oldstyle"); + assertTrue(o.isNoEcho(), "property noecho"); + assertTrue(o.supportUTF8Subjects(), "property utf8"); + assertTrue(o.isDiscardMessagesWhenOutgoingQueueFull(), "property discard messages when outgoing queue full"); + assertNotNull(o.getSslContext(), "property opentls"); + } + + @Test + public void testPropertiesStringOptions() { + Properties props = new Properties(); + props.setProperty(Options.PROP_USERNAME, "hello"); + props.setProperty(Options.PROP_PASSWORD, "world"); + props.setProperty(Options.PROP_CONNECTION_NAME, "name"); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertArrayEquals("hello".toCharArray(), o.getUsernameChars(), "property username"); + assertArrayEquals("world".toCharArray(), o.getPasswordChars(), "property password"); + assertEquals("name", o.getConnectionName(), "property connection name"); + + // COVERAGE + props.setProperty(Options.PROP_CONNECTION_NAME, ""); + new Options.Builder(props).build(); + + props.remove(Options.PROP_CONNECTION_NAME); + new Options.Builder(props).build(); + } + + @Test + public void testPropertiesSSLOptions() throws Exception { + // don't use default for tests, issues with forcing algorithm exception in other tests break it + SSLContext.setDefault(TestSSLUtils.createTestSSLContext()); + Properties props = new Properties(); + props.setProperty(Options.PROP_SECURE, "true"); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertNotNull(o.getSslContext(), "property context"); + } + + @Test + public void testBuilderCoverageOptions() { + Options o = new Options.Builder().build(); + assertTrue(o.clientSideLimitChecks()); + assertNull(o.getServerPool()); // there is a default provider + + o = new Options.Builder().clientSideLimitChecks(true).build(); + assertTrue(o.clientSideLimitChecks()); + + o = new Options.Builder() + .clientSideLimitChecks(false) + .serverPool(new NatsServerPool()) + .build(); + assertFalse(o.clientSideLimitChecks()); + assertNotNull(o.getServerPool()); + } + + @Test + public void testPropertiesCoverageOptions() throws Exception { + // don't use default for tests, issues with forcing algorithm exception in other tests break it + SSLContext.setDefault(TestSSLUtils.createTestSSLContext()); + Properties props = new Properties(); + props.setProperty(Options.PROP_SECURE, "false"); + props.setProperty(Options.PROP_OPENTLS, "false"); + props.setProperty(Options.PROP_NO_HEADERS, "true"); + props.setProperty(Options.PROP_NO_NORESPONDERS, "true"); + props.setProperty(Options.PROP_RECONNECT_JITTER, "1000"); + props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "2000"); + props.setProperty(Options.PROP_CLIENT_SIDE_LIMIT_CHECKS, "true"); + props.setProperty(Options.PROP_IGNORE_DISCOVERED_SERVERS, "true"); + props.setProperty(Options.PROP_SERVERS_POOL_IMPLEMENTATION_CLASS, "io.nats.client.utils.CoverageServerPool"); + props.setProperty(Options.PROP_NO_RESOLVE_HOSTNAMES, "true"); + + Options o = new Options.Builder(props).build(); + assertNull(o.getSslContext(), "property context"); + assertTrue(o.isNoHeaders()); + assertTrue(o.isNoNoResponders()); + assertTrue(o.clientSideLimitChecks()); + assertTrue(o.isIgnoreDiscoveredServers()); + assertNotNull(o.getServerPool()); + assertTrue(o.isNoResolveHostnames()); + } + + @Test + public void testPropertyIntOptions() { + Properties props = new Properties(); + props.setProperty(Options.PROP_MAX_RECONNECT, "100"); + props.setProperty(Options.PROP_MAX_PINGS, "200"); + props.setProperty(Options.PROP_RECONNECT_BUF_SIZE, "300"); + props.setProperty(Options.PROP_MAX_CONTROL_LINE, "400"); + props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "500"); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(100, o.getMaxReconnect(), "property max reconnect"); + assertEquals(200, o.getMaxPingsOut(), "property ping max"); + assertEquals(300, o.getReconnectBufferSize(), "property reconnect buffer size"); + assertEquals(400, o.getMaxControlLine(), "property max control line"); + assertEquals(500, o.getMaxMessagesInOutgoingQueue(), "property max messages in outgoing queue"); + } + + @Test + public void testDefaultPropertyIntOptions() { + Properties props = new Properties(); + props.setProperty(Options.PROP_RECONNECT_WAIT, "-1"); + props.setProperty(Options.PROP_RECONNECT_JITTER, "-1"); + props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "-1"); + props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "-1"); + props.setProperty(Options.PROP_PING_INTERVAL, "-1"); + props.setProperty(Options.PROP_CLEANUP_INTERVAL, "-1"); + props.setProperty(Options.PROP_MAX_CONTROL_LINE, "-1"); + props.setProperty(Options.PROP_MAX_MESSAGES_IN_OUTGOING_QUEUE, "-1"); + + Options o = new Options.Builder(props).build(); + assertEquals(Options.DEFAULT_MAX_CONTROL_LINE, o.getMaxControlLine(), "default max control line"); + assertEquals(Options.DEFAULT_RECONNECT_WAIT, o.getReconnectWait(), "default reconnect wait"); + assertEquals(Options.DEFAULT_CONNECTION_TIMEOUT, o.getConnectionTimeout(), "default connection timeout"); + assertEquals(Options.DEFAULT_PING_INTERVAL, o.getPingInterval(), "default ping interval"); + assertEquals(Options.DEFAULT_REQUEST_CLEANUP_INTERVAL, o.getRequestCleanupInterval(), + "default cleanup interval"); + assertEquals(Options.DEFAULT_MAX_MESSAGES_IN_OUTGOING_QUEUE, o.getMaxMessagesInOutgoingQueue(), + "default max messages in outgoing queue"); + } + + @Test + public void testPropertyDurationOptions() { + Properties props = new Properties(); + props.setProperty(Options.PROP_RECONNECT_WAIT, "101"); + props.setProperty(Options.PROP_CONNECTION_TIMEOUT, "202"); + props.setProperty(Options.PROP_PING_INTERVAL, "303"); + props.setProperty(Options.PROP_CLEANUP_INTERVAL, "404"); + props.setProperty(Options.PROP_RECONNECT_JITTER, "505"); + props.setProperty(Options.PROP_RECONNECT_JITTER_TLS, "606"); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertEquals(Duration.ofMillis(101), o.getReconnectWait(), "property reconnect wait"); + assertEquals(Duration.ofMillis(202), o.getConnectionTimeout(), "property connection timeout"); + assertEquals(Duration.ofMillis(303), o.getPingInterval(), "property ping interval"); + assertEquals(Duration.ofMillis(404), o.getRequestCleanupInterval(), "property cleanup interval"); + assertEquals(Duration.ofMillis(505), o.getReconnectJitter(), "property reconnect jitter"); + assertEquals(Duration.ofMillis(606), o.getReconnectJitterTls(), "property reconnect jitter tls"); + } + + @Test + public void testPropertyErrorHandler() { + Properties props = new Properties(); + props.setProperty(Options.PROP_ERROR_LISTENER, TestHandler.class.getCanonicalName()); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertNotNull(o.getErrorListener(), "property error handler"); + + o.getErrorListener().errorOccurred(null, "bad subject"); + assertEquals(((TestHandler) o.getErrorListener()).getCount(), 1, "property error handler class"); + } + + @Test + public void testPropertyConnectionListeners() { + Properties props = new Properties(); + props.setProperty(Options.PROP_CONNECTION_CB, TestHandler.class.getCanonicalName()); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertNotNull(o.getConnectionListener(), "property connection handler"); + + o.getConnectionListener().connectionEvent(null, Events.DISCONNECTED); + o.getConnectionListener().connectionEvent(null, Events.RECONNECTED); + o.getConnectionListener().connectionEvent(null, Events.CLOSED); + + assertEquals(((TestHandler) o.getConnectionListener()).getCount(), 3, "property connect handler class"); + } + + @Test + public void testChainOverridesProperties() { + Properties props = new Properties(); + props.setProperty(Options.PROP_TOKEN, "token"); + props.setProperty(Options.PROP_CONNECTION_NAME, "name"); + + Options o = new Options.Builder(props).connectionName("newname").build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + assertArrayEquals("token".toCharArray(), o.getTokenChars(), "property token"); + assertEquals("newname", o.getConnectionName(), "property connection name"); + } + + @Test + public void testDefaultConnectOptions() { + Options o = new Options.Builder().build(); + String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; + assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "default connect options"); + } + + @Test + public void testNonDefaultConnectOptions() { + Options o = new Options.Builder().noNoResponders().noHeaders().noEcho().pedantic().verbose().build(); + String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":true,\"pedantic\":true,\"tls_required\":false,\"echo\":false,\"headers\":false,\"no_responders\":false}"; + assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "non default connect options"); + } + + @Test + public void testConnectOptionsWithNameAndContext() throws Exception { + SSLContext ctx = TestSSLUtils.createTestSSLContext(); + Options o = new Options.Builder().sslContext(ctx).connectionName("c1").build(); + String expected = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\",\"name\":\"c1\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":true,\"echo\":true,\"headers\":true,\"no_responders\":true}"; + assertEquals(expected, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "default connect options"); + } + + @Test + public void testAuthConnectOptions() { + Options o = new Options.Builder().userInfo("hello".toCharArray(), "world".toCharArray()).build(); + String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; + String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true" + + ",\"user\":\"hello\",\"pass\":\"world\"}"; + assertEquals(expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, null).toString(), "no auth connect options"); + assertEquals(expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, null).toString(), "auth connect options"); + } + /* + expected: <{"lang":"java","version":"2.8.0","protocol":1,"verbose":false,"pedantic":false,"tls_required":false,"echo":true}> + but was: <{"lang":"java","version":"2.8.0","protocol":1,"verbose":false,"pedantic":false,"tls_required":false,"echo":true,"headers":true}> + */ + + @Test + public void testNKeyConnectOptions() throws Exception { + TestAuthHandler th = new TestAuthHandler(); + byte[] nonce = "abcdefg".getBytes(StandardCharsets.UTF_8); + String sig = Base64.getUrlEncoder().withoutPadding().encodeToString(th.sign(nonce)); + + Options o = new Options.Builder().authHandler(th).build(); + String expectedNoAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true,\"no_responders\":true}"; + String expectedWithAuth = "{\"lang\":\"java\",\"version\":\"" + Nats.CLIENT_VERSION + "\"" + + ",\"protocol\":1,\"verbose\":false,\"pedantic\":false,\"tls_required\":false,\"echo\":true,\"headers\":true" + + ",\"no_responders\":true,\"nkey\":\""+new String(th.getID())+"\",\"sig\":\""+sig+"\",\"jwt\":\"\"}"; + assertEquals(expectedNoAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", false, nonce).toString(), "no auth connect options"); + assertEquals(expectedWithAuth, o.buildProtocolConnectOptionsString("nats://localhost:4222", true, nonce).toString(), "auth connect options"); + } + + @Test + public void testDefaultDataPort() { + Options o = new Options.Builder().build(); + DataPort dataPort = o.buildDataPort(); + + assertNotNull(dataPort); + assertEquals(Options.DEFAULT_DATA_PORT_TYPE, dataPort.getClass().getCanonicalName(), "default dataPort"); + } + + @Test + public void testPropertyDataPortType() { + Properties props = new Properties(); + props.setProperty(Options.PROP_DATA_PORT_TYPE, CloseOnUpgradeAttempt.class.getCanonicalName()); + + Options o = new Options.Builder(props).build(); + assertFalse(o.isVerbose(), "default verbose"); // One from a different type + + assertEquals(CloseOnUpgradeAttempt.class.getCanonicalName(), o.buildDataPort().getClass().getCanonicalName(), + "property data port class"); + } + + @Test + public void testJetStreamProperties() { + Properties props = new Properties(); + props.setProperty(Options.PROP_INBOX_PREFIX, "custom-inbox-no-dot"); + Options o = new Options.Builder(props).build(); + assertEquals("custom-inbox-no-dot.", o.getInboxPrefix()); + + props.setProperty(Options.PROP_INBOX_PREFIX, "custom-inbox-ends-dot."); + o = new Options.Builder(props).build(); + assertEquals("custom-inbox-ends-dot.", o.getInboxPrefix()); + } + + @Test + public void testUserPassInURL() { + String serverURI = "nats://derek:password@localhost:2222"; + Options o = new Options.Builder().server(serverURI).build(); + + String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString(); + assertTrue(connectString.contains("\"user\":\"derek\"")); + assertTrue(connectString.contains("\"pass\":\"password\"")); + assertFalse(connectString.contains("\"token\":")); + } + + @Test + public void testTokenInURL() { + String serverURI = "nats://alberto@localhost:2222"; + Options o = new Options.Builder().server(serverURI).build(); + + String connectString = o.buildProtocolConnectOptionsString(serverURI, true, null).toString(); + assertTrue(connectString.contains("\"auth_token\":\"alberto\"")); + assertFalse(connectString.contains("\"user\":")); + assertFalse(connectString.contains("\"pass\":")); + } + + @Test + public void testThrowOnNoProps() { + assertThrows(IllegalArgumentException.class, () -> new Options.Builder(null)); + } + + @Test + public void testServerInProperties() { + Properties props = new Properties(); + props.setProperty(Options.PROP_URL, URL_PROTO_HOST_PORT_8080); + assertServersAndUnprocessed(false, new Options.Builder(props).build()); + } + + @Test + public void testServersInProperties() { + Properties props = new Properties(); + String urls = URL_PROTO_HOST_PORT_8080 + ", " + URL_HOST_PORT_8081; + props.setProperty(Options.PROP_SERVERS, urls); + assertServersAndUnprocessed(true, new Options.Builder(props).build()); + } + + @Test + public void testServers() { + String[] serverUrls = {URL_PROTO_HOST_PORT_8080, URL_HOST_PORT_8081}; + assertServersAndUnprocessed(true, new Options.Builder().servers(serverUrls).build()); + } + + @Test + public void testServersWithCommas() { + String serverURLs = URL_PROTO_HOST_PORT_8080 + "," + URL_HOST_PORT_8081; + assertServersAndUnprocessed(true, new Options.Builder().server(serverURLs).build()); + } + + @Test + public void testEmptyAndNullStringsInServers() { + String[] serverUrls = {"", null, URL_PROTO_HOST_PORT_8080, URL_HOST_PORT_8081}; + assertServersAndUnprocessed(true, new Options.Builder().servers(serverUrls).build()); + } + + private void assertServersAndUnprocessed(boolean two, Options o) { + Collection servers = o.getServers(); + URI[] serverArray = servers.toArray(new URI[0]); + List un = o.getUnprocessedServers(); + + int size = two ? 2 : 1; + assertEquals(size, serverArray.length); + assertEquals(size, un.size()); + + assertEquals(URL_PROTO_HOST_PORT_8080, serverArray[0].toString(), "property server"); + assertEquals(URL_PROTO_HOST_PORT_8080, un.get(0), "unprocessed server"); + + if (two) { + assertEquals(URL_PROTO_HOST_PORT_8081, serverArray[1].toString(), "property server"); + assertEquals(URL_HOST_PORT_8081, un.get(1), "unprocessed server"); + } + } + + @Test + public void testBadClassInPropertyConnectionListeners() { + assertThrows(IllegalArgumentException.class, () -> { + Properties props = new Properties(); + props.setProperty(Options.PROP_CONNECTION_CB, "foo"); + new Options.Builder(props); + }); + } + + @Test + public void testTokenAndUserThrows() { + assertThrows(IllegalStateException.class, + () -> new Options.Builder().token("foo".toCharArray()).userInfo("foo".toCharArray(), "bar".toCharArray()).build()); + } + + @Test + public void testThrowOnBadServerURI() { + assertThrows(IllegalArgumentException.class, + () -> new Options.Builder().server("foo:/bar\\:blammer").build()); + } + + @Test + public void testThrowOnEmptyServersProp() { + assertThrows(IllegalArgumentException.class, () -> { + Properties props = new Properties(); + props.setProperty(Options.PROP_SERVERS, ""); + new Options.Builder(props).build(); + }); + } + + @Test + public void testThrowOnBadServersURI() { + assertThrows(IllegalArgumentException.class, () -> { + String url1 = URL_PROTO_HOST_PORT_8080; + String url2 = "foo:/bar\\:blammer"; + String[] serverUrls = {url1, url2}; + new Options.Builder().servers(serverUrls).build(); + }); + } + + @Test + public void testSetExectuor() { + ExecutorService exec = Executors.newCachedThreadPool(); + Options options = new Options.Builder().executor(exec).build(); + assertEquals(exec, options.getExecutor()); + } + + @Test + public void testDefaultExecutor() throws Exception { + Options options = new Options.Builder().connectionName("test").build(); + Future future = options.getExecutor().submit(() -> Thread.currentThread().getName()); + String name = future.get(5, TimeUnit.SECONDS); + assertTrue(name.startsWith("test")); + + options = new Options.Builder().build(); + future = options.getExecutor().submit(() -> Thread.currentThread().getName()); + name = future.get(5, TimeUnit.SECONDS); + assertTrue(name.startsWith(Options.DEFAULT_THREAD_NAME_PREFIX)); + } + + String[] schemes = new String[] { "NATS", "unk", "tls", "opentls", "ws", "wss", "nats"}; + boolean[] secures = new boolean[] { false, false, true, true, false, true, false}; + boolean[] wses = new boolean[] { false, false, false, false, true, true, false}; + String[] hosts = new String[] { "host", "1.2.3.4", "[1:2:3:4::5]", null}; + boolean[] ips = new boolean[] { false, true, true, false}; + Integer[] ports = new Integer[] {1122, null}; + String[] userInfos = new String[] {null, "u:p"}; + + @Test + public void testNatsUri() throws URISyntaxException { + for (int e = 0; e < schemes.length; e++) { + _testNatsUri(e, null); + if (e > 1) { + _testNatsUri(-e, schemes[e]); + } + } + + // coverage + //noinspection SimplifiableAssertion,ConstantValue + assertFalse(new NatsUri(Options.DEFAULT_URL).equals(null)); + //noinspection SimplifiableAssertion + assertFalse(new NatsUri(Options.DEFAULT_URL).equals(new Object())); + } + + private void _testNatsUri(int e, String nullScheme) throws URISyntaxException { + String scheme = e < 0 ? null : schemes[e]; + e = Math.abs(e); + for (int h = 0; h < hosts.length; h++) { + String host = hosts[h]; + for (Integer port : ports) { + for (String userInfo : userInfos) { + StringBuilder sb = new StringBuilder(); + String expectedScheme; + if (scheme == null) { + expectedScheme = nullScheme; + } + else { + expectedScheme = scheme; + sb.append(scheme).append("://"); + } + if (userInfo != null) { + sb.append(userInfo).append("@"); + } + if (host != null) { + sb.append(host); + } + int expectedPort; + if (port == null) { + expectedPort = DEFAULT_PORT; + } + else { + expectedPort = port; + sb.append(":").append(port); + } + if (host == null || "unk".equals(scheme)) { + assertThrows(URISyntaxException.class, () -> new NatsUri(sb.toString())); + } + else { + NatsUri uri1 = scheme == null ? new NatsUri(sb.toString(), nullScheme) : new NatsUri(sb.toString()); + NatsUri uri2 = new NatsUri(uri1.getUri()); + assertEquals(uri1, uri2); + checkCreate(uri1, secures[e], wses[e], ips[h], expectedScheme, host, expectedPort, userInfo); + checkCreate(uri2, secures[e], wses[e], ips[h], expectedScheme, host, expectedPort, userInfo); + } + } + } + } + } + + private static void checkCreate(NatsUri uri, boolean secure, boolean ws, boolean ip, String scheme, String host, int port, String userInfo) throws URISyntaxException { + scheme = scheme.toLowerCase(); + assertEquals(secure, uri.isSecure()); + assertEquals(ws, uri.isWebsocket()); + assertEquals(scheme, uri.getScheme()); + assertEquals(host, uri.getHost()); + assertEquals(port, uri.getPort()); + assertEquals(userInfo, uri.getUserInfo()); + String expectedUri = userInfo == null + ? scheme + "://" + host + ":" + port + : scheme + "://" + userInfo + "@" + host + ":" + port; + assertEquals(expectedUri, uri.toString()); + assertEquals(expectedUri.replace(host, "rehost"), uri.reHost("rehost").toString()); + assertEquals(ip, uri.hostIsIpAddress()); + } + + @Test + public void testReconnectDelayHandler() { + ReconnectDelayHandler rdh = l -> Duration.ofSeconds(l * 2); + + Options o = new Options.Builder().reconnectDelayHandler(rdh).build(); + ReconnectDelayHandler rdhO = o.getReconnectDelayHandler(); + + assertNotNull(rdhO); + assertEquals(10, rdhO.getWaitTime(5).getSeconds()); + } + + @Test + public void testInboxPrefixCoverage() { + Options o = new Options.Builder().inboxPrefix("foo").build(); + assertEquals("foo.", o.getInboxPrefix()); + o = new Options.Builder().inboxPrefix("foo.").build(); + assertEquals("foo.", o.getInboxPrefix()); + } + + @Test + public void testSslContextIsProvided() { + Options o = new Options.Builder().server("nats://localhost").build(); + assertNull(o.getSslContext()); + o = new Options.Builder().server("ws://localhost").build(); + assertNull(o.getSslContext()); + o = new Options.Builder().server("localhost").build(); + assertNull(o.getSslContext()); + o = new Options.Builder().server("tls://localhost").build(); + assertNotNull(o.getSslContext()); + o = new Options.Builder().server("wss://localhost").build(); + assertNotNull(o.getSslContext()); + o = new Options.Builder().server("opentls://localhost").build(); + assertNotNull(o.getSslContext()); + o = new Options.Builder().server("nats://localhost,tls://localhost").build(); + assertNotNull(o.getSslContext()); + } + + @SuppressWarnings("deprecation") + @Test + public void coverageForDeprecated() { + Options o = new Options.Builder() + .token("deprecated") + .build(); + assertEquals("deprecated", o.getToken()); + assertNull(o.getUsername()); + assertNull(o.getPassword()); + + o = new Options.Builder() + .userInfo("user", "pass") + .build(); + assertEquals("user", o.getUsername()); + assertEquals("pass", o.getPassword()); + assertNull(o.getToken()); + } + +/* These next three require that no default is set anywhere, if another test + requires SSLContext.setDefault() and runs before these, they will fail. Commenting + out for now, this can be run manually. + + @Test(expected=NoSuchAlgorithmException.class) + public void testThrowOnBadContextForSecure() throws Exception { + try { + System.setProperty("javax.net.ssl.keyStore", "foo"); + System.setProperty("javax.net.ssl.trustStore", "bar"); + new Options.Builder().secure().build(); + assertFalse(true); + } + finally { + System.clearProperty("javax.net.ssl.keyStore"); + System.clearProperty("javax.net.ssl.trustStore"); + } + } + + @Test(expected=IllegalStateException.class) + public void testThrowOnBadContextForTLSUrl() throws Exception { + try { + System.setProperty("javax.net.ssl.keyStore", "foo"); + System.setProperty("javax.net.ssl.trustStore", "bar"); + new Options.Builder().server("tls://localhost:4242").build(); + assertFalse(true); + } + finally { + System.clearProperty("javax.net.ssl.keyStore"); + System.clearProperty("javax.net.ssl.trustStore"); + } + } + + @Test(expected=IllegalArgumentException.class) + public void testThrowOnBadContextSecureProp() { + try { + System.setProperty("javax.net.ssl.keyStore", "foo"); + System.setProperty("javax.net.ssl.trustStore", "bar"); + + Properties props = new Properties(); + props.setProperty(Options.PROP_SECURE, "true"); + new Options.Builder(props).build(); + assertFalse(true); + } + finally { + System.clearProperty("javax.net.ssl.keyStore"); + System.clearProperty("javax.net.ssl.trustStore"); + } + } + */ } \ No newline at end of file diff --git a/src/test/java/io/nats/client/impl/ErrorListenerTests.java b/src/test/java/io/nats/client/impl/ErrorListenerTests.java index f0ace44a3..7049e381a 100644 --- a/src/test/java/io/nats/client/impl/ErrorListenerTests.java +++ b/src/test/java/io/nats/client/impl/ErrorListenerTests.java @@ -343,6 +343,8 @@ public void testCoverage() { AtomicBoolean messageDiscardedFlag = new AtomicBoolean(); AtomicBoolean heartbeatAlarmFlag = new AtomicBoolean(); AtomicBoolean unhandledStatusFlag = new AtomicBoolean(); + AtomicBoolean pullStatusWarningFlag = new AtomicBoolean(); + AtomicBoolean pullStatusErrorFlag = new AtomicBoolean(); AtomicBoolean flowControlProcessedFlag = new AtomicBoolean(); _cover(new ErrorListener() { @@ -376,6 +378,16 @@ public void unhandledStatus(Connection conn, JetStreamSubscription sub, Status s unhandledStatusFlag.set(true); } + @Override + public void pullStatusWarning(Connection conn, JetStreamSubscription sub, Status status) { + pullStatusWarningFlag.set(true); + } + + @Override + public void pullStatusError(Connection conn, JetStreamSubscription sub, Status status) { + pullStatusErrorFlag.set(true); + } + @Override public void flowControlProcessed(Connection conn, JetStreamSubscription sub, String subject, FlowControlSource source) { flowControlProcessedFlag.set(true); @@ -388,8 +400,9 @@ public void flowControlProcessed(Connection conn, JetStreamSubscription sub, Str assertTrue(messageDiscardedFlag.get()); assertTrue(heartbeatAlarmFlag.get()); assertTrue(unhandledStatusFlag.get()); + assertTrue(pullStatusWarningFlag.get()); + assertTrue(pullStatusErrorFlag.get()); assertTrue(flowControlProcessedFlag.get()); - } private void _cover(ErrorListener el) { @@ -399,6 +412,8 @@ private void _cover(ErrorListener el) { el.messageDiscarded(null, null); el.heartbeatAlarm(null, null, -1, -1); el.unhandledStatus(null, null, null); + el.pullStatusWarning(null, null, null); + el.pullStatusError(null, null, null); el.flowControlProcessed(null, null, null, null); } } diff --git a/src/test/java/io/nats/client/impl/JetStreamOrderedConsumerTests.java b/src/test/java/io/nats/client/impl/JetStreamConsumerTests.java similarity index 58% rename from src/test/java/io/nats/client/impl/JetStreamOrderedConsumerTests.java rename to src/test/java/io/nats/client/impl/JetStreamConsumerTests.java index 3ea1793ed..6ed659083 100644 --- a/src/test/java/io/nats/client/impl/JetStreamOrderedConsumerTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamConsumerTests.java @@ -27,28 +27,28 @@ import static io.nats.client.support.NatsJetStreamClientError.JsSubOrderedNotAllowOnQueues; import static org.junit.jupiter.api.Assertions.*; -public class JetStreamOrderedConsumerTests extends JetStreamTestBase { +public class JetStreamConsumerTests extends JetStreamTestBase { // ------------------------------------------------------------------------------------------ // This allows me to intercept messages before it gets to the connection queue // which is before the messages is available for nextMessage, or before // it gets dispatched to a handler. - static class OrderedTestDropSimulator extends OrderedManager { - public OrderedTestDropSimulator(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, NatsDispatcher dispatcher) { - super(conn, js, stream, so, serverCC, queueMode, dispatcher); + static class OrderedTestDropSimulator extends OrderedMessageManager { + public OrderedTestDropSimulator(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, boolean syncMode) { + super(conn, js, stream, so, serverCC, queueMode, syncMode); } @Override - NatsMessage beforeQueueProcessor(NatsMessage msg) { - msg = super.beforeQueueProcessor(msg); - if (msg != null && msg.isJetStream()) { + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + if (msg.isJetStream()) { long ss = msg.metaData().streamSequence(); long cs = msg.metaData().consumerSequence(); if ((ss == 2 && cs == 2) || (ss == 5 && cs == 4)) { - return null; + return false; } } - return msg; + + return super.beforeQueueProcessorImpl(msg); } } @@ -66,7 +66,7 @@ public void testOrderedConsumerSync() throws Exception { createMemoryStream(jsm, stream(111), subject); // Get this in place before any subscriptions are made - ((NatsJetStream)js).PUSH_MESSAGE_MANAGER_FACTORY = OrderedTestDropSimulator::new; + ((NatsJetStream)js)._pushOrderedMessageManagerFactory = OrderedTestDropSimulator::new; // The options will be used in various ways PushSubscribeOptions pso = PushSubscribeOptions.builder().ordered(true).build(); @@ -107,7 +107,7 @@ public void testOrderedConsumerAsync() throws Exception { createMemoryStream(jsm, stream(222), subject); // Get this in place before any subscriptions are made - ((NatsJetStream)js).PUSH_MESSAGE_MANAGER_FACTORY = OrderedTestDropSimulator::new; + ((NatsJetStream)js)._pushOrderedMessageManagerFactory = OrderedTestDropSimulator::new; // The options will be used in various ways PushSubscribeOptions pso = PushSubscribeOptions.builder().ordered(true).build(); @@ -152,95 +152,108 @@ public void testOrderedConsumerAsync() throws Exception { }); } - static class OrderedMissHeartbeatSimulator extends OrderedManager { - public AtomicInteger skip = new AtomicInteger(5); - public AtomicInteger startups = new AtomicInteger(0); - public AtomicInteger handles = new AtomicInteger(0); - public CountDownLatch latch = new CountDownLatch(1); + static class HeartbeatErrorSimulator extends PushMessageManager { + public CountDownLatch latch = new CountDownLatch(2); - public OrderedMissHeartbeatSimulator(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, NatsDispatcher dispatcher) { - super(conn, js, stream, so, serverCC, queueMode, dispatcher); + public HeartbeatErrorSimulator(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, boolean syncMode) { + super(conn, js, stream, so, serverCC, queueMode, syncMode); } @Override - void startup(NatsJetStreamSubscription sub) { - super.startup(sub); - startups.incrementAndGet(); + protected void handleHeartbeatError() { + super.handleHeartbeatError(); + latch.countDown(); + } + + @Override + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + return false; + } + } + + static class HeartbeatErrorOrderedSimulator extends OrderedMessageManager { + public CountDownLatch latch = new CountDownLatch(2); + + public HeartbeatErrorOrderedSimulator(NatsConnection conn, NatsJetStream js, String stream, SubscribeOptions so, ConsumerConfiguration serverCC, boolean queueMode, boolean syncMode) { + super(conn, js, stream, so, serverCC, queueMode, syncMode); } @Override protected void handleHeartbeatError() { - handles.incrementAndGet(); - skip.set(9999); // more than the number of messages left super.handleHeartbeatError(); latch.countDown(); } @Override - NatsMessage beforeQueueProcessor(NatsMessage msg) { - if (skip.decrementAndGet() < 0) { - return null; - } - return super.beforeQueueProcessor(msg); - } - - public String SID() { - return sub.getSID(); + protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { + return false; } } @Test - public void testOrderedConsumerHbSync() throws Exception { - runInJsServer(nc -> { + public void testHeartbeatError() throws Exception { + TestHandler testHandler = new TestHandler(); + runInJsServer(testHandler, nc -> { JetStream js = nc.jetStream(); JetStreamManagement jsm = nc.jetStreamManagement(); - String subject = subject(333); - createMemoryStream(jsm, stream(333), subject); - - // Get this in place before any subscriptions are made - AtomicReference simRef = new AtomicReference<>(); - ((NatsJetStream)js).PUSH_MESSAGE_MANAGER_FACTORY = (conn, lJs, stream, so, serverCC, qmode, dispatcher) -> { - OrderedMissHeartbeatSimulator sim = new OrderedMissHeartbeatSimulator(conn, lJs, stream, so, serverCC, qmode, dispatcher); - simRef.set(sim); - return sim; - }; - - jsPublish(js, subject, 1, 10); + String subject = "hbe"; + createMemoryStream(jsm, "hbestream", subject); - // Setup subscription - PushSubscribeOptions pso = PushSubscribeOptions.builder().ordered(true) - .configuration(ConsumerConfiguration.builder().flowControl(500).build()) - .build(); + Dispatcher d = nc.createDispatcher(); + ConsumerConfiguration cc = ConsumerConfiguration.builder().flowControl(2000).idleHeartbeat(100).build(); + PushSubscribeOptions pso = PushSubscribeOptions.builder().configuration(cc).build(); + AtomicReference simRef = setFactory(js); JetStreamSubscription sub = js.subscribe(subject, pso); - nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server + validate(sub, testHandler, simRef.get().latch, null); - OrderedMissHeartbeatSimulator sim = simRef.get(); + simRef = setFactory(js); + sub = js.subscribe(subject, d, m -> {}, false, pso); + validate(sub, testHandler, simRef.get().latch, d); - String firstSid = null; - int expectedStreamSeq = 1; - while (expectedStreamSeq <= 5) { - Message m = sub.nextMessage(Duration.ofSeconds(1)); // use duration version here for coverage - if (m != null) { - if (firstSid == null) { - firstSid = sim.SID(); - } - else { - assertEquals(firstSid, sim.SID()); - } - assertEquals(expectedStreamSeq++, m.metaData().streamSequence()); - } - } - sim.latch.await(10, TimeUnit.SECONDS); + pso = PushSubscribeOptions.builder().ordered(true).configuration(cc).build(); + AtomicReference simOrderedRef = setOrderedFactory(js); + sub = js.subscribe(subject, pso); + validate(sub, testHandler, simOrderedRef.get().latch, null); - while (expectedStreamSeq <= 10) { - Message m = sub.nextMessage(Duration.ofSeconds(1)); // use duration version here for coverage - if (m != null) { - assertNotEquals(firstSid, sim.SID()); - assertEquals(expectedStreamSeq++, m.metaData().streamSequence()); - } - } + simOrderedRef = setOrderedFactory(js); + sub = js.subscribe(subject, d, m -> {}, false, pso); + validate(sub, testHandler, simOrderedRef.get().latch, d); }); } + + private static void validate(JetStreamSubscription sub, TestHandler handler, CountDownLatch latch, Dispatcher d) throws InterruptedException { + latch.await(10, TimeUnit.SECONDS); + if (d == null) { + sub.unsubscribe(); + } + else { + d.unsubscribe(sub); + } + assertEquals(0, latch.getCount()); + assertTrue(handler.getHeartbeatAlarms().size() > 0); + handler.reset(); + assertEquals(0, handler.getHeartbeatAlarms().size()); + } + + private static AtomicReference setFactory(JetStream js) { + AtomicReference simRef = new AtomicReference<>(); + ((NatsJetStream)js)._pushStandardMessageManagerFactory = (conn, lJs, stream, so, serverCC, qmode, dispatcher) -> { + HeartbeatErrorSimulator sim = new HeartbeatErrorSimulator(conn, lJs, stream, so, serverCC, qmode, dispatcher); + simRef.set(sim); + return sim; + }; + return simRef; + } + + private static AtomicReference setOrderedFactory(JetStream js) { + AtomicReference simRef = new AtomicReference<>(); + ((NatsJetStream)js)._pushOrderedMessageManagerFactory = (conn, lJs, stream, so, serverCC, qmode, dispatcher) -> { + HeartbeatErrorOrderedSimulator sim = new HeartbeatErrorOrderedSimulator(conn, lJs, stream, so, serverCC, qmode, dispatcher); + simRef.set(sim); + return sim; + }; + return simRef; + } } diff --git a/src/test/java/io/nats/client/impl/JetStreamPullTests.java b/src/test/java/io/nats/client/impl/JetStreamPullTests.java index 7e93135f6..8a92860c9 100644 --- a/src/test/java/io/nats/client/impl/JetStreamPullTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamPullTests.java @@ -15,6 +15,7 @@ import io.nats.client.*; import io.nats.client.api.ConsumerConfiguration; +import io.nats.client.support.PullStatus; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -23,6 +24,8 @@ import java.util.List; import java.util.concurrent.TimeoutException; +import static io.nats.client.api.ConsumerConfiguration.builder; +import static io.nats.client.support.NatsJetStreamConstants.*; import static org.junit.jupiter.api.Assertions.*; public class JetStreamPullTests extends JetStreamTestBase { @@ -669,22 +672,231 @@ public void testPullRequestOptionsBuilder() { assertFalse(pro.isNoWait()); } - @Test - public void testMaxPullRequests() throws Exception { - runInJsServer(true, nc -> { + interface ConflictSetup { + JetStreamSubscription setup(JetStreamManagement jsm, JetStream js) throws Exception; + } + + // int type 1 == error 2 == warning 0 == ignore + private void testConflictStatus(String message, int type, boolean syncMode, ConflictSetup setup) throws Exception { + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { createDefaultTestStream(nc); + JetStreamManagement jsm = nc.jetStreamManagement(); JetStream js = nc.jetStream(); - ((NatsJetStream)js).PULL_MESSAGE_MANAGER_FACTORY = NoopMessageManager::new; + JetStreamSubscription sub = setup.setup(jsm, js); + if (type == 1 && syncMode) { + assertThrows(JetStreamStatusException.class, () -> sub.nextMessage(500)); + } + else { + sub.nextMessage(500); + } + sleep(100); // give enough time for handler to receive message + }); + if (type == 1) { + assertEquals(0, handler.getPullStatusWarnings().size()); + TestHandler.StatusEvent se = handler.getPullStatusErrors().get(0); + assertTrue(se.status.getMessage().startsWith(message)); + } + else if (type == 2) { + assertEquals(0, handler.getPullStatusErrors().size()); + TestHandler.StatusEvent se = handler.getPullStatusWarnings().get(0); + assertTrue(se.status.getMessage().startsWith(message)); + } + else { + assertEquals(0, handler.getPullStatusWarnings().size()); + assertEquals(0, handler.getPullStatusErrors().size()); + } + } - PullSubscribeOptions plso = ConsumerConfiguration.builder().maxPullWaiting(1).buildPullSubscribeOptions(); - JetStreamSubscription sub = js.subscribe(SUBJECT, plso); - js.publish(SUBJECT, new byte[0]); + @Test + public void testExceedsMaxWaiting() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().maxPullWaiting(1).buildPullSubscribeOptions(); + testConflictStatus(EXCEEDED_MAX_WAITING, 2, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); sub.pull(1); sub.pull(1); + return sub; + }); + } + + @Test + public void testExceedsMaxRequestBatch() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().maxBatch(1).buildPullSubscribeOptions(); + testConflictStatus(EXCEEDED_MAX_REQUEST_BATCH, 2, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pull(2); + return sub; + }); + } + + @Test + public void testMessageSizeExceedsMaxBytes() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().buildPullSubscribeOptions(); + testConflictStatus(MESSAGE_SIZE_EXCEEDS_MAX_BYTES, 2, true, (jsm, js) -> { + js.publish(SUBJECT, new byte[1000]); + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pull(PullRequestOptions.builder(1).maxBytes(100).build()); + return sub; + }); + } + + @Test // TODO BOTH CASES + public void testExceedsMaxRequestBytes() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().maxBytes(1).buildPullSubscribeOptions(); + testConflictStatus(EXCEEDED_MAX_REQUEST_MAX_BYTES, 2, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pull(PullRequestOptions.builder(1).maxBytes(2).build()); + return sub; + }); + } + + @Test + public void testExceedsMaxRequestExpires() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().maxExpires(1000).buildPullSubscribeOptions(); + testConflictStatus(EXCEEDED_MAX_REQUEST_EXPIRES, 2, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pullExpiresIn(1, 2000); + return sub; + }); + } + + @Test + public void testConsumerIsPushBased() throws Exception { + PullSubscribeOptions so = PullSubscribeOptions.bind(STREAM, durable(1)); + testConflictStatus(CONSUMER_IS_PUSH_BASED, 1, true, (jsm, js) -> { + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).build()); + JetStreamSubscription sub = js.subscribe(null, so); + jsm.deleteConsumer(STREAM, durable(1)); + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).deliverSubject(deliver(1)).build()); sub.pull(1); - System.out.println(sub.nextMessage(1000)); - System.out.println(sub.nextMessage(1000)); - System.out.println(sub.nextMessage(1000)); + return sub; + }); + } + + @Test + public void testConsumerDeleted() throws Exception { + PullSubscribeOptions so = PullSubscribeOptions.bind(STREAM, durable(1)); + testConflictStatus(CONSUMER_DELETED, 1, true, (jsm, js) -> { + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).build()); + JetStreamSubscription sub = js.subscribe(null, so); + sub.pull(1); + jsm.deleteConsumer(STREAM, durable(1)); + return sub; + }); + } + + @Test + public void testBadRequest() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().buildPullSubscribeOptions(); + testConflictStatus(BAD_REQUEST, 1, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pull(PullRequestOptions.builder(1).noWait().idleHeartbeat(1).build()); + return sub; + }); + } + + @Test + public void testNotFound() throws Exception { + PullSubscribeOptions so = ConsumerConfiguration.builder().buildPullSubscribeOptions(); + testConflictStatus(NO_MESSAGES, 0, true, (jsm, js) -> { + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + sub.pullNoWait(1); + return sub; + }); + } + + @Test + public void testConsumerDeletedNotFound() throws Exception { + PullSubscribeOptions so = PullSubscribeOptions.bind(STREAM, durable(1)); + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + createDefaultTestStream(nc); + JetStreamManagement jsm = nc.jetStreamManagement(); + JetStream js = nc.jetStream(); + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).build()); + + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + + jsm.deleteConsumer(STREAM, durable(1)); + sub.pull(PullRequestOptions.builder(1).noWait().expiresIn(5000).idleHeartbeat(100).build()); +// jsm.deleteConsumer(STREAM, durable(1)); + + System.out.println(sub.nextMessage(3000)); + }); + int x = 0; + } + + @Test + public void testPullStatusMessages() throws Exception { + PullSubscribeOptions so = PullSubscribeOptions.bind(STREAM, durable(1)); + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + createDefaultTestStream(nc); + JetStreamManagement jsm = nc.jetStreamManagement(); + JetStream js = nc.jetStream(); + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).build()); + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + + jsPublish(js, SUBJECT, 5); + sub.pull(PullRequestOptions.builder(10).expiresIn(1000).build()); + PullStatus ps = sub.getPullStatus(); + assertFalse(ps.isTrackingHeartbeats()); + assertEquals(10, ps.getPendingMessages()); + Message m = sub.nextMessage(500); + while (m != null) { + m.ack(); + m = sub.nextMessage(500); + } + ps = sub.getPullStatus(); + assertEquals(5, ps.getPendingMessages()); + sleep(1000); + sub.nextMessage(500); + ps = sub.getPullStatus(); + assertEquals(0, ps.getPendingMessages()); + }); + } + + @Test + public void testPullStatusBytes() throws Exception { + PullSubscribeOptions so = PullSubscribeOptions.bind(STREAM, durable(1)); + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + createDefaultTestStream(nc); + JetStreamManagement jsm = nc.jetStreamManagement(); + JetStream js = nc.jetStream(); + jsm.addOrUpdateConsumer(STREAM, builder().durable(durable(1)).build()); + JetStreamSubscription sub = js.subscribe(SUBJECT, so); + + // subject 7 + reply ~52 + bytes 100 = 159 + // subject 7 + reply ~52 + bytes 100 + headers 21 = 180 + js.publish(SUBJECT, new byte[100]); + js.publish(SUBJECT, new Headers().add("foo", "bar"), new byte[100]); + js.publish(SUBJECT, new byte[700]); + + sub.pull(PullRequestOptions.builder(10).maxBytes(1000).expiresIn(1000).build()); + int pb = 1000; + PullStatus ps = sub.getPullStatus(); + assertFalse(ps.isTrackingHeartbeats()); + assertEquals(10, ps.getPendingMessages()); + assertEquals(pb, ps.getPendingBytes()); + + Message m = sub.nextMessage(500); + pb = pb - 7 - 100 - m.getReplyTo().length(); + ps = sub.getPullStatus(); + assertEquals(9, ps.getPendingMessages()); + assertEquals(pb, ps.getPendingBytes()); + + m = sub.nextMessage(500); + pb = pb - 7 - 100 - 21 - m.getReplyTo().length(); + ps = sub.getPullStatus(); + assertEquals(8, ps.getPendingMessages()); + assertEquals(pb, ps.getPendingBytes()); + + sleep(1000); // let it timeout + sub.nextMessage(500); + ps = sub.getPullStatus(); + assertEquals(0, ps.getPendingMessages()); + assertEquals(0, ps.getPendingBytes()); }); } } diff --git a/src/test/java/io/nats/client/impl/JetStreamPushTests.java b/src/test/java/io/nats/client/impl/JetStreamPushTests.java index 71e0215f6..4470fad0e 100644 --- a/src/test/java/io/nats/client/impl/JetStreamPushTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamPushTests.java @@ -316,6 +316,7 @@ private void assertCantPullOnPushSub(JetStreamSubscription sub) { assertThrows(IllegalStateException.class, () -> sub.fetch(1, Duration.ofSeconds(1))); assertThrows(IllegalStateException.class, () -> sub.iterate(1, 1000)); assertThrows(IllegalStateException.class, () -> sub.iterate(1, Duration.ofSeconds(1))); + assertThrows(IllegalStateException.class, sub::getPullStatus); } @Test diff --git a/src/test/java/io/nats/client/impl/JetStreamTestBase.java b/src/test/java/io/nats/client/impl/JetStreamTestBase.java index fd50d6f4f..e2b4c2588 100644 --- a/src/test/java/io/nats/client/impl/JetStreamTestBase.java +++ b/src/test/java/io/nats/client/impl/JetStreamTestBase.java @@ -74,8 +74,6 @@ public NatsMessage getTestMessage(String replyTo, String sid) { return new IncomingMessageFactory(sid, "subj", replyTo, 0, false).getMessage(); } - static class NoopMessageManager extends MessageManager {} - // ---------------------------------------------------------------------------------------------------- // Management // ---------------------------------------------------------------------------------------------------- @@ -140,6 +138,12 @@ public static void jsPublish(JetStream js, String subject, int startId, int coun } } + public static void jsPublishBytes(JetStream js, String subject, int count, byte[] bytes) throws IOException, JetStreamApiException { + for (int x = 0; x < count; x++) { + js.publish(subject, bytes); + } + } + public static void jsPublish(JetStream js, String subject, int count) throws IOException, JetStreamApiException { jsPublish(js, subject, 1, count); } @@ -153,11 +157,7 @@ public static void jsPublish(Connection nc, String subject, int startId, int cou } public static PublishAck jsPublish(JetStream js, String subject, String data) throws IOException, JetStreamApiException { - Message msg = NatsMessage.builder() - .subject(subject) - .data(data.getBytes(StandardCharsets.US_ASCII)) - .build(); - return js.publish(msg); + return js.publish(NatsMessage.builder().subject(subject).data(data.getBytes(StandardCharsets.US_ASCII)).build()); } public static PublishAck jsPublish(JetStream js) throws IOException, JetStreamApiException { diff --git a/src/test/java/io/nats/client/impl/MessageManagerTests.java b/src/test/java/io/nats/client/impl/MessageManagerTests.java index 54cc47fd8..0ccb39d5c 100644 --- a/src/test/java/io/nats/client/impl/MessageManagerTests.java +++ b/src/test/java/io/nats/client/impl/MessageManagerTests.java @@ -16,13 +16,15 @@ import io.nats.client.*; import io.nats.client.api.ConsumerConfiguration; import io.nats.client.support.IncomingHeadersProcessor; -import io.nats.client.support.Status; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; -import static io.nats.client.support.NatsJetStreamConstants.CONSUMER_STALLED_HDR; +import static io.nats.client.support.NatsJetStreamConstants.*; import static io.nats.client.support.Status.*; import static org.junit.jupiter.api.Assertions.*; @@ -32,116 +34,264 @@ public class MessageManagerTests extends JetStreamTestBase { @Test public void testConstruction() throws Exception { runInJsServer(nc -> { - NatsJetStreamSubscription sub = genericSub(nc); - + NatsJetStreamSubscription sub = genericPushSub(nc); _pushConstruction(nc, true, true, push_hb_fc(), sub); _pushConstruction(nc, true, false, push_hb_xfc(), sub); - _pushConstruction(nc, false, false, push_xhb_xfc(), sub); }); } - private void _pushConstruction(Connection conn, boolean hb, boolean fc, SubscribeOptions so, NatsJetStreamSubscription sub) { - PushMessageManager manager = getManager(conn, so, sub, true, false); - assertTrue(manager.isSyncMode()); - assertFalse(manager.isQueueMode()); - assertEquals(hb, manager.isHb()); - assertEquals(fc, manager.isFc()); + private void tf(Consumer c) { + for (int tf = 0; tf < 2; tf++) { + c.accept(tf == 0); + } + } - manager = getManager(conn, so, sub, true, true); - assertTrue(manager.isSyncMode()); - assertTrue(manager.isQueueMode()); - assertFalse(manager.isHb()); - assertFalse(manager.isFc()); + private void _pushConstruction(Connection conn, boolean hb, boolean fc, SubscribeOptions so, NatsJetStreamSubscription sub) { + tf(ordered -> tf(syncMode -> tf(queueMode -> { + PushMessageManager manager = getPushManager(conn, so, sub, ordered, syncMode, queueMode); + assertEquals(syncMode, manager.isSyncMode()); + assertEquals(queueMode, manager.isQueueMode()); + if (queueMode) { + assertFalse(manager.isHb()); + assertFalse(manager.isFc()); + } + else { + assertEquals(hb, manager.isHb()); + assertEquals(fc, manager.isFc()); + } + }))); } @Test - public void test_status_handle_pushSync() throws Exception { - runInJsServer(nc -> { - NatsJetStreamSubscription sub = genericSub(nc); - _status_handle_pushSync(nc, sub, push_hb_fc()); - _status_handle_pushSync(nc, sub, push_hb_xfc()); - _status_handle_pushSync(nc, sub, push_xhb_xfc()); + public void testPushBeforeQueueProcessorAndManage() throws Exception { + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + NatsJetStreamSubscription sub = genericPushSub(nc); + + PushMessageManager pushMgr = getPushManager(nc, push_hb_fc(), sub, false, true, false); + testPushBqpAndManage(sub, handler, pushMgr); + + pushMgr = getPushManager(nc, push_hb_xfc(), sub, false, true, false); + testPushBqpAndManage(sub, handler, pushMgr); + + pushMgr = getPushManager(nc, push_xhb_xfc(), sub, false, true, false); + testPushBqpAndManage(sub, handler, pushMgr); + + pushMgr = getPushManager(nc, push_hb_fc(), sub, false, false, false); + testPushBqpAndManage(sub, handler, pushMgr); + + pushMgr = getPushManager(nc, push_hb_xfc(), sub, false, false, false); + testPushBqpAndManage(sub, handler, pushMgr); + + pushMgr = getPushManager(nc, push_xhb_xfc(), sub, false, false, false); + testPushBqpAndManage(sub, handler, pushMgr); }); } - private void _status_handle_pushSync(Connection conn, NatsJetStreamSubscription sub, SubscribeOptions so) { - PushMessageManager manager = getManager(conn, so, sub, true, false); + private void testPushBqpAndManage(NatsJetStreamSubscription sub, TestHandler handler, PushMessageManager manager) { + handler.reset(); String sid = sub.getSID(); + + assertTrue(manager.beforeQueueProcessorImpl(getTestJsMessage(1, sid))); assertFalse(manager.manage(getTestJsMessage(1, sid))); - assertTrue(manager.manage(getFlowControl(1, sid))); - assertTrue(manager.manage(getFcHeartbeat(1, sid))); - _status_handle_throws(sub, manager, get404(sid)); - _status_handle_throws(sub, manager, get408(sid)); - _status_handle_throws(sub, manager, getUnkStatus(sid)); + + assertEquals(!manager.hb, manager.beforeQueueProcessorImpl(getHeartbeat(sid))); + + List unhandledCodes = new ArrayList<>(); + assertTrue(manager.beforeQueueProcessorImpl(getFlowControl(1, sid))); + assertTrue(manager.beforeQueueProcessorImpl(getFcHeartbeat(1, sid))); + if (manager.fc) { + assertTrue(manager.manage(getFlowControl(1, sid))); + assertTrue(manager.manage(getFcHeartbeat(1, sid))); + } + else { + if (manager.syncMode) { + assertThrows(JetStreamStatusException.class, () -> manager.manage(getFlowControl(1, sid))); + assertThrows(JetStreamStatusException.class, () -> manager.manage(getFcHeartbeat(1, sid))); + } + else { + manager.manage(getFlowControl(1, sid)); + manager.manage(getFcHeartbeat(1, sid)); + } + unhandledCodes.add(FLOW_OR_HEARTBEAT_STATUS_CODE); // fc + unhandledCodes.add(FLOW_OR_HEARTBEAT_STATUS_CODE); // hb + } + + assertTrue(manager.beforeQueueProcessorImpl(getUnkownStatus(sid))); + if (manager.syncMode) { + assertThrows(JetStreamStatusException.class, () -> manager.manage(getUnkownStatus(sid))); + } + else { + manager.manage(getUnkownStatus(sid)); + } + unhandledCodes.add(999); + + sleep(100); + List list = handler.getUnhandledStatuses(); + assertEquals(unhandledCodes.size(), list.size()); + for (int x = 0; x < list.size(); x++) { + TestHandler.StatusEvent se = list.get(x); + assertSame(sub.getSID(), se.sid); + assertEquals(unhandledCodes.get(x), se.status.getCode()); + } } @Test - public void test_status_handle_pull() throws Exception { - runInJsServer(nc -> { - NatsJetStreamSubscription sub = genericSub(nc); - PullMessageManager manager = new PullMessageManager(); - manager.startup(sub); - String sid = sub.getSID(); - assertFalse(manager.manage(getTestJsMessage(1, sid))); - assertTrue(manager.manage(get404(sid))); - assertTrue(manager.manage(get408(sid))); - _status_handle_throws(sub, manager, getFlowControl(1, sid)); - _status_handle_throws(sub, manager, getFcHeartbeat(1, sid)); - _status_handle_throws(sub, manager, getUnkStatus(sid)); + public void testPullBeforeQueueProcessorAndManage() throws Exception { + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + NatsJetStreamSubscription sub = genericPullSub(nc); + + PullMessageManager pullMgr = getPullManager(nc, sub, true); + pullMgr.startPullRequest(PullRequestOptions.builder(1).build()); + testPullBqpAndManage(sub, handler, pullMgr); + + pullMgr = getPullManager(nc, sub, true); + pullMgr.startPullRequest(PullRequestOptions.builder(1).idleHeartbeat(100).build()); + testPullBqpAndManage(sub, handler, pullMgr); }); } - private void _status_handle_throws(NatsJetStreamSubscription sub, MessageManager asm, Message m) { - JetStreamStatusException jsse = assertThrows(JetStreamStatusException.class, () -> asm.manage(m)); - assertSame(sub, jsse.getSubscription()); - assertSame(m.getStatus(), jsse.getStatus()); + @Test + public void testPushManagerHeartbeats() throws Exception { + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + PushMessageManager pushMgr = getPushManager(nc, push_xhb_xfc(), null, false, true, false); + NatsJetStreamSubscription sub = mockSub((NatsConnection)nc, pushMgr); + pushMgr.startup(sub); + List list = handler.getHeartbeatAlarms(); + assertEquals(0, list.size()); + + pushMgr = getPushManager(nc, push_xhb_xfc(), null, false, false, false); + sub = mockSub((NatsConnection)nc, pushMgr); + pushMgr.startup(sub); + list = handler.getHeartbeatAlarms(); + assertEquals(0, list.size()); + + PushSubscribeOptions pso = ConsumerConfiguration.builder().idleHeartbeat(100).buildPushSubscribeOptions(); + pushMgr = getPushManager(nc, pso, null, false, true, false); + sub = mockSub((NatsConnection)nc, pushMgr); + pushMgr.startup(sub); + // give time for heartbeats to be missed + sleep(400); + list = handler.getHeartbeatAlarms(); + assertTrue(list.size() > 0); + + pushMgr = getPushManager(nc, pso, null, false, false, false); + sub = mockSub((NatsConnection)nc, pushMgr); + pushMgr.startup(sub); + // give time for heartbeats to be missed + sleep(400); + list = handler.getHeartbeatAlarms(); + assertTrue(list.size() > 0); + }); } @Test - public void test_status_handle_pushAsync() throws Exception { - MmtEl el = new MmtEl(); - runInJsServer(optsWithEl(el), nc -> { - NatsJetStreamSubscription sub = genericSub(nc); - _status_handle_pushAsync(el, nc, sub, push_hb_fc()); - _status_handle_pushAsync(el, nc, sub, push_hb_xfc()); - _status_handle_pushAsync(el, nc, sub, push_xhb_xfc()); + public void testPullManagerHeartbeats() throws Exception { + TestHandler handler = new TestHandler(); + runInJsServer(handler, nc -> { + PullMessageManager pullMgr = getPullManager(nc, null, true); + NatsJetStreamSubscription sub = mockSub((NatsConnection)nc, pullMgr); + pullMgr.startup(sub); + pullMgr.startPullRequest(PullRequestOptions.builder(1).build()); + assertEquals(0, handler.getHeartbeatAlarms().size()); + assertNull(pullMgr.heartbeatTimer); + + handler.reset(); + pullMgr.startPullRequest(PullRequestOptions.builder(1).idleHeartbeat(100).build()); + sleep(400); // give time for heartbeats to be missed + assertTrue(handler.getHeartbeatAlarms().size() > 0); + assertNotNull(pullMgr.heartbeatTimer); + + handler.reset(); + pullMgr.startPullRequest(PullRequestOptions.builder(1).idleHeartbeat(100).build()); + sleep(400); // give time for heartbeats to be missed + assertTrue(handler.getHeartbeatAlarms().size() > 0); + assertNotNull(pullMgr.heartbeatTimer); + + handler.reset(); + pullMgr.startPullRequest(PullRequestOptions.builder(1).build()); + sleep(400); // give time for heartbeats to be missed + assertEquals(0, handler.getHeartbeatAlarms().size()); + assertNull(pullMgr.heartbeatTimer); }); } - private void _status_handle_pushAsync(MmtEl el, Connection conn, NatsJetStreamSubscription sub, SubscribeOptions so) { - PushMessageManager manager = getManager(conn, so, sub, false, false); - el.reset(sub); + private void testPullBqpAndManage(NatsJetStreamSubscription sub, TestHandler handler, PullMessageManager manager) { + handler.reset(); String sid = sub.getSID(); + + assertTrue(manager.beforeQueueProcessorImpl(getTestJsMessage(1, sid))); assertFalse(manager.manage(getTestJsMessage(1, sid))); - assertTrue(manager.manage(getFlowControl(1, sid))); - assertTrue(manager.manage(getFcHeartbeat(1, sid))); - Message m = get404(sid); - assertTrue(manager.manage(m)); - sleep(100); // the error listener is called async, need to give it time to be called. - assertSame(sub, el.sub); - assertSame(m.getStatus(), el.status); + assertEquals(!manager.hb, manager.beforeQueueProcessorImpl(getHeartbeat(sid))); + assertEquals(!manager.hb, manager.beforeQueueProcessorImpl(getHeartbeat(sid))); + + // ignores + assertTrue(manager.beforeQueueProcessorImpl(getNotFoundStatus(sid))); + assertTrue(manager.beforeQueueProcessorImpl(getRequestTimeoutStatus(sid))); + manager.manage(getNotFoundStatus(sid)); + manager.manage(getRequestTimeoutStatus(sid)); + + // warnings + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, MESSAGE_SIZE_EXCEEDS_MAX_BYTES))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, EXCEEDED_MAX_WAITING))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_BATCH))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_EXPIRES))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_MAX_BYTES))); + + manager.manage(getConflictStatus(sid, MESSAGE_SIZE_EXCEEDS_MAX_BYTES)); + manager.manage(getConflictStatus(sid, EXCEEDED_MAX_WAITING)); + manager.manage(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_BATCH)); + manager.manage(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_EXPIRES)); + manager.manage(getConflictStatus(sid, EXCEEDED_MAX_REQUEST_MAX_BYTES)); + + // errors + assertTrue(manager.beforeQueueProcessorImpl(getBadRequest(sid))); + assertTrue(manager.beforeQueueProcessorImpl(getUnkownStatus(sid))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, CONSUMER_DELETED))); + assertTrue(manager.beforeQueueProcessorImpl(getConflictStatus(sid, CONSUMER_IS_PUSH_BASED))); + + if (manager.syncMode) { + assertThrows(JetStreamStatusException.class, () -> manager.manage(getBadRequest(sid))); + assertThrows(JetStreamStatusException.class, () -> manager.manage(getUnkownStatus(sid))); + assertThrows(JetStreamStatusException.class, () -> manager.manage(getConflictStatus(sid, CONSUMER_DELETED))); + assertThrows(JetStreamStatusException.class, () -> manager.manage(getConflictStatus(sid, CONSUMER_IS_PUSH_BASED))); + } + else { + manager.manage(getBadRequest(sid)); + manager.manage(getUnkownStatus(sid)); + manager.manage(getConflictStatus(sid, CONSUMER_DELETED)); + manager.manage(getConflictStatus(sid, CONSUMER_IS_PUSH_BASED)); + } + + sleep(100); - m = get408(sid); - assertTrue(manager.manage(m)); - sleep(100); // the error listener is called async, need to give it time to be called. - assertSame(sub, el.sub); - assertSame(m.getStatus(), el.status); + List list = handler.getPullStatusWarnings(); + assertEquals(5, list.size()); + for (int x = 0; x < list.size(); x++) { + TestHandler.StatusEvent se = list.get(x); + assertSame(sub.getSID(), se.sid); + assertEquals(CONFLICT_CODE, se.status.getCode()); + } - m = getUnkStatus(sid); - assertTrue(manager.manage(m)); - sleep(100); // the error listener is called async, need to give it time to be called. - assertSame(sub, el.sub); - assertSame(m.getStatus(), el.status); + list = handler.getPullStatusErrors(); + assertEquals(4, list.size()); + int[] codes = new int[]{BAD_REQUEST_CODE, 999, CONFLICT_CODE, CONFLICT_CODE}; + for (int x = 0; x < list.size(); x++) { + TestHandler.StatusEvent se = list.get(x); + assertSame(sub.getSID(), se.sid); + assertEquals(codes[x], se.status.getCode()); + } } @Test public void test_push_fc() { SubscribeOptions so = push_hb_fc(); MockPublishInternal mpi = new MockPublishInternal(); - NatsDispatcher natsDispatcher = new NatsDispatcher(null, null); - PushMessageManager pmm = new PushMessageManager(mpi, null, null, so, so.getConsumerConfiguration(), false, natsDispatcher); + PushMessageManager pmm = new PushMessageManager(mpi, null, null, so, so.getConsumerConfiguration(), false, true); NatsJetStreamSubscription sub = mockSub(mpi, pmm); String sid = sub.getSID(); pmm.startup(sub); @@ -172,7 +322,7 @@ public void test_push_fc() { assertEquals(getFcSubject(3), mpi.fcSubject); assertEquals(3, mpi.pubCount); - pmm.manage(getHeartbeat(sid)); + assertThrows(JetStreamStatusException.class, () -> pmm.manage(getHeartbeat(sid))); assertEquals(getFcSubject(3), pmm.getLastFcSubject()); assertEquals(getFcSubject(3), mpi.fcSubject); assertEquals(3, mpi.pubCount); @@ -180,17 +330,11 @@ public void test_push_fc() { // coverage sequences pmm.manage(getTestJsMessage(1, sid)); assertEquals(1, pmm.getLastStreamSequence()); - assertEquals(1, pmm.getLastConsumerSequence()); + assertEquals(1, pmm.getInternalConsumerSequence()); pmm.manage(getTestJsMessage(2, sid)); assertEquals(2, pmm.getLastStreamSequence()); - assertEquals(2, pmm.getLastConsumerSequence()); - - // coverage beforeQueueProcessor - assertNotNull(pmm.beforeQueueProcessor(getTestJsMessage(3, sid))); - assertNotNull(pmm.beforeQueueProcessor(get408(sid))); - assertNotNull(pmm.beforeQueueProcessor(getFcHeartbeat(9, sid))); - assertNull(pmm.beforeQueueProcessor(getHeartbeat(sid))); + assertEquals(2, pmm.getInternalConsumerSequence()); // coverage extractFcSubject assertNull(pmm.extractFcSubject(getTestJsMessage(4, sid))); @@ -206,18 +350,18 @@ public void test_push_xfc() { private void _push_xfc(SubscribeOptions so) { MockPublishInternal mpi = new MockPublishInternal(); - PushMessageManager pmm = new PushMessageManager(mpi, null, null, so, so.getConsumerConfiguration(), false, new NatsDispatcher(null, null)); + PushMessageManager pmm = new PushMessageManager(mpi, null, null, so, so.getConsumerConfiguration(), false, true); NatsJetStreamSubscription sub = mockSub(mpi, pmm); String sid = sub.getSID(); pmm.startup(sub); assertNull(pmm.getLastFcSubject()); - pmm.manage(getFlowControl(1, sid)); + assertThrows(JetStreamStatusException.class, () -> pmm.manage(getFlowControl(1, sid))); assertNull(pmm.getLastFcSubject()); assertNull(mpi.fcSubject); assertEquals(0, mpi.pubCount); - pmm.manage(getHeartbeat(sid)); + assertThrows(JetStreamStatusException.class, () -> pmm.manage(getHeartbeat(sid))); assertNull(pmm.getLastFcSubject()); assertNull(mpi.fcSubject); assertEquals(0, mpi.pubCount); @@ -225,17 +369,28 @@ private void _push_xfc(SubscribeOptions so) { // coverage sequences pmm.manage(getTestJsMessage(1, sid)); assertEquals(1, pmm.getLastStreamSequence()); - assertEquals(1, pmm.getLastConsumerSequence()); + assertEquals(1, pmm.getInternalConsumerSequence()); pmm.manage(getTestJsMessage(2, sid)); assertEquals(2, pmm.getLastStreamSequence()); - assertEquals(2, pmm.getLastConsumerSequence()); + assertEquals(2, pmm.getInternalConsumerSequence()); + + // coverage beforeQueueProcessor + assertTrue(pmm.beforeQueueProcessorImpl(getFlowControl(1, sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getUnkownStatus(sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getFcHeartbeat(1, sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getTestJsMessage(1, sid))); + + // coverage manager + assertFalse(pmm.manage(getTestJsMessage(1, sid))); + assertThrows(JetStreamStatusException.class, () -> pmm.manage(getFlowControl(1, sid))); + assertThrows(JetStreamStatusException.class, () -> pmm.manage(getFcHeartbeat(1, sid))); // coverage beforeQueueProcessor - assertNotNull(pmm.beforeQueueProcessor(getTestJsMessage(3, sid))); - assertNotNull(pmm.beforeQueueProcessor(get408(sid))); - assertNotNull(pmm.beforeQueueProcessor(getFcHeartbeat(9, sid))); - assertNull(pmm.beforeQueueProcessor(getHeartbeat(sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getTestJsMessage(3, sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getRequestTimeoutStatus(sid))); + assertTrue(pmm.beforeQueueProcessorImpl(getFcHeartbeat(9, sid))); + assertEquals(!pmm.hb, pmm.beforeQueueProcessorImpl(getHeartbeat(sid))); // coverage extractFcSubject assertNull(pmm.extractFcSubject(getTestJsMessage())); @@ -289,31 +444,31 @@ private void _received_time_no(JetStream js, JetStreamManagement jsm, JetStreamS @Test public void test_hb_yes_settings() throws Exception { runInJsServer(nc -> { - NatsJetStreamSubscription sub = genericSub(nc); + NatsJetStreamSubscription sub = genericPushSub(nc); ConsumerConfiguration cc = ConsumerConfiguration.builder().idleHeartbeat(1000).build(); // MessageAlarmTime default PushSubscribeOptions so = new PushSubscribeOptions.Builder().configuration(cc).build(); - PushMessageManager manager = getManager(nc, so, sub); + PushMessageManager manager = getPushManager(nc, so, sub, false); assertEquals(1000, manager.getIdleHeartbeatSetting()); assertEquals(3000, manager.getAlarmPeriodSetting()); // MessageAlarmTime < idleHeartbeat so = new PushSubscribeOptions.Builder().configuration(cc).messageAlarmTime(999).build(); - manager = getManager(nc, so, sub); + manager = getPushManager(nc, so, sub, false); assertEquals(1000, manager.getIdleHeartbeatSetting()); assertEquals(3000, manager.getAlarmPeriodSetting()); // MessageAlarmTime == idleHeartbeat so = new PushSubscribeOptions.Builder().configuration(cc).messageAlarmTime(1000).build(); - manager = getManager(nc, so, sub); + manager = getPushManager(nc, so, sub, false); assertEquals(1000, manager.getIdleHeartbeatSetting()); assertEquals(1000, manager.getAlarmPeriodSetting()); // MessageAlarmTime > idleHeartbeat so = new PushSubscribeOptions.Builder().configuration(cc).messageAlarmTime(2000).build(); - manager = getManager(nc, so, sub); + manager = getPushManager(nc, so, sub, false); assertEquals(1000, manager.getIdleHeartbeatSetting()); assertEquals(2000, manager.getAlarmPeriodSetting()); }); @@ -322,9 +477,9 @@ public void test_hb_yes_settings() throws Exception { @Test public void test_hb_no_settings() throws Exception { runInJsServer(nc -> { - NatsJetStreamSubscription sub = genericSub(nc); + NatsJetStreamSubscription sub = genericPushSub(nc); SubscribeOptions so = push_xhb_xfc(); - PushMessageManager manager = getManager(nc, so, sub); + PushMessageManager manager = getPushManager(nc, so, sub, false); assertEquals(0, manager.getIdleHeartbeatSetting()); assertEquals(0, manager.getAlarmPeriodSetting()); }); @@ -354,15 +509,30 @@ private PushSubscribeOptions push_xhb_xfc() { return new PushSubscribeOptions.Builder().configuration(cc_xfc_xhb()).build(); } - private PushMessageManager getManager(Connection conn, SubscribeOptions so, NatsJetStreamSubscription sub) { - return getManager(conn, so, sub, true, false); + private PushMessageManager getPushManager(Connection conn, SubscribeOptions so, NatsJetStreamSubscription sub, boolean ordered) { + return getPushManager(conn, so, sub, ordered, true, false); + } + + private PushMessageManager getPushManager(Connection conn, SubscribeOptions so, NatsJetStreamSubscription sub, boolean ordered, boolean syncMode, boolean queueMode) { + PushMessageManager manager; + if (ordered) { + manager = new OrderedMessageManager((NatsConnection) conn, null, null, so, so.getConsumerConfiguration(), queueMode, syncMode); + } + else { + manager = new PushMessageManager((NatsConnection) conn, null, null, so, so.getConsumerConfiguration(), queueMode, syncMode); + } + if (sub != null) { + manager.startup(sub); + } + return manager; } - private PushMessageManager getManager(Connection conn, SubscribeOptions so, NatsJetStreamSubscription sub, boolean syncMode, boolean queueMode) { - NatsDispatcher natsDispatcher = syncMode ? null : new NatsDispatcher(null, null); - PushMessageManager asm = new PushMessageManager((NatsConnection)conn, null, null, so, so.getConsumerConfiguration(), queueMode, natsDispatcher); - asm.startup(sub); - return asm; + private PullMessageManager getPullManager(Connection conn, NatsJetStreamSubscription sub, boolean syncMode) { + PullMessageManager manager = new PullMessageManager((NatsConnection) conn, syncMode); + if (sub != null) { + manager.startup(sub); + } + return manager; } private NatsMessage getFlowControl(int replyToId, String sid) { @@ -383,22 +553,30 @@ private NatsMessage getFcHeartbeat(int replyToId, String sid) { } private NatsMessage getHeartbeat(String sid) { - IncomingMessageFactory imf = new IncomingMessageFactory(mockSid(), "subj", null, 0, false); + IncomingMessageFactory imf = new IncomingMessageFactory(sid, "subj", null, 0, false); String s = "NATS/1.0 " + FLOW_OR_HEARTBEAT_STATUS_CODE + " " + HEARTBEAT_TEXT + "\r\n"; imf.setHeaders(new IncomingHeadersProcessor(s.getBytes())); return imf.getMessage(); } - private NatsMessage get404(String sid) { - return getStatus(404, "not found", sid); + private NatsMessage getBadRequest(String sid) { + return getStatus(BAD_REQUEST_CODE, BAD_REQUEST, sid); + } + + private NatsMessage getNotFoundStatus(String sid) { + return getStatus(NOT_FOUND_CODE, "not found", sid); } - private NatsMessage get408(String sid) { - return getStatus(408, "expired", sid); + private NatsMessage getRequestTimeoutStatus(String sid) { + return getStatus(REQUEST_TIMEOUT_CODE, "expired", sid); } - private NatsMessage getUnkStatus(String sid) { - return getStatus(999, "blah blah", sid); + private NatsMessage getConflictStatus(String sid, String message) { + return getStatus(CONFLICT_CODE, message, sid); + } + + private NatsMessage getUnkownStatus(String sid) { + return getStatus(999, "unknown", sid); } private NatsMessage getStatus(int code, String message, String sid) { @@ -407,43 +585,12 @@ private NatsMessage getStatus(int code, String message, String sid) { return imf.getMessage(); } - static class MmtEl implements ErrorListener { - JetStreamSubscription sub; - long lastStreamSequence = -1; - long lastConsumerSequence = -1; - long expectedConsumerSeq = -1; - long receivedConsumerSeq = -1; - Status status; - - public void reset(JetStreamSubscription sub) { - this.sub = sub; - expectedConsumerSeq = -1; - receivedConsumerSeq = -1; - status = null; - } - - @Override - public void errorOccurred(Connection conn, String error) {} - - @Override - public void exceptionOccurred(Connection conn, Exception exp) {} - - @Override - public void slowConsumerDetected(Connection conn, Consumer consumer) {} - - @Override - public void unhandledStatus(Connection conn, JetStreamSubscription sub, Status status) { - this.sub = sub; - this.status = status; - } - } - static class MockPublishInternal extends NatsConnection { int pubCount; String fcSubject; public MockPublishInternal() { - this(new Options.Builder().build()); + this(new Options.Builder().errorListener(new ErrorListener() {}).build()); } public MockPublishInternal(Options options) { @@ -458,13 +605,24 @@ void publishInternal(String subject, String replyTo, Headers headers, byte[] dat } static AtomicInteger ID = new AtomicInteger(); - private static NatsJetStreamSubscription genericSub(Connection nc) throws IOException, JetStreamApiException { + private static NatsJetStreamSubscription genericPushSub(Connection nc) throws IOException, JetStreamApiException { + String subject = genericSub(nc); + JetStream js = nc.jetStream(); + return (NatsJetStreamSubscription) js.subscribe(subject); + } + + private static NatsJetStreamSubscription genericPullSub(Connection nc) throws IOException, JetStreamApiException { + String subject = genericSub(nc); + JetStream js = nc.jetStream(); + return (NatsJetStreamSubscription) js.subscribe(subject, PullSubscribeOptions.DEFAULT_PULL_OPTS); + } + + private static String genericSub(Connection nc) throws IOException, JetStreamApiException { String id = "-" + ID.incrementAndGet() + "-" + System.currentTimeMillis(); String stream = STREAM + id; String subject = STREAM + id; createMemoryStream(nc, stream, subject); - JetStream js = nc.jetStream(); - return (NatsJetStreamSubscription) js.subscribe(subject); + return subject; } private static NatsJetStreamSubscription mockSub(NatsConnection connection, MessageManager manager) { @@ -475,6 +633,23 @@ private static NatsJetStreamSubscription mockSub(NatsConnection connection, Mess } static class TestMessageManager extends MessageManager { + public TestMessageManager() { + super(null, true); + } + + @Override + protected boolean manage(Message msg) { + return false; + } + + @Override + protected void startup(NatsJetStreamSubscription sub) { + this.sub = sub; + } + + @Override + protected void shutdown() {} + NatsJetStreamSubscription getSub() { return sub; } } @@ -484,8 +659,6 @@ public void testMessageManagerInterfaceDefaultImplCoverage() { NatsJetStreamSubscription sub = new NatsJetStreamSubscription(mockSid(), "sub", null, null, null, null, "stream", "con", tmm); tmm.startup(sub); - assertFalse(tmm.manage(null)); assertSame(sub, tmm.getSub()); - tmm.shutdown(); } } diff --git a/src/test/java/io/nats/client/impl/TestHandler.java b/src/test/java/io/nats/client/impl/TestHandler.java index 9d6ccbe1a..32976ab1f 100644 --- a/src/test/java/io/nats/client/impl/TestHandler.java +++ b/src/test/java/io/nats/client/impl/TestHandler.java @@ -14,6 +14,7 @@ package io.nats.client.impl; import io.nats.client.*; +import io.nats.client.support.Status; import java.util.ArrayList; import java.util.HashMap; @@ -38,8 +39,13 @@ public class TestHandler implements ErrorListener, ConnectionListener { private String errorToWaitFor; private Connection connection; - private final ArrayList slowConsumers = new ArrayList<>(); - private final ArrayList discardedMessages = new ArrayList<>(); + private final List slowConsumers = new ArrayList<>(); + private final List discardedMessages = new ArrayList<>(); + private final List unhandledStatuses = new ArrayList<>(); + private final List pullStatusWarnings = new ArrayList<>(); + private final List pullStatusErrors = new ArrayList<>(); + private final List heartbeatAlarms = new ArrayList<>(); + private final List flowControlProcesseds = new ArrayList<>(); private final boolean printExceptions; private final boolean verbose; @@ -53,6 +59,25 @@ public TestHandler(boolean printExceptions, boolean verbose) { this.verbose = verbose; } + public void reset() { + count.set(0); + eventCounts.clear(); + errorCounts.clear(); + exceptionCount.set(0); + statusChanged = null; + slowSubscriber = null; + errorWaitFuture = null; + eventToWaitFor = null; + errorToWaitFor = null; + slowConsumers.clear(); + discardedMessages.clear(); + unhandledStatuses.clear(); + pullStatusWarnings.clear(); + pullStatusErrors.clear(); + heartbeatAlarms.clear(); + flowControlProcesseds.clear(); + } + public void prepForStatusChange(Events waitFor) { lock.lock(); try { @@ -220,6 +245,26 @@ public List getDiscardedMessages() { return discardedMessages; } + public List getUnhandledStatuses() { + return unhandledStatuses; + } + + public List getPullStatusWarnings() { + return pullStatusWarnings; + } + + public List getPullStatusErrors() { + return pullStatusErrors; + } + + public List getHeartbeatAlarms() { + return heartbeatAlarms; + } + + public List getFlowControlProcessedEvents() { + return flowControlProcesseds; + } + public int getCount() { return count.get(); } @@ -272,4 +317,93 @@ public void dumpErrorCountsToStdOut() { public Connection getConnection() { return connection; } + + @Override + public void unhandledStatus(Connection conn, JetStreamSubscription sub, Status status) { + unhandledStatuses.add(new StatusEvent(sub, status)); + } + + @Override + public void pullStatusWarning(Connection conn, JetStreamSubscription sub, Status status) { + pullStatusWarnings.add(new StatusEvent(sub, status)); + } + + @Override + public void pullStatusError(Connection conn, JetStreamSubscription sub, Status status) { + pullStatusErrors.add(new StatusEvent(sub, status)); + } + + @Override + public void heartbeatAlarm(Connection conn, JetStreamSubscription sub, long lastStreamSequence, long lastConsumerSequence) { + heartbeatAlarms.add(new HeartbeatAlarmEvent(sub, lastStreamSequence, lastConsumerSequence)); + } + + @Override + public void flowControlProcessed(Connection conn, JetStreamSubscription sub, String subject, FlowControlSource source) { + ErrorListener.super.flowControlProcessed(conn, sub, subject, source); + } + + public static class StatusEvent { + String sid; + Status status; + + public StatusEvent(JetStreamSubscription sub, Status status) { + this.sid = extractSid(sub); + this.status = status; + } + + @Override + public String toString() { + return "StatusEvent{" + + "sid='" + sid + '\'' + + ", status=" + status + + '}'; + } + } + + public static class HeartbeatAlarmEvent { + String sid; + long lastStreamSequence; + long lastConsumerSequence; + + public HeartbeatAlarmEvent(JetStreamSubscription sub, long lastStreamSequence, long lastConsumerSequence) { + this.sid = extractSid(sub); + this.lastStreamSequence = lastStreamSequence; + this.lastConsumerSequence = lastConsumerSequence; + } + + @Override + public String toString() { + return "HeartbeatAlarmEvent{" + + "sid='" + sid + '\'' + + ", lastStreamSequence=" + lastStreamSequence + + ", lastConsumerSequence=" + lastConsumerSequence + + '}'; + } + } + + public static class FlowControlProcessedEvent { + String sid; + String subject; + FlowControlSource source; + + public FlowControlProcessedEvent(JetStreamSubscription sub, String subject, FlowControlSource source) { + this.sid = extractSid(sub); + this.subject = subject; + this.source = source; + } + + @Override + public String toString() { + return "FlowControlProcessedEvent{" + + "sid='" + sid + '\'' + + ", subject='" + subject + '\'' + + ", source=" + source + + '}'; + } + } + + private static String extractSid(JetStreamSubscription sub) { + return ((NatsJetStreamSubscription)sub).getSID(); + } } \ No newline at end of file diff --git a/src/test/java/io/nats/client/utils/TestBase.java b/src/test/java/io/nats/client/utils/TestBase.java index 61d08a0bf..7da83ba61 100644 --- a/src/test/java/io/nats/client/utils/TestBase.java +++ b/src/test/java/io/nats/client/utils/TestBase.java @@ -21,6 +21,7 @@ import org.opentest4j.AssertionFailedError; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; @@ -88,6 +89,11 @@ public static void runInJsServer(InServerTest inServerTest) throws Exception { runInServer(false, true, inServerTest); } + public static void runInJsServer(ErrorListener el, InServerTest inServerTest) throws Exception { + Options.Builder builder = new Options.Builder().errorListener(el); + runInServer(false, true, builder, inServerTest); + } + public static void runInJsServer(Options.Builder builder, InServerTest inServerTest) throws Exception { runInServer(false, true, builder, inServerTest); } @@ -374,6 +380,21 @@ public static void debugPrintln(Object... debug) { System.out.println(sb.toString()); } + + static class DummyOut extends OutputStream { + @Override + public void write(byte[] b) throws IOException { + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + } + + @Override + public void write(int b) throws IOException { + } + } + // ---------------------------------------------------------------------------------------------------- // flush // ----------------------------------------------------------------------------------------------------