From 2a425f9a5b5ca33098a9c46e4f66ada9bc61507f Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Mon, 16 Sep 2024 15:38:41 +0200 Subject: [PATCH 01/17] Add Direct Get by start time Signed-off-by: Maurice van Veen --- .../io/nats/client/JetStreamManagement.java | 11 +++++++++ .../io/nats/client/api/MessageGetRequest.java | 23 +++++++++++++------ .../client/impl/NatsJetStreamManagement.java | 8 +++++++ .../io/nats/client/support/ApiConstants.java | 1 + .../client/impl/JetStreamManagementTests.java | 2 ++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index c1058da4d..b51ffa0ad 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -282,6 +282,17 @@ public interface JetStreamManagement { */ MessageInfo getFirstMessage(String streamName, String subject) throws IOException, JetStreamApiException; + /** + * Get MessageInfo for the first message created at or after the start time. + * @param streamName the name of the stream. + * @param startTime the start time to get the first message for. + * @return The MessageInfo + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) throws IOException, JetStreamApiException; + /** * Get MessageInfo for the message of the message sequence * is equal to or greater the requested sequence for the subject. diff --git a/src/main/java/io/nats/client/api/MessageGetRequest.java b/src/main/java/io/nats/client/api/MessageGetRequest.java index 60d9d6851..262f45d22 100644 --- a/src/main/java/io/nats/client/api/MessageGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageGetRequest.java @@ -15,6 +15,8 @@ import io.nats.client.support.JsonSerializable; +import java.time.ZonedDateTime; + import static io.nats.client.support.ApiConstants.*; import static io.nats.client.support.JsonUtils.*; @@ -25,21 +27,26 @@ public class MessageGetRequest implements JsonSerializable { private final long sequence; private final String lastBySubject; private final String nextBySubject; + private final ZonedDateTime startTime; public static MessageGetRequest forSequence(long sequence) { - return new MessageGetRequest(sequence, null, null); + return new MessageGetRequest(sequence, null, null, null); } public static MessageGetRequest lastForSubject(String subject) { - return new MessageGetRequest(-1, subject, null); + return new MessageGetRequest(-1, subject, null, null); } public static MessageGetRequest firstForSubject(String subject) { - return new MessageGetRequest(-1, null, subject); + return new MessageGetRequest(-1, null, subject, null); + } + + public static MessageGetRequest firstForStartTime(ZonedDateTime startTime) { + return new MessageGetRequest(-1, null, null, startTime); } public static MessageGetRequest nextForSubject(long sequence, String subject) { - return new MessageGetRequest(sequence, null, subject); + return new MessageGetRequest(sequence, null, subject, null); } /** @@ -69,7 +76,7 @@ public static byte[] lastBySubjectBytes(String subject) { */ @Deprecated public MessageGetRequest(long sequence) { - this(sequence, null, null); + this(sequence, null, null, null); } /** @@ -79,13 +86,14 @@ public MessageGetRequest(long sequence) { */ @Deprecated public MessageGetRequest(String lastBySubject) { - this(-1, lastBySubject, null); + this(-1, lastBySubject, null, null); } - private MessageGetRequest(long sequence, String lastBySubject, String nextBySubject) { + private MessageGetRequest(long sequence, String lastBySubject, String nextBySubject, ZonedDateTime startTime) { this.sequence = sequence; this.lastBySubject = lastBySubject; this.nextBySubject = nextBySubject; + this.startTime = startTime; } public long getSequence() { @@ -118,6 +126,7 @@ public String toJson() { addField(sb, SEQ, sequence); addField(sb, LAST_BY_SUBJECT, lastBySubject); addField(sb, NEXT_BY_SUBJECT, nextBySubject); + addField(sb, START_TIME, startTime); return endJson(sb).toString(); } } diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index 0cd15fb86..17d772b25 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -289,6 +289,14 @@ public MessageInfo getFirstMessage(String streamName, String subject) throws IOE return _getMessage(streamName, MessageGetRequest.firstForSubject(subject)); } + /** + * {@inheritDoc} + */ + @Override + public MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) throws IOException, JetStreamApiException { + return _getMessage(streamName, MessageGetRequest.firstForStartTime(startTime)); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/io/nats/client/support/ApiConstants.java b/src/main/java/io/nats/client/support/ApiConstants.java index 3d6025c1f..16ff26f17 100644 --- a/src/main/java/io/nats/client/support/ApiConstants.java +++ b/src/main/java/io/nats/client/support/ApiConstants.java @@ -176,6 +176,7 @@ public interface ApiConstants { String SOURCES = "sources"; String SRC = "src"; String STARTED = "started"; + String START_TIME = "start_time"; String STATE = "state"; String STATS = "stats"; String STORAGE = "storage"; diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index a8413e12b..bc1deffa6 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -1293,6 +1293,7 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, 0, tsc.subject(1)), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getFirstMessage(tsc.stream, tsc.subject(1)), beforeCreated); + assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, beforeCreated), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getNextMessage(tsc.stream, 1, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, 1, tsc.subject(1)), beforeCreated); @@ -1305,6 +1306,7 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, -1))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 0))); + assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, DEFAULT_TIME))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 9))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getLastMessage(tsc.stream, "not-a-subject"))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, "not-a-subject"))); From 368921d4300df9c76a104320bd16d1c788b35c30 Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Tue, 17 Sep 2024 17:30:29 +0200 Subject: [PATCH 02/17] Add MessageGetRequest Builder Signed-off-by: Maurice van Veen --- .../io/nats/client/api/MessageGetRequest.java | 92 ++++++++++++++++--- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/src/main/java/io/nats/client/api/MessageGetRequest.java b/src/main/java/io/nats/client/api/MessageGetRequest.java index 262f45d22..c96ae74ee 100644 --- a/src/main/java/io/nats/client/api/MessageGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageGetRequest.java @@ -30,23 +30,23 @@ public class MessageGetRequest implements JsonSerializable { private final ZonedDateTime startTime; public static MessageGetRequest forSequence(long sequence) { - return new MessageGetRequest(sequence, null, null, null); + return builder().sequence(sequence).build(); } public static MessageGetRequest lastForSubject(String subject) { - return new MessageGetRequest(-1, subject, null, null); + return builder().lastBySubject(subject).build(); } public static MessageGetRequest firstForSubject(String subject) { - return new MessageGetRequest(-1, null, subject, null); + return builder().nextBySubject(subject).build(); } public static MessageGetRequest firstForStartTime(ZonedDateTime startTime) { - return new MessageGetRequest(-1, null, null, startTime); + return builder().startTime(startTime).build(); } public static MessageGetRequest nextForSubject(long sequence, String subject) { - return new MessageGetRequest(sequence, null, subject, null); + return builder().sequence(sequence).nextBySubject(subject).build(); } /** @@ -76,7 +76,7 @@ public static byte[] lastBySubjectBytes(String subject) { */ @Deprecated public MessageGetRequest(long sequence) { - this(sequence, null, null, null); + this(builder().sequence(sequence)); } /** @@ -86,14 +86,14 @@ public MessageGetRequest(long sequence) { */ @Deprecated public MessageGetRequest(String lastBySubject) { - this(-1, lastBySubject, null, null); + this(builder().lastBySubject(lastBySubject)); } - private MessageGetRequest(long sequence, String lastBySubject, String nextBySubject, ZonedDateTime startTime) { - this.sequence = sequence; - this.lastBySubject = lastBySubject; - this.nextBySubject = nextBySubject; - this.startTime = startTime; + private MessageGetRequest(Builder b) { + this.sequence = b.sequence; + this.lastBySubject = b.lastBySubject; + this.nextBySubject = b.nextBySubject; + this.startTime = b.startTime; } public long getSequence() { @@ -129,4 +129,72 @@ public String toJson() { addField(sb, START_TIME, startTime); return endJson(sb).toString(); } + + /** + * Creates a builder for the options. + * @return a {@link MessageGetRequest} builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for the options. + * @param req the {@link MessageGetRequest} + * @return a {@link MessageGetRequest} builder + */ + public static Builder builder(MessageGetRequest req) { + return req == null ? new Builder() : new Builder(req); + } + + public static class Builder { + private long sequence = -1; + private String lastBySubject = null; + private String nextBySubject = null; + private ZonedDateTime startTime = null; + + /** + * Construct the builder + */ + public Builder() {} + + /** + * Construct the builder and initialize values with the existing {@link MessageGetRequest} + * @param req the {@link MessageGetRequest} to clone + */ + public Builder(MessageGetRequest req) { + if (req == null) { + return; + } + + this.sequence = req.sequence; + this.lastBySubject = req.lastBySubject; + this.nextBySubject = req.nextBySubject; + this.startTime = req.startTime; + } + + public Builder sequence(long sequence) { + this.sequence = sequence; + return this; + } + + public Builder lastBySubject(String lastBySubject) { + this.lastBySubject = lastBySubject; + return this; + } + + public Builder nextBySubject(String nextBySubject) { + this.nextBySubject = nextBySubject; + return this; + } + + public Builder startTime(ZonedDateTime startTime) { + this.startTime = startTime; + return this; + } + + public MessageGetRequest build() { + return new MessageGetRequest(this); + } + } } From c1a0a95fcbe790ef9e30df2e8e9001a3d1cfb7e5 Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Tue, 17 Sep 2024 18:26:27 +0200 Subject: [PATCH 03/17] Add getMessage by MessageGetRequest Signed-off-by: Maurice van Veen --- src/main/java/io/nats/client/JetStreamManagement.java | 11 +++++++++++ .../io/nats/client/impl/NatsJetStreamManagement.java | 8 ++++++++ .../io/nats/client/impl/JetStreamManagementTests.java | 7 +++++++ 3 files changed, 26 insertions(+) diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index b51ffa0ad..5c06103b4 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -260,6 +260,17 @@ public interface JetStreamManagement { */ MessageInfo getMessage(String streamName, long seq) throws IOException, JetStreamApiException; + /** + * Get MessageInfo for the message matching the {@link MessageGetRequest}. + * @param streamName the name of the stream. + * @param messageGetRequest the {@link MessageGetRequest} to get a message + * @return The MessageInfo + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + MessageInfo getMessage(String streamName, MessageGetRequest messageGetRequest) throws IOException, JetStreamApiException; + /** * Get MessageInfo for the last message of the subject. * @param streamName the name of the stream. diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index 17d772b25..e326552c3 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -273,6 +273,14 @@ public MessageInfo getMessage(String streamName, long seq) throws IOException, J return _getMessage(streamName, MessageGetRequest.forSequence(seq)); } + /** + * {@inheritDoc} + */ + @Override + public MessageInfo getMessage(String streamName, MessageGetRequest messageGetRequest) throws IOException, JetStreamApiException { + return _getMessage(streamName, messageGetRequest); + } + /** * {@inheritDoc} */ diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index bc1deffa6..ad80c6b2a 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -1304,6 +1304,13 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertMessageInfo(tsc, 0, 5, jsm.getNextMessage(tsc.stream, 5, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 6, jsm.getNextMessage(tsc.stream, 5, tsc.subject(1)), beforeCreated); + assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().sequence(1).build()), beforeCreated); + assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().nextBySubject(tsc.subject(0)).build()), beforeCreated); + assertMessageInfo(tsc, 0, 5, jsm.getMessage(tsc.stream, MessageGetRequest.builder().lastBySubject(tsc.subject(0)).build()), beforeCreated); + assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().startTime(beforeCreated).build()), beforeCreated); + assertMessageInfo(tsc, 1, 2, jsm.getMessage(tsc.stream, MessageGetRequest.builder().startTime(beforeCreated).nextBySubject(tsc.subject(1)).build()), beforeCreated); + + assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, MessageGetRequest.builder().build()))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, -1))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 0))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, DEFAULT_TIME))); From 6b7406a3e14fdcd2b18cc79671e4abe318c071f1 Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Tue, 17 Sep 2024 18:39:25 +0200 Subject: [PATCH 04/17] Add docs Signed-off-by: Maurice van Veen --- .../io/nats/client/api/MessageGetRequest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/main/java/io/nats/client/api/MessageGetRequest.java b/src/main/java/io/nats/client/api/MessageGetRequest.java index c96ae74ee..c3df982ee 100644 --- a/src/main/java/io/nats/client/api/MessageGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageGetRequest.java @@ -173,26 +173,57 @@ public Builder(MessageGetRequest req) { this.startTime = req.startTime; } + /** + * Get a message with the exact sequence. + * Or when combined with other settings, where message sequence >= provided sequence. + * + * @param sequence which matches one message in a stream, or used as a minimum sequence for filtering + * @return Builder + */ public Builder sequence(long sequence) { this.sequence = sequence; return this; } + /** + * Get the last message by specified subject. + * + * @param lastBySubject the subject to get the last message for + * @return Builder + */ public Builder lastBySubject(String lastBySubject) { this.lastBySubject = lastBySubject; return this; } + /** + * Get the next message by specified subject. + * Either the first message when not combined with sequence, + * or a next message where message sequence >= provided sequence. + * + * @param nextBySubject the subject to get the first/next message for + * @return Builder + */ public Builder nextBySubject(String nextBySubject) { this.nextBySubject = nextBySubject; return this; } + /** + * Get message at or after the specified start time. + * + * @param startTime the minimum start time of the returned message + * @return Builder + */ public Builder startTime(ZonedDateTime startTime) { this.startTime = startTime; return this; } + /** + * Builds the {@link MessageGetRequest} with this configuration. + * @return MessageGetRequest + */ public MessageGetRequest build() { return new MessageGetRequest(this); } From 8de8df8efddc48aa58c60e1692ff470c70c35993 Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Tue, 17 Sep 2024 21:38:26 +0200 Subject: [PATCH 05/17] Add missing start_time,next_by_subj combo & remove builder Signed-off-by: Maurice van Veen --- .../io/nats/client/JetStreamManagement.java | 23 ++-- .../io/nats/client/api/MessageGetRequest.java | 127 +++--------------- .../client/impl/NatsJetStreamManagement.java | 16 +-- .../client/impl/JetStreamManagementTests.java | 8 +- 4 files changed, 37 insertions(+), 137 deletions(-) diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index 5c06103b4..8d29ef4dd 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -260,17 +260,6 @@ public interface JetStreamManagement { */ MessageInfo getMessage(String streamName, long seq) throws IOException, JetStreamApiException; - /** - * Get MessageInfo for the message matching the {@link MessageGetRequest}. - * @param streamName the name of the stream. - * @param messageGetRequest the {@link MessageGetRequest} to get a message - * @return The MessageInfo - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - MessageInfo getMessage(String streamName, MessageGetRequest messageGetRequest) throws IOException, JetStreamApiException; - /** * Get MessageInfo for the last message of the subject. * @param streamName the name of the stream. @@ -304,6 +293,18 @@ public interface JetStreamManagement { */ MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) throws IOException, JetStreamApiException; + /** + * Get MessageInfo for the first message created at or after the start time matching the subject. + * @param streamName the name of the stream. + * @param startTime the start time to get the first message for. + * @param subject the subject to get the first message for. + * @return The MessageInfo + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime, String subject) throws IOException, JetStreamApiException; + /** * Get MessageInfo for the message of the message sequence * is equal to or greater the requested sequence for the subject. diff --git a/src/main/java/io/nats/client/api/MessageGetRequest.java b/src/main/java/io/nats/client/api/MessageGetRequest.java index c3df982ee..da608587e 100644 --- a/src/main/java/io/nats/client/api/MessageGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageGetRequest.java @@ -30,23 +30,27 @@ public class MessageGetRequest implements JsonSerializable { private final ZonedDateTime startTime; public static MessageGetRequest forSequence(long sequence) { - return builder().sequence(sequence).build(); + return new MessageGetRequest(sequence, null, null, null); } public static MessageGetRequest lastForSubject(String subject) { - return builder().lastBySubject(subject).build(); + return new MessageGetRequest(-1, subject, null, null); } public static MessageGetRequest firstForSubject(String subject) { - return builder().nextBySubject(subject).build(); + return new MessageGetRequest(-1, null, subject, null); } public static MessageGetRequest firstForStartTime(ZonedDateTime startTime) { - return builder().startTime(startTime).build(); + return new MessageGetRequest(-1, null, null, startTime); + } + + public static MessageGetRequest firstForStartTimeAndSubject(ZonedDateTime startTime, String subject) { + return new MessageGetRequest(-1, null, subject, startTime); } public static MessageGetRequest nextForSubject(long sequence, String subject) { - return builder().sequence(sequence).nextBySubject(subject).build(); + return new MessageGetRequest(sequence, null, subject, null); } /** @@ -76,7 +80,7 @@ public static byte[] lastBySubjectBytes(String subject) { */ @Deprecated public MessageGetRequest(long sequence) { - this(builder().sequence(sequence)); + this(sequence, null, null, null); } /** @@ -86,14 +90,14 @@ public MessageGetRequest(long sequence) { */ @Deprecated public MessageGetRequest(String lastBySubject) { - this(builder().lastBySubject(lastBySubject)); + this(-1, lastBySubject, null, null); } - private MessageGetRequest(Builder b) { - this.sequence = b.sequence; - this.lastBySubject = b.lastBySubject; - this.nextBySubject = b.nextBySubject; - this.startTime = b.startTime; + private MessageGetRequest(long sequence, String lastBySubject, String nextBySubject, ZonedDateTime startTime) { + this.sequence = sequence; + this.lastBySubject = lastBySubject; + this.nextBySubject = nextBySubject; + this.startTime = startTime; } public long getSequence() { @@ -129,103 +133,4 @@ public String toJson() { addField(sb, START_TIME, startTime); return endJson(sb).toString(); } - - /** - * Creates a builder for the options. - * @return a {@link MessageGetRequest} builder - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a builder for the options. - * @param req the {@link MessageGetRequest} - * @return a {@link MessageGetRequest} builder - */ - public static Builder builder(MessageGetRequest req) { - return req == null ? new Builder() : new Builder(req); - } - - public static class Builder { - private long sequence = -1; - private String lastBySubject = null; - private String nextBySubject = null; - private ZonedDateTime startTime = null; - - /** - * Construct the builder - */ - public Builder() {} - - /** - * Construct the builder and initialize values with the existing {@link MessageGetRequest} - * @param req the {@link MessageGetRequest} to clone - */ - public Builder(MessageGetRequest req) { - if (req == null) { - return; - } - - this.sequence = req.sequence; - this.lastBySubject = req.lastBySubject; - this.nextBySubject = req.nextBySubject; - this.startTime = req.startTime; - } - - /** - * Get a message with the exact sequence. - * Or when combined with other settings, where message sequence >= provided sequence. - * - * @param sequence which matches one message in a stream, or used as a minimum sequence for filtering - * @return Builder - */ - public Builder sequence(long sequence) { - this.sequence = sequence; - return this; - } - - /** - * Get the last message by specified subject. - * - * @param lastBySubject the subject to get the last message for - * @return Builder - */ - public Builder lastBySubject(String lastBySubject) { - this.lastBySubject = lastBySubject; - return this; - } - - /** - * Get the next message by specified subject. - * Either the first message when not combined with sequence, - * or a next message where message sequence >= provided sequence. - * - * @param nextBySubject the subject to get the first/next message for - * @return Builder - */ - public Builder nextBySubject(String nextBySubject) { - this.nextBySubject = nextBySubject; - return this; - } - - /** - * Get message at or after the specified start time. - * - * @param startTime the minimum start time of the returned message - * @return Builder - */ - public Builder startTime(ZonedDateTime startTime) { - this.startTime = startTime; - return this; - } - - /** - * Builds the {@link MessageGetRequest} with this configuration. - * @return MessageGetRequest - */ - public MessageGetRequest build() { - return new MessageGetRequest(this); - } - } } diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index e326552c3..b6fe2c7cd 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -273,14 +273,6 @@ public MessageInfo getMessage(String streamName, long seq) throws IOException, J return _getMessage(streamName, MessageGetRequest.forSequence(seq)); } - /** - * {@inheritDoc} - */ - @Override - public MessageInfo getMessage(String streamName, MessageGetRequest messageGetRequest) throws IOException, JetStreamApiException { - return _getMessage(streamName, messageGetRequest); - } - /** * {@inheritDoc} */ @@ -305,6 +297,14 @@ public MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) t return _getMessage(streamName, MessageGetRequest.firstForStartTime(startTime)); } + /** + * {@inheritDoc} + */ + @Override + public MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime, String subject) throws IOException, JetStreamApiException { + return _getMessage(streamName, MessageGetRequest.firstForStartTimeAndSubject(startTime, subject)); + } + /** * {@inheritDoc} */ diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index ad80c6b2a..47f25d989 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -1294,6 +1294,7 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getFirstMessage(tsc.stream, tsc.subject(1)), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, beforeCreated), beforeCreated); + assertMessageInfo(tsc, 1, 2, jsm.getFirstMessage(tsc.stream, beforeCreated, tsc.subject(1)), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getNextMessage(tsc.stream, 1, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, 1, tsc.subject(1)), beforeCreated); @@ -1304,13 +1305,6 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertMessageInfo(tsc, 0, 5, jsm.getNextMessage(tsc.stream, 5, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 6, jsm.getNextMessage(tsc.stream, 5, tsc.subject(1)), beforeCreated); - assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().sequence(1).build()), beforeCreated); - assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().nextBySubject(tsc.subject(0)).build()), beforeCreated); - assertMessageInfo(tsc, 0, 5, jsm.getMessage(tsc.stream, MessageGetRequest.builder().lastBySubject(tsc.subject(0)).build()), beforeCreated); - assertMessageInfo(tsc, 0, 1, jsm.getMessage(tsc.stream, MessageGetRequest.builder().startTime(beforeCreated).build()), beforeCreated); - assertMessageInfo(tsc, 1, 2, jsm.getMessage(tsc.stream, MessageGetRequest.builder().startTime(beforeCreated).nextBySubject(tsc.subject(1)).build()), beforeCreated); - - assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, MessageGetRequest.builder().build()))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, -1))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 0))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, DEFAULT_TIME))); From de4c0c89b5208120f26d4d1267032adb01ba904a Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Mon, 23 Sep 2024 17:00:08 +0200 Subject: [PATCH 06/17] Add experimental note Signed-off-by: Maurice van Veen --- src/main/java/io/nats/client/JetStreamManagement.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index 8d29ef4dd..499c8ef4d 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -284,6 +284,9 @@ public interface JetStreamManagement { /** * Get MessageInfo for the first message created at or after the start time. + *

+ * This API is currently EXPERIMENTAL and is subject to change. + * * @param streamName the name of the stream. * @param startTime the start time to get the first message for. * @return The MessageInfo @@ -295,6 +298,9 @@ public interface JetStreamManagement { /** * Get MessageInfo for the first message created at or after the start time matching the subject. + *

+ * This API is currently EXPERIMENTAL and is subject to change. + * * @param streamName the name of the stream. * @param startTime the start time to get the first message for. * @param subject the subject to get the first message for. From e6426b7c140a1737e7b6910d6ccae35e0ebd9e76 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Mon, 23 Sep 2024 14:04:45 -0400 Subject: [PATCH 07/17] Add publish of test code jar to builds. (#1230) --- build.gradle | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 41fef81a7..a82084764 100644 --- a/build.gradle +++ b/build.gradle @@ -138,6 +138,11 @@ task sourcesJar(type: Jar) { from sourceSets.main.allSource } +task testsJar(type: Jar) { + archiveClassifier.set('tests') + from sourceSets.test.allSource +} + // run build before running fat jar to get classes task fatJar(type: Jar) { archiveClassifier.set('fat') @@ -168,7 +173,7 @@ jacocoTestReport { } artifacts { - archives javadocJar, sourcesJar, examplesJar + archives javadocJar, sourcesJar, examplesJar, testsJar } nexusPublishing { @@ -187,6 +192,7 @@ publishing { artifact sourcesJar artifact examplesJar artifact javadocJar + artifact testsJar pom { name = 'jnats' packaging = 'jar' From 27efeab58732d1fbf321d7010d68fb31149c0960 Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Thu, 3 Oct 2024 12:19:25 +0200 Subject: [PATCH 08/17] Add batch direct get (#1229) --- README.md | 2 + .../io/nats/client/JetStreamManagement.java | 43 +++ .../io/nats/client/MessageInfoHandler.java | 29 ++ .../java/io/nats/client/api/ApiResponse.java | 6 + .../client/api/MessageBatchGetRequest.java | 347 ++++++++++++++++++ .../java/io/nats/client/api/MessageInfo.java | 54 ++- .../nats/client/impl/NatsJetStreamImpl.java | 3 + .../client/impl/NatsJetStreamManagement.java | 108 ++++++ .../io/nats/client/support/ApiConstants.java | 3 + .../support/NatsJetStreamClientError.java | 2 + .../support/NatsJetStreamConstants.java | 3 +- .../io/nats/client/support/Validator.java | 7 + .../client/impl/JetStreamManagementTests.java | 233 +++++++++++- 13 files changed, 830 insertions(+), 10 deletions(-) create mode 100644 src/main/java/io/nats/client/MessageInfoHandler.java create mode 100644 src/main/java/io/nats/client/api/MessageBatchGetRequest.java diff --git a/README.md b/README.md index 192b2782b..907d8ab1c 100644 --- a/README.md +++ b/README.md @@ -980,6 +980,8 @@ You can however set the deliver policy which will be used to start the subscript | JsConsumerCreate290NotAvailable | CON-90301 | Name field not valid when v2.9.0 consumer create api is not available. | | JsConsumerNameDurableMismatch | CON-90302 | Name must match durable if both are supplied. | | JsMultipleFilterSubjects210NotAvailable | CON-90303 | Multiple filter subjects not available until server version 2.10.0. | +| JsAllowDirectRequired | CON-90304 | Stream must have allow direct set. | +| JsDirectBatchGet211NotAvailable | CON-90305 | Batch direct get not available until server version 2.11.0. | | OsObjectNotFound | OS-90201 | The object was not found. | | OsObjectIsDeleted | OS-90202 | The object is deleted. | | OsObjectAlreadyExists | OS-90203 | An object with that name already exists. | diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index 499c8ef4d..6755e49d0 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.time.ZonedDateTime; import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; /** * JetStream Management context for creation and access to streams and consumers in NATS. @@ -324,6 +325,48 @@ public interface JetStreamManagement { */ MessageInfo getNextMessage(String streamName, long seq, String subject) throws IOException, JetStreamApiException; + /** + * Request a batch of messages using a {@link MessageBatchGetRequest}. + *

+ * This API is currently EXPERIMENTAL and is subject to change. + * + * @param streamName the name of the stream + * @param messageBatchGetRequest the request details + * @return a list containing {@link MessageInfo} + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + List fetchMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException; + + /** + * Request a batch of messages using a {@link MessageBatchGetRequest}. + *

+ * This API is currently EXPERIMENTAL and is subject to change. + * + * @param streamName the name of the stream + * @param messageBatchGetRequest the request details + * @return a queue used to asynchronously receive {@link MessageInfo} + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + LinkedBlockingQueue queueMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException; + + /** + * Request a batch of messages using a {@link MessageBatchGetRequest}. + *

+ * This API is currently EXPERIMENTAL and is subject to change. + * + * @param streamName the name of the stream + * @param messageBatchGetRequest the request details + * @param handler the handler used for receiving {@link MessageInfo} + * @throws IOException covers various communication issues with the NATS + * server such as timeout or interruption + * @throws JetStreamApiException the request had an error related to the data + */ + void requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) throws IOException, JetStreamApiException; + /** * Deletes a message, overwriting the message data with garbage * This can be considered an expensive (time-consuming) operation, but is more secure. diff --git a/src/main/java/io/nats/client/MessageInfoHandler.java b/src/main/java/io/nats/client/MessageInfoHandler.java new file mode 100644 index 000000000..6a9697a31 --- /dev/null +++ b/src/main/java/io/nats/client/MessageInfoHandler.java @@ -0,0 +1,29 @@ +// Copyright 2024 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.api.MessageInfo; + +/** + * Handler for {@link MessageInfo}. + */ +public interface MessageInfoHandler { + /** + * Called to deliver a {@link MessageInfo} to the handler. + * + * @param messageInfo the received {@link MessageInfo} + * @throws InterruptedException if the thread for this handler is interrupted + */ + void onMessageInfo(MessageInfo messageInfo) throws InterruptedException; +} diff --git a/src/main/java/io/nats/client/api/ApiResponse.java b/src/main/java/io/nats/client/api/ApiResponse.java index 2dbe3a348..e97a42a41 100644 --- a/src/main/java/io/nats/client/api/ApiResponse.java +++ b/src/main/java/io/nats/client/api/ApiResponse.java @@ -76,6 +76,12 @@ public ApiResponse() { type = NO_TYPE; } + public ApiResponse(Error error) { + jv = null; + this.error = error; + type = NO_TYPE; + } + @SuppressWarnings("unchecked") public T throwOnHasError() throws JetStreamApiException { if (hasError()) { diff --git a/src/main/java/io/nats/client/api/MessageBatchGetRequest.java b/src/main/java/io/nats/client/api/MessageBatchGetRequest.java new file mode 100644 index 000000000..3be679f43 --- /dev/null +++ b/src/main/java/io/nats/client/api/MessageBatchGetRequest.java @@ -0,0 +1,347 @@ +// Copyright 2024 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.api; + +import io.nats.client.support.JsonSerializable; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static io.nats.client.support.ApiConstants.*; +import static io.nats.client.support.JsonUtils.*; +import static io.nats.client.support.Validator.*; + +/** + * Object used to make a request for message batch get requests. + */ +public class MessageBatchGetRequest implements JsonSerializable { + + private final Duration timeout; + private final int batch; + private final int maxBytes; + private final long sequence; + private final ZonedDateTime startTime; + private final String nextBySubject; + private final List multiLastFor; + private final long upToSequence; + private final ZonedDateTime upToTime; + + MessageBatchGetRequest(Builder b) { + this.timeout = b.timeout; + this.batch = b.batch; + this.maxBytes = b.maxBytes; + this.sequence = b.sequence; + this.startTime = b.startTime; + this.nextBySubject = b.nextBySubject; + this.multiLastFor = b.multiLastFor; + this.upToSequence = b.upToSequence; + this.upToTime = b.upToTime; + } + + /** + * Timeout used for the request. + * + * @return Duration + */ + public Duration getTimeout() { + return timeout; + } + + /** + * Maximum amount of messages to be returned for this request. + * + * @return batch size + */ + public int getBatch() { + return batch; + } + + /** + * Maximum amount of returned bytes for this request. + * Limits the amount of returned messages to not exceed this. + * + * @return maximum bytes + */ + public int getMaxBytes() { + return maxBytes; + } + + /** + * Minimum sequence for returned messages. + * All returned messages will have a sequence equal to or higher than this. + * + * @return minimum message sequence + */ + public long getSequence() { + return sequence; + } + + /** + * Minimum start time for returned messages. + * All returned messages will have a start time equal to or higher than this. + * + * @return minimum message start time + */ + public ZonedDateTime getStartTime() { + return startTime; + } + + /** + * Subject used to filter messages that should be returned. + * + * @return the subject to filter + */ + public String getSubject() { + return nextBySubject; + } + + /** + * Subjects filter used, these can include wildcards. + * Will get the last messages matching the subjects. + * + * @return the subjects to get the last messages for + */ + public List getMultiLastForSubjects() { + return multiLastFor; + } + + /** + * Only return messages up to this sequence. + * + * @return the maximum message sequence to return results for + */ + public long getUpToSequence() { + return upToSequence; + } + + /** + * Only return messages up to this time. + * + * @return the maximum message time to return results for + */ + public ZonedDateTime getUpToTime() { + return upToTime; + } + + @Override + public String toJson() { + StringBuilder sb = beginJson(); + addField(sb, BATCH, batch); + addField(sb, MAX_BYTES, maxBytes); + addField(sb, SEQ, sequence); + addField(sb, START_TIME, startTime); + addField(sb, NEXT_BY_SUBJECT, nextBySubject); + addStrings(sb, MULTI_LAST, multiLastFor); + addField(sb, UP_TO_SEQ, upToSequence); + addField(sb, UP_TO_TIME, upToTime); + return endJson(sb).toString(); + } + + /** + * Creates a builder for the request. + * + * @return Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a builder for the request. + * + * @param req the {@link MessageBatchGetRequest} + * @return Builder + */ + public static Builder builder(MessageBatchGetRequest req) { + return req == null ? new Builder() : new Builder(req); + } + + /** + * {@link MessageBatchGetRequest} is created using a Builder. The builder supports chaining and will + * create a default set of options if no methods are calls. + * + *

{@code MessageBatchGetRequest.builder().build()} will create a default {@link MessageBatchGetRequest}. + */ + public static class Builder { + private Duration timeout = Duration.ofSeconds(5); + private int batch = -1; + private int maxBytes = -1; + private long sequence = -1; + private ZonedDateTime startTime = null; + private String nextBySubject = null; + private List multiLastFor = new ArrayList<>(); + private long upToSequence = -1; + private ZonedDateTime upToTime = null; + + /** + * Construct the builder + */ + public Builder() { + } + + /** + * Construct the builder and initialize values with the existing {@link MessageBatchGetRequest} + * + * @param req the {@link MessageBatchGetRequest} to clone + */ + public Builder(MessageBatchGetRequest req) { + if (req != null) { + this.timeout = req.timeout; + this.batch = req.batch; + this.maxBytes = req.maxBytes; + this.sequence = req.sequence; + this.startTime = req.startTime; + this.nextBySubject = req.nextBySubject; + this.multiLastFor = req.multiLastFor; + this.upToSequence = req.upToSequence; + this.upToTime = req.upToTime; + } + } + + /** + * Set the timeout used for the request. + * + * @param timeout the timeout + * @return Builder + */ + public Builder timeout(Duration timeout) { + validateDurationRequired(timeout); + this.timeout = timeout; + return this; + } + + /** + * Set the maximum amount of messages to be returned for this request. + * + * @param batch the batch size + * @return Builder + */ + public Builder batch(int batch) { + validateGtZero(batch, "Request batch size"); + this.batch = batch; + return this; + } + + /** + * Maximum amount of returned bytes for this request. + * Limits the amount of returned messages to not exceed this. + * + * @param maxBytes the maximum bytes + * @return Builder + */ + public Builder maxBytes(int maxBytes) { + this.maxBytes = maxBytes; + return this; + } + + /** + * Minimum sequence for returned messages. + * All returned messages will have a sequence equal to or higher than this. + * + * @param sequence the minimum message sequence + * @return Builder + */ + public Builder sequence(long sequence) { + validateGtEqZero(sequence, "Sequence"); + this.sequence = sequence; + return this; + } + + /** + * Minimum start time for returned messages. + * All returned messages will have a start time equal to or higher than this. + * + * @param startTime the minimum message start time + * @return Builder + */ + public Builder startTime(ZonedDateTime startTime) { + this.startTime = startTime; + return this; + } + + /** + * Subject used to filter messages that should be returned. + * + * @param subject the subject to filter + * @return Builder + */ + public Builder subject(String subject) { + this.nextBySubject = subject; + return this; + } + + /** + * Subjects filter used, these can include wildcards. + * Will get the last messages matching the subjects. + * + * @param subjects the subjects to get the last messages for + * @return Builder + */ + public Builder multiLastForSubjects(String... subjects) { + this.multiLastFor.clear(); + this.multiLastFor.addAll(Arrays.asList(subjects)); + return this; + } + + /** + * Subjects filter used, these can include wildcards. + * Will get the last messages matching the subjects. + * + * @param subjects the subjects to get the last messages for + * @return Builder + */ + public Builder multiLastForSubjects(Collection subjects) { + this.multiLastFor.clear(); + this.multiLastFor.addAll(subjects); + return this; + } + + /** + * Only return messages up to this sequence. + * If not set, will be last sequence for the stream. + * + * @param upToSequence the maximum message sequence to return results for + * @return Builder + */ + public Builder upToSequence(long upToSequence) { + validateGtZero(upToSequence, "Up to sequence"); + this.upToSequence = upToSequence; + return this; + } + + /** + * Only return messages up to this time. + * + * @param upToTime the maximum message time to return results for + * @return Builder + */ + public Builder upToTime(ZonedDateTime upToTime) { + this.upToTime = upToTime; + return this; + } + + /** + * Build the {@link MessageBatchGetRequest}. + * + * @return MessageBatchGetRequest + */ + public MessageBatchGetRequest build() { + return new MessageBatchGetRequest(this); + } + } +} diff --git a/src/main/java/io/nats/client/api/MessageInfo.java b/src/main/java/io/nats/client/api/MessageInfo.java index 206cd8b92..5ded6746f 100644 --- a/src/main/java/io/nats/client/api/MessageInfo.java +++ b/src/main/java/io/nats/client/api/MessageInfo.java @@ -32,6 +32,11 @@ */ public class MessageInfo extends ApiResponse { + /** + * Message returned as a response in {@link MessageBatchGetRequest} to signal end of data. + */ + public static final MessageInfo EOD = new MessageInfo(null, false); + private final boolean direct; private final String subject; private final long seq; @@ -40,6 +45,7 @@ public class MessageInfo extends ApiResponse { private final Headers headers; private final String stream; private final long lastSeq; + private final long numPending; /** * Create a Message Info @@ -51,6 +57,25 @@ public MessageInfo(Message msg) { this(msg, null, false); } + /** + * Create a Message Info + * This signature is public for testing purposes and is not intended to be used externally. + * @param error the error + * @param direct true if the object is being created from a get direct api call instead of the standard get message + */ + public MessageInfo(Error error, boolean direct) { + super(error); + this.direct = direct; + subject = null; + data = null; + seq = -1; + time = null; + headers = null; + stream = null; + lastSeq = -1; + numPending = -1; + } + /** * Create a Message Info * This signature is public for testing purposes and is not intended to be used externally. @@ -70,12 +95,20 @@ public MessageInfo(Message msg, String streamName, boolean direct) { seq = Long.parseLong(msgHeaders.getLast(NATS_SEQUENCE)); time = DateTimeUtils.parseDateTime(msgHeaders.getLast(NATS_TIMESTAMP)); stream = msgHeaders.getLast(NATS_STREAM); - String temp = msgHeaders.getLast(NATS_LAST_SEQUENCE); - if (temp == null) { + String tempLastSeq = msgHeaders.getLast(NATS_LAST_SEQUENCE); + if (tempLastSeq == null) { lastSeq = -1; } else { - lastSeq = JsonUtils.safeParseLong(temp, -1); + lastSeq = JsonUtils.safeParseLong(tempLastSeq, -1); + } + String tempNumPending = msgHeaders.getLast(NATS_NUM_PENDING); + if (tempNumPending == null) { + numPending = -1; + } + else { + // Num pending is +1 since it includes EOB message, correct that here. + numPending = Long.parseLong(tempNumPending) - 1; } // these are control headers, not real headers so don't give them to the user. headers = new Headers(msgHeaders, true, MESSAGE_INFO_HEADERS); @@ -88,6 +121,7 @@ else if (hasError()) { headers = null; stream = null; lastSeq = -1; + numPending = -1; } else { JsonValue mjv = readValue(jv, MESSAGE); @@ -99,6 +133,7 @@ else if (hasError()) { headers = hdrBytes == null ? null : new IncomingHeadersProcessor(hdrBytes).getHeaders(); stream = streamName; lastSeq = -1; + numPending = -1; } } @@ -158,11 +193,19 @@ public long getLastSeq() { return lastSeq; } + /** + * Amount of pending messages that can be requested with a subsequent batch request. + * @return number of pending messages + */ + public long getNumPending() { + return numPending; + } + @Override public String toString() { StringBuilder sb = JsonUtils.beginJsonPrefixed("\"MessageInfo\":"); JsonUtils.addField(sb, "direct", direct); - JsonUtils.addField(sb, "error", getError()); + JsonUtils.addField(sb, ERROR, getError()); JsonUtils.addField(sb, SUBJECT, subject); JsonUtils.addField(sb, SEQ, seq); if (data == null) { @@ -173,7 +216,8 @@ public String toString() { } JsonUtils.addField(sb, TIME, time); JsonUtils.addField(sb, STREAM, stream); - JsonUtils.addField(sb, "last_seq", lastSeq); + JsonUtils.addField(sb, LAST_SEQ, lastSeq); + JsonUtils.addField(sb, NUM_PENDING, numPending); JsonUtils.addField(sb, SUBJECT, subject); JsonUtils.addField(sb, HDRS, headers); return JsonUtils.endJson(sb).toString(); diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java index 853bfc3ab..b712cff27 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java @@ -47,6 +47,7 @@ public CachedStreamInfo(StreamInfo si) { final JetStreamOptions jso; final boolean consumerCreate290Available; final boolean multipleSubjectFilter210Available; + final boolean directBatchGet211Available; // ---------------------------------------------------------------------------------------------------- // Create / Init @@ -63,6 +64,7 @@ public CachedStreamInfo(StreamInfo si) { consumerCreate290Available = conn.getInfo().isSameOrNewerThanVersion("2.9.0") && !jso.isOptOut290ConsumerCreate(); multipleSubjectFilter210Available = conn.getInfo().isNewerVersionThan("2.9.99"); + directBatchGet211Available = conn.getInfo().isNewerVersionThan("2.10.99"); } NatsJetStreamImpl(NatsJetStreamImpl impl) { @@ -70,6 +72,7 @@ public CachedStreamInfo(StreamInfo si) { jso = impl.jso; consumerCreate290Available = impl.consumerCreate290Available; multipleSubjectFilter210Available = impl.multipleSubjectFilter210Available; + directBatchGet211Available = impl.directBatchGet211Available; } // ---------------------------------------------------------------------------------------------------- diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index b6fe2c7cd..7ac235f55 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -16,12 +16,17 @@ import io.nats.client.*; import io.nats.client.api.Error; import io.nats.client.api.*; +import io.nats.client.support.Status; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import static io.nats.client.support.NatsJetStreamClientError.JsAllowDirectRequired; +import static io.nats.client.support.NatsJetStreamClientError.JsDirectBatchGet211NotAvailable; import static io.nats.client.support.Validator.*; public class NatsJetStreamManagement extends NatsJetStreamImpl implements JetStreamManagement { @@ -340,6 +345,109 @@ private MessageInfo _getMessage(String streamName, MessageGetRequest messageGetR } } + /** + * {@inheritDoc} + */ + @Override + public List fetchMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { + validateMessageBatchGetRequest(streamName, messageBatchGetRequest); + List results = new ArrayList<>(); + _requestMessageBatch(streamName, messageBatchGetRequest, msg -> { + if (msg != MessageInfo.EOD) { + results.add(msg); + } + }); + return results; + } + + /** + * {@inheritDoc} + */ + @Override + public LinkedBlockingQueue queueMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { + validateMessageBatchGetRequest(streamName, messageBatchGetRequest); + final LinkedBlockingQueue q = new LinkedBlockingQueue<>(); + conn.getOptions().getExecutor().submit(() -> _requestMessageBatch(streamName, messageBatchGetRequest, q::add)); + return q; + } + + /** + * {@inheritDoc} + */ + @Override + public void requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) throws IOException, JetStreamApiException { + validateMessageBatchGetRequest(streamName, messageBatchGetRequest); + _requestMessageBatch(streamName, messageBatchGetRequest, handler); + } + + public void _requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) { + Subscription sub = null; + try { + String replyTo = conn.createInbox(); + sub = conn.subscribe(replyTo); + + String requestSubject = prependPrefix(String.format(JSAPI_DIRECT_GET, streamName)); + conn.publish(requestSubject, replyTo, messageBatchGetRequest.serialize()); + + long start = System.currentTimeMillis(); + long maxTimeMillis = messageBatchGetRequest.getTimeout().toMillis(); + long timeLeft = maxTimeMillis; + while (true) { + Message msg = sub.nextMessage(timeLeft); + if (msg == null) { + break; + } + if (msg.isStatusMessage()) { + Status status = msg.getStatus(); + // Report error, otherwise successful status. + if (status.getCode() < 200 || status.getCode() > 299) { + MessageInfo messageInfo = new MessageInfo(Error.convert(status), true); + handler.onMessageInfo(messageInfo); + } + break; + } + + Headers headers = msg.getHeaders(); + if (headers == null || headers.getLast(NATS_NUM_PENDING) == null) { + throw JsDirectBatchGet211NotAvailable.instance(); + } + + MessageInfo messageInfo = new MessageInfo(msg, streamName, true); + handler.onMessageInfo(messageInfo); + timeLeft = maxTimeMillis - (System.currentTimeMillis() - start); + } + } catch (InterruptedException e) { + // sub.nextMessage was fetching one message + // and data is not completely read + // so it seems like this is an error condition + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } finally { + try { + handler.onMessageInfo(MessageInfo.EOD); + } catch (Exception ignore) { + } + try { + //noinspection DataFlowIssue + sub.unsubscribe(); + } catch (Exception ignore) { + } + } + } + + private void validateMessageBatchGetRequest(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { + validateNotNull(messageBatchGetRequest, "Message Batch Get Request"); + + if (!directBatchGet211Available) { + throw JsDirectBatchGet211NotAvailable.instance(); + } + + CachedStreamInfo csi = getCachedStreamInfo(streamName); + if (!csi.allowDirect) { + throw JsAllowDirectRequired.instance(); + } + } + /** * {@inheritDoc} */ diff --git a/src/main/java/io/nats/client/support/ApiConstants.java b/src/main/java/io/nats/client/support/ApiConstants.java index 16ff26f17..e3479b4b8 100644 --- a/src/main/java/io/nats/client/support/ApiConstants.java +++ b/src/main/java/io/nats/client/support/ApiConstants.java @@ -126,6 +126,7 @@ public interface ApiConstants { String MTIME = "mtime"; String MIRROR = "mirror"; String MSGS = "msgs"; + String MULTI_LAST = "multi_last"; String NAME = "name"; String NEXT_BY_SUBJECT = "next_by_subj"; String NO_ACK = "no_ack"; @@ -202,5 +203,7 @@ public interface ApiConstants { String TLS_AVAILABLE = "tls_available"; String TOTAL = "total"; String TYPE = "type"; + String UP_TO_SEQ = "up_to_seq"; + String UP_TO_TIME = "up_to_time"; String VERSION = "version"; } diff --git a/src/main/java/io/nats/client/support/NatsJetStreamClientError.java b/src/main/java/io/nats/client/support/NatsJetStreamClientError.java index 6bda9e345..5da56cdff 100644 --- a/src/main/java/io/nats/client/support/NatsJetStreamClientError.java +++ b/src/main/java/io/nats/client/support/NatsJetStreamClientError.java @@ -70,6 +70,8 @@ public class NatsJetStreamClientError { public static final NatsJetStreamClientError JsConsumerCreate290NotAvailable = new NatsJetStreamClientError(CON, 90301, "Name field not valid when v2.9.0 consumer create api is not available."); public static final NatsJetStreamClientError JsConsumerNameDurableMismatch = new NatsJetStreamClientError(CON, 90302, "Name must match durable if both are supplied."); public static final NatsJetStreamClientError JsMultipleFilterSubjects210NotAvailable = new NatsJetStreamClientError(CON, 90303, "Multiple filter subjects not available until server version 2.10.0."); + public static final NatsJetStreamClientError JsAllowDirectRequired = new NatsJetStreamClientError(CON, 90304, "Stream must have allow direct set."); + public static final NatsJetStreamClientError JsDirectBatchGet211NotAvailable = new NatsJetStreamClientError(CON, 90305, "Batch direct get not available until server version 2.11.0."); @Deprecated // Fixed spelling error public static final NatsJetStreamClientError JsSubFcHbHbNotValidQueue = new NatsJetStreamClientError(SUB, 90006, "Flow Control and/or heartbeat is not valid in queue mode."); diff --git a/src/main/java/io/nats/client/support/NatsJetStreamConstants.java b/src/main/java/io/nats/client/support/NatsJetStreamConstants.java index 45aed12b2..c7e4d4a3b 100644 --- a/src/main/java/io/nats/client/support/NatsJetStreamConstants.java +++ b/src/main/java/io/nats/client/support/NatsJetStreamConstants.java @@ -100,7 +100,8 @@ public interface NatsJetStreamConstants { String NATS_TIMESTAMP = "Nats-Time-Stamp"; String NATS_SUBJECT = "Nats-Subject"; String NATS_LAST_SEQUENCE = "Nats-Last-Sequence"; - String[] MESSAGE_INFO_HEADERS = new String[]{NATS_SUBJECT, NATS_SEQUENCE, NATS_TIMESTAMP, NATS_STREAM, NATS_LAST_SEQUENCE}; + String NATS_NUM_PENDING = "Nats-Num-Pending"; + String[] MESSAGE_INFO_HEADERS = new String[]{NATS_SUBJECT, NATS_SEQUENCE, NATS_TIMESTAMP, NATS_STREAM, NATS_LAST_SEQUENCE, NATS_NUM_PENDING}; String NATS_PENDING_MESSAGES = "Nats-Pending-Messages"; String NATS_PENDING_BYTES = "Nats-Pending-Bytes"; diff --git a/src/main/java/io/nats/client/support/Validator.java b/src/main/java/io/nats/client/support/Validator.java index 517832637..e8dcbf109 100644 --- a/src/main/java/io/nats/client/support/Validator.java +++ b/src/main/java/io/nats/client/support/Validator.java @@ -383,6 +383,13 @@ public static int validateGtZero(int i, String label) { return i; } + public static long validateGtZero(long l, String label) { + if (l < 1) { + throw new IllegalArgumentException(label + " must be greater than zero"); + } + return l; + } + public static long validateGtZeroOrMinus1(long l, String label) { if (zeroOrLtMinus1(l)) { throw new IllegalArgumentException(label + " must be greater than zero or -1 for unlimited"); diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index 47f25d989..e05f21434 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -23,11 +23,13 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import static io.nats.client.support.DateTimeUtils.DEFAULT_TIME; import static io.nats.client.support.DateTimeUtils.ZONE_ID_GMT; @@ -1548,4 +1550,227 @@ public void testCreateConsumerUpdateConsumer() throws Exception { assertEquals(fs1, ci.getConsumerConfiguration().getFilterSubject()); }); } + + @Test + public void testBatchDirectGet() throws Exception { + jsServer.run(TestBase::atLeast2_11, nc -> { + JetStream js = nc.jetStream(); + JetStreamManagement jsm = nc.jetStreamManagement(); + + TestingStreamContainer tsc = new TestingStreamContainer(nc); + assertFalse(tsc.si.getConfiguration().getAllowDirect()); + + List expected = Arrays.asList("foo", "bar", "baz"); + for (String data : expected) { + js.publish(tsc.subject(), data.getBytes(StandardCharsets.UTF_8)); + } + + List batch = new ArrayList<>(); + MessageInfoHandler handler = msg -> { + if (!msg.hasError() && msg != MessageInfo.EOD) { + batch.add(msg); + } + }; + + // Stream doesn't have AllowDirect enabled, will error. + assertThrows(IllegalArgumentException.class, () -> { + MessageBatchGetRequest request = MessageBatchGetRequest.builder().build(); + jsm.requestMessageBatch(tsc.stream, request, handler); + }); + + // Enable AllowDirect. + StreamConfiguration sc = StreamConfiguration.builder(tsc.si.getConfiguration()).allowDirect(true).build(); + StreamInfo si = jsm.updateStream(sc); + assertTrue(si.getConfiguration().getAllowDirect()); + + // Empty request errors. + AtomicBoolean hasError = new AtomicBoolean(); + MessageInfoHandler errorHandler = msg -> { + hasError.compareAndSet(false, msg.hasError()); + }; + MessageBatchGetRequest request = MessageBatchGetRequest.builder().build(); + jsm.requestMessageBatch(tsc.stream, request, errorHandler); + assertTrue(hasError.get()); + List list = jsm.fetchMessageBatch(tsc.stream, request); + assertEquals(1, list.size()); + assertTrue(list.get(0).hasError()); + LinkedBlockingQueue queue = jsm.queueMessageBatch(tsc.stream, request); + assertTrue(queue.take().hasError()); + assertEquals(MessageInfo.EOD, queue.take()); + + // First batch gets first two messages. + request = MessageBatchGetRequest.builder() + .batch(2) + .subject(tsc.subject()) + .build(); + jsm.requestMessageBatch(tsc.stream, request, handler); + MessageInfo last = batch.get(batch.size() - 1); + assertEquals(1, last.getNumPending()); + assertEquals(2, last.getSeq()); + assertEquals(1, last.getLastSeq()); + + // Second batch gets last message. + request = MessageBatchGetRequest.builder(request) + .sequence(last.getSeq() + 1) + .build(); + jsm.requestMessageBatch(tsc.stream, request, handler); + + List actual = batch.stream().map(m -> new String(m.getData())).collect(Collectors.toList()); + assertEquals(expected, actual); + + last = batch.get(batch.size() - 1); + assertEquals(0, last.getNumPending()); + assertEquals(3, last.getSeq()); + assertEquals(0, last.getLastSeq()); + }); + } + + @Test + public void testBatchDirectGetAlternatives() throws Exception { + jsServer.run(TestBase::atLeast2_11, nc -> { + JetStream js = nc.jetStream(); + JetStreamManagement jsm = nc.jetStreamManagement(); + + TestingStreamContainer tsc = new TestingStreamContainer(nc); + assertFalse(tsc.si.getConfiguration().getAllowDirect()); + + // Enable AllowDirect. + StreamConfiguration sc = StreamConfiguration.builder(tsc.si.getConfiguration()).allowDirect(true).build(); + StreamInfo si = jsm.updateStream(sc); + assertTrue(si.getConfiguration().getAllowDirect()); + + List expected = Arrays.asList("foo", "bar", "baz"); + for (String data : expected) { + js.publish(tsc.subject(), data.getBytes(StandardCharsets.UTF_8)); + } + + // Request stays the same for all options. + MessageBatchGetRequest request = MessageBatchGetRequest.builder() + .batch(3) + .subject(tsc.subject()) + .build(); + + // Get using handler. + List batch = new ArrayList<>(); + MessageInfoHandler handler = msg -> { + if (!msg.hasError() && msg != MessageInfo.EOD) { + batch.add(msg); + } + }; + jsm.requestMessageBatch(tsc.stream, request, handler); + assertEquals(3, batch.size()); + MessageInfo last = batch.get(batch.size() - 1); + assertEquals(0, last.getNumPending()); + assertEquals(3, last.getSeq()); + assertEquals(2, last.getLastSeq()); + + // Get using queue. + batch.clear(); + LinkedBlockingQueue queue = jsm.queueMessageBatch(tsc.stream, request); + MessageInfo msg; + while ((msg = queue.take()) != MessageInfo.EOD) { + if (!msg.hasError()) { + batch.add(msg); + } + } + assertEquals(3, batch.size()); + last = batch.get(batch.size() - 1); + assertEquals(0, last.getNumPending()); + assertEquals(3, last.getSeq()); + assertEquals(2, last.getLastSeq()); + + // Get using fetch. + batch.clear(); + batch.addAll(jsm.fetchMessageBatch(tsc.stream, request)); + assertEquals(3, batch.size()); + last = batch.get(batch.size() - 1); + assertEquals(0, last.getNumPending()); + assertEquals(3, last.getSeq()); + assertEquals(2, last.getLastSeq()); + }); + } + + @Test + public void testBatchDirectGetMultiLast() throws Exception { + jsServer.run(TestBase::atLeast2_11, nc -> { + JetStream js = nc.jetStream(); + JetStreamManagement jsm = nc.jetStreamManagement(); + + String stream = stream(); + jsm.addStream(StreamConfiguration.builder() + .name(stream) + .subjects(stream + ".a.>") + .allowDirect(true) + .build()); + + String subjectAFoo = stream + ".a.foo"; + String subjectABar = stream + ".a.bar"; + String subjectABaz = stream + ".a.baz"; + js.publish(subjectAFoo, "foo".getBytes(StandardCharsets.UTF_8)); + js.publish(subjectABar, "bar".getBytes(StandardCharsets.UTF_8)); + js.publish(subjectABaz, "baz".getBytes(StandardCharsets.UTF_8)); + + MessageBatchGetRequest request = MessageBatchGetRequest.builder() + .multiLastForSubjects(subjectAFoo, subjectABaz) + .build(); + + List keys = new ArrayList<>(); + MessageInfoHandler handler = msg -> { + if (!msg.hasError() && msg != MessageInfo.EOD) { + keys.add(msg.getSubject()); + } + }; + jsm.requestMessageBatch(stream, request, handler); + assertEquals(2, keys.size()); + assertEquals(subjectAFoo, keys.get(0)); + assertEquals(subjectABaz, keys.get(1)); + }); + } + + @Test + public void testBatchDirectGetBuilder() { + // Default timeout + assertEquals(Duration.ofSeconds(5), MessageBatchGetRequest.builder().build().getTimeout()); + + // Request options. + MessageBatchGetRequest requestOptions = MessageBatchGetRequest.builder() + .timeout(Duration.ofSeconds(1)) + .maxBytes(1234) + .batch(2) + .build(); + assertEquals(Duration.ofSeconds(1), requestOptions.getTimeout()); + assertEquals(1234, requestOptions.getMaxBytes()); + assertEquals(2, requestOptions.getBatch()); + assertEquals("{\"batch\":2,\"max_bytes\":1234}", requestOptions.toJson()); + + // Batch direct get - simple + ZonedDateTime time = Instant.EPOCH.atZone(ZoneOffset.UTC); + MessageBatchGetRequest simple = MessageBatchGetRequest.builder() + .sequence(1) + .startTime(time) + .subject("subject") + .build(); + assertEquals(1, simple.getSequence()); + assertEquals(time, simple.getStartTime()); + assertEquals("subject", simple.getSubject()); + assertEquals("{\"seq\":1,\"start_time\":\"1970-01-01T00:00:00.000000000Z\",\"next_by_subj\":\"subject\"}", simple.toJson()); + + // Batch direct get - multi last + List multiLastFor = Collections.singletonList("multi.last"); + MessageBatchGetRequest multiLast = MessageBatchGetRequest.builder() + .multiLastForSubjects("multi.last") + .upToSequence(1) + .upToTime(time) + .build(); + assertEquals(Collections.singletonList("multi.last"), multiLast.getMultiLastForSubjects()); + assertEquals(1, multiLast.getUpToSequence()); + assertEquals(time, multiLast.getUpToTime()); + assertEquals("{\"multi_last\":[\"multi.last\"],\"up_to_seq\":1,\"up_to_time\":\"1970-01-01T00:00:00.000000000Z\"}", multiLast.toJson()); + + MessageBatchGetRequest multiLastAlternative = MessageBatchGetRequest.builder() + .multiLastForSubjects(multiLastFor) + .build(); + assertEquals(multiLastFor, multiLastAlternative.getMultiLastForSubjects()); + assertEquals("{\"multi_last\":[\"multi.last\"]}", multiLastAlternative.toJson()); + } } From d2636672d6a0beca6f8a386c43becf31b4961907 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Tue, 8 Oct 2024 15:18:02 -0400 Subject: [PATCH 09/17] Easier access to timeout in JetStream implementations (#1236) --- .../io/nats/client/impl/NatsFeatureBase.java | 2 +- .../io/nats/client/impl/NatsJetStream.java | 4 +-- .../nats/client/impl/NatsJetStreamImpl.java | 18 ++++++++---- .../client/impl/NatsJetStreamManagement.java | 28 +++++++++---------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/nats/client/impl/NatsFeatureBase.java b/src/main/java/io/nats/client/impl/NatsFeatureBase.java index cc2b9b38b..fae6ed4df 100644 --- a/src/main/java/io/nats/client/impl/NatsFeatureBase.java +++ b/src/main/java/io/nats/client/impl/NatsFeatureBase.java @@ -88,7 +88,7 @@ protected void visitSubject(List subjects, DeliverPolicy deliverPolicy, .configuration(ccb.build()) .build(); - Duration timeout = js.jso.getRequestTimeout(); + Duration timeout = js.getTimeout(); JetStreamSubscription sub = js.subscribe(null, pso); try { boolean lastWasNull = false; diff --git a/src/main/java/io/nats/client/impl/NatsJetStream.java b/src/main/java/io/nats/client/impl/NatsJetStream.java index 27ed3ad22..4f2b85844 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStream.java +++ b/src/main/java/io/nats/client/impl/NatsJetStream.java @@ -150,9 +150,7 @@ private PublishAck publishSyncInternal(String subject, Headers headers, byte[] d return null; } - Duration timeout = options == null ? jso.getRequestTimeout() : options.getStreamTimeout(); - - Message resp = makeInternalRequestResponseRequired(subject, merged, data, timeout, CancelAction.COMPLETE, validateSubjectAndReplyTo); + Message resp = makeInternalRequestResponseRequired(subject, merged, data, getTimeout(), CancelAction.COMPLETE, validateSubjectAndReplyTo); return processPublishResponse(resp, options); } diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java index b712cff27..b010ead63 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java @@ -45,6 +45,7 @@ public CachedStreamInfo(StreamInfo si) { final NatsConnection conn; final JetStreamOptions jso; + final Duration timeout; final boolean consumerCreate290Available; final boolean multipleSubjectFilter210Available; final boolean directBatchGet211Available; @@ -59,8 +60,8 @@ public CachedStreamInfo(StreamInfo si) { // Clone the input jsOptions (JetStreamOptions.builder(...) handles null. // If jsOptions is not supplied or the jsOptions request timeout // was not set, use the connection options connect timeout. - Duration rt = jsOptions == null || jsOptions.getRequestTimeout() == null ? conn.getOptions().getConnectionTimeout() : jsOptions.getRequestTimeout(); - jso = JetStreamOptions.builder(jsOptions).requestTimeout(rt).build(); + timeout = jsOptions == null || jsOptions.getRequestTimeout() == null ? conn.getOptions().getConnectionTimeout() : jsOptions.getRequestTimeout(); + jso = JetStreamOptions.builder(jsOptions).requestTimeout(timeout).build(); consumerCreate290Available = conn.getInfo().isSameOrNewerThanVersion("2.9.0") && !jso.isOptOut290ConsumerCreate(); multipleSubjectFilter210Available = conn.getInfo().isNewerVersionThan("2.9.99"); @@ -70,17 +71,22 @@ public CachedStreamInfo(StreamInfo si) { NatsJetStreamImpl(NatsJetStreamImpl impl) { conn = impl.conn; jso = impl.jso; + timeout = impl.timeout; consumerCreate290Available = impl.consumerCreate290Available; multipleSubjectFilter210Available = impl.multipleSubjectFilter210Available; directBatchGet211Available = impl.directBatchGet211Available; } + Duration getTimeout() { + return timeout; + } + // ---------------------------------------------------------------------------------------------------- // Management that is also needed by regular context // ---------------------------------------------------------------------------------------------------- ConsumerInfo _getConsumerInfo(String streamName, String consumerName) throws IOException, JetStreamApiException { String subj = String.format(JSAPI_CONSUMER_INFO, streamName, consumerName); - Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, null, getTimeout()); return new ConsumerInfo(resp).throwOnHasError(); } @@ -122,7 +128,7 @@ else if (durable == null) { } ConsumerCreateRequest ccr = new ConsumerCreateRequest(streamName, config, action); - Message resp = makeRequestResponseRequired(subj, ccr.serialize(), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, ccr.serialize(), getTimeout()); return new ConsumerInfo(resp).throwOnHasError(); } @@ -147,7 +153,7 @@ StreamInfo _getStreamInfo(String streamName, StreamInfoOptions options) throws I String subj = String.format(JSAPI_STREAM_INFO, streamName); StreamInfoReader sir = new StreamInfoReader(); while (sir.hasMore()) { - Message resp = makeRequestResponseRequired(subj, sir.nextJson(options), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, sir.nextJson(options), getTimeout()); sir.process(resp); } return cacheStreamInfo(streamName, sir.getStreamInfo()); @@ -170,7 +176,7 @@ List cacheStreamInfo(List list) { List _getStreamNames(String subjectFilter) throws IOException, JetStreamApiException { StreamNamesReader snr = new StreamNamesReader(); while (snr.hasMore()) { - Message resp = makeRequestResponseRequired(JSAPI_STREAM_NAMES, snr.nextJson(subjectFilter), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(JSAPI_STREAM_NAMES, snr.nextJson(subjectFilter), getTimeout()); snr.process(resp); } return snr.getStrings(); diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index 7ac235f55..b5f69aee0 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -41,7 +41,7 @@ public NatsJetStreamManagement(NatsConnection connection, JetStreamOptions jsOpt */ @Override public AccountStatistics getAccountStatistics() throws IOException, JetStreamApiException { - Message resp = makeRequestResponseRequired(JSAPI_ACCOUNT_INFO, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(JSAPI_ACCOUNT_INFO, null, getTimeout()); return new AccountStatistics(resp).throwOnHasError(); } @@ -69,7 +69,7 @@ private StreamInfo addOrUpdateStream(StreamConfiguration config, String template } String subj = String.format(template, streamName); - Message resp = makeRequestResponseRequired(subj, config.toJson().getBytes(StandardCharsets.UTF_8), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, config.toJson().getBytes(StandardCharsets.UTF_8), getTimeout()); return createAndCacheStreamInfoThrowOnError(streamName, resp); } @@ -80,7 +80,7 @@ private StreamInfo addOrUpdateStream(StreamConfiguration config, String template public boolean deleteStream(String streamName) throws IOException, JetStreamApiException { validateNotNull(streamName, "Stream Name"); String subj = String.format(JSAPI_STREAM_DELETE, streamName); - Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, null, getTimeout()); return new SuccessApiResponse(resp).throwOnHasError().getSuccess(); } @@ -109,7 +109,7 @@ public StreamInfo getStreamInfo(String streamName, StreamInfoOptions options) th public PurgeResponse purgeStream(String streamName) throws IOException, JetStreamApiException { validateNotNull(streamName, "Stream Name"); String subj = String.format(JSAPI_STREAM_PURGE, streamName); - Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, null, getTimeout()); return new PurgeResponse(resp).throwOnHasError(); } @@ -122,7 +122,7 @@ public PurgeResponse purgeStream(String streamName, PurgeOptions options) throws validateNotNull(options, "Purge Options"); String subj = String.format(JSAPI_STREAM_PURGE, streamName); byte[] body = options.toJson().getBytes(StandardCharsets.UTF_8); - Message resp = makeRequestResponseRequired(subj, body, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, body, getTimeout()); return new PurgeResponse(resp).throwOnHasError(); } @@ -164,7 +164,7 @@ public boolean deleteConsumer(String streamName, String consumerName) throws IOE validateNotNull(streamName, "Stream Name"); validateNotNull(consumerName, "Consumer Name"); String subj = String.format(JSAPI_CONSUMER_DELETE, streamName, consumerName); - Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, null, getTimeout()); return new SuccessApiResponse(resp).throwOnHasError().getSuccess(); } @@ -177,7 +177,7 @@ public ConsumerPauseResponse pauseConsumer(String streamName, String consumerNam validateNotNull(consumerName, "Consumer Name"); String subj = String.format(JSAPI_CONSUMER_PAUSE, streamName, consumerName); ConsumerPauseRequest pauseRequest = new ConsumerPauseRequest(pauseUntil); - Message resp = makeRequestResponseRequired(subj, pauseRequest.serialize(), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, pauseRequest.serialize(), getTimeout()); return new ConsumerPauseResponse(resp).throwOnHasError(); } @@ -189,7 +189,7 @@ public boolean resumeConsumer(String streamName, String consumerName) throws IOE validateNotNull(streamName, "Stream Name"); validateNotNull(consumerName, "Consumer Name"); String subj = String.format(JSAPI_CONSUMER_PAUSE, streamName, consumerName); - Message resp = makeRequestResponseRequired(subj, null, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, null, getTimeout()); ConsumerPauseResponse response = new ConsumerPauseResponse(resp).throwOnHasError(); return !response.isPaused(); } @@ -216,7 +216,7 @@ private List getConsumerNames(String streamName, String filter) throws I String subj = String.format(JSAPI_CONSUMER_NAMES, streamName); ConsumerNamesReader cnr = new ConsumerNamesReader(); while (cnr.hasMore()) { - Message resp = makeRequestResponseRequired(subj, cnr.nextJson(filter), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, cnr.nextJson(filter), getTimeout()); cnr.process(resp); } return cnr.getStrings(); @@ -230,7 +230,7 @@ public List getConsumers(String streamName) throws IOException, Je String subj = String.format(JSAPI_CONSUMER_LIST, streamName); ConsumerListReader clg = new ConsumerListReader(); while (clg.hasMore()) { - Message resp = makeRequestResponseRequired(subj, clg.nextJson(), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, clg.nextJson(), getTimeout()); clg.process(resp); } return clg.getConsumers(); @@ -264,7 +264,7 @@ public List getStreams() throws IOException, JetStreamApiException { public List getStreams(String subjectFilter) throws IOException, JetStreamApiException { StreamListReader slr = new StreamListReader(); while (slr.hasMore()) { - Message resp = makeRequestResponseRequired(JSAPI_STREAM_LIST, slr.nextJson(subjectFilter), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(JSAPI_STREAM_LIST, slr.nextJson(subjectFilter), getTimeout()); slr.process(resp); } return cacheStreamInfo(slr.getStreams()); @@ -332,7 +332,7 @@ private MessageInfo _getMessage(String streamName, MessageGetRequest messageGetR subject = String.format(JSAPI_DIRECT_GET, streamName); payload = messageGetRequest.serialize(); } - Message resp = makeRequestResponseRequired(subject, payload, jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subject, payload, getTimeout()); if (resp.isStatusMessage()) { throw new JetStreamApiException(Error.convert(resp.getStatus())); } @@ -340,7 +340,7 @@ private MessageInfo _getMessage(String streamName, MessageGetRequest messageGetR } else { String getSubject = String.format(JSAPI_MSG_GET, streamName); - Message resp = makeRequestResponseRequired(getSubject, messageGetRequest.serialize(), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(getSubject, messageGetRequest.serialize(), getTimeout()); return new MessageInfo(resp, streamName, false).throwOnHasError(); } } @@ -464,7 +464,7 @@ public boolean deleteMessage(String streamName, long seq, boolean erase) throws validateNotNull(streamName, "Stream Name"); String subj = String.format(JSAPI_MSG_DELETE, streamName); MessageDeleteRequest mdr = new MessageDeleteRequest(seq, erase); - Message resp = makeRequestResponseRequired(subj, mdr.serialize(), jso.getRequestTimeout()); + Message resp = makeRequestResponseRequired(subj, mdr.serialize(), getTimeout()); return new SuccessApiResponse(resp).throwOnHasError().getSuccess(); } From 30b264c924a2ef23c6eaab3f5fd4d9d009ad61fd Mon Sep 17 00:00:00 2001 From: Maurice van Veen Date: Wed, 9 Oct 2024 16:15:25 +0200 Subject: [PATCH 10/17] Use internal timeout in direct batch get, remove configuration (#1235) --- .../client/api/MessageBatchGetRequest.java | 25 ------------------- .../client/impl/NatsJetStreamManagement.java | 4 +-- .../client/impl/JetStreamManagementTests.java | 5 ---- 3 files changed, 2 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/nats/client/api/MessageBatchGetRequest.java b/src/main/java/io/nats/client/api/MessageBatchGetRequest.java index 3be679f43..860497b2d 100644 --- a/src/main/java/io/nats/client/api/MessageBatchGetRequest.java +++ b/src/main/java/io/nats/client/api/MessageBatchGetRequest.java @@ -31,7 +31,6 @@ */ public class MessageBatchGetRequest implements JsonSerializable { - private final Duration timeout; private final int batch; private final int maxBytes; private final long sequence; @@ -42,7 +41,6 @@ public class MessageBatchGetRequest implements JsonSerializable { private final ZonedDateTime upToTime; MessageBatchGetRequest(Builder b) { - this.timeout = b.timeout; this.batch = b.batch; this.maxBytes = b.maxBytes; this.sequence = b.sequence; @@ -53,15 +51,6 @@ public class MessageBatchGetRequest implements JsonSerializable { this.upToTime = b.upToTime; } - /** - * Timeout used for the request. - * - * @return Duration - */ - public Duration getTimeout() { - return timeout; - } - /** * Maximum amount of messages to be returned for this request. * @@ -178,7 +167,6 @@ public static Builder builder(MessageBatchGetRequest req) { *

{@code MessageBatchGetRequest.builder().build()} will create a default {@link MessageBatchGetRequest}. */ public static class Builder { - private Duration timeout = Duration.ofSeconds(5); private int batch = -1; private int maxBytes = -1; private long sequence = -1; @@ -201,7 +189,6 @@ public Builder() { */ public Builder(MessageBatchGetRequest req) { if (req != null) { - this.timeout = req.timeout; this.batch = req.batch; this.maxBytes = req.maxBytes; this.sequence = req.sequence; @@ -213,18 +200,6 @@ public Builder(MessageBatchGetRequest req) { } } - /** - * Set the timeout used for the request. - * - * @param timeout the timeout - * @return Builder - */ - public Builder timeout(Duration timeout) { - validateDurationRequired(timeout); - this.timeout = timeout; - return this; - } - /** * Set the maximum amount of messages to be returned for this request. * diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index b5f69aee0..21ecf8a98 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -389,9 +389,9 @@ public void _requestMessageBatch(String streamName, MessageBatchGetRequest messa String requestSubject = prependPrefix(String.format(JSAPI_DIRECT_GET, streamName)); conn.publish(requestSubject, replyTo, messageBatchGetRequest.serialize()); - long start = System.currentTimeMillis(); - long maxTimeMillis = messageBatchGetRequest.getTimeout().toMillis(); + long maxTimeMillis = getTimeout().toMillis(); long timeLeft = maxTimeMillis; + long start = System.currentTimeMillis(); while (true) { Message msg = sub.nextMessage(timeLeft); if (msg == null) { diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index e05f21434..757b79f2d 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -1729,16 +1729,11 @@ public void testBatchDirectGetMultiLast() throws Exception { @Test public void testBatchDirectGetBuilder() { - // Default timeout - assertEquals(Duration.ofSeconds(5), MessageBatchGetRequest.builder().build().getTimeout()); - // Request options. MessageBatchGetRequest requestOptions = MessageBatchGetRequest.builder() - .timeout(Duration.ofSeconds(1)) .maxBytes(1234) .batch(2) .build(); - assertEquals(Duration.ofSeconds(1), requestOptions.getTimeout()); assertEquals(1234, requestOptions.getMaxBytes()); assertEquals(2, requestOptions.getBatch()); assertEquals("{\"batch\":2,\"max_bytes\":1234}", requestOptions.toJson()); From 43f9a011e3f8d1ef35a4e94e3eb292aec09d6eef Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Thu, 10 Oct 2024 16:14:37 -0400 Subject: [PATCH 11/17] Minor object store improvement - complete faster (#1237) * Minor object store improvement - complete faster * validation order * short circuit --- .../io/nats/client/impl/NatsFeatureBase.java | 2 +- .../io/nats/client/impl/NatsObjectStore.java | 32 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/nats/client/impl/NatsFeatureBase.java b/src/main/java/io/nats/client/impl/NatsFeatureBase.java index fae6ed4df..7f1490a5c 100644 --- a/src/main/java/io/nats/client/impl/NatsFeatureBase.java +++ b/src/main/java/io/nats/client/impl/NatsFeatureBase.java @@ -29,7 +29,7 @@ public class NatsFeatureBase { protected final NatsJetStream js; - protected final JetStreamManagement jsm; + protected final NatsJetStreamManagement jsm; protected String streamName; NatsFeatureBase(NatsConnection connection, FeatureOptions fo) throws IOException { diff --git a/src/main/java/io/nats/client/impl/NatsObjectStore.java b/src/main/java/io/nats/client/impl/NatsObjectStore.java index 30d697a3a..5d3118973 100644 --- a/src/main/java/io/nats/client/impl/NatsObjectStore.java +++ b/src/main/java/io/nats/client/impl/NatsObjectStore.java @@ -22,7 +22,6 @@ import java.io.*; import java.nio.file.Files; import java.security.NoSuchAlgorithmException; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -218,6 +217,7 @@ public ObjectInfo get(String objectName, OutputStream out) throws IOException, J Digester digester = new Digester(); long totalBytes = 0; long totalChunks = 0; + long expectedChunks = oi.getChunks(); // if there is one chunk, just go get the message directly and we're done. if (oi.getChunks() == 1) { @@ -236,30 +236,42 @@ public ObjectInfo get(String objectName, OutputStream out) throws IOException, J JetStreamSubscription sub = js.subscribe(rawChunkSubject(oi.getNuid()), PushSubscribeOptions.builder().stream(streamName).ordered(true).build()); - Message m = sub.nextMessage(Duration.ofSeconds(1)); + Message m = sub.nextMessage(jsm.getTimeout()); while (m != null) { + // track the byte count and chunks + long pending = m.metaData().pendingCount(); + if (expectedChunks != pending + (++totalChunks)) { + throw OsGetChunksMismatch.instance(); // short circuit, we already know there are not enough chunks. + } + byte[] data = m.getData(); + totalBytes += data.length; - // track the byte count and chunks // update the digest - // write the bytes to the output file - totalBytes += data.length; - totalChunks++; digester.update(data); + + // write the bytes to the output file out.write(data); // read until the subject is complete - m = sub.nextMessage(Duration.ofSeconds(1)); + if (pending == 0) { + break; + } + m = sub.nextMessage(jsm.getTimeout()); } - sub.unsubscribe(); + try { + sub.unsubscribe(); + } + catch (RuntimeException ignore) {} } - out.flush(); - if (totalBytes != oi.getSize()) { throw OsGetSizeMismatch.instance(); } if (totalChunks != oi.getChunks()) { throw OsGetChunksMismatch.instance(); } + if (totalBytes != oi.getSize()) { throw OsGetSizeMismatch.instance(); } if (!digester.matches(oi.getDigest())) { throw OsGetDigestMismatch.instance(); } + out.flush(); // moved after validation, no need if invalid + return oi; } From e0d25fda3b806d206f084cde56766d12f14f659f Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Thu, 10 Oct 2024 17:26:43 -0400 Subject: [PATCH 12/17] Test base code improvements (#1240) --- .../java/io/nats/client/utils/TestBase.java | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/test/java/io/nats/client/utils/TestBase.java b/src/test/java/io/nats/client/utils/TestBase.java index 5f41f5173..c048b8655 100644 --- a/src/test/java/io/nats/client/utils/TestBase.java +++ b/src/test/java/io/nats/client/utils/TestBase.java @@ -237,10 +237,8 @@ public static void runInServer(boolean debug, boolean jetstream, Options.Builder { initRunServerInfo(nc); - if (vc != null) { - if (!vc.runTest(RUN_SERVER_INFO)) { - return; - } + if (vc != null && !vc.runTest(RUN_SERVER_INFO)) { + return; } try { @@ -257,41 +255,52 @@ public static void runInServer(boolean debug, boolean jetstream, Options.Builder public static class LongRunningNatsTestServer extends NatsTestServer { public final boolean jetstream; public final Options.Builder builder; - public final Connection nc; public LongRunningNatsTestServer(boolean debug, boolean jetstream, Options.Builder builder) throws IOException, InterruptedException { super(debug, jetstream); this.jetstream = jetstream; this.builder = builder == null ? new Options.Builder() : builder; - nc = connect(); - initRunServerInfo(nc); } public Connection connect() throws IOException, InterruptedException { return standardConnection(builder.server(getURI()).build()); } - @Override - public void close() throws Exception { - try { nc.close(); } catch (Exception ignore) {}; - super.close(); - } - public void run(InServerTest inServerTest) throws Exception { - run(null, inServerTest); + run(null, null, inServerTest); } public void run(VersionCheck vc, InServerTest inServerTest) throws Exception { - if (vc != null && !vc.runTest(RUN_SERVER_INFO)) { - return; + run(null, vc, inServerTest); + } + + public void run(Options.Builder builder, VersionCheck vc, InServerTest inServerTest) throws Exception { + if (vc != null && RUN_SERVER_INFO != null) { + if (!vc.runTest(RUN_SERVER_INFO)) { + return; + } + vc = null; // since we've already determined it should run, null this out so we don't check below } - try { - inServerTest.test(nc); + if (builder == null) { + builder = new Options.Builder(); } - finally { - if (jetstream) { - cleanupJs(nc); + + try (Connection nc = standardConnection(builder.server(getURI()).build())) + { + initRunServerInfo(nc); + + if (vc != null && !vc.runTest(RUN_SERVER_INFO)) { + return; + } + + try { + inServerTest.test(nc); + } + finally { + if (jetstream) { + cleanupJs(nc); + } } } } From dd8a3a13cfe78c4861a83018219a92d1acd9313c Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Fri, 11 Oct 2024 13:07:13 -0400 Subject: [PATCH 13/17] Extract from main 2.11 branch things not specific to 2.11 (#1241) --- .../java/io/nats/client/api/SourceBase.java | 4 +- .../io/nats/client/impl/NatsJetStream.java | 6 +- .../io/nats/client/support/ApiConstants.java | 5 ++ .../java/io/nats/client/support/Status.java | 1 + .../io/nats/client/support/Validator.java | 2 +- .../nats/client/support/ValidatorTests.java | 82 +++++++++---------- 6 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/main/java/io/nats/client/api/SourceBase.java b/src/main/java/io/nats/client/api/SourceBase.java index 9dfbdfa8c..3b499e427 100644 --- a/src/main/java/io/nats/client/api/SourceBase.java +++ b/src/main/java/io/nats/client/api/SourceBase.java @@ -29,7 +29,7 @@ import static io.nats.client.support.JsonUtils.beginJson; import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readValue; -import static io.nats.client.support.Validator.consumerFilterSubjectsAreEquivalent; +import static io.nats.client.support.Validator.listsAreEquivalent; public abstract class SourceBase implements JsonSerializable { private final String name; @@ -194,7 +194,7 @@ public boolean equals(Object o) { if (!Objects.equals(filterSubject, that.filterSubject)) return false; if (!Objects.equals(external, that.external)) return false; - return consumerFilterSubjectsAreEquivalent(subjectTransforms, that.subjectTransforms); + return listsAreEquivalent(subjectTransforms, that.subjectTransforms); } @Override diff --git a/src/main/java/io/nats/client/impl/NatsJetStream.java b/src/main/java/io/nats/client/impl/NatsJetStream.java index 4f2b85844..c3c612a0b 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStream.java +++ b/src/main/java/io/nats/client/impl/NatsJetStream.java @@ -395,7 +395,7 @@ else if (!serverCC.getDeliverGroup().equals(settledDeliverGroup)) { settledFilterSubjects = serverCC.getFilterSubjects(); } } - else if (!consumerFilterSubjectsAreEquivalent(settledFilterSubjects, serverCC.getFilterSubjects())) { + else if (!listsAreEquivalent(settledFilterSubjects, serverCC.getFilterSubjects())) { throw JsSubSubjectDoesNotMatchFilter.instance(); } @@ -528,9 +528,9 @@ public List getChanges(ConsumerConfiguration serverCc) { if (deliverSubject != null && !deliverSubject.equals(serverCcc.deliverSubject)) { changes.add("deliverSubject"); } if (deliverGroup != null && !deliverGroup.equals(serverCcc.deliverGroup)) { changes.add("deliverGroup"); } - if (backoff != null && !consumerFilterSubjectsAreEquivalent(backoff, serverCcc.backoff)) { changes.add("backoff"); } + if (backoff != null && !listsAreEquivalent(backoff, serverCcc.backoff)) { changes.add("backoff"); } if (metadata != null && !mapsAreEquivalent(metadata, serverCcc.metadata)) { changes.add("metadata"); } - if (filterSubjects != null && !consumerFilterSubjectsAreEquivalent(filterSubjects, serverCcc.filterSubjects)) { changes.add("filterSubjects"); } + if (filterSubjects != null && !listsAreEquivalent(filterSubjects, serverCcc.filterSubjects)) { changes.add("filterSubjects"); } // do not need to check Durable because the original is retrieved by the durable name diff --git a/src/main/java/io/nats/client/support/ApiConstants.java b/src/main/java/io/nats/client/support/ApiConstants.java index e3479b4b8..823f60dcc 100644 --- a/src/main/java/io/nats/client/support/ApiConstants.java +++ b/src/main/java/io/nats/client/support/ApiConstants.java @@ -78,6 +78,7 @@ public interface ApiConstants { String FIRST_TS = "first_ts"; String FLOW_CONTROL = "flow_control"; String GO = "go"; + String GROUP = "group"; String HDRS = "hdrs"; String HEADERS = "headers"; String HEADERS_ONLY = "headers_only"; @@ -117,6 +118,8 @@ public interface ApiConstants { String MAX_STORAGE = "max_storage"; String MAX_STREAMS = "max_streams"; String MAX_WAITING = "max_waiting"; // this is correct! the meaning name is different than the field name + String MIN_PENDING = "min_pending"; + String MIN_ACK_PENDING = "min_ack_pending"; String MEM_STORAGE = "mem_storage"; String MEMORY = "memory"; String MEMORY_MAX_STREAM_BYTES = "memory_max_stream_bytes"; @@ -153,6 +156,8 @@ public interface ApiConstants { String PAUSE_UNTIL = "pause_until"; String PLACEMENT = "placement"; String PORT = "port"; + String PRIORITY_GROUPS = "priority_groups"; + String PRIORITY_POLICY = "priority_policy"; String PROCESSING_TIME = "processing_time"; String PROTO = "proto"; String PURGED = "purged"; diff --git a/src/main/java/io/nats/client/support/Status.java b/src/main/java/io/nats/client/support/Status.java index ecdf0ab79..dd2de4705 100644 --- a/src/main/java/io/nats/client/support/Status.java +++ b/src/main/java/io/nats/client/support/Status.java @@ -27,6 +27,7 @@ public class Status { public static final int NOT_FOUND_CODE = 404; public static final int REQUEST_TIMEOUT_CODE = 408; public static final int CONFLICT_CODE = 409; + public static final int EOB = 204; public static String BAD_REQUEST = "Bad Request"; // 400 public static String NO_MESSAGES = "No Messages"; // 404 diff --git a/src/main/java/io/nats/client/support/Validator.java b/src/main/java/io/nats/client/support/Validator.java index e8dcbf109..e650da816 100644 --- a/src/main/java/io/nats/client/support/Validator.java +++ b/src/main/java/io/nats/client/support/Validator.java @@ -623,7 +623,7 @@ public static boolean isSemVer(String s) { // This function tests filter subject equivalency // It does not care what order and also assumes that there are no duplicates. // From the server: consumer subject filters cannot overlap [10138] - public static boolean consumerFilterSubjectsAreEquivalent(List l1, List l2) + public static boolean listsAreEquivalent(List l1, List l2) { if (l1 == null || l1.isEmpty()) { return l2 == null || l2.isEmpty(); diff --git a/src/test/java/io/nats/client/support/ValidatorTests.java b/src/test/java/io/nats/client/support/ValidatorTests.java index 381dc1f38..2563db948 100644 --- a/src/test/java/io/nats/client/support/ValidatorTests.java +++ b/src/test/java/io/nats/client/support/ValidatorTests.java @@ -560,47 +560,47 @@ public void testConsumerFilterSubjectsAreEquivalent() { List l5 = null; List l6 = new ArrayList<>(); - assertTrue(consumerFilterSubjectsAreEquivalent(l1, l1)); - assertTrue(consumerFilterSubjectsAreEquivalent(l1, l2)); - assertFalse(consumerFilterSubjectsAreEquivalent(l1, l3)); - assertFalse(consumerFilterSubjectsAreEquivalent(l1, l4)); - assertFalse(consumerFilterSubjectsAreEquivalent(l1, l5)); - assertFalse(consumerFilterSubjectsAreEquivalent(l1, l6)); - - assertTrue(consumerFilterSubjectsAreEquivalent(l2, l1)); - assertTrue(consumerFilterSubjectsAreEquivalent(l2, l2)); - assertFalse(consumerFilterSubjectsAreEquivalent(l2, l3)); - assertFalse(consumerFilterSubjectsAreEquivalent(l2, l4)); - assertFalse(consumerFilterSubjectsAreEquivalent(l2, l5)); - assertFalse(consumerFilterSubjectsAreEquivalent(l2, l6)); - - assertFalse(consumerFilterSubjectsAreEquivalent(l3, l1)); - assertFalse(consumerFilterSubjectsAreEquivalent(l3, l2)); - assertTrue(consumerFilterSubjectsAreEquivalent(l3, l3)); - assertFalse(consumerFilterSubjectsAreEquivalent(l3, l4)); - assertFalse(consumerFilterSubjectsAreEquivalent(l3, l5)); - assertFalse(consumerFilterSubjectsAreEquivalent(l3, l6)); - - assertFalse(consumerFilterSubjectsAreEquivalent(l4, l1)); - assertFalse(consumerFilterSubjectsAreEquivalent(l4, l2)); - assertFalse(consumerFilterSubjectsAreEquivalent(l4, l3)); - assertTrue(consumerFilterSubjectsAreEquivalent(l4, l4)); - assertFalse(consumerFilterSubjectsAreEquivalent(l4, l5)); - assertFalse(consumerFilterSubjectsAreEquivalent(l4, l6)); - - assertFalse(consumerFilterSubjectsAreEquivalent(l5, l1)); - assertFalse(consumerFilterSubjectsAreEquivalent(l5, l2)); - assertFalse(consumerFilterSubjectsAreEquivalent(l5, l3)); - assertFalse(consumerFilterSubjectsAreEquivalent(l5, l4)); - assertTrue(consumerFilterSubjectsAreEquivalent(l5, l5)); - assertTrue(consumerFilterSubjectsAreEquivalent(l5, l6)); - - assertFalse(consumerFilterSubjectsAreEquivalent(l6, l1)); - assertFalse(consumerFilterSubjectsAreEquivalent(l6, l2)); - assertFalse(consumerFilterSubjectsAreEquivalent(l6, l3)); - assertFalse(consumerFilterSubjectsAreEquivalent(l6, l4)); - assertTrue(consumerFilterSubjectsAreEquivalent(l6, l5)); - assertTrue(consumerFilterSubjectsAreEquivalent(l6, l6)); + assertTrue(listsAreEquivalent(l1, l1)); + assertTrue(listsAreEquivalent(l1, l2)); + assertFalse(listsAreEquivalent(l1, l3)); + assertFalse(listsAreEquivalent(l1, l4)); + assertFalse(listsAreEquivalent(l1, l5)); + assertFalse(listsAreEquivalent(l1, l6)); + + assertTrue(listsAreEquivalent(l2, l1)); + assertTrue(listsAreEquivalent(l2, l2)); + assertFalse(listsAreEquivalent(l2, l3)); + assertFalse(listsAreEquivalent(l2, l4)); + assertFalse(listsAreEquivalent(l2, l5)); + assertFalse(listsAreEquivalent(l2, l6)); + + assertFalse(listsAreEquivalent(l3, l1)); + assertFalse(listsAreEquivalent(l3, l2)); + assertTrue(listsAreEquivalent(l3, l3)); + assertFalse(listsAreEquivalent(l3, l4)); + assertFalse(listsAreEquivalent(l3, l5)); + assertFalse(listsAreEquivalent(l3, l6)); + + assertFalse(listsAreEquivalent(l4, l1)); + assertFalse(listsAreEquivalent(l4, l2)); + assertFalse(listsAreEquivalent(l4, l3)); + assertTrue(listsAreEquivalent(l4, l4)); + assertFalse(listsAreEquivalent(l4, l5)); + assertFalse(listsAreEquivalent(l4, l6)); + + assertFalse(listsAreEquivalent(l5, l1)); + assertFalse(listsAreEquivalent(l5, l2)); + assertFalse(listsAreEquivalent(l5, l3)); + assertFalse(listsAreEquivalent(l5, l4)); + assertTrue(listsAreEquivalent(l5, l5)); + assertTrue(listsAreEquivalent(l5, l6)); + + assertFalse(listsAreEquivalent(l6, l1)); + assertFalse(listsAreEquivalent(l6, l2)); + assertFalse(listsAreEquivalent(l6, l3)); + assertFalse(listsAreEquivalent(l6, l4)); + assertTrue(listsAreEquivalent(l6, l5)); + assertTrue(listsAreEquivalent(l6, l6)); } @Test From 1fbee9ba12f3fe95fc3d162c9d5fe12d69fdbf6f Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Tue, 15 Oct 2024 12:47:18 -0400 Subject: [PATCH 14/17] Extract non 211 Part 2 (#1242) --- .../java/io/nats/client/FeatureOptions.java | 3 +- .../java/io/nats/client/SubscribeOptions.java | 3 +- .../client/impl/JetStreamGeneralTests.java | 18 ++++---- .../client/impl/JetStreamManagementTests.java | 24 +++++------ .../nats/client/impl/JetStreamPullTests.java | 26 ++++++------ .../client/impl/JetStreamPushQueueTests.java | 2 +- .../nats/client/impl/JetStreamTestBase.java | 6 +-- .../nats/client/impl/SimplificationTests.java | 42 +++++++++---------- .../nats/client/support/ValidatorTests.java | 2 +- 9 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/main/java/io/nats/client/FeatureOptions.java b/src/main/java/io/nats/client/FeatureOptions.java index 1ca3cb8be..38b085e1b 100644 --- a/src/main/java/io/nats/client/FeatureOptions.java +++ b/src/main/java/io/nats/client/FeatureOptions.java @@ -22,8 +22,7 @@ public abstract class FeatureOptions { private final JetStreamOptions jso; - @SuppressWarnings("rawtypes") // Don't need the type of the builder to get its vars - protected FeatureOptions(Builder b) { + protected FeatureOptions(Builder b) { jso = b.jsoBuilder.build(); } diff --git a/src/main/java/io/nats/client/SubscribeOptions.java b/src/main/java/io/nats/client/SubscribeOptions.java index 74a75907e..acfd3e03b 100644 --- a/src/main/java/io/nats/client/SubscribeOptions.java +++ b/src/main/java/io/nats/client/SubscribeOptions.java @@ -38,8 +38,7 @@ public abstract class SubscribeOptions { protected final long pendingByteLimit; // Only applicable for non dispatched (sync) push consumers. protected final String name; - @SuppressWarnings("rawtypes") // Don't need the type of the builder to get its vars - protected SubscribeOptions(Builder builder, boolean isPull, + protected SubscribeOptions(Builder builder, boolean isPull, String deliverSubject, String deliverGroup, long pendingMessageLimit, long pendingByteLimit) { diff --git a/src/test/java/io/nats/client/impl/JetStreamGeneralTests.java b/src/test/java/io/nats/client/impl/JetStreamGeneralTests.java index 138cf238c..9a47ad375 100644 --- a/src/test/java/io/nats/client/impl/JetStreamGeneralTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamGeneralTests.java @@ -462,7 +462,7 @@ public void testBindPush() throws Exception { jsPublish(js, tsc.subject(), 1, 1); PushSubscribeOptions pso = PushSubscribeOptions.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .build(); JetStreamSubscription s = js.subscribe(tsc.subject(), pso); Message m = s.nextMessage(DEFAULT_TIMEOUT); @@ -474,7 +474,7 @@ public void testBindPush() throws Exception { jsPublish(js, tsc.subject(), 2, 1); pso = PushSubscribeOptions.builder() .stream(tsc.stream) - .durable(tsc.name()) + .durable(tsc.consumerName()) .bind(true) .build(); s = js.subscribe(tsc.subject(), pso); @@ -485,7 +485,7 @@ public void testBindPush() throws Exception { unsubscribeEnsureNotBound(s); jsPublish(js, tsc.subject(), 3, 1); - pso = PushSubscribeOptions.bind(tsc.stream, tsc.name()); + pso = PushSubscribeOptions.bind(tsc.stream, tsc.consumerName()); s = js.subscribe(tsc.subject(), pso); m = s.nextMessage(DEFAULT_TIMEOUT); assertNotNull(m); @@ -495,7 +495,7 @@ public void testBindPush() throws Exception { () -> PushSubscribeOptions.builder().stream(tsc.stream).bind(true).build()); assertThrows(IllegalArgumentException.class, - () -> PushSubscribeOptions.builder().durable(tsc.name()).bind(true).build()); + () -> PushSubscribeOptions.builder().durable(tsc.consumerName()).bind(true).build()); assertThrows(IllegalArgumentException.class, () -> PushSubscribeOptions.builder().stream(EMPTY).bind(true).build()); @@ -514,7 +514,7 @@ public void testBindPull() throws Exception { jsPublish(js, tsc.subject(), 1, 1); PullSubscribeOptions pso = PullSubscribeOptions.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .build(); JetStreamSubscription s = js.subscribe(tsc.subject(), pso); s.pull(1); @@ -527,7 +527,7 @@ public void testBindPull() throws Exception { jsPublish(js, tsc.subject(), 2, 1); pso = PullSubscribeOptions.builder() .stream(tsc.stream) - .durable(tsc.name()) + .durable(tsc.consumerName()) .bind(true) .build(); s = js.subscribe(tsc.subject(), pso); @@ -539,7 +539,7 @@ public void testBindPull() throws Exception { unsubscribeEnsureNotBound(s); jsPublish(js, tsc.subject(), 3, 1); - pso = PullSubscribeOptions.bind(tsc.stream, tsc.name()); + pso = PullSubscribeOptions.bind(tsc.stream, tsc.consumerName()); s = js.subscribe(tsc.subject(), pso); s.pull(1); m = s.nextMessage(DEFAULT_TIMEOUT); @@ -960,9 +960,9 @@ public void testInternalLookupConsumerInfoCoverage() throws Exception { // - consumer not found // - stream does not exist JetStreamSubscription sub = js.subscribe(tsc.subject()); - assertNull(((NatsJetStream)js).lookupConsumerInfo(tsc.stream, tsc.name())); + assertNull(((NatsJetStream)js).lookupConsumerInfo(tsc.stream, tsc.consumerName())); assertThrows(JetStreamApiException.class, - () -> ((NatsJetStream)js).lookupConsumerInfo(stream(999), tsc.name())); + () -> ((NatsJetStream)js).lookupConsumerInfo(stream(999), tsc.consumerName())); }); } diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index 757b79f2d..2012b6ee2 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -827,7 +827,7 @@ public void testAddPausedConsumer() throws Exception { ZonedDateTime pauseUntil = ZonedDateTime.now(ZONE_ID_GMT).plusMinutes(2); ConsumerConfiguration cc = ConsumerConfiguration.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .pauseUntil(pauseUntil) .build(); @@ -849,7 +849,7 @@ public void testPauseResumeConsumer() throws Exception { assertEquals(0, list.size()); ConsumerConfiguration cc = ConsumerConfiguration.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .build(); // durable and name can both be null @@ -886,9 +886,9 @@ public void testPauseResumeConsumer() throws Exception { ci = jsm.getConsumerInfo(tsc.stream, ci.getName()); assertFalse(ci.getPaused()); - assertThrows(JetStreamApiException.class, () -> jsm.pauseConsumer(stream(), tsc.name(), pauseUntil)); + assertThrows(JetStreamApiException.class, () -> jsm.pauseConsumer(stream(), tsc.consumerName(), pauseUntil)); assertThrows(JetStreamApiException.class, () -> jsm.pauseConsumer(tsc.stream, name(), pauseUntil)); - assertThrows(JetStreamApiException.class, () -> jsm.resumeConsumer(stream(), tsc.name())); + assertThrows(JetStreamApiException.class, () -> jsm.resumeConsumer(stream(), tsc.consumerName())); assertThrows(JetStreamApiException.class, () -> jsm.resumeConsumer(tsc.stream, name())); }); } @@ -1009,7 +1009,7 @@ public void testConsumerMetadata() throws Exception { TestingStreamContainer tsc = new TestingStreamContainer(jsm); ConsumerConfiguration cc = ConsumerConfiguration.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .metadata(metaData) .build(); @@ -1065,14 +1065,14 @@ public void testGetConsumerInfo() throws Exception { jsServer.run(nc -> { JetStreamManagement jsm = nc.jetStreamManagement(); TestingStreamContainer tsc = new TestingStreamContainer(jsm); - assertThrows(JetStreamApiException.class, () -> jsm.getConsumerInfo(tsc.stream, tsc.name())); - ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.name()).build(); + assertThrows(JetStreamApiException.class, () -> jsm.getConsumerInfo(tsc.stream, tsc.consumerName())); + ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.consumerName()).build(); ConsumerInfo ci = jsm.addOrUpdateConsumer(tsc.stream, cc); assertEquals(tsc.stream, ci.getStreamName()); - assertEquals(tsc.name(), ci.getName()); - ci = jsm.getConsumerInfo(tsc.stream, tsc.name()); + assertEquals(tsc.consumerName(), ci.getName()); + ci = jsm.getConsumerInfo(tsc.stream, tsc.consumerName()); assertEquals(tsc.stream, ci.getStreamName()); - assertEquals(tsc.name(), ci.getName()); + assertEquals(tsc.consumerName(), ci.getName()); assertThrows(JetStreamApiException.class, () -> jsm.getConsumerInfo(tsc.stream, durable(999))); if (nc.getServerInfo().isSameOrNewerThanVersion("2.10")) { assertNotNull(ci.getTimestamp()); @@ -1228,14 +1228,14 @@ public void testConsumerReplica() throws Exception { TestingStreamContainer tsc = new TestingStreamContainer(nc); final ConsumerConfiguration cc0 = ConsumerConfiguration.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .build(); ConsumerInfo ci = jsm.addOrUpdateConsumer(tsc.stream, cc0); // server returns 0 when value is not set assertEquals(0, ci.getConsumerConfiguration().getNumReplicas()); final ConsumerConfiguration cc1 = ConsumerConfiguration.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .numReplicas(1) .build(); ci = jsm.addOrUpdateConsumer(tsc.stream, cc1); diff --git a/src/test/java/io/nats/client/impl/JetStreamPullTests.java b/src/test/java/io/nats/client/impl/JetStreamPullTests.java index 54f56ef88..3a29ca946 100644 --- a/src/test/java/io/nats/client/impl/JetStreamPullTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamPullTests.java @@ -64,12 +64,12 @@ public void testFetch() throws Exception { .build(); PullSubscribeOptions options = PullSubscribeOptions.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .configuration(cc) .build(); JetStreamSubscription sub = js.subscribe(tsc.subject(), options); - assertSubscription(sub, tsc.stream, tsc.name(), null, true); + assertSubscription(sub, tsc.stream, tsc.consumerName(), null, true); nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server List messages = sub.fetch(10, fetchDur); @@ -139,12 +139,12 @@ public void testIterate() throws Exception { .build(); PullSubscribeOptions options = PullSubscribeOptions.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .configuration(cc) .build(); JetStreamSubscription sub = js.subscribe(tsc.subject(), options); - assertSubscription(sub, tsc.stream, tsc.name(), null, true); + assertSubscription(sub, tsc.stream, tsc.consumerName(), null, true); nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server Iterator iterator = sub.iterate(10, fetchDur); @@ -218,11 +218,11 @@ public void testBasic() throws Exception { TestingStreamContainer tsc = new TestingStreamContainer(nc); // Build our subscription options. - PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.name()).build(); + PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.consumerName()).build(); // Subscribe synchronously. JetStreamSubscription sub = js.subscribe(tsc.subject(), options); - assertSubscription(sub, tsc.stream, tsc.name(), null, true); + assertSubscription(sub, tsc.stream, tsc.consumerName(), null, true); nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server // publish some amount of messages, but not entire pull size @@ -317,11 +317,11 @@ public void testNoWait() throws Exception { TestingStreamContainer tsc = new TestingStreamContainer(nc); // Build our subscription options. - PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.name()).build(); + PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.consumerName()).build(); // Subscribe synchronously. JetStreamSubscription sub = js.subscribe(tsc.subject(), options); - assertSubscription(sub, tsc.stream, tsc.name(), null, true); + assertSubscription(sub, tsc.stream, tsc.consumerName(), null, true); nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server // publish 10 messages @@ -391,11 +391,11 @@ public void testPullExpires() throws Exception { TestingStreamContainer tsc = new TestingStreamContainer(nc); // Build our subscription options. - PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.name()).build(); + PullSubscribeOptions options = PullSubscribeOptions.builder().durable(tsc.consumerName()).build(); // Subscribe synchronously. JetStreamSubscription sub = js.subscribe(tsc.subject(), options); - assertSubscription(sub, tsc.stream, tsc.name(), null, true); + assertSubscription(sub, tsc.stream, tsc.consumerName(), null, true); nc.flush(Duration.ofSeconds(1)); // flush outgoing communication with/to the server long expires = 500; // millis @@ -574,7 +574,7 @@ public void testAckWaitTimeout() throws Exception { .ackWait(1500) .build(); PullSubscribeOptions pso = PullSubscribeOptions.builder() - .durable(tsc.name()) + .durable(tsc.consumerName()) .configuration(cc) .build(); @@ -1108,10 +1108,10 @@ public void testReader() throws Exception { JetStream js = nc.jetStream(); // Pre define a consumer - ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.name()).filterSubjects(tsc.subject()).build(); + ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.consumerName()).filterSubjects(tsc.subject()).build(); jsm.addOrUpdateConsumer(tsc.stream, cc); - PullSubscribeOptions so = PullSubscribeOptions.bind(tsc.stream, tsc.name()); + PullSubscribeOptions so = PullSubscribeOptions.bind(tsc.stream, tsc.consumerName()); JetStreamSubscription sub = js.subscribe(tsc.subject(), so); JetStreamReader reader = sub.reader(500, 125); diff --git a/src/test/java/io/nats/client/impl/JetStreamPushQueueTests.java b/src/test/java/io/nats/client/impl/JetStreamPushQueueTests.java index e7c32397f..47064559b 100644 --- a/src/test/java/io/nats/client/impl/JetStreamPushQueueTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamPushQueueTests.java @@ -43,7 +43,7 @@ public void testQueueSubWorkflow() throws Exception { // - the PushSubscribeOptions can be re-used since all the subscribers are the same // - use a concurrent integer to track all the messages received // - have a list of subscribers and threads so I can track them - PushSubscribeOptions pso = PushSubscribeOptions.builder().durable(tsc.name()).build(); + PushSubscribeOptions pso = PushSubscribeOptions.builder().durable(tsc.consumerName()).build(); AtomicInteger allReceived = new AtomicInteger(); List subscribers = new ArrayList<>(); List subThreads = new ArrayList<>(); diff --git a/src/test/java/io/nats/client/impl/JetStreamTestBase.java b/src/test/java/io/nats/client/impl/JetStreamTestBase.java index a52f80849..31e2b476d 100644 --- a/src/test/java/io/nats/client/impl/JetStreamTestBase.java +++ b/src/test/java/io/nats/client/impl/JetStreamTestBase.java @@ -141,11 +141,11 @@ public String subject(Object variant) { return subjects.computeIfAbsent(variant, TestBase::subject); } - public String name() { - return name(defaultNameVariant); + public String consumerName() { + return consumerName(defaultNameVariant); } - public String name(Object variant) { + public String consumerName(Object variant) { return names.computeIfAbsent(variant, TestBase::name); } } diff --git a/src/test/java/io/nats/client/impl/SimplificationTests.java b/src/test/java/io/nats/client/impl/SimplificationTests.java index de77a3b12..0c2f9b07c 100644 --- a/src/test/java/io/nats/client/impl/SimplificationTests.java +++ b/src/test/java/io/nats/client/impl/SimplificationTests.java @@ -268,11 +268,11 @@ public void testIterableConsumer() throws Exception { JetStream js = nc.jetStream(); // Pre define a consumer - ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.name()).build(); + ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.consumerName()).build(); jsm.addOrUpdateConsumer(tsc.stream, cc); // Consumer[Context] - ConsumerContext consumerContext = js.getConsumerContext(tsc.stream, tsc.name()); + ConsumerContext consumerContext = js.getConsumerContext(tsc.stream, tsc.consumerName()); int stopCount = 500; // create the consumer then use it @@ -355,11 +355,11 @@ public void testConsumeWithHandler() throws Exception { jsPublish(js, tsc.subject(), 2500); // Pre define a consumer - ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.name()).build(); + ConsumerConfiguration cc = ConsumerConfiguration.builder().durable(tsc.consumerName()).build(); jsm.addOrUpdateConsumer(tsc.stream, cc); // Consumer[Context] - ConsumerContext consumerContext = js.getConsumerContext(tsc.stream, tsc.name()); + ConsumerContext consumerContext = js.getConsumerContext(tsc.stream, tsc.consumerName()); int stopCount = 500; @@ -428,10 +428,10 @@ public void testCoverage() throws Exception { JetStream js = nc.jetStream(); // Pre define a consumer - jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.name(1)).build()); - jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.name(2)).build()); - jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.name(3)).build()); - jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.name(4)).build()); + jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.consumerName(1)).build()); + jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.consumerName(2)).build()); + jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.consumerName(3)).build()); + jsm.addOrUpdateConsumer(tsc.stream, ConsumerConfiguration.builder().durable(tsc.consumerName(4)).build()); // Stream[Context] StreamContext sctx1 = nc.getStreamContext(tsc.stream); @@ -439,19 +439,19 @@ public void testCoverage() throws Exception { js.getStreamContext(tsc.stream); // Consumer[Context] - ConsumerContext cctx1 = nc.getConsumerContext(tsc.stream, tsc.name(1)); - ConsumerContext cctx2 = nc.getConsumerContext(tsc.stream, tsc.name(2), JetStreamOptions.DEFAULT_JS_OPTIONS); - ConsumerContext cctx3 = js.getConsumerContext(tsc.stream, tsc.name(3)); - ConsumerContext cctx4 = sctx1.getConsumerContext(tsc.name(4)); - ConsumerContext cctx5 = sctx1.createOrUpdateConsumer(ConsumerConfiguration.builder().durable(tsc.name(5)).build()); - ConsumerContext cctx6 = sctx1.createOrUpdateConsumer(ConsumerConfiguration.builder().durable(tsc.name(6)).build()); - - after(cctx1.iterate(), tsc.name(1), true); - after(cctx2.iterate(ConsumeOptions.DEFAULT_CONSUME_OPTIONS), tsc.name(2), true); - after(cctx3.consume(m -> {}), tsc.name(3), true); - after(cctx4.consume(ConsumeOptions.DEFAULT_CONSUME_OPTIONS, m -> {}), tsc.name(4), true); - after(cctx5.fetchMessages(1), tsc.name(5), false); - after(cctx6.fetchBytes(1000), tsc.name(6), false); + ConsumerContext cctx1 = nc.getConsumerContext(tsc.stream, tsc.consumerName(1)); + ConsumerContext cctx2 = nc.getConsumerContext(tsc.stream, tsc.consumerName(2), JetStreamOptions.DEFAULT_JS_OPTIONS); + ConsumerContext cctx3 = js.getConsumerContext(tsc.stream, tsc.consumerName(3)); + ConsumerContext cctx4 = sctx1.getConsumerContext(tsc.consumerName(4)); + ConsumerContext cctx5 = sctx1.createOrUpdateConsumer(ConsumerConfiguration.builder().durable(tsc.consumerName(5)).build()); + ConsumerContext cctx6 = sctx1.createOrUpdateConsumer(ConsumerConfiguration.builder().durable(tsc.consumerName(6)).build()); + + after(cctx1.iterate(), tsc.consumerName(1), true); + after(cctx2.iterate(ConsumeOptions.DEFAULT_CONSUME_OPTIONS), tsc.consumerName(2), true); + after(cctx3.consume(m -> {}), tsc.consumerName(3), true); + after(cctx4.consume(ConsumeOptions.DEFAULT_CONSUME_OPTIONS, m -> {}), tsc.consumerName(4), true); + after(cctx5.fetchMessages(1), tsc.consumerName(5), false); + after(cctx6.fetchBytes(1000), tsc.consumerName(6), false); }); } diff --git a/src/test/java/io/nats/client/support/ValidatorTests.java b/src/test/java/io/nats/client/support/ValidatorTests.java index 2563db948..594c902dc 100644 --- a/src/test/java/io/nats/client/support/ValidatorTests.java +++ b/src/test/java/io/nats/client/support/ValidatorTests.java @@ -552,7 +552,7 @@ public void testSemver() { } @Test - public void testConsumerFilterSubjectsAreEquivalent() { + public void testListsAreEquivalent() { List l1 = Arrays.asList("one", "two"); List l2 = Arrays.asList("two", "one"); List l3 = Arrays.asList("one", "not"); From dabca743b26fce5b9dcb87186f6106cfbf12a3d3 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Wed, 16 Oct 2024 13:31:32 -0400 Subject: [PATCH 15/17] Extract from main 2.11 branch things not specific to 2.11 Part 3 (#1243) * Extract 2.11 changes part 3 * remove tests too --- .../io/nats/client/JetStreamManagement.java | 43 ---- .../java/io/nats/client/api/MessageInfo.java | 44 ---- .../client/impl/NatsJetStreamManagement.java | 108 --------- .../client/impl/JetStreamManagementTests.java | 228 +----------------- 4 files changed, 4 insertions(+), 419 deletions(-) diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index 6755e49d0..499c8ef4d 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.time.ZonedDateTime; import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; /** * JetStream Management context for creation and access to streams and consumers in NATS. @@ -325,48 +324,6 @@ public interface JetStreamManagement { */ MessageInfo getNextMessage(String streamName, long seq, String subject) throws IOException, JetStreamApiException; - /** - * Request a batch of messages using a {@link MessageBatchGetRequest}. - *

- * This API is currently EXPERIMENTAL and is subject to change. - * - * @param streamName the name of the stream - * @param messageBatchGetRequest the request details - * @return a list containing {@link MessageInfo} - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - List fetchMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException; - - /** - * Request a batch of messages using a {@link MessageBatchGetRequest}. - *

- * This API is currently EXPERIMENTAL and is subject to change. - * - * @param streamName the name of the stream - * @param messageBatchGetRequest the request details - * @return a queue used to asynchronously receive {@link MessageInfo} - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - LinkedBlockingQueue queueMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException; - - /** - * Request a batch of messages using a {@link MessageBatchGetRequest}. - *

- * This API is currently EXPERIMENTAL and is subject to change. - * - * @param streamName the name of the stream - * @param messageBatchGetRequest the request details - * @param handler the handler used for receiving {@link MessageInfo} - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - void requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) throws IOException, JetStreamApiException; - /** * Deletes a message, overwriting the message data with garbage * This can be considered an expensive (time-consuming) operation, but is more secure. diff --git a/src/main/java/io/nats/client/api/MessageInfo.java b/src/main/java/io/nats/client/api/MessageInfo.java index 5ded6746f..dbdf0d7b6 100644 --- a/src/main/java/io/nats/client/api/MessageInfo.java +++ b/src/main/java/io/nats/client/api/MessageInfo.java @@ -32,11 +32,6 @@ */ public class MessageInfo extends ApiResponse { - /** - * Message returned as a response in {@link MessageBatchGetRequest} to signal end of data. - */ - public static final MessageInfo EOD = new MessageInfo(null, false); - private final boolean direct; private final String subject; private final long seq; @@ -45,7 +40,6 @@ public class MessageInfo extends ApiResponse { private final Headers headers; private final String stream; private final long lastSeq; - private final long numPending; /** * Create a Message Info @@ -57,25 +51,6 @@ public MessageInfo(Message msg) { this(msg, null, false); } - /** - * Create a Message Info - * This signature is public for testing purposes and is not intended to be used externally. - * @param error the error - * @param direct true if the object is being created from a get direct api call instead of the standard get message - */ - public MessageInfo(Error error, boolean direct) { - super(error); - this.direct = direct; - subject = null; - data = null; - seq = -1; - time = null; - headers = null; - stream = null; - lastSeq = -1; - numPending = -1; - } - /** * Create a Message Info * This signature is public for testing purposes and is not intended to be used externally. @@ -102,14 +77,6 @@ public MessageInfo(Message msg, String streamName, boolean direct) { else { lastSeq = JsonUtils.safeParseLong(tempLastSeq, -1); } - String tempNumPending = msgHeaders.getLast(NATS_NUM_PENDING); - if (tempNumPending == null) { - numPending = -1; - } - else { - // Num pending is +1 since it includes EOB message, correct that here. - numPending = Long.parseLong(tempNumPending) - 1; - } // these are control headers, not real headers so don't give them to the user. headers = new Headers(msgHeaders, true, MESSAGE_INFO_HEADERS); } @@ -121,7 +88,6 @@ else if (hasError()) { headers = null; stream = null; lastSeq = -1; - numPending = -1; } else { JsonValue mjv = readValue(jv, MESSAGE); @@ -133,7 +99,6 @@ else if (hasError()) { headers = hdrBytes == null ? null : new IncomingHeadersProcessor(hdrBytes).getHeaders(); stream = streamName; lastSeq = -1; - numPending = -1; } } @@ -193,14 +158,6 @@ public long getLastSeq() { return lastSeq; } - /** - * Amount of pending messages that can be requested with a subsequent batch request. - * @return number of pending messages - */ - public long getNumPending() { - return numPending; - } - @Override public String toString() { StringBuilder sb = JsonUtils.beginJsonPrefixed("\"MessageInfo\":"); @@ -217,7 +174,6 @@ public String toString() { JsonUtils.addField(sb, TIME, time); JsonUtils.addField(sb, STREAM, stream); JsonUtils.addField(sb, LAST_SEQ, lastSeq); - JsonUtils.addField(sb, NUM_PENDING, numPending); JsonUtils.addField(sb, SUBJECT, subject); JsonUtils.addField(sb, HDRS, headers); return JsonUtils.endJson(sb).toString(); diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index 21ecf8a98..3d3dc3eeb 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -16,17 +16,12 @@ import io.nats.client.*; import io.nats.client.api.Error; import io.nats.client.api.*; -import io.nats.client.support.Status; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; -import static io.nats.client.support.NatsJetStreamClientError.JsAllowDirectRequired; -import static io.nats.client.support.NatsJetStreamClientError.JsDirectBatchGet211NotAvailable; import static io.nats.client.support.Validator.*; public class NatsJetStreamManagement extends NatsJetStreamImpl implements JetStreamManagement { @@ -345,109 +340,6 @@ private MessageInfo _getMessage(String streamName, MessageGetRequest messageGetR } } - /** - * {@inheritDoc} - */ - @Override - public List fetchMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { - validateMessageBatchGetRequest(streamName, messageBatchGetRequest); - List results = new ArrayList<>(); - _requestMessageBatch(streamName, messageBatchGetRequest, msg -> { - if (msg != MessageInfo.EOD) { - results.add(msg); - } - }); - return results; - } - - /** - * {@inheritDoc} - */ - @Override - public LinkedBlockingQueue queueMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { - validateMessageBatchGetRequest(streamName, messageBatchGetRequest); - final LinkedBlockingQueue q = new LinkedBlockingQueue<>(); - conn.getOptions().getExecutor().submit(() -> _requestMessageBatch(streamName, messageBatchGetRequest, q::add)); - return q; - } - - /** - * {@inheritDoc} - */ - @Override - public void requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) throws IOException, JetStreamApiException { - validateMessageBatchGetRequest(streamName, messageBatchGetRequest); - _requestMessageBatch(streamName, messageBatchGetRequest, handler); - } - - public void _requestMessageBatch(String streamName, MessageBatchGetRequest messageBatchGetRequest, MessageInfoHandler handler) { - Subscription sub = null; - try { - String replyTo = conn.createInbox(); - sub = conn.subscribe(replyTo); - - String requestSubject = prependPrefix(String.format(JSAPI_DIRECT_GET, streamName)); - conn.publish(requestSubject, replyTo, messageBatchGetRequest.serialize()); - - long maxTimeMillis = getTimeout().toMillis(); - long timeLeft = maxTimeMillis; - long start = System.currentTimeMillis(); - while (true) { - Message msg = sub.nextMessage(timeLeft); - if (msg == null) { - break; - } - if (msg.isStatusMessage()) { - Status status = msg.getStatus(); - // Report error, otherwise successful status. - if (status.getCode() < 200 || status.getCode() > 299) { - MessageInfo messageInfo = new MessageInfo(Error.convert(status), true); - handler.onMessageInfo(messageInfo); - } - break; - } - - Headers headers = msg.getHeaders(); - if (headers == null || headers.getLast(NATS_NUM_PENDING) == null) { - throw JsDirectBatchGet211NotAvailable.instance(); - } - - MessageInfo messageInfo = new MessageInfo(msg, streamName, true); - handler.onMessageInfo(messageInfo); - timeLeft = maxTimeMillis - (System.currentTimeMillis() - start); - } - } catch (InterruptedException e) { - // sub.nextMessage was fetching one message - // and data is not completely read - // so it seems like this is an error condition - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } finally { - try { - handler.onMessageInfo(MessageInfo.EOD); - } catch (Exception ignore) { - } - try { - //noinspection DataFlowIssue - sub.unsubscribe(); - } catch (Exception ignore) { - } - } - } - - private void validateMessageBatchGetRequest(String streamName, MessageBatchGetRequest messageBatchGetRequest) throws IOException, JetStreamApiException { - validateNotNull(messageBatchGetRequest, "Message Batch Get Request"); - - if (!directBatchGet211Available) { - throw JsDirectBatchGet211NotAvailable.instance(); - } - - CachedStreamInfo csi = getCachedStreamInfo(streamName); - if (!csi.allowDirect) { - throw JsAllowDirectRequired.instance(); - } - } - /** * {@inheritDoc} */ diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index 2012b6ee2..826a5f772 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -23,13 +23,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.time.Instant; -import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.*; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static io.nats.client.support.DateTimeUtils.DEFAULT_TIME; import static io.nats.client.support.DateTimeUtils.ZONE_ID_GMT; @@ -1550,222 +1548,4 @@ public void testCreateConsumerUpdateConsumer() throws Exception { assertEquals(fs1, ci.getConsumerConfiguration().getFilterSubject()); }); } - - @Test - public void testBatchDirectGet() throws Exception { - jsServer.run(TestBase::atLeast2_11, nc -> { - JetStream js = nc.jetStream(); - JetStreamManagement jsm = nc.jetStreamManagement(); - - TestingStreamContainer tsc = new TestingStreamContainer(nc); - assertFalse(tsc.si.getConfiguration().getAllowDirect()); - - List expected = Arrays.asList("foo", "bar", "baz"); - for (String data : expected) { - js.publish(tsc.subject(), data.getBytes(StandardCharsets.UTF_8)); - } - - List batch = new ArrayList<>(); - MessageInfoHandler handler = msg -> { - if (!msg.hasError() && msg != MessageInfo.EOD) { - batch.add(msg); - } - }; - - // Stream doesn't have AllowDirect enabled, will error. - assertThrows(IllegalArgumentException.class, () -> { - MessageBatchGetRequest request = MessageBatchGetRequest.builder().build(); - jsm.requestMessageBatch(tsc.stream, request, handler); - }); - - // Enable AllowDirect. - StreamConfiguration sc = StreamConfiguration.builder(tsc.si.getConfiguration()).allowDirect(true).build(); - StreamInfo si = jsm.updateStream(sc); - assertTrue(si.getConfiguration().getAllowDirect()); - - // Empty request errors. - AtomicBoolean hasError = new AtomicBoolean(); - MessageInfoHandler errorHandler = msg -> { - hasError.compareAndSet(false, msg.hasError()); - }; - MessageBatchGetRequest request = MessageBatchGetRequest.builder().build(); - jsm.requestMessageBatch(tsc.stream, request, errorHandler); - assertTrue(hasError.get()); - List list = jsm.fetchMessageBatch(tsc.stream, request); - assertEquals(1, list.size()); - assertTrue(list.get(0).hasError()); - LinkedBlockingQueue queue = jsm.queueMessageBatch(tsc.stream, request); - assertTrue(queue.take().hasError()); - assertEquals(MessageInfo.EOD, queue.take()); - - // First batch gets first two messages. - request = MessageBatchGetRequest.builder() - .batch(2) - .subject(tsc.subject()) - .build(); - jsm.requestMessageBatch(tsc.stream, request, handler); - MessageInfo last = batch.get(batch.size() - 1); - assertEquals(1, last.getNumPending()); - assertEquals(2, last.getSeq()); - assertEquals(1, last.getLastSeq()); - - // Second batch gets last message. - request = MessageBatchGetRequest.builder(request) - .sequence(last.getSeq() + 1) - .build(); - jsm.requestMessageBatch(tsc.stream, request, handler); - - List actual = batch.stream().map(m -> new String(m.getData())).collect(Collectors.toList()); - assertEquals(expected, actual); - - last = batch.get(batch.size() - 1); - assertEquals(0, last.getNumPending()); - assertEquals(3, last.getSeq()); - assertEquals(0, last.getLastSeq()); - }); - } - - @Test - public void testBatchDirectGetAlternatives() throws Exception { - jsServer.run(TestBase::atLeast2_11, nc -> { - JetStream js = nc.jetStream(); - JetStreamManagement jsm = nc.jetStreamManagement(); - - TestingStreamContainer tsc = new TestingStreamContainer(nc); - assertFalse(tsc.si.getConfiguration().getAllowDirect()); - - // Enable AllowDirect. - StreamConfiguration sc = StreamConfiguration.builder(tsc.si.getConfiguration()).allowDirect(true).build(); - StreamInfo si = jsm.updateStream(sc); - assertTrue(si.getConfiguration().getAllowDirect()); - - List expected = Arrays.asList("foo", "bar", "baz"); - for (String data : expected) { - js.publish(tsc.subject(), data.getBytes(StandardCharsets.UTF_8)); - } - - // Request stays the same for all options. - MessageBatchGetRequest request = MessageBatchGetRequest.builder() - .batch(3) - .subject(tsc.subject()) - .build(); - - // Get using handler. - List batch = new ArrayList<>(); - MessageInfoHandler handler = msg -> { - if (!msg.hasError() && msg != MessageInfo.EOD) { - batch.add(msg); - } - }; - jsm.requestMessageBatch(tsc.stream, request, handler); - assertEquals(3, batch.size()); - MessageInfo last = batch.get(batch.size() - 1); - assertEquals(0, last.getNumPending()); - assertEquals(3, last.getSeq()); - assertEquals(2, last.getLastSeq()); - - // Get using queue. - batch.clear(); - LinkedBlockingQueue queue = jsm.queueMessageBatch(tsc.stream, request); - MessageInfo msg; - while ((msg = queue.take()) != MessageInfo.EOD) { - if (!msg.hasError()) { - batch.add(msg); - } - } - assertEquals(3, batch.size()); - last = batch.get(batch.size() - 1); - assertEquals(0, last.getNumPending()); - assertEquals(3, last.getSeq()); - assertEquals(2, last.getLastSeq()); - - // Get using fetch. - batch.clear(); - batch.addAll(jsm.fetchMessageBatch(tsc.stream, request)); - assertEquals(3, batch.size()); - last = batch.get(batch.size() - 1); - assertEquals(0, last.getNumPending()); - assertEquals(3, last.getSeq()); - assertEquals(2, last.getLastSeq()); - }); - } - - @Test - public void testBatchDirectGetMultiLast() throws Exception { - jsServer.run(TestBase::atLeast2_11, nc -> { - JetStream js = nc.jetStream(); - JetStreamManagement jsm = nc.jetStreamManagement(); - - String stream = stream(); - jsm.addStream(StreamConfiguration.builder() - .name(stream) - .subjects(stream + ".a.>") - .allowDirect(true) - .build()); - - String subjectAFoo = stream + ".a.foo"; - String subjectABar = stream + ".a.bar"; - String subjectABaz = stream + ".a.baz"; - js.publish(subjectAFoo, "foo".getBytes(StandardCharsets.UTF_8)); - js.publish(subjectABar, "bar".getBytes(StandardCharsets.UTF_8)); - js.publish(subjectABaz, "baz".getBytes(StandardCharsets.UTF_8)); - - MessageBatchGetRequest request = MessageBatchGetRequest.builder() - .multiLastForSubjects(subjectAFoo, subjectABaz) - .build(); - - List keys = new ArrayList<>(); - MessageInfoHandler handler = msg -> { - if (!msg.hasError() && msg != MessageInfo.EOD) { - keys.add(msg.getSubject()); - } - }; - jsm.requestMessageBatch(stream, request, handler); - assertEquals(2, keys.size()); - assertEquals(subjectAFoo, keys.get(0)); - assertEquals(subjectABaz, keys.get(1)); - }); - } - - @Test - public void testBatchDirectGetBuilder() { - // Request options. - MessageBatchGetRequest requestOptions = MessageBatchGetRequest.builder() - .maxBytes(1234) - .batch(2) - .build(); - assertEquals(1234, requestOptions.getMaxBytes()); - assertEquals(2, requestOptions.getBatch()); - assertEquals("{\"batch\":2,\"max_bytes\":1234}", requestOptions.toJson()); - - // Batch direct get - simple - ZonedDateTime time = Instant.EPOCH.atZone(ZoneOffset.UTC); - MessageBatchGetRequest simple = MessageBatchGetRequest.builder() - .sequence(1) - .startTime(time) - .subject("subject") - .build(); - assertEquals(1, simple.getSequence()); - assertEquals(time, simple.getStartTime()); - assertEquals("subject", simple.getSubject()); - assertEquals("{\"seq\":1,\"start_time\":\"1970-01-01T00:00:00.000000000Z\",\"next_by_subj\":\"subject\"}", simple.toJson()); - - // Batch direct get - multi last - List multiLastFor = Collections.singletonList("multi.last"); - MessageBatchGetRequest multiLast = MessageBatchGetRequest.builder() - .multiLastForSubjects("multi.last") - .upToSequence(1) - .upToTime(time) - .build(); - assertEquals(Collections.singletonList("multi.last"), multiLast.getMultiLastForSubjects()); - assertEquals(1, multiLast.getUpToSequence()); - assertEquals(time, multiLast.getUpToTime()); - assertEquals("{\"multi_last\":[\"multi.last\"],\"up_to_seq\":1,\"up_to_time\":\"1970-01-01T00:00:00.000000000Z\"}", multiLast.toJson()); - - MessageBatchGetRequest multiLastAlternative = MessageBatchGetRequest.builder() - .multiLastForSubjects(multiLastFor) - .build(); - assertEquals(multiLastFor, multiLastAlternative.getMultiLastForSubjects()); - assertEquals("{\"multi_last\":[\"multi.last\"]}", multiLastAlternative.toJson()); - } } From 1ceea556591e3acbb3d14b2439c6072494de73f8 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Thu, 31 Oct 2024 09:59:29 -0400 Subject: [PATCH 16/17] Don't flush after the request from publishAsync calls (#1220) --- src/main/java/io/nats/client/Options.java | 36 +++++++++++++++++-- .../io/nats/client/impl/NatsConnection.java | 30 +++++++++------- .../io/nats/client/impl/NatsJetStream.java | 19 +++++----- .../nats/client/impl/NatsJetStreamImpl.java | 6 ++-- .../impl/NatsJetStreamPullSubscription.java | 2 +- .../java/io/nats/client/OptionsTests.java | 2 ++ 6 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/main/java/io/nats/client/Options.java b/src/main/java/io/nats/client/Options.java index 99213bfc5..c4970aa83 100644 --- a/src/main/java/io/nats/client/Options.java +++ b/src/main/java/io/nats/client/Options.java @@ -56,19 +56,19 @@ public class Options { // NOTE TO DEVS!!! To add an option, you have to address: // ---------------------------------------------------------------------------------------------------- // CONSTANTS * optionally add a default value constant - // ENVIRONMENT PROPERTIES * most of the time add an environment property, should always be in the form PFX + + // ENVIRONMENT PROPERTIES * always add an environment property. Constant always starts with PFX, but code accepts without // PROTOCOL CONNECT OPTION CONSTANTS * not related to options, but here because Options code uses them // CLASS VARIABLES * add a variable to the class // BUILDER VARIABLES * add a variable in builder + // BUILDER COPY CONSTRUCTOR * update builder constructor to ensure new variables are set // BUILD CONSTRUCTOR PROPS * update build props constructor to read new props // BUILDER METHODS * add a chainable method in builder for new variable // BUILD IMPL * update build() implementation if needed - // BUILDER COPY CONSTRUCTOR * update builder constructor to ensure new variables are set // CONSTRUCTOR * update constructor to ensure new variables are set from builder // GETTERS * update getter to be able to retrieve class variable value // HELPER FUNCTIONS * just helpers // ---------------------------------------------------------------------------------------------------- - // README - if you add a property or change it's comment, add it to or update the readme + // README - if you add a property or change its comment, add it to or update the readme // ---------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------- @@ -492,6 +492,10 @@ public class Options { * {@link Builder#useDispatcherWithExecutor()}. */ public static final String PROP_USE_DISPATCHER_WITH_EXECUTOR = PFX + "use.dispatcher.with.executor"; + /** + * Property used to configure a builder from a Properties object. {@value}, see {@link Builder#forceFlushOnRequest() forceFlushOnRequest}. + */ + public static final String PROP_FORCE_FLUSH_ON_REQUEST = PFX + "force.flush.on.request"; // ---------------------------------------------------------------------------------------------------- // PROTOCOL CONNECT OPTION CONSTANTS @@ -625,6 +629,7 @@ public class Options { private final boolean tlsFirst; private final boolean useTimeoutException; private final boolean useDispatcherWithExecutor; + private final boolean forceFlushOnRequest; private final AuthHandler authHandler; private final ReconnectDelayHandler reconnectDelayHandler; @@ -741,6 +746,7 @@ public static class Builder { private boolean tlsFirst = false; private boolean useTimeoutException = false; private boolean useDispatcherWithExecutor = false; + private boolean forceFlushOnRequest = true; // true since it's the original b/w compatible way private ServerPool serverPool = null; private DispatcherFactory dispatcherFactory = null; @@ -876,6 +882,7 @@ public Builder properties(Properties props) { booleanProperty(props, PROP_TLS_FIRST, b -> this.tlsFirst = b); booleanProperty(props, PROP_USE_TIMEOUT_EXCEPTION, b -> this.useTimeoutException = b); booleanProperty(props, PROP_USE_DISPATCHER_WITH_EXECUTOR, b -> this.useDispatcherWithExecutor = b); + booleanProperty(props, PROP_FORCE_FLUSH_ON_REQUEST, b -> this.forceFlushOnRequest = b); classnameProperty(props, PROP_SERVERS_POOL_IMPLEMENTATION_CLASS, o -> this.serverPool = (ServerPool) o); classnameProperty(props, PROP_DISPATCHER_FACTORY_CLASS, o -> this.dispatcherFactory = (DispatcherFactory) o); @@ -1658,6 +1665,15 @@ public Builder useDispatcherWithExecutor() { return this; } + /** + * Instruct requests to turn off flush on requests. + * @return the Builder for chaining + */ + public Builder dontForceFlushOnRequest() { + this.forceFlushOnRequest = false; + return this; + } + /** * Set the ServerPool implementation for connections to use instead of the default implementation * @param serverPool the implementation @@ -1905,6 +1921,7 @@ public Builder(Options o) { this.tlsFirst = o.tlsFirst; this.useTimeoutException = o.useTimeoutException; this.useDispatcherWithExecutor = o.useDispatcherWithExecutor; + this.forceFlushOnRequest = o.forceFlushOnRequest; this.serverPool = o.serverPool; this.dispatcherFactory = o.dispatcherFactory; @@ -1969,6 +1986,7 @@ private Options(Builder b) { this.tlsFirst = b.tlsFirst; this.useTimeoutException = b.useTimeoutException; this.useDispatcherWithExecutor = b.useDispatcherWithExecutor; + this.forceFlushOnRequest = b.forceFlushOnRequest; this.serverPool = b.serverPool; this.dispatcherFactory = b.dispatcherFactory; @@ -2405,8 +2423,20 @@ public boolean useTimeoutException() { return useTimeoutException; } + /** + * Whether the dispatcher should use an executor to async messages to handlers + * @return the flag + */ public boolean useDispatcherWithExecutor() { return useDispatcherWithExecutor; } + /** + * Whether to flush on any user request + * @return the flag + */ + public boolean forceFlushOnRequest() { + return forceFlushOnRequest; + } + /** * Get the ServerPool implementation. If null, a default implementation is used. * @return the ServerPool implementation diff --git a/src/main/java/io/nats/client/impl/NatsConnection.java b/src/main/java/io/nats/client/impl/NatsConnection.java index f0c56cbdc..72ff04a6f 100644 --- a/src/main/java/io/nats/client/impl/NatsConnection.java +++ b/src/main/java/io/nats/client/impl/NatsConnection.java @@ -47,6 +47,7 @@ class NatsConnection implements Connection { public static final double NANOS_PER_SECOND = 1_000_000_000.0; private final Options options; + final boolean forceFlushOnRequest; private final StatisticsCollector statistics; @@ -112,6 +113,7 @@ class NatsConnection implements Connection { timeTraceLogger.trace("creating connection object"); this.options = options; + forceFlushOnRequest = options.forceFlushOnRequest(); advancedTracking = options.isTrackAdvancedStats(); this.statistics = options.getStatisticsCollector() == null ? new NatsStatistics() : options.getStatisticsCollector(); @@ -1205,7 +1207,7 @@ else if (future.isDone()) { */ @Override public Message request(String subject, byte[] body, Duration timeout) throws InterruptedException { - return requestInternal(subject, null, body, timeout, cancelAction, true); + return requestInternal(subject, null, body, timeout, cancelAction, true, forceFlushOnRequest); } /** @@ -1213,7 +1215,7 @@ public Message request(String subject, byte[] body, Duration timeout) throws Int */ @Override public Message request(String subject, Headers headers, byte[] body, Duration timeout) throws InterruptedException { - return requestInternal(subject, headers, body, timeout, cancelAction, true); + return requestInternal(subject, headers, body, timeout, cancelAction, true, forceFlushOnRequest); } /** @@ -1222,11 +1224,12 @@ public Message request(String subject, Headers headers, byte[] body, Duration ti @Override public Message request(Message message, Duration timeout) throws InterruptedException { validateNotNull(message, "Message"); - return requestInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false); + return requestInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false, forceFlushOnRequest); } - Message requestInternal(String subject, Headers headers, byte[] data, Duration timeout, CancelAction cancelAction, boolean validateSubjectAndReplyTo) throws InterruptedException { - CompletableFuture incoming = requestFutureInternal(subject, headers, data, timeout, cancelAction, validateSubjectAndReplyTo); + Message requestInternal(String subject, Headers headers, byte[] data, Duration timeout, + CancelAction cancelAction, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) throws InterruptedException { + CompletableFuture incoming = requestFutureInternal(subject, headers, data, timeout, cancelAction, validateSubjectAndReplyTo, flushImmediatelyAfterPublish); try { return incoming.get(timeout.toNanos(), TimeUnit.NANOSECONDS); } catch (TimeoutException | ExecutionException | CancellationException e) { @@ -1239,7 +1242,7 @@ Message requestInternal(String subject, Headers headers, byte[] data, Duration t */ @Override public CompletableFuture request(String subject, byte[] body) { - return requestFutureInternal(subject, null, body, null, cancelAction, true); + return requestFutureInternal(subject, null, body, null, cancelAction, true, forceFlushOnRequest); } /** @@ -1247,7 +1250,7 @@ public CompletableFuture request(String subject, byte[] body) { */ @Override public CompletableFuture request(String subject, Headers headers, byte[] body) { - return requestFutureInternal(subject, headers, body, null, cancelAction, true); + return requestFutureInternal(subject, headers, body, null, cancelAction, true, forceFlushOnRequest); } /** @@ -1255,7 +1258,7 @@ public CompletableFuture request(String subject, Headers headers, byte[ */ @Override public CompletableFuture requestWithTimeout(String subject, byte[] body, Duration timeout) { - return requestFutureInternal(subject, null, body, timeout, cancelAction, true); + return requestFutureInternal(subject, null, body, timeout, cancelAction, true, forceFlushOnRequest); } /** @@ -1263,7 +1266,7 @@ public CompletableFuture requestWithTimeout(String subject, byte[] body */ @Override public CompletableFuture requestWithTimeout(String subject, Headers headers, byte[] body, Duration timeout) { - return requestFutureInternal(subject, headers, body, timeout, cancelAction, true); + return requestFutureInternal(subject, headers, body, timeout, cancelAction, true, forceFlushOnRequest); } /** @@ -1272,7 +1275,7 @@ public CompletableFuture requestWithTimeout(String subject, Headers hea @Override public CompletableFuture requestWithTimeout(Message message, Duration timeout) { validateNotNull(message, "Message"); - return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false); + return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), timeout, cancelAction, false, forceFlushOnRequest); } /** @@ -1281,10 +1284,11 @@ public CompletableFuture requestWithTimeout(Message message, Duration t @Override public CompletableFuture request(Message message) { validateNotNull(message, "Message"); - return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), null, cancelAction, false); + return requestFutureInternal(message.getSubject(), message.getHeaders(), message.getData(), null, cancelAction, false, forceFlushOnRequest); } - CompletableFuture requestFutureInternal(String subject, Headers headers, byte[] data, Duration futureTimeout, CancelAction cancelAction, boolean validateSubjectAndReplyTo) { + CompletableFuture requestFutureInternal(String subject, Headers headers, byte[] data, Duration futureTimeout, + CancelAction cancelAction, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) { checkPayloadSize(data); if (isClosed()) { @@ -1336,7 +1340,7 @@ CompletableFuture requestFutureInternal(String subject, Headers headers responsesAwaiting.put(sub.getSID(), future); } - publishInternal(subject, responseInbox, headers, data, validateSubjectAndReplyTo, true); + publishInternal(subject, responseInbox, headers, data, validateSubjectAndReplyTo, flushImmediatelyAfterPublish); statistics.incrementRequestsSent(); return future; diff --git a/src/main/java/io/nats/client/impl/NatsJetStream.java b/src/main/java/io/nats/client/impl/NatsJetStream.java index c3c612a0b..8363e2ac7 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStream.java +++ b/src/main/java/io/nats/client/impl/NatsJetStream.java @@ -18,7 +18,6 @@ import io.nats.client.support.Validator; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -97,7 +96,7 @@ public PublishAck publish(Message message, PublishOptions options) throws IOExce */ @Override public CompletableFuture publishAsync(String subject, byte[] body) { - return publishAsyncInternal(subject, null, body, null, null, true); + return publishAsyncInternal(subject, null, body, null, true); } /** @@ -105,7 +104,7 @@ public CompletableFuture publishAsync(String subject, byte[] body) { */ @Override public CompletableFuture publishAsync(String subject, Headers headers, byte[] body) { - return publishAsyncInternal(subject, headers, body, null, null, true); + return publishAsyncInternal(subject, headers, body, null, true); } /** @@ -113,7 +112,7 @@ public CompletableFuture publishAsync(String subject, Headers header */ @Override public CompletableFuture publishAsync(String subject, byte[] body, PublishOptions options) { - return publishAsyncInternal(subject, null, body, options, null, true); + return publishAsyncInternal(subject, null, body, options, true); } /** @@ -121,7 +120,7 @@ public CompletableFuture publishAsync(String subject, byte[] body, P */ @Override public CompletableFuture publishAsync(String subject, Headers headers, byte[] body, PublishOptions options) { - return publishAsyncInternal(subject, headers, body, options, null, true); + return publishAsyncInternal(subject, headers, body, options, true); } /** @@ -130,7 +129,7 @@ public CompletableFuture publishAsync(String subject, Headers header @Override public CompletableFuture publishAsync(Message message) { validateNotNull(message, "Message"); - return publishAsyncInternal(message.getSubject(), message.getHeaders(), message.getData(), null, null, false); + return publishAsyncInternal(message.getSubject(), message.getHeaders(), message.getData(), null, false); } /** @@ -139,7 +138,7 @@ public CompletableFuture publishAsync(Message message) { @Override public CompletableFuture publishAsync(Message message, PublishOptions options) { validateNotNull(message, "Message"); - return publishAsyncInternal(message.getSubject(), message.getHeaders(), message.getData(), options, null, false); + return publishAsyncInternal(message.getSubject(), message.getHeaders(), message.getData(), options, false); } private PublishAck publishSyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, boolean validateSubjectAndReplyTo) throws IOException, JetStreamApiException { @@ -150,11 +149,11 @@ private PublishAck publishSyncInternal(String subject, Headers headers, byte[] d return null; } - Message resp = makeInternalRequestResponseRequired(subject, merged, data, getTimeout(), CancelAction.COMPLETE, validateSubjectAndReplyTo); + Message resp = makeInternalRequestResponseRequired(subject, merged, data, getTimeout(), CancelAction.COMPLETE, validateSubjectAndReplyTo, conn.forceFlushOnRequest); return processPublishResponse(resp, options); } - private CompletableFuture publishAsyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, Duration knownTimeout, boolean validateSubjectAndReplyTo) { + private CompletableFuture publishAsyncInternal(String subject, Headers headers, byte[] data, PublishOptions options, boolean validateSubjectAndReplyTo) { Headers merged = mergePublishOptions(headers, options); if (jso.isPublishNoAck()) { @@ -162,7 +161,7 @@ private CompletableFuture publishAsyncInternal(String subject, Heade return null; } - CompletableFuture future = conn.requestFutureInternal(subject, merged, data, knownTimeout, CancelAction.COMPLETE, validateSubjectAndReplyTo); + CompletableFuture future = conn.requestFutureInternal(subject, merged, data, null, CancelAction.COMPLETE, validateSubjectAndReplyTo, conn.forceFlushOnRequest); return future.thenCompose(resp -> { try { diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java index b010ead63..16f9c35e0 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamImpl.java @@ -53,7 +53,7 @@ public CachedStreamInfo(StreamInfo si) { // ---------------------------------------------------------------------------------------------------- // Create / Init // ---------------------------------------------------------------------------------------------------- - NatsJetStreamImpl(NatsConnection connection, JetStreamOptions jsOptions) throws IOException { + NatsJetStreamImpl(NatsConnection connection, JetStreamOptions jsOptions) { conn = connection; // Get a working version of JetStream Options... @@ -246,9 +246,9 @@ Message makeRequestResponseRequired(String subject, byte[] bytes, Duration timeo } } - Message makeInternalRequestResponseRequired(String subject, Headers headers, byte[] data, Duration timeout, CancelAction cancelAction, boolean validateSubjectAndReplyTo) throws IOException { + Message makeInternalRequestResponseRequired(String subject, Headers headers, byte[] data, Duration timeout, CancelAction cancelAction, boolean validateSubjectAndReplyTo, boolean flushImmediatelyAfterPublish) throws IOException { try { - return responseRequired(conn.requestInternal(subject, headers, data, timeout, cancelAction, validateSubjectAndReplyTo)); + return responseRequired(conn.requestInternal(subject, headers, data, timeout, cancelAction, validateSubjectAndReplyTo, flushImmediatelyAfterPublish)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(e); diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java b/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java index 6ba4e1021..f87144f83 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamPullSubscription.java @@ -62,7 +62,7 @@ protected String _pull(PullRequestOptions pullRequestOptions, boolean raiseStatu String publishSubject = js.prependPrefix(String.format(JSAPI_CONSUMER_MSG_NEXT, stream, consumerName)); String pullSubject = getSubject().replace("*", Long.toString(this.pullSubjectIdHolder.incrementAndGet())); manager.startPullRequest(pullSubject, pullRequestOptions, raiseStatusWarnings, pullManagerObserver); - connection.publishInternal(publishSubject, pullSubject, null, pullRequestOptions.serialize(), true, true); + connection.publishInternal(publishSubject, pullSubject, null, pullRequestOptions.serialize(), true, connection.forceFlushOnRequest); return pullSubject; } diff --git a/src/test/java/io/nats/client/OptionsTests.java b/src/test/java/io/nats/client/OptionsTests.java index cf2108185..6c0ab4f0d 100644 --- a/src/test/java/io/nats/client/OptionsTests.java +++ b/src/test/java/io/nats/client/OptionsTests.java @@ -540,6 +540,7 @@ public void testPropertiesCoverageOptions() throws Exception { props.setProperty(Options.PROP_CLIENT_SIDE_LIMIT_CHECKS, "true"); // deprecated props.setProperty(Options.PROP_IGNORE_DISCOVERED_SERVERS, "true"); props.setProperty(Options.PROP_NO_RESOLVE_HOSTNAMES, "true"); + props.setProperty(PROP_FORCE_FLUSH_ON_REQUEST, "false"); Options o = new Options.Builder(props).build(); _testPropertiesCoverageOptions(o); @@ -553,6 +554,7 @@ private static void _testPropertiesCoverageOptions(Options o) { assertTrue(o.clientSideLimitChecks()); assertTrue(o.isIgnoreDiscoveredServers()); assertTrue(o.isNoResolveHostnames()); + assertFalse(o.forceFlushOnRequest()); } @Test From 10db91db986cf1a5d504d39022857e5f5864b405 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Fri, 1 Nov 2024 11:42:59 -0400 Subject: [PATCH 17/17] getFirstMessage is a 2.11 feature (#1246) --- .../io/nats/client/BaseConsumeOptions.java | 4 +- .../io/nats/client/ConnectionListener.java | 8 ++-- .../io/nats/client/JetStreamManagement.java | 40 ------------------- .../java/io/nats/client/StreamContext.java | 10 ----- .../client/impl/NatsJetStreamManagement.java | 24 ----------- .../nats/client/impl/NatsStreamContext.java | 8 ---- .../client/impl/JetStreamManagementTests.java | 6 --- .../nats/client/impl/SimplificationTests.java | 9 ----- 8 files changed, 6 insertions(+), 103 deletions(-) diff --git a/src/main/java/io/nats/client/BaseConsumeOptions.java b/src/main/java/io/nats/client/BaseConsumeOptions.java index 58a54000a..c6cc5b6ae 100644 --- a/src/main/java/io/nats/client/BaseConsumeOptions.java +++ b/src/main/java/io/nats/client/BaseConsumeOptions.java @@ -49,10 +49,10 @@ public class BaseConsumeOptions implements JsonSerializable { protected BaseConsumeOptions(Builder b) { bytes = b.bytes; if (bytes > 0) { - messages = b.messages == -1 ? DEFAULT_MESSAGE_COUNT_WHEN_BYTES : b.messages; + messages = b.messages < 0 ? DEFAULT_MESSAGE_COUNT_WHEN_BYTES : b.messages; } else { - messages = b.messages == -1 ? DEFAULT_MESSAGE_COUNT : b.messages; + messages = b.messages < 0 ? DEFAULT_MESSAGE_COUNT : b.messages; } // validation handled in builder diff --git a/src/main/java/io/nats/client/ConnectionListener.java b/src/main/java/io/nats/client/ConnectionListener.java index 0e49d6d6f..bc681f781 100644 --- a/src/main/java/io/nats/client/ConnectionListener.java +++ b/src/main/java/io/nats/client/ConnectionListener.java @@ -18,7 +18,7 @@ * listener is configured in the {@link Options Options} at creation time. */ public interface ConnectionListener { - public enum Events { + enum Events { /** The connection has successfully completed the handshake with the nats-server. */ CONNECTED(true, "opened"), /** The connection is permanently closed, either by manual action or failed reconnects. */ @@ -29,7 +29,7 @@ public enum Events { RECONNECTED(true, "reconnected"), /** The connection was reconnected and the server has been notified of all subscriptions. */ RESUBSCRIBED(false, "subscriptions re-established"), - /** The connection was told about new servers from, from the current server. */ + /** The connection was made aware of new servers from the current server connection. */ DISCOVERED_SERVERS(false, "discovered servers"), /** Server Sent a lame duck mode. */ LAME_DUCK(false, "lame duck mode"); @@ -77,5 +77,5 @@ public String toString() { * @param conn the connection associated with the error * @param type the type of event that has occurred */ - public void connectionEvent(Connection conn, Events type); -} \ No newline at end of file + void connectionEvent(Connection conn, Events type); +} diff --git a/src/main/java/io/nats/client/JetStreamManagement.java b/src/main/java/io/nats/client/JetStreamManagement.java index 499c8ef4d..b712fbe51 100644 --- a/src/main/java/io/nats/client/JetStreamManagement.java +++ b/src/main/java/io/nats/client/JetStreamManagement.java @@ -271,46 +271,6 @@ public interface JetStreamManagement { */ MessageInfo getLastMessage(String streamName, String subject) throws IOException, JetStreamApiException; - /** - * Get MessageInfo for the first message of the subject. - * @param streamName the name of the stream. - * @param subject the subject to get the first message for. - * @return The MessageInfo - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - MessageInfo getFirstMessage(String streamName, String subject) throws IOException, JetStreamApiException; - - /** - * Get MessageInfo for the first message created at or after the start time. - *

- * This API is currently EXPERIMENTAL and is subject to change. - * - * @param streamName the name of the stream. - * @param startTime the start time to get the first message for. - * @return The MessageInfo - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) throws IOException, JetStreamApiException; - - /** - * Get MessageInfo for the first message created at or after the start time matching the subject. - *

- * This API is currently EXPERIMENTAL and is subject to change. - * - * @param streamName the name of the stream. - * @param startTime the start time to get the first message for. - * @param subject the subject to get the first message for. - * @return The MessageInfo - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime, String subject) throws IOException, JetStreamApiException; - /** * Get MessageInfo for the message of the message sequence * is equal to or greater the requested sequence for the subject. diff --git a/src/main/java/io/nats/client/StreamContext.java b/src/main/java/io/nats/client/StreamContext.java index c3849ea6c..d8fa7a398 100644 --- a/src/main/java/io/nats/client/StreamContext.java +++ b/src/main/java/io/nats/client/StreamContext.java @@ -163,16 +163,6 @@ public interface StreamContext { */ MessageInfo getLastMessage(String subject) throws IOException, JetStreamApiException; - /** - * Get MessageInfo for the first message of the subject. - * @param subject the subject to get the first message for. - * @return The MessageInfo - * @throws IOException covers various communication issues with the NATS - * server such as timeout or interruption - * @throws JetStreamApiException the request had an error related to the data - */ - MessageInfo getFirstMessage(String subject) throws IOException, JetStreamApiException; - /** * Get MessageInfo for the message of the message sequence * is equal to or greater the requested sequence for the subject. diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java index 3d3dc3eeb..a8ae1ab3d 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamManagement.java @@ -281,30 +281,6 @@ public MessageInfo getLastMessage(String streamName, String subject) throws IOEx return _getMessage(streamName, MessageGetRequest.lastForSubject(subject)); } - /** - * {@inheritDoc} - */ - @Override - public MessageInfo getFirstMessage(String streamName, String subject) throws IOException, JetStreamApiException { - return _getMessage(streamName, MessageGetRequest.firstForSubject(subject)); - } - - /** - * {@inheritDoc} - */ - @Override - public MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime) throws IOException, JetStreamApiException { - return _getMessage(streamName, MessageGetRequest.firstForStartTime(startTime)); - } - - /** - * {@inheritDoc} - */ - @Override - public MessageInfo getFirstMessage(String streamName, ZonedDateTime startTime, String subject) throws IOException, JetStreamApiException { - return _getMessage(streamName, MessageGetRequest.firstForStartTimeAndSubject(startTime, subject)); - } - /** * {@inheritDoc} */ diff --git a/src/main/java/io/nats/client/impl/NatsStreamContext.java b/src/main/java/io/nats/client/impl/NatsStreamContext.java index fce003af7..d6510be2c 100644 --- a/src/main/java/io/nats/client/impl/NatsStreamContext.java +++ b/src/main/java/io/nats/client/impl/NatsStreamContext.java @@ -147,14 +147,6 @@ public MessageInfo getLastMessage(String subject) throws IOException, JetStreamA return jsm.getLastMessage(streamName, subject); } - /** - * {@inheritDoc} - */ - @Override - public MessageInfo getFirstMessage(String subject) throws IOException, JetStreamApiException { - return jsm.getFirstMessage(streamName, subject); - } - /** * {@inheritDoc} */ diff --git a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java index 826a5f772..c0ab24fcb 100644 --- a/src/test/java/io/nats/client/impl/JetStreamManagementTests.java +++ b/src/test/java/io/nats/client/impl/JetStreamManagementTests.java @@ -1291,10 +1291,6 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, -1, tsc.subject(1)), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getNextMessage(tsc.stream, 0, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, 0, tsc.subject(1)), beforeCreated); - assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, tsc.subject(0)), beforeCreated); - assertMessageInfo(tsc, 1, 2, jsm.getFirstMessage(tsc.stream, tsc.subject(1)), beforeCreated); - assertMessageInfo(tsc, 0, 1, jsm.getFirstMessage(tsc.stream, beforeCreated), beforeCreated); - assertMessageInfo(tsc, 1, 2, jsm.getFirstMessage(tsc.stream, beforeCreated, tsc.subject(1)), beforeCreated); assertMessageInfo(tsc, 0, 1, jsm.getNextMessage(tsc.stream, 1, tsc.subject(0)), beforeCreated); assertMessageInfo(tsc, 1, 2, jsm.getNextMessage(tsc.stream, 1, tsc.subject(1)), beforeCreated); @@ -1307,10 +1303,8 @@ private void validateGetMessage(JetStreamManagement jsm, TestingStreamContainer assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, -1))); assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 0))); - assertStatus(10003, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, DEFAULT_TIME))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getMessage(tsc.stream, 9))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getLastMessage(tsc.stream, "not-a-subject"))); - assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getFirstMessage(tsc.stream, "not-a-subject"))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getNextMessage(tsc.stream, 9, tsc.subject(0)))); assertStatus(10037, assertThrows(JetStreamApiException.class, () -> jsm.getNextMessage(tsc.stream, 1, "not-a-subject"))); } diff --git a/src/test/java/io/nats/client/impl/SimplificationTests.java b/src/test/java/io/nats/client/impl/SimplificationTests.java index 0c2f9b07c..e8178e541 100644 --- a/src/test/java/io/nats/client/impl/SimplificationTests.java +++ b/src/test/java/io/nats/client/impl/SimplificationTests.java @@ -109,9 +109,6 @@ private static void _testStreamContext(JetStream js, TestingStreamContainer tsc, MessageInfo mi = streamContext.getMessage(1); assertEquals(1, mi.getSeq()); - mi = streamContext.getFirstMessage(tsc.subject()); - assertEquals(1, mi.getSeq()); - mi = streamContext.getLastMessage(tsc.subject()); assertEquals(6, mi.getSeq()); @@ -123,12 +120,6 @@ private static void _testStreamContext(JetStream js, TestingStreamContainer tsc, streamContext.purge(PurgeOptions.builder().sequence(5).build()); assertThrows(JetStreamApiException.class, () -> streamContext.getMessage(1)); - - mi = streamContext.getFirstMessage(tsc.subject()); - assertEquals(5, mi.getSeq()); - - streamContext.purge(); - assertThrows(JetStreamApiException.class, () -> streamContext.getFirstMessage(tsc.subject())); } static int FETCH_EPHEMERAL = 1;