Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 117 additions & 2 deletions src/main/java/io/nats/client/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,62 @@ public enum SubjectValidationType {
* Strict Subject Validation
*/
Strict;

/**
* Resolve a {@link SubjectValidationType} from a string (case-insensitive name match).
* Returns {@link #Lenient} if the value is null or does not match any constant.
*
* @param value the string value
* @return the matching behavior, or {@link #Lenient} as the default
*/
public static SubjectValidationType get(String value) {
if (value != null) {
for (SubjectValidationType svt : SubjectValidationType.values()) {
if (svt.name().equalsIgnoreCase(value)) {
return svt;
}
}
}
return Lenient;
}
}

/**
* Controls when the {@link ReconnectDelayHandler} is invoked during reconnect attempts.
*/
public enum ReconnectDelayBehavior {
Comment thread
scottf marked this conversation as resolved.
/**
* Invoke the reconnect delay only before subsequent rounds, never before the first round.
* The first round of reconnect attempts runs immediately; the delay applies between rounds
* after a full round has been attempted and failed. This is the historical default.
* {@link ReconnectDelayHandler#getWaitTime(long)} will only be called with
* {@code totalTries} greater than or equal to 1.
*/
BeforeSubsequentRounds,
/**
* Invoke reconnect delay behavior before each full round of reconnect attempts.
* {@link ReconnectDelayHandler#getWaitTime(long)} will be called with
* {@code totalTries} greater than or equal to 0.
*/
BeforeAllRounds;

/**
* Resolve a {@link ReconnectDelayBehavior} from a string (case-insensitive name match).
* Returns {@link #BeforeSubsequentRounds} if the value is null or does not match any constant.
*
* @param value the string value
* @return the matching behavior, or {@link #BeforeSubsequentRounds} as the default
*/
public static ReconnectDelayBehavior get(String value) {
if (value != null) {
for (ReconnectDelayBehavior rdb : ReconnectDelayBehavior.values()) {
if (rdb.name().equalsIgnoreCase(value)) {
return rdb;
}
}
}
return BeforeSubsequentRounds;
}
}

/**
Expand Down Expand Up @@ -439,6 +495,16 @@ public static HostnameResolveMode get(String value) {
* reconnectJitterTls}.
*/
public static final String PROP_RECONNECT_JITTER_TLS = PFX + "reconnect.jitter.tls";
/**
* Property used to set class name for the ReconnectDelayHandler implementation
* {@link Builder#reconnectDelayHandler(ReconnectDelayHandler) reconnectDelayHandler}.
*/
public static final String PROP_RECONNECT_DELAY_HANDLER_CLASS = PFX + "reconnect.delay.handler.class";
/**
* Property used to configure a builder from a Properties object. {@value}, see {@link Builder#reconnectDelayBehavior(ReconnectDelayBehavior)
* reconnectDelayBehavior}. Value is the name of a {@link ReconnectDelayBehavior} constant (e.g. {@code BeforeSubsequentRounds}, {@code BeforeAllRounds}).
*/
public static final String PROP_RECONNECT_DELAY_BEHAVIOR = PFX + "reconnect.delay.behavior";
/**
* Property used to configure a builder from a Properties object. {@value}, see {@link Builder#pedantic() pedantic}.
*/
Expand Down Expand Up @@ -487,13 +553,28 @@ public static HostnameResolveMode get(String value) {
*/
public static final String PROP_HOSTNAME_RESOLVE_MODE = PFX + "hostnameResolveMode";
/**
* Property used to configure a builder from a Properties object. {@value}, see {@link Builder#noSubjectValidation() noSubjectValidation}.
* Property used to configure a builder from a Properties object. {@value}, when {@code true} sets the
* {@link SubjectValidationType} to {@link SubjectValidationType#None}. See {@link Builder#noSubjectValidation() noSubjectValidation}.
* @deprecated use {@link #PROP_SUBJECT_VALIDATION_TYPE} with value {@code None} instead
*/
@Deprecated
public static final String PROP_NO_SUBJECT_VALIDATION = PFX + "noSubjectValidation";
/**
* Property used to configure a builder from a Properties object. {@value}, see {@link Builder#noSubjectValidation() noSubjectValidation}.
* Property used to configure a builder from a Properties object. {@value}, when {@code true} sets the
* {@link SubjectValidationType} to {@link SubjectValidationType#Strict}. See {@link Builder#strictSubjectValidation() strictSubjectValidation}.
* @deprecated use {@link #PROP_SUBJECT_VALIDATION_TYPE} with value {@code Strict} instead
*/
@Deprecated
public static final String PROP_STRICT_SUBJECT_VALIDATION = PFX + "strictSubjectValidation";
/**
* Property used to configure a builder from a Properties object. {@value}, sets the
* {@link SubjectValidationType} used to validate subjects. Value is the case-insensitive name
* of a {@link SubjectValidationType} constant (e.g. {@code None}, {@code Lenient}, {@code Strict}).
* Unrecognized or missing values fall back to {@link SubjectValidationType#Lenient}.
* Processed after {@link #PROP_NO_SUBJECT_VALIDATION} and {@link #PROP_STRICT_SUBJECT_VALIDATION},
* so when set this property takes precedence over those legacy boolean properties.
*/
public static final String PROP_SUBJECT_VALIDATION_TYPE = PFX + "subjectValidationType";
/**
* Property used to configure a builder from a Properties object. {@value}, see {@link Builder#reportNoResponders() reportNoResponders}.
*/
Expand Down Expand Up @@ -821,6 +902,7 @@ public static HostnameResolveMode get(String value) {

private final AuthHandler authHandler;
private final ReconnectDelayHandler reconnectDelayHandler;
private final ReconnectDelayBehavior reconnectDelayBehavior;

private final ErrorListener errorListener;
private final TimeTraceLogger timeTraceLogger;
Expand Down Expand Up @@ -987,6 +1069,7 @@ public static class Builder {

private AuthHandler authHandler;
private ReconnectDelayHandler reconnectDelayHandler;
private ReconnectDelayBehavior reconnectDelayBehavior = ReconnectDelayBehavior.BeforeSubsequentRounds;

private ErrorListener errorListener = null;
private TimeTraceLogger timeTraceLogger = null;
Expand Down Expand Up @@ -1085,6 +1168,8 @@ public Builder properties(Properties props) {
booleanProperty(props, PROP_NORANDOMIZE, b -> this.noRandomize = b);
booleanPropertyIfTrue(props, PROP_NO_SUBJECT_VALIDATION, b -> subjectValidationType = SubjectValidationType.None);
booleanPropertyIfTrue(props, PROP_STRICT_SUBJECT_VALIDATION, b -> subjectValidationType = SubjectValidationType.Strict);
stringProperty(props, PROP_SUBJECT_VALIDATION_TYPE, s -> this.subjectValidationType = SubjectValidationType.get(s));

booleanProperty(props, PROP_REPORT_NO_RESPONDERS, b -> this.reportNoResponders = b);

stringProperty(props, PROP_CONNECTION_NAME, s -> this.connectionName = s);
Expand All @@ -1101,6 +1186,9 @@ public Builder properties(Properties props) {
durationProperty(props, PROP_RECONNECT_JITTER, d -> this.reconnectJitter = d);
durationProperty(props, PROP_RECONNECT_JITTER_TLS, d -> this.reconnectJitterTls = d);
longProperty(props, PROP_RECONNECT_BUF_SIZE, l -> this.reconnectBufferSize = l);

classnameProperty(props, PROP_RECONNECT_DELAY_HANDLER_CLASS, o -> this.reconnectDelayHandler = (ReconnectDelayHandler) o);
stringProperty(props, PROP_RECONNECT_DELAY_BEHAVIOR, s -> this.reconnectDelayBehavior = ReconnectDelayBehavior.get(s));
durationProperty(props, PROP_CONNECTION_TIMEOUT, d -> this.connectionTimeout = d);
intProperty(props, PROP_SOCKET_READ_TIMEOUT_MS, i -> this.socketReadTimeoutMillis = i);
durationProperty(props, PROP_SOCKET_WRITE_TIMEOUT, d -> this.socketWriteTimeout = d);
Expand Down Expand Up @@ -1259,7 +1347,9 @@ public Builder hostnameResolveMode(HostnameResolveMode hostnameResolveMode) {
* Fastest validation, but use with caution
* If you know your subjects are always valid.
* @return the Builder for chaining
* @deprecated use {@link #subjectValidationType(SubjectValidationType)} with {@link SubjectValidationType#None} instead
*/
@Deprecated
public Builder noSubjectValidation() {
this.subjectValidationType = SubjectValidationType.None;
return this;
Expand All @@ -1271,7 +1361,9 @@ public Builder noSubjectValidation() {
* Slower validation, but may be useful when exposing the ability
* of an application user to set a subject.
* @return the Builder for chaining
* @deprecated use {@link #subjectValidationType(SubjectValidationType)} with {@link SubjectValidationType#Strict} instead
*/
@Deprecated
public Builder strictSubjectValidation() {
this.subjectValidationType = SubjectValidationType.Strict;
return this;
Expand Down Expand Up @@ -1867,6 +1959,19 @@ public Builder reconnectDelayHandler(ReconnectDelayHandler handler) {
return this;
}

/**
* Set the {@link ReconnectDelayBehavior ReconnectDelayBehavior} that controls when the
* {@link ReconnectDelayHandler} is invoked during reconnect attempts. Defaults to
* {@link ReconnectDelayBehavior#BeforeSubsequentRounds}. A null value resets to {@link ReconnectDelayBehavior#BeforeSubsequentRounds}.
*
* @param reconnectDelayBehavior the behavior
* @return the Builder for chaining
*/
public Builder reconnectDelayBehavior(ReconnectDelayBehavior reconnectDelayBehavior) {
this.reconnectDelayBehavior = reconnectDelayBehavior == null ? ReconnectDelayBehavior.BeforeSubsequentRounds : reconnectDelayBehavior;
return this;
}

/**
* Set the {@link ErrorListener ErrorListener} to receive asynchronous error events related to this
* connection.
Expand Down Expand Up @@ -2347,6 +2452,7 @@ public Builder(Options o) {

this.authHandler = o.authHandler;
this.reconnectDelayHandler = o.reconnectDelayHandler;
this.reconnectDelayBehavior = o.reconnectDelayBehavior;

this.errorListener = o.errorListener;
this.timeTraceLogger = o.timeTraceLogger;
Expand Down Expand Up @@ -2424,6 +2530,7 @@ private Options(Builder b) {

this.authHandler = b.authHandler;
this.reconnectDelayHandler = b.reconnectDelayHandler;
this.reconnectDelayBehavior = b.reconnectDelayBehavior;

this.errorListener = b.errorListener;
this.timeTraceLogger = b.timeTraceLogger;
Expand Down Expand Up @@ -2734,6 +2841,14 @@ public ReconnectDelayHandler getReconnectDelayHandler() {
return this.reconnectDelayHandler;
}

/**
* the reconnect delay behavior, see {@link Builder#reconnectDelayBehavior(ReconnectDelayBehavior) reconnectDelayBehavior()} in the builder doc
* @return the behavior, never null (defaults to {@link ReconnectDelayBehavior#BeforeSubsequentRounds})
*/
public ReconnectDelayBehavior reconnectDelayBehavior() {
return this.reconnectDelayBehavior;
}

/**
* the DataPort class type for connections created by this options object, see {@link Builder#dataPortType(String) dataPortType()} in the builder doc
* @return the DataPort class type
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/nats/client/impl/NatsConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ protected void reconnectImplConnect() throws InterruptedException {
while ((cur = serverPool.nextServer()) != null) {
if (first == null) {
first = cur;
if (options.reconnectDelayBehavior() == Options.ReconnectDelayBehavior.BeforeAllRounds) {
invokeReconnectDelayHandler(0);
Comment thread
scottf marked this conversation as resolved.
}
}
else if (first.equals(cur)) {
// went around the pool an entire time
Expand Down
128 changes: 128 additions & 0 deletions src/test/java/io/nats/client/OptionsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,48 @@ public void testPropertiesSubjectValidationType() {
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.Strict, o.subjectValidationType());

// PROP_SUBJECT_VALIDATION_TYPE — case-insensitive enum name match for each value
props.clear();
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "None");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.None, o.subjectValidationType());

props.clear();
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "lenient");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.Lenient, o.subjectValidationType());

props.clear();
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "STRICT");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.Strict, o.subjectValidationType());

// Unrecognized value falls back to Lenient
props.clear();
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "garbage");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.Lenient, o.subjectValidationType());

// PROP_SUBJECT_VALIDATION_TYPE is processed last, so it wins over the legacy booleans
props.clear();
props.setProperty(Options.PROP_NO_SUBJECT_VALIDATION, "true");
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "Strict");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.Strict, o.subjectValidationType());

props.clear();
props.setProperty(Options.PROP_STRICT_SUBJECT_VALIDATION, "true");
props.setProperty(Options.PROP_SUBJECT_VALIDATION_TYPE, "None");
o = new Options.Builder(props).build();
assertEquals(SubjectValidationType.None, o.subjectValidationType());

// SubjectValidationType.get direct coverage
assertEquals(SubjectValidationType.None, SubjectValidationType.get("none"));
assertEquals(SubjectValidationType.Lenient, SubjectValidationType.get("Lenient"));
assertEquals(SubjectValidationType.Strict, SubjectValidationType.get("STRICT"));
assertEquals(SubjectValidationType.Lenient, SubjectValidationType.get(null));
assertEquals(SubjectValidationType.Lenient, SubjectValidationType.get("garbage"));

o = new Options.Builder().build();
assertEquals(SubjectValidationType.Lenient, o.subjectValidationType());

Expand All @@ -732,6 +774,77 @@ public void testPropertiesSubjectValidationType() {

o = new Options.Builder().strictSubjectValidation().build();
assertEquals(SubjectValidationType.Strict, o.subjectValidationType());

// Builder.subjectValidationType — all three values plus null reset
o = new Options.Builder().subjectValidationType(SubjectValidationType.None).build();
assertEquals(SubjectValidationType.None, o.subjectValidationType());

o = new Options.Builder().subjectValidationType(SubjectValidationType.Lenient).build();
assertEquals(SubjectValidationType.Lenient, o.subjectValidationType());

o = new Options.Builder().subjectValidationType(SubjectValidationType.Strict).build();
assertEquals(SubjectValidationType.Strict, o.subjectValidationType());

// null resets to Lenient
o = new Options.Builder().subjectValidationType(SubjectValidationType.Strict)
.subjectValidationType(null).build();
assertEquals(SubjectValidationType.Lenient, o.subjectValidationType());
}

@Test
public void testReconnectDelayBehavior() {
// default
Options o = new Options.Builder().build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

// builder set explicitly
o = new Options.Builder().reconnectDelayBehavior(ReconnectDelayBehavior.BeforeSubsequentRounds).build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

o = new Options.Builder().reconnectDelayBehavior(ReconnectDelayBehavior.BeforeAllRounds).build();
assertEquals(ReconnectDelayBehavior.BeforeAllRounds, o.reconnectDelayBehavior());

// null resets to BeforeSubsequentRounds (default)
o = new Options.Builder().reconnectDelayBehavior(ReconnectDelayBehavior.BeforeAllRounds)
.reconnectDelayBehavior(null).build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

// via properties — case-insensitive enum name match
Properties props = new Properties();
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

props.clear();
props.setProperty(Options.PROP_RECONNECT_DELAY_BEHAVIOR, "BeforeSubsequentRounds");
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

props.clear();
props.setProperty(Options.PROP_RECONNECT_DELAY_BEHAVIOR, "BeforeAllRounds");
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeAllRounds, o.reconnectDelayBehavior());

props.clear();
props.setProperty(Options.PROP_RECONNECT_DELAY_BEHAVIOR, "beforeallrounds");
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeAllRounds, o.reconnectDelayBehavior());

props.clear();
props.setProperty(Options.PROP_RECONNECT_DELAY_BEHAVIOR, "BEFOREALLROUNDS");
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeAllRounds, o.reconnectDelayBehavior());

// unrecognized value falls back to BeforeSubsequentRounds (default)
props.clear();
props.setProperty(Options.PROP_RECONNECT_DELAY_BEHAVIOR, "garbage");
o = new Options.Builder(props).build();
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, o.reconnectDelayBehavior());

// ReconnectDelayBehavior.get direct coverage
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, ReconnectDelayBehavior.get("beforesubsequentrounds"));
assertEquals(ReconnectDelayBehavior.BeforeAllRounds, ReconnectDelayBehavior.get("BEFOREALLROUNDS"));
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, ReconnectDelayBehavior.get(null));
assertEquals(ReconnectDelayBehavior.BeforeSubsequentRounds, ReconnectDelayBehavior.get("garbage"));
}

@Test
Expand Down Expand Up @@ -1304,6 +1417,21 @@ public void testReconnectDelayHandler() {

assertNotNull(rdhO);
assertEquals(10, rdhO.getWaitTime(5).getSeconds());

// PROP_RECONNECT_DELAY_HANDLER_CLASS — instantiate by class name
Properties props = new Properties();
props.setProperty(Options.PROP_RECONNECT_DELAY_HANDLER_CLASS,
"io.nats.client.utils.CoverageReconnectDelayHandler");
o = new Options.Builder(props).build();
rdhO = o.getReconnectDelayHandler();
assertNotNull(rdhO);
assertInstanceOf(io.nats.client.utils.CoverageReconnectDelayHandler.class, rdhO);
assertEquals(7, rdhO.getWaitTime(7).toMillis());

// bad classname surfaces as IllegalArgumentException at build time
Properties badProps = new Properties();
badProps.setProperty(Options.PROP_RECONNECT_DELAY_HANDLER_CLASS, "does.not.Exist");
assertThrows(IllegalArgumentException.class, () -> new Options.Builder(badProps).build());
}

@Test
Expand Down
Loading
Loading