Skip to content

Commit e24f438

Browse files
committed
feat: implement NATS failover connection manager with configurable reconnection logic
1 parent a76654a commit e24f438

File tree

5 files changed

+416
-10
lines changed

5 files changed

+416
-10
lines changed

api/src/main/java/app/simplecloud/api/CloudApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public interface CloudApi {
4848
* <li>SIMPLECLOUD_NETWORK_ID (default: "default")</li>
4949
* <li>SIMPLECLOUD_NETWORK_SECRET (default: "")</li>
5050
* <li>SIMPLECLOUD_NATS_URL (default: "nats://platform.simplecloud.app:4222")</li>
51+
* <li>SIMPLECLOUD_NATS_FAILOVER_RECONNECT_AFTER (default: "30s")</li>
5152
* <li>SIMPLECLOUD_CONTROLLER_URL (default: "https://controller.platform.simplecloud.app")</li>
5253
* </ul>
5354
*
@@ -143,4 +144,3 @@ static CloudApi create(CloudApiOptions options) {
143144
QueryCache cache();
144145

145146
}
146-

api/src/main/java/app/simplecloud/api/CloudApiOptions.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@
22

33
import app.simplecloud.api.cache.CacheConfig;
44

5+
import java.time.Duration;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
59
public class CloudApiOptions {
610

711
public final static CloudApiOptions DEFAULT = new Builder().build();
12+
private static final Pattern DURATION_PATTERN = Pattern.compile("^(\\d+)(ms|s|m|h)$");
813

914
private final String natsUrl;
15+
private final Duration natsFailoverReconnectAfter;
1016
private final String controllerUrl;
1117
private final String networkId;
1218
private final String networkSecret;
1319
private final CacheConfig cacheConfig;
1420

1521
private CloudApiOptions(Builder builder) {
1622
this.natsUrl = builder.natsUrl;
23+
this.natsFailoverReconnectAfter = builder.natsFailoverReconnectAfter;
1724
this.controllerUrl = builder.controllerUrl;
1825
this.networkId = builder.networkId;
1926
this.networkSecret = builder.networkSecret;
@@ -24,6 +31,10 @@ public String getNatsUrl() {
2431
return natsUrl;
2532
}
2633

34+
public Duration getNatsFailoverReconnectAfter() {
35+
return natsFailoverReconnectAfter;
36+
}
37+
2738
public String getControllerUrl() {
2839
return controllerUrl;
2940
}
@@ -51,13 +62,18 @@ public static Builder builder() {
5162

5263
public static class Builder {
5364
private String natsUrl;
65+
private Duration natsFailoverReconnectAfter;
5466
private String controllerUrl;
5567
private String networkId;
5668
private String networkSecret;
5769
private CacheConfig cacheConfig = CacheConfig.DEFAULT;
5870

5971
public Builder() {
6072
this.natsUrl = System.getenv().getOrDefault("SIMPLECLOUD_NATS_URL", "nats://platform.simplecloud.app:4222");
73+
this.natsFailoverReconnectAfter = parseDuration(
74+
System.getenv("SIMPLECLOUD_NATS_FAILOVER_RECONNECT_AFTER"),
75+
Duration.ofSeconds(30)
76+
);
6177
this.controllerUrl = System.getenv().getOrDefault("SIMPLECLOUD_CONTROLLER_URL", "https://controller.platform.simplecloud.app");
6278
this.networkId = System.getenv().getOrDefault("SIMPLECLOUD_NETWORK_ID", "default");
6379
this.networkSecret = System.getenv().getOrDefault("SIMPLECLOUD_NETWORK_SECRET", "");
@@ -68,6 +84,17 @@ public Builder natsUrl(String natsUrl) {
6884
return this;
6985
}
7086

87+
public Builder natsFailoverReconnectAfter(Duration natsFailoverReconnectAfter) {
88+
if (natsFailoverReconnectAfter == null) {
89+
throw new IllegalArgumentException("natsFailoverReconnectAfter must not be null");
90+
}
91+
if (natsFailoverReconnectAfter.isNegative()) {
92+
throw new IllegalArgumentException("natsFailoverReconnectAfter must be >= 0");
93+
}
94+
this.natsFailoverReconnectAfter = natsFailoverReconnectAfter;
95+
return this;
96+
}
97+
7198
public Builder controllerUrl(String controllerUrl) {
7299
this.controllerUrl = controllerUrl;
73100
return this;
@@ -111,5 +138,38 @@ public Builder disableCache() {
111138
public CloudApiOptions build() {
112139
return new CloudApiOptions(this);
113140
}
141+
142+
private Duration parseDuration(String raw, Duration defaultValue) {
143+
if (raw == null || raw.isBlank()) {
144+
return defaultValue;
145+
}
146+
147+
String value = raw.trim().toLowerCase();
148+
Matcher matcher = DURATION_PATTERN.matcher(value);
149+
if (matcher.matches()) {
150+
long amount = Long.parseLong(matcher.group(1));
151+
return switch (matcher.group(2)) {
152+
case "ms" -> Duration.ofMillis(amount);
153+
case "s" -> Duration.ofSeconds(amount);
154+
case "m" -> Duration.ofMinutes(amount);
155+
case "h" -> Duration.ofHours(amount);
156+
default -> defaultValue;
157+
};
158+
}
159+
160+
try {
161+
Duration duration = Duration.parse(raw);
162+
if (duration.isNegative()) {
163+
throw new IllegalArgumentException("Duration must be >= 0");
164+
}
165+
return duration;
166+
} catch (Exception e) {
167+
throw new IllegalArgumentException(
168+
"Invalid SIMPLECLOUD_NATS_FAILOVER_RECONNECT_AFTER value '" + raw +
169+
"'. Use values like 30s, 2m, 1h, 500ms, or ISO-8601 (e.g. PT30S).",
170+
e
171+
);
172+
}
173+
}
114174
}
115175
}

api/src/main/java/app/simplecloud/api/internal/CloudApiImpl.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,22 @@
1111
import app.simplecloud.api.internal.cache.QueryCacheImpl;
1212
import app.simplecloud.api.internal.event.EventApiImpl;
1313
import app.simplecloud.api.internal.group.GroupApiImpl;
14+
import app.simplecloud.api.internal.nats.NatsFailoverConnectionManager;
1415
import app.simplecloud.api.internal.persistentserver.PersistentServerApiImpl;
1516
import app.simplecloud.api.internal.server.ServerApiImpl;
1617
import app.simplecloud.api.internal.player.PlayerApiImpl;
1718
import app.simplecloud.api.persistentserver.PersistentServerApi;
1819
import app.simplecloud.api.player.PlayerApi;
1920
import app.simplecloud.api.server.ServerApi;
2021
import io.nats.client.Connection;
21-
import io.nats.client.Nats;
22-
import io.nats.client.Options;
2322

2423
import java.io.IOException;
2524

2625
public class CloudApiImpl implements CloudApi {
2726

2827
private final CloudApiOptions options;
2928

29+
private final NatsFailoverConnectionManager natsConnectionManager;
3030
private final Connection natsClient;
3131
private final QueryCache queryCache;
3232
private final CacheEventListener cacheEventListener;
@@ -40,12 +40,13 @@ public CloudApiImpl(CloudApiOptions options) {
4040
this.options = options;
4141

4242
try {
43-
this.natsClient = Nats.connect(
44-
Options.builder()
45-
.server(options.getNatsUrl())
46-
.userInfo(options.getNetworkId(), options.getNetworkSecret())
47-
.build()
43+
this.natsConnectionManager = new NatsFailoverConnectionManager(
44+
options.getNatsUrl(),
45+
options.getNetworkId(),
46+
options.getNetworkSecret(),
47+
options.getNatsFailoverReconnectAfter()
4848
);
49+
this.natsClient = natsConnectionManager.getConnection();
4950
} catch (IOException | InterruptedException e) {
5051
throw new RuntimeException(e);
5152
}
@@ -120,4 +121,3 @@ public QueryCache cache() {
120121
}
121122

122123
}
123-

0 commit comments

Comments
 (0)