// Copyright 2015-2018 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.streaming; import static io.nats.streaming.NatsStreaming.ERR_CONNECTION_REQ_TIMEOUT; import static io.nats.streaming.NatsStreaming.ERR_SUB_REQ_TIMEOUT; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import io.nats.client.Connection; import io.nats.client.Dispatcher; import io.nats.client.ErrorListener; import io.nats.client.Message; import io.nats.client.NUID; import io.nats.client.Nats; import io.nats.streaming.protobuf.Ack; import io.nats.streaming.protobuf.CloseRequest; import io.nats.streaming.protobuf.CloseResponse; import io.nats.streaming.protobuf.ConnectRequest; import io.nats.streaming.protobuf.ConnectResponse; import io.nats.streaming.protobuf.MsgProto; import io.nats.streaming.protobuf.Ping; import io.nats.streaming.protobuf.PingResponse; import io.nats.streaming.protobuf.PubAck; import io.nats.streaming.protobuf.PubMsg; import io.nats.streaming.protobuf.SubscriptionRequest; import io.nats.streaming.protobuf.SubscriptionResponse; import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; class StreamingConnectionImpl implements StreamingConnection, io.nats.client.MessageHandler { static final String ERR_MANUAL_ACK = NatsStreaming.PFX + "cannot manually ack in auto-ack mode"; static final String INBOX_PREFIX = "_INBOX."; private final ReadWriteLock mu = new ReentrantReadWriteLock(); private String clientId; private String clusterId; private String connectionId; String pubPrefix; // Publish prefix set by streaming, append our subject. String subRequests; // Subject to send subscription requests. String unsubRequests; // Subject to send unsubscribe requests. String subCloseRequests; // Subject to send subscription close requests. String closeRequests; // Subject to send close requests. String ackSubject; // publish acks String hbSubject; String pingInbox; Duration pingInterval; int pingMaxOut; byte[] pingBytes; String pingRequests; int pingsOut; Timer pingTimer; Map<String, Subscription> subMap; Map<String, AckClosure> pubAckMap; private BlockingQueue<PubAck> pubAckChan; Options opts; io.nats.client.Connection nc; io.nats.client.Dispatcher ackDispatcher; io.nats.client.Dispatcher messageDispatcher; Map<String, io.nats.client.Dispatcher> customDispatchers; io.nats.client.Dispatcher dispatcher; // used for internal messaging, heartbeats and pings io.nats.client.Subscription pingSub; io.nats.client.Subscription hbSub; io.nats.client.NUID nuid; final Timer ackTimer = new Timer("jnats-streaming ack timeout thread", true); boolean ncOwned = false; StreamingConnectionImpl(String clusterId, String clientId, Options opts) { this.clusterId = clusterId; this.clientId = clientId; this.nuid = new NUID(); this.opts = opts; this.connectionId = this.nuid.next(); if (opts == null) { opts = new Options.Builder().build(); } // Check if the user has provided a connection as an option if (this.opts.getNatsConn() != null) { setNatsConnection(this.opts.getNatsConn()); } } StreamingConnectionImpl(Options opts) { this(opts.getClusterId(), opts.getClientId(), opts); } void timeTrace(boolean trace, String format, Object... args) { if (trace) { String timeStr = DateTimeFormatter.ISO_TIME.format(LocalDateTime.now()); System.out.printf("[%s] connect trace: ", timeStr); System.out.printf(format, args); System.out.println(); } } // Connect will form a connection to the STAN subsystem. StreamingConnectionImpl connect() throws IOException, InterruptedException { boolean exThrown = false; boolean trace = opts.isTraceConnection(); timeTrace(trace, "starting connection to streaming cluster %s as %s", this.clusterId, this.clientId); io.nats.client.Connection nc = getNatsConnection(); // Create a NATS connection if it doesn't exist if (nc == null) { nc = createNatsConnection(); setNatsConnection(nc); ncOwned = true; } else if (nc.getStatus() != Connection.Status.CONNECTED) { // Bail if the custom NATS connection is disconnected throw new IOException(NatsStreaming.ERR_BAD_CONNECTION); } else { timeTrace(trace, "skipped NATS connection, using existing one"); } try { timeTrace(trace, "creating inboxes"); this.hbSubject = this.newInbox(); this.pingInbox = this.newInbox(); this.ackSubject = String.format("%s.%s", NatsStreaming.DEFAULT_ACK_PREFIX, this.nuid.next()); timeTrace(trace, "creating ack dispatcher"); this.ackDispatcher = nc.createDispatcher(msg -> { this.processAck(msg); }); this.ackDispatcher.subscribe(this.ackSubject); timeTrace(trace, "creating hb/ping dispatcher and subscribing"); this.dispatcher = nc.createDispatcher(msg -> {}); this.hbSub = this.dispatcher.subscribe(this.hbSubject, msg -> { this.processHeartBeat(msg); }); this.pingSub = this.dispatcher.subscribe(this.pingInbox, msg -> { this.processPing(msg); }); timeTrace(trace, "setting pending limits on dispatchers"); this.dispatcher.setPendingLimits(-1, -1); this.ackDispatcher.setPendingLimits(-1, -1); this.customDispatchers = new HashMap<>(); // Send Request to discover the cluster String discoverSubject = String.format("%s.%s", opts.getDiscoverPrefix(), clusterId); // For tests we set to negative for millis // otherwise convert to seconds long pingInterval = opts.getPingInterval().toMillis(); if (pingInterval < 1000) { pingInterval = -pingInterval; } else { pingInterval = pingInterval/1000; } timeTrace(trace, "sending connection request"); ConnectRequest req = ConnectRequest.newBuilder() .setClientID(clientId) .setConnID(ByteString.copyFromUtf8(this.connectionId)) .setHeartbeatInbox(this.hbSubject) .setProtocol(NatsStreaming.PROTOCOL_ONE) .setPingInterval((int)pingInterval) .setPingMaxOut(opts.getMaxPingsOut()).build(); byte[] bytes = req.toByteArray(); Message reply = nc.request(discoverSubject, bytes, opts.getConnectTimeout()); if (reply == null) { throw new IOException(ERR_CONNECTION_REQ_TIMEOUT); } timeTrace(trace, "received connection request"); ConnectResponse cr = ConnectResponse.parseFrom(reply.getData()); if (!cr.getError().isEmpty()) { // This is already a properly formatted streaming error message // (StreamingConnectionImpl.SERVER_ERR_INVALID_CLIENT) throw new IOException(cr.getError()); } // Capture cluster configuration endpoints to publish and // subscribe/unsubscribe. pubPrefix = cr.getPubPrefix(); subRequests = cr.getSubRequests(); unsubRequests = cr.getUnsubRequests(); subCloseRequests = cr.getSubCloseRequests(); closeRequests = cr.getCloseRequests(); boolean unsubPings = true; if (cr.getProtocol() >= NatsStreaming.PROTOCOL_ONE) { timeTrace(trace, "setting up server ping"); // Note that in the future server may override client ping // interval value sent in ConnectRequest, so use the // value in ConnectResponse to decide if we send PINGs // and at what interval. // In tests, the interval could be negative to indicate // milliseconds. if (cr.getPingInterval() != 0) { unsubPings = false; // These will be immutable. this.pingRequests = cr.getPingRequests(); // In test, it is possible that we get a negative value // to represent milliseconds. if (cr.getPingInterval() < 0) { this.pingInterval = Duration.ofMillis(-cr.getPingInterval()); } else { // PingInterval is otherwise assumed to be in seconds. this.pingInterval = Duration.ofSeconds(cr.getPingInterval()); } this.pingMaxOut = cr.getPingMaxOut(); this.pingBytes = Ping.newBuilder().setConnID(ByteString.copyFromUtf8(this.connectionId)).build().toByteArray(); // Set the timer now that we are set. Use lock to create // synchronization point. this.pingTimer = new Timer("jnats streaming ping timer", true); this.pingTimer.schedule(new TimerTask() { public void run() { try { pingServer(); } catch (Exception e) { // catch exception to prevent the timer to be closed, but cancel this task cancel(); // TODO: Ignore, but re-evaluate this } } }, this.pingInterval.toMillis(), this.pingInterval.toMillis()); } } if (unsubPings) { timeTrace(trace, "removing ping subscriber, not supported by server"); this.dispatcher.unsubscribe(pingSub); this.pingSub = null; } // Setup the ACK subscription pubAckMap = new HashMap<>(); // Create Subscription map subMap = new HashMap<>(); pubAckChan = new LinkedBlockingQueue<>(opts.getMaxPubAcksInFlight()); } catch (IOException e) { exThrown = true; throw e; } finally { if (exThrown) { try { close(); } catch (Exception e) { /* NOOP -- can't do anything if close fails */ } } else { timeTrace(trace, "connection complete"); } } return this; } io.nats.client.Connection createNatsConnection() throws IOException, InterruptedException { io.nats.client.Connection nc = null; if (getNatsConnection() == null) { if (opts.getNatsUrl() != null) { io.nats.client.Options.Builder natsOpts = new io.nats.client.Options.Builder(). connectionName(clientId). errorListener(opts.getErrorListener()). connectionListener(opts.getConnectionListener()). server(opts.getNatsUrl()); if (opts.isTraceConnection()) { natsOpts.traceConnection(); } nc = Nats.connect(natsOpts.build()); } else { nc = Nats.connect(); } ncOwned = true; } return nc; } @Override public void close() throws IOException, InterruptedException { this.close(false); } // If silent is true we don't try to notify the server void close(boolean silent) throws IOException, InterruptedException { io.nats.client.Connection nc; this.lock(); try { // Capture for NATS calls below if (getNatsConnection() == null) { // We are already closed return; } // Capture for NATS calls below. nc = getNatsConnection(); // if ncOwned, we close it at the end try { // Signals we are closed. setNatsConnection(null); if (pingTimer != null) { pingTimer.cancel(); } if (this.pubAckMap != null) { for (AckClosure ac : this.pubAckMap.values()) { ac.ackTask.cancel(); if (!ac.ch.isEmpty()) { ac.ch.take(); } } } ackTimer.cancel(); if (this.messageDispatcher != null && this.messageDispatcher.isActive()) { nc.closeDispatcher(this.messageDispatcher); } for (Dispatcher d : this.customDispatchers.values()) { if (d.isActive()) { nc.closeDispatcher(d); } } if (this.ackDispatcher != null && this.ackDispatcher.isActive()) { nc.closeDispatcher(this.ackDispatcher); } if (this.dispatcher != null && this.dispatcher.isActive()) { nc.closeDispatcher(this.dispatcher); } if (!silent) { CloseRequest req = CloseRequest.newBuilder().setClientID(clientId).build(); byte[] bytes = req.toByteArray(); Message reply = nc.request(closeRequests, bytes, opts.getConnectTimeout()); if (reply == null) { throw new IOException(NatsStreaming.ERR_CLOSE_REQ_TIMEOUT); } if (reply.getData() != null) { CloseResponse cr = CloseResponse.parseFrom(reply.getData()); if (!cr.getError().isEmpty()) { throw new IOException(cr.getError()); } } } } finally { if (ncOwned) { try { nc.close(); } catch (Exception ignore) { // ignore } } } // first finally } finally { this.unlock(); } } private SubscriptionImpl createSubscription(String subject, String qgroup, io.nats.streaming.MessageHandler cb, StreamingConnectionImpl conn, SubscriptionOptions opts) { return new SubscriptionImpl(subject, qgroup, cb, conn, opts); } void processHeartBeat(Message msg) { // No payload assumed, just reply. io.nats.client.Connection nc; this.rLock(); nc = this.nc; this.rUnlock(); if (nc != null) { nc.publish(msg.getReplyTo(), null); } } BlockingQueue<String> createErrorChannel() { return new LinkedBlockingQueue<>(); } // Publish will publish to the cluster and wait for an ACK. // Prior to 2.2.0 this method did not block properly. @Override public void publish(String subject, byte[] data) throws IOException, InterruptedException, TimeoutException { final BlockingQueue<String> ch = createErrorChannel(); Duration ackTimeout = opts.getAckTimeout(); publish(subject, data, null, ch); // Should get a timeout from the timer at ackTimeout, but fail no matter what // don't leave a dangling "take" call that can lock the thread String err = ch.poll(2 * ackTimeout.toMillis(), TimeUnit.MILLISECONDS); if (err == null) { throw new TimeoutException(NatsStreaming.ERR_TIMEOUT); } else if (!err.isEmpty()) { throw new IOException(err); } } /* * Publish with an ack handler will publish to the cluster on pubPrefix+subject and asynchronously process the * ACK or error state. It will return the GUID for the message being sent. */ @Override public String publish(String subject, byte[] data, AckHandler ah) throws IOException, InterruptedException, TimeoutException { return publish(subject, data, ah, null); } private String publish(String subject, byte[] data, AckHandler ah, BlockingQueue<String> ch) throws IOException, InterruptedException, TimeoutException { String subj; String ackSubject; Duration ackTimeout = opts.getAckTimeout(); BlockingQueue<PubAck> pac; final AckClosure a= new AckClosure(ah, subject, (ah != null && ah.includeDataWithAck()) ? data : null, ch); final PubMsg pe; String guid; byte[] bytes; this.lock(); try { if (getNatsConnection() == null) { throw new IllegalStateException(NatsStreaming.ERR_CONNECTION_CLOSED); } subj = pubPrefix + "." + subject; guid = NUID.nextGlobal(); PubMsg.Builder pb = PubMsg.newBuilder().setClientID(clientId).setGuid(guid).setSubject(subject); if (data != null) { pb = pb.setData(ByteString.copyFrom(data)); } pe = pb.build(); bytes = pe.toByteArray(); // Map ack to guid pubAckMap.put(guid, a); // snapshot ackSubject = this.ackSubject; pac = pubAckChan; } finally { this.unlock(); } // Use the buffered channel to control the number of outstanding acks. try { pac.put(PubAck.getDefaultInstance()); } catch (InterruptedException e) { // TODO: Reevaluate this. // Eat this because you can't really do anything with it } nc.publish(subj, ackSubject, bytes); // Setup the timer for expiration. this.lock(); try { a.ackTask = createAckTimerTask(guid); ackTimer.schedule(a.ackTask, ackTimeout.toMillis()); } finally { this.unlock(); } return guid; } Dispatcher getDispatcherByName(String name) { Dispatcher d = null; this.lock(); try { if (name == null || name.isEmpty()) { if (this.messageDispatcher == null) { this.messageDispatcher = nc.createDispatcher(msg -> { this.processMsg(msg); }); this.messageDispatcher.setPendingLimits(-1, -1); } return this.messageDispatcher; } d = customDispatchers.get(name); if (d == null) { d = this.getNatsConnection().createDispatcher(msg -> { this.processMsg(msg); }); d.setPendingLimits(-1, -1); customDispatchers.put(name, d); } } finally { this.unlock(); } return d; } @Override public Subscription subscribe(String subject, io.nats.streaming.MessageHandler cb) throws IOException, InterruptedException, TimeoutException { return subscribe(subject, cb, null); } @Override public Subscription subscribe(String subject, io.nats.streaming.MessageHandler cb, SubscriptionOptions opts) throws IOException, InterruptedException, TimeoutException { return subscribe(subject, null, cb, opts); } @Override public Subscription subscribe(String subject, String queue, io.nats.streaming.MessageHandler cb) throws IOException, InterruptedException, TimeoutException { return subscribe(subject, queue, cb, null); } @Override public Subscription subscribe(String subject, String queue, io.nats.streaming.MessageHandler cb, SubscriptionOptions opts) throws IOException, InterruptedException, TimeoutException { SubscriptionImpl sub; io.nats.client.Connection nc; if (opts == null) { opts = new SubscriptionOptions.Builder().build(); } this.lock(); try { if (getNatsConnection() == null) { throw new IllegalStateException(NatsStreaming.ERR_CONNECTION_CLOSED); } sub = createSubscription(subject, queue, cb, this, opts); // Register subscription. subMap.put(sub.inbox, sub); nc = getNatsConnection(); } finally { this.unlock(); } // Hold lock throughout. sub.wLock(); try { Dispatcher d = this.getDispatcherByName(opts.getDispatcherName()); // Listen for actual messages d.subscribe(sub.inbox); // Create a subscription request // FIXME(dlc) add others. SubscriptionRequest sr = createSubscriptionRequest(sub); Message reply = nc.request(subRequests, sr.toByteArray(), opts.getSubscriptionTimeout()); if (reply == null) { d.unsubscribe(sub.inbox); throw new IOException(ERR_SUB_REQ_TIMEOUT); } SubscriptionResponse response; try { response = SubscriptionResponse.parseFrom(reply.getData()); } catch (InvalidProtocolBufferException e) { d.unsubscribe(sub.inbox); throw e; } if (!response.getError().isEmpty()) { d.unsubscribe(sub.inbox); throw new IOException(response.getError()); } sub.setAckInbox(response.getAckInbox()); } finally { sub.wUnlock(); } return sub; } SubscriptionRequest createSubscriptionRequest(SubscriptionImpl sub) { SubscriptionOptions subOpts = sub.getOptions(); SubscriptionRequest.Builder srb = SubscriptionRequest.newBuilder(); String clientId = sub.getConnection().getClientId(); String queue = sub.getQueue(); String subject = sub.getSubject(); srb.setClientID(clientId).setSubject(subject).setQGroup(queue == null ? "" : queue) .setInbox(sub.getInbox()).setMaxInFlight(subOpts.getMaxInFlight()) .setAckWaitInSecs((int) subOpts.getAckWait().getSeconds()); switch (subOpts.getStartAt()) { case First: break; case LastReceived: break; case NewOnly: break; case SequenceStart: srb.setStartSequence(subOpts.getStartSequence()); break; case TimeDeltaStart: long delta = ChronoUnit.NANOS.between(subOpts.getStartTime(), Instant.now()); srb.setStartTimeDelta(delta); break; case UNRECOGNIZED: default: break; } srb.setStartPosition(subOpts.getStartAt()); if (subOpts.getDurableName() != null) { srb.setDurableName(subOpts.getDurableName()); } return srb.build(); } // Process an ack from the STAN cluster void processAck(Message msg) { PubAck pa; Exception ex = null; try { pa = PubAck.parseFrom(msg.getData()); } catch (InvalidProtocolBufferException e) { // If we are speaking to a server we don't understand, let the // user know. System.err.println("Protocol error: " + e.getStackTrace()); return; } // Remove AckClosure ackClosure = removeAck(pa.getGuid()); if (ackClosure != null) { // Capture error if it exists. String ackError = pa.getError(); if (ackClosure.ah != null) { if (!ackError.isEmpty()) { ex = new IOException(ackError); } // Perform the ackHandler callback ackClosure.ah.onAck(pa.getGuid(), ackClosure.subject, ackClosure.data, ex); // clean up to allow GC of data ackClosure.subject = null; ackClosure.data = null; } else if (ackClosure.ch != null) { try { ackClosure.ch.put(ackError); } catch (InterruptedException e) { // ignore } } } } TimerTask createAckTimerTask(String guid) { return new TimerTask() { public void run() { try { processAckTimeout(guid); } catch (Exception e) { // catch exception to prevent the timer to be closed, but cancel this task cancel(); } } }; } void processAckTimeout(String guid) { AckClosure ackClosure = removeAck(guid); if (ackClosure == null) { return; } if (ackClosure.ah != null) { ackClosure.ah.onAck(guid, ackClosure.subject, ackClosure.data, new TimeoutException(NatsStreaming.ERR_TIMEOUT)); } else if (ackClosure.ch != null) { try { ackClosure.ch.put(NatsStreaming.ERR_TIMEOUT); } catch (InterruptedException e) { // ignore } } } AckClosure removeAck(String guid) { AckClosure ackClosure; BlockingQueue<PubAck> pac; TimerTask timerTask = null; this.lock(); try { ackClosure = pubAckMap.get(guid); if (ackClosure != null) { timerTask = ackClosure.ackTask; pubAckMap.remove(guid); } pac = pubAckChan; } finally { this.unlock(); } // Cancel timer if needed if (timerTask != null) { timerTask.cancel(); } // Remove from channel to unblock async publish if (ackClosure != null && pac.size() > 0) { try { // remove from queue to unblock publish pac.take(); } catch (InterruptedException e) { // TODO: Ignore, but re-evaluate this } } return ackClosure; } /** * Closes a connection and invoke the connection error callback if one * was registered when the connection was created. */ void closeDueToPing(String error) { boolean isClosed = false; ConnectionLostHandler lost = null; Exception ex = null; lock(); try { isClosed = (getNatsConnection() == null); lost = opts.getConnectionLostHandler(); } finally { unlock(); } // Check if connection has been closed. if (isClosed) { return; } try { close(true); } catch (Exception exp) { ex = exp; } if (lost != null) { if (ex == null) { ex = new Exception(error); } else { ex = new Exception(error, ex); } lost.connectionLost(this, ex); } } /** * Sends a PING (containing the connection's ID) to the server at intervals * specified by PingInterval option when connection is created. * Everytime a PING is sent, the number of outstanding PINGs is increased. * If the total number is > than the PingMaxOut option, then the connection * is closed, and connection error callback invoked if one was specified. */ void pingServer() { this.lock(); try { this.pingsOut++; if (this.pingsOut > this.pingMaxOut) { this.unlock(); this.closeDueToPing(NatsStreaming.SERVER_ERR_MAX_PINGS); return; } Connection conn = this.nc; this.unlock(); // Send the PING now. If the NATS connection is reported closed, // we are done. try { conn.publish(this.pingRequests, this.pingInbox, this.pingBytes); } catch (Exception exp) { if (conn.getStatus() == io.nats.client.Connection.Status.CLOSED) { this.closeDueToPing(exp.getMessage()); } } } catch (Exception exp) { this.unlock(); throw exp; } } /** * Receives PING responses from the server. * If the response contains an error message, the connection is closed * and the connection error callback is invoked (if one is specified). * If no error, the number of ping out is reset to 0. There is no * decrement by one since for a given PING, the client may received * many responses when servers are running in channel partitioning mode. * Regardless, any positive response from the server ought to signal * that the connection is ok. */ void processPing(Message msg) { // No data means OK (we don't have to call Unmarshal) if (msg.getData() != null && msg.getData().length > 0) { try { PingResponse pingResp = PingResponse.parseFrom(msg.getData()); String error = pingResp.getError(); if (error != null && !error.isEmpty()) { closeDueToPing(error); return; } } catch (Exception e) { return; // exception here stops us from reseting pings out } } // Do not attempt to decrement, simply reset to 0. this.lock(); this.pingsOut = 0; this.unlock(); } @Override public void onMessage(io.nats.client.Message msg) { // For handling inbound NATS messages processMsg(msg); } io.nats.streaming.Message createStanMessage(MsgProto msgp) { return new io.nats.streaming.Message(msgp); } void processMsg(io.nats.client.Message raw) { io.nats.streaming.Message stanMsg = null; boolean isClosed; SubscriptionImpl sub; io.nats.client.Connection nc; try { MsgProto msgp = MsgProto.parseFrom(raw.getData()); stanMsg = createStanMessage(msgp); } catch (InvalidProtocolBufferException e) { // TODO: Ignore, but re-evaluate this } // Lookup the subscription lock(); try { nc = getNatsConnection(); isClosed = (nc == null); sub = (SubscriptionImpl) subMap.get(raw.getSubject()); } finally { unlock(); } // Check if sub is no longer valid or connection has been closed. if (sub == null || isClosed) { return; } // Store in msg for backlink stanMsg.setSubscription(sub); io.nats.streaming.MessageHandler cb; String ackSubject; boolean isManualAck; StreamingConnectionImpl subsc; sub.rLock(); try { cb = sub.getMessageHandler(); ackSubject = sub.getAckInbox(); isManualAck = sub.getOptions().isManualAcks(); subsc = sub.getConnection(); // Can be nil if sub has been unsubscribed } finally { sub.rUnlock(); } // Perform the callback if (cb != null && subsc != null) { try { cb.onMessage(stanMsg); } catch (Exception e) { ErrorListener handler = nc.getOptions().getErrorListener(); if (handler != null) { try { handler.exceptionOccurred(this.nc, e); } catch (Exception ex) { // Now we just have to eat it } } } } // Process auto-ack if (!isManualAck) { Ack ack = Ack.newBuilder().setSubject(stanMsg.getSubject()) .setSequence(stanMsg.getSequence()).build(); nc.publish(ackSubject, ack.toByteArray()); } } public String getClientId() { return this.clientId; } @Override public io.nats.client.Connection getNatsConnection() { return this.nc; } private void setNatsConnection(io.nats.client.Connection nc) { this.nc = nc; } public String newInbox() { StringBuilder builder = new StringBuilder(); builder.append(INBOX_PREFIX); builder.append(this.nuid.next()); return builder.toString(); } void lock() { mu.writeLock().lock(); } void unlock() { mu.writeLock().unlock(); } private void rLock() { mu.readLock().lock(); } private void rUnlock() { mu.readLock().unlock(); } class AckClosure { TimerTask ackTask; AckHandler ah; String subject; byte[] data; BlockingQueue<String> ch; AckClosure(final AckHandler ah, final String subject, final byte[] data, final BlockingQueue<String> ch) { this.ah = ah; this.ch = ch; this.subject = subject; this.data = data; } } }