io.nats.service.Service Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jnats Show documentation
Show all versions of jnats Show documentation
Client library for working with the NATS messaging system.
// Copyright 2023 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.service;
import io.nats.client.Connection;
import io.nats.client.Dispatcher;
import io.nats.client.support.DateTimeUtils;
import io.nats.client.support.JsonUtils;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static io.nats.client.support.ApiConstants.*;
import static io.nats.client.support.JsonUtils.endJson;
import static io.nats.client.support.Validator.nullOrEmpty;
/**
* The Services Framework introduces a higher-level API for implementing services with NATS.
* Services automatically contain Ping, Info and Stats responders.
* Services have one or more service endpoints. {@link ServiceEndpoint}
* When multiple instances of a service endpoints are active they work in a queue, meaning only one listener responds to any given request.
*/
public class Service {
public static final String SRV_PING = "PING";
public static final String SRV_INFO = "INFO";
public static final String SRV_STATS = "STATS";
public static final String DEFAULT_SERVICE_PREFIX = "$SRV.";
private final Connection conn;
private final Duration drainTimeout;
private final Map serviceContexts;
private final List discoveryContexts;
private final List dInternals;
private final PingResponse pingResponse;
private final InfoResponse infoResponse;
private final ReentrantLock startStopLock;
private CompletableFuture runningIndicator;
private ZonedDateTime started;
Service(ServiceBuilder b) {
String id = new io.nats.client.NUID().next();
conn = b.conn;
drainTimeout = b.drainTimeout;
dInternals = new ArrayList<>();
startStopLock = new ReentrantLock();
// set up the service contexts
// ? do we need an internal dispatcher for any user endpoints
// ! also while we are here, we need to collect the endpoints for the SchemaResponse
Dispatcher dTemp = null;
serviceContexts = new HashMap<>();
for (ServiceEndpoint se : b.serviceEndpoints.values()) {
if (se.getDispatcher() == null) {
if (dTemp == null) {
dTemp = conn.createDispatcher();
}
serviceContexts.put(se.getName(), new EndpointContext(conn, dTemp, false, se));
}
else {
serviceContexts.put(se.getName(), new EndpointContext(conn, null, false, se));
}
}
if (dTemp != null) {
dInternals.add(dTemp);
}
// build static responses
pingResponse = new PingResponse(id, b.name, b.version, b.metadata);
infoResponse = new InfoResponse(id, b.name, b.version, b.metadata, b.description, b.serviceEndpoints.values());
if (b.pingDispatcher == null || b.infoDispatcher == null || b.schemaDispatcher == null || b.statsDispatcher == null) {
dTemp = conn.createDispatcher();
dInternals.add(dTemp);
}
else {
dTemp = null;
}
discoveryContexts = new ArrayList<>();
addDiscoveryContexts(SRV_PING, pingResponse, b.pingDispatcher, dTemp);
addDiscoveryContexts(SRV_INFO, infoResponse, b.infoDispatcher, dTemp);
addStatsContexts(b.statsDispatcher, dTemp);
}
private void addDiscoveryContexts(String discoveryName, Dispatcher dUser, Dispatcher dInternal, ServiceMessageHandler handler) {
Endpoint[] endpoints = new Endpoint[] {
internalEndpoint(discoveryName, null, null),
internalEndpoint(discoveryName, pingResponse.getName(), null),
internalEndpoint(discoveryName, pingResponse.getName(), pingResponse.getId())
};
for (Endpoint endpoint : endpoints) {
discoveryContexts.add(
new EndpointContext(conn, dInternal, true,
new ServiceEndpoint(endpoint, handler, dUser)));
}
}
private void addDiscoveryContexts(String discoveryName, ServiceResponse sr, Dispatcher dUser, Dispatcher dInternal) {
final byte[] responseBytes = sr.serialize();
ServiceMessageHandler handler = smsg -> smsg.respond(conn, responseBytes);
addDiscoveryContexts(discoveryName, dUser, dInternal, handler);
}
private void addStatsContexts(Dispatcher dUser, Dispatcher dInternal) {
ServiceMessageHandler handler = smsg -> smsg.respond(conn, getStatsResponse().serialize());
addDiscoveryContexts(SRV_STATS, dUser, dInternal, handler);
}
private Endpoint internalEndpoint(String discoveryName, String optionalServiceNameSegment, String optionalServiceIdSegment) {
String subject = toDiscoverySubject(discoveryName, optionalServiceNameSegment, optionalServiceIdSegment);
return new Endpoint(subject, subject, null, null, false);
}
static String toDiscoverySubject(String discoveryName, String optionalServiceNameSegment, String optionalServiceIdSegment) {
if (nullOrEmpty(optionalServiceIdSegment)) {
if (nullOrEmpty(optionalServiceNameSegment)) {
return DEFAULT_SERVICE_PREFIX + discoveryName;
}
return DEFAULT_SERVICE_PREFIX + discoveryName + "." + optionalServiceNameSegment;
}
return DEFAULT_SERVICE_PREFIX + discoveryName + "." + optionalServiceNameSegment + "." + optionalServiceIdSegment;
}
/**
* Start the service
* @return a future that can be held to see if another thread called stop
*/
public CompletableFuture startService() {
startStopLock.lock();
try {
if (runningIndicator == null) {
runningIndicator = new CompletableFuture<>();
for (EndpointContext ctx : serviceContexts.values()) {
ctx.start();
}
for (EndpointContext ctx : discoveryContexts) {
ctx.start();
}
started = DateTimeUtils.gmtNow();
}
return runningIndicator;
}
finally {
startStopLock.unlock();
}
}
/**
* Get an instance of a ServiceBuilder.
* @return the instance
*/
public static ServiceBuilder builder() {
return new ServiceBuilder();
}
/**
* Stop the service by draining.
*/
public void stop() {
stop(true, null);
}
/**
* Stop the service by draining. Mark the future that was received from the start method that the service completed exceptionally.
* @param t the error cause
*/
public void stop(Throwable t) {
stop(true, t);
}
/**
* Stop the service, optionally draining.
* @param drain the flag indicating to drain or not
*/
public void stop(boolean drain) {
stop(drain, null);
}
/**
* Stop the service, optionally draining and optionally with an error cause
* @param drain the flag indicating to drain or not
* @param t the optional error cause. If supplied, mark the future that was received from the start method that the service completed exceptionally
*/
public void stop(boolean drain, Throwable t) {
startStopLock.lock();
try {
if (runningIndicator != null) {
if (drain) {
List> futures = new ArrayList<>();
for (Dispatcher d : dInternals) {
try {
futures.add(d.drain(drainTimeout));
}
catch (Exception e) { /* nothing I can really do, we are stopping anyway */ }
}
for (EndpointContext c : serviceContexts.values()) {
if (c.isNotInternalDispatcher()) {
try {
futures.add(c.getSub().drain(drainTimeout));
}
catch (Exception e) { /* nothing I can really do, we are stopping anyway */ }
}
}
for (EndpointContext c : discoveryContexts) {
if (c.isNotInternalDispatcher()) {
try {
futures.add(c.getSub().drain(drainTimeout));
}
catch (Exception e) { /* nothing I can really do, we are stopping anyway */ }
}
}
// make sure drain is done before closing dispatcher
long drainTimeoutMillis = drainTimeout.toMillis();
for (CompletableFuture f : futures) {
try {
f.get(drainTimeoutMillis, TimeUnit.MILLISECONDS);
}
catch (Exception ignore) {
// don't care if it completes successfully or not, just that it's done.
}
}
}
// close internal dispatchers
for (Dispatcher d : dInternals) {
conn.closeDispatcher(d);
}
// ok we are done
if (t == null) {
runningIndicator.complete(true);
}
else {
runningIndicator.completeExceptionally(t);
}
runningIndicator = null; // we don't need a copy anymore
}
}
finally {
startStopLock.unlock();
}
}
/**
* Reset the statistics for the endpoints
*/
public void reset() {
started = DateTimeUtils.gmtNow();
for (EndpointContext c : discoveryContexts) {
c.reset();
}
for (EndpointContext c : serviceContexts.values()) {
c.reset();
}
}
/**
* Get the id of the service
* @return the id
*/
public String getId() {
return infoResponse.getId();
}
/**
* Get the name of the service
* @return the name
*/
public String getName() {
return infoResponse.getName();
}
/**
* Get the version of the service
* @return the version
*/
public String getVersion() {
return infoResponse.getVersion();
}
/**
* Get the description of the service
* @return the description
*/
public String getDescription() {
return infoResponse.getDescription();
}
/**
* Get the drain timeout setting
* @return the drain timeout setting
*/
public Duration getDrainTimeout() {
return drainTimeout;
}
/**
* Get the pre-constructed ping response.
* @return the ping response
*/
public PingResponse getPingResponse() {
return pingResponse;
}
/**
* Get the pre-constructed info response.
* @return the info response
*/
public InfoResponse getInfoResponse() {
return infoResponse;
}
/**
* Get the up-to-date stats response which contains a list of all {@link EndpointStats}
* @return the stats response
*/
public StatsResponse getStatsResponse() {
List endpointStats = new ArrayList<>();
for (EndpointContext c : serviceContexts.values()) {
endpointStats.add(c.getEndpointStats());
}
return new StatsResponse(pingResponse, started, endpointStats);
}
/**
* Get the up-to-date {@link EndpointStats} for a specific endpoint
* @param endpointName the endpoint name
* @return the EndpointStats or null if the name is not found.
*/
public EndpointStats getEndpointStats(String endpointName) {
EndpointContext c = serviceContexts.get(endpointName);
return c == null ? null : c.getEndpointStats();
}
@Override
public String toString() {
StringBuilder sb = JsonUtils.beginJsonPrefixed("\"Service\":");
JsonUtils.addField(sb, ID, infoResponse.getId());
JsonUtils.addField(sb, NAME, infoResponse.getName());
JsonUtils.addField(sb, VERSION, infoResponse.getVersion());
JsonUtils.addField(sb, DESCRIPTION, infoResponse.getDescription());
return endJson(sb).toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy