Skip to content
Open
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
2 changes: 1 addition & 1 deletion deploy/helm/icms/icms-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,4 @@ sis:
AWS_REGION: "ncp"

# NATS Configuration
NATS_RECONNECT_ALLOWED: "true"
NATS_UNLIMITED_RECONNECTS: "true"
Comment thread
dmikhaylovnv marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

Lists of 293 third-party dependencies.
Lists of 290 third-party dependencies.
(Apache License, Version 2.0) LZ4 Java Compression (at.yawk.lz4:lz4-java:1.10.3 - https://github.com/yawkat/lz4-java)
(EPL-2.0) (LGPL-2.1-only) Logback Classic Module (ch.qos.logback:logback-classic:1.5.37 - http://logback.qos.ch)
(EPL-2.0) (LGPL-2.1-only) Logback Core Module (ch.qos.logback:logback-core:1.5.37 - http://logback.qos.ch)
Expand Down Expand Up @@ -69,9 +69,6 @@ Lists of 293 third-party dependencies.
(The Apache Software License, Version 2.0) micrometer-tracing (io.micrometer:micrometer-tracing:1.6.6 - https://github.com/micrometer-metrics/tracing)
(The Apache Software License, Version 2.0) micrometer-tracing-bridge-otel (io.micrometer:micrometer-tracing-bridge-otel:1.6.6 - https://github.com/micrometer-metrics/tracing)
(The Apache License, Version 2.0) jnats (io.nats:jnats:2.23.0 - https://github.com/nats-io/nats.java)
(The Apache License, Version 2.0) jnats-json (io.nats:jnats-json:2.0.1 - https://github.com/nats-io/nats.java.json)
(The Apache License, Version 2.0) jwt-java (io.nats:jwt-java:2.2.0 - https://github.com/nats-io/jwt.java)
(The Apache License, Version 2.0) nkeys-java (io.nats:nkeys-java:2.1.1 - https://github.com/nats-io/nkeys.java)
(Apache License, Version 2.0) Netty/Buffer (io.netty:netty-buffer:4.2.15.Final - https://netty.io/)
(Apache License, Version 2.0) Netty/Codec (io.netty:netty-codec:4.2.15.Final - https://netty.io/)
(Apache License, Version 2.0) Netty/Codec/Base (io.netty:netty-codec-base:4.2.15.Final - https://netty.io/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ ICMS_CORE_DEPS = [
"@nv_third_party_deps//:io_micrometer_micrometer_registry_prometheus",
"@nv_third_party_deps//:io_micrometer_micrometer_tracing",
"@nv_third_party_deps//:io_nats_jnats",
"@nv_third_party_deps//:io_nats_jwt_java",
"@nv_third_party_deps//:io_netty_netty_transport",
"@nv_third_party_deps//:io_netty_netty_handler",
"@nv_third_party_deps//:io_opentelemetry_opentelemetry_exporter_otlp",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* 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 com.nvidia.icms.configuration.nats;

import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.tracing.Tracer;
import io.nats.client.Connection;
import io.nats.client.Connection.Status;
import io.nats.client.ConnectionListener.Events;
import io.nats.client.ErrorListener;
import io.nats.client.ForceReconnectOptions;
import io.nats.client.JetStream;
import io.nats.client.JetStreamManagement;
import io.nats.client.Nats;
import io.nats.client.Options;
import io.nats.client.Statistics;
import io.nats.client.impl.TracedNatsConnection;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.ToDoubleFunction;
import java.util.function.ToLongFunction;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Slf4j
@Configuration(proxyBeanMethods = false)
public final class NatsConfiguration {

private static Connection connect(Options options, Tracer tracer)
throws IOException, InterruptedException {
TracedNatsConnection conn = new TracedNatsConnection(options, tracer);
conn.connect(false);
return conn;
}

@Bean
@Scope(SCOPE_PROTOTYPE)
public Connection natsConnection(
NatsConfigurationProperties natsProperties,
Tracer tracer)
throws IOException, InterruptedException {
if (natsProperties.isEnabled()) {
var options = createDefaultOptions(natsProperties);
return connect(options, tracer);
}

return null;
}

Options createDefaultOptions(NatsConfigurationProperties natsConfigurationProperties) {
var flushDuration = natsConfigurationProperties.getForceReconnectFlush().isPositive() ?
natsConfigurationProperties.getForceReconnectFlush() : Duration.ofSeconds(5);
var builder = Options.builder()
.server(natsConfigurationProperties.getNatsUrl())
.connectionTimeout(natsConfigurationProperties.getConnectionTimeout())
.useDispatcherWithExecutor()
.errorListener(new LoggingNatsErrorListener())
.connectionListener(
(conn, type) -> {
log.info("nats connection event {} {}", type,
conn.getServerInfo());
if (type == Events.LAME_DUCK) {
CompletableFuture.runAsync(() -> {
try {
// jitter
Thread.sleep(RandomUtils.secure().randomInt(0, 5000));
// this may cause issues, but hopefully the active force
// reconnection is a smaller error window than waiting to get
// booted and detecting it normally.
log.info("client id {} force reconnecting to nats",
conn.getServerInfo().getClientId());
conn.forceReconnect(ForceReconnectOptions.builder()
.flush(flushDuration)
.build());
log.info("client id {} reconnected to nats",
conn.getServerInfo().getClientId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("client id {} reconnect interrupted",
conn.getServerInfo().getClientId(), e);
throw new RuntimeException(e);
} catch (Exception e) {
log.warn("client id {} failed to reconnect to nats",
conn.getServerInfo().getClientId(), e);
throw new RuntimeException(e);
}
});
}
});

Comment thread
dmikhaylovnv marked this conversation as resolved.
if (natsConfigurationProperties.getNkeySeed().isPresent()) {
var authHandler = Nats.staticCredentials(null,
natsConfigurationProperties.getNkeySeed()
.get().toCharArray());
builder = builder.authHandler(authHandler);
}

if (natsConfigurationProperties.getReconnectJitter().isPositive()) {
Duration reconnectJitter = natsConfigurationProperties.getReconnectJitter();
builder.reconnectJitter(reconnectJitter).reconnectJitterTls(reconnectJitter);
}

if (natsConfigurationProperties.isUnlimitedReconnects()) {
builder = builder.maxReconnects(-1);
}

if (natsConfigurationProperties.getPingInterval().isPositive()) {
builder.pingInterval(natsConfigurationProperties.getPingInterval());
} else {
builder.pingInterval(Duration.ofSeconds(5));
}

if (natsConfigurationProperties.getReconnectWait().isPositive()) {
builder.reconnectWait(natsConfigurationProperties.getReconnectWait());
} else {
builder.reconnectWait(Duration.ofMillis(100));
}

return builder.build();
}

/**
* Custom error listener for logging NATS errors and exceptions.
*/
@Slf4j
private static class LoggingNatsErrorListener implements ErrorListener {

/**
* Logs errors that occur during the NATS connection lifecycle.
*
* @param conn The NATS connection where the error occurred.
* @param error The error message.
*/
@Override
public void errorOccurred(Connection conn, String error) {
log.error("nats error occurred {} {}", conn.getServerInfo(), error);
}

/**
* Logs exceptions that occur during the NATS connection lifecycle.
*
* @param conn The NATS connection where the exception occurred.
* @param exp The exception instance.
*/
@Override
public void exceptionOccurred(Connection conn, Exception exp) {
log.error("nats exception occurred {}", conn.getServerInfo(), exp);
}
}

@Slf4j
@Service
public static class FixedNatsPool implements AutoCloseable {

private final Connection[] connections;
private final JetStream[] jetStreams;
private final JetStreamManagement[] jetStreamManagements;
private final AtomicInteger index = new AtomicInteger();

public FixedNatsPool(ApplicationContext applicationContext,
NatsConfigurationProperties natsProperties) throws IOException {
int processors = Runtime.getRuntime().availableProcessors();
int poolSize = natsProperties.isEnabled() ?
Math.min(processors, natsProperties.getMaxPoolSize()) : 0;
this.connections = new Connection[poolSize];
this.jetStreams = new JetStream[connections.length];
this.jetStreamManagements = new JetStreamManagement[connections.length];
for (int i = 0; i < connections.length; i++) {
connections[i] = applicationContext.getBean(Connection.class);
jetStreams[i] = connections[i].jetStream();
jetStreamManagements[i] = connections[i].jetStreamManagement();
// force connection use
connections[i].RTT();
}
Comment thread
dmikhaylovnv marked this conversation as resolved.
}
Comment thread
dmikhaylovnv marked this conversation as resolved.

@Override
public void close() throws Exception {
Comment thread
dmikhaylovnv marked this conversation as resolved.
for (Connection connection : connections) {
connection.close();
}
}

private int nextIndex() {
if (connections.length == 0) {
Comment thread
dmikhaylovnv marked this conversation as resolved.
throw new IllegalStateException("NATS is not enabled");
}
return Math.floorMod(index.getAndIncrement(), connections.length);
}

public Connection borrowConnection() {
return connections[nextIndex()];
}

public JetStream borrowJetStream() {
return jetStreams[nextIndex()];
}

public JetStreamManagement borrowJetStreamManagement() {
return jetStreamManagements[nextIndex()];
}

public boolean healthy() {
for (Connection connection : connections) {
if (connection.getStatus() != Status.CONNECTED) {
log.warn("unhealthy nats connection {}", connection.getServerInfo());
return false;
}
}
return true;
}
}

@Configuration(proxyBeanMethods = false)
static class NatsMetricsConfiguration implements InitializingBean {

private final Collection<Statistics> statistics;
private final MeterRegistry meterRegistry;

NatsMetricsConfiguration(FixedNatsPool fixedNatsPool, MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.statistics = Arrays.stream(fixedNatsPool.connections)
.map(Connection::getStatistics)
.toList();
}

@Override
public void afterPropertiesSet() {
meterRegistry.more().counter("nats.pings", List.of(),
statistics, sumProperties(Statistics::getPings));
meterRegistry.more().counter("nats.reconnects", List.of(),
statistics, sumProperties(Statistics::getReconnects));
meterRegistry.more().counter("nats.dropped", List.of(),
statistics, sumProperties(Statistics::getDroppedCount));
meterRegistry.more().counter("nats.oks", List.of(),
statistics, sumProperties(Statistics::getOKs));
meterRegistry.more().counter("nats.errs", List.of(),
statistics, sumProperties(Statistics::getErrs));
meterRegistry.more().counter("nats.exceptions", List.of(),
statistics, sumProperties(Statistics::getExceptions));
meterRegistry.more().counter("nats.requests.sent", List.of(),
statistics, sumProperties(Statistics::getRequestsSent));
meterRegistry.more().counter("nats.replies.received", List.of(), statistics,
sumProperties(Statistics::getRepliesReceived));
meterRegistry.more().counter("nats.replies.received.duplicate", List.of(), statistics,
sumProperties(Statistics::getDuplicateRepliesReceived));
meterRegistry.more().counter("nats.replies.received.orphan", List.of(), statistics,
sumProperties(Statistics::getOrphanRepliesReceived));
meterRegistry.more().counter("nats.msgs.in", List.of(),
statistics, sumProperties(Statistics::getInMsgs));
meterRegistry.more().counter("nats.msgs.out", List.of(),
statistics, sumProperties(Statistics::getOutMsgs));
meterRegistry.more().counter("nats.bytes.in", List.of(),
statistics, sumProperties(Statistics::getInBytes));
meterRegistry.more().counter("nats.bytes.out", List.of(),
statistics, sumProperties(Statistics::getOutBytes));
meterRegistry.more().counter("nats.flush", List.of(),
statistics, sumProperties(Statistics::getFlushCounter));
meterRegistry.gauge("nats.requests.outstanding", statistics,
sumProperties(Statistics::getOutstandingRequests));
}

private static <T> ToDoubleFunction<Collection<T>> sumProperties(
ToLongFunction<T> propertyExtractor) {
return collection -> collection.stream().mapToLong(propertyExtractor).sum();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.nvidia.icms.configuration.bean;
package com.nvidia.icms.configuration.nats;

import java.time.Duration;
import java.util.Optional;
Expand All @@ -31,15 +31,15 @@
@Slf4j
public class NatsConfigurationProperties {

private boolean natsEnabled;
private boolean enabled;
private String natsUrl;
private int maxPoolSize = 8;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
private boolean createNatsStreams;
private boolean createNatsConsumers;
private Duration connectionTimeout = Duration.ZERO;
private Duration pingInterval = Duration.ZERO;
private Duration reconnectWait = Duration.ZERO;
private Duration reconnectJitter = Duration.ZERO;
private boolean reconnectAllowed;
private boolean unlimitedReconnects;
private Duration forceReconnectFlush = Duration.ZERO;
private Duration delayBetweenMessages = Duration.ZERO;
private Duration messageTtl = Duration.ZERO;
Expand Down
Loading
Loading