/* * Copyright 2011 the original author or 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.vertx.ext.amqp.impl.protocol; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import io.vertx.core.net.NetClient; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetServerOptions; import io.vertx.ext.amqp.*; import io.vertx.ext.amqp.impl.*; import io.vertx.ext.amqp.impl.protocol.ConnectionImpl.State; import io.vertx.ext.amqp.impl.util.LogManager; import org.apache.qpid.proton.message.Message; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import static io.vertx.ext.amqp.impl.protocol.SessionImpl.SETTLE; import static io.vertx.ext.amqp.impl.util.Functions.format; /* * The goal is to completely isolate AMQP Link management away from the Service impl. * LinkManager should hide all Links and only allow the service to interact via the link id (link-name in AMQP) and the endpoint address. */ public class LinkManager extends AbstractAmqpEventListener { private static final LogManager LOG = LogManager.get("LINK_MGT:", LinkManager.class); private static final OutgoingLinkOptions DEFAULT_OUTGOING_LINK_OPTIONS = new OutgoingLinkOptions(); protected final List<ManagedConnection> _outboundConnections = new CopyOnWriteArrayList<ManagedConnection>(); protected final List<ManagedConnection> _inboundConnections = new CopyOnWriteArrayList<ManagedConnection>(); protected final Map<String, Incoming> _incomingLinks = new ConcurrentHashMap<String, Incoming>(); protected final Map<String, Outgoing> _outgoingLinks = new ConcurrentHashMap<String, Outgoing>(); protected final Map<String, String> _sharedIncomingLinks = new ConcurrentHashMap<String, String>(); protected final Map<String, String> _sharedOutgoingLinks = new ConcurrentHashMap<String, String>(); protected final Map<String, ManagedSession> _msgRefToSsnMap = new ConcurrentHashMap<String, ManagedSession>(); protected final NetClient _client; protected final Vertx _vertx; protected final NetServer _server; protected final AmqpServiceConfig _config; protected final LinkEventListener _listener; private Map<String, ConnectionSettings> URL_CACHE; @SuppressWarnings("serial") public LinkManager(Vertx vertx, AmqpServiceConfig config, AMQPServiceImpl parent) { DEFAULT_OUTGOING_LINK_OPTIONS.setReliability(ReliabilityMode.AT_LEAST_ONCE); _vertx = vertx; _config = config; _listener = parent; _client = _vertx.createNetClient(new NetClientOptions()); URL_CACHE = Collections.synchronizedMap(new LinkedHashMap<String, ConnectionSettings>(config .getMaxedCachedURLEntries() + 1, 1.1f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, ConnectionSettings> eldest) { return size() > config.getMaxedCachedURLEntries(); } }); NetServerOptions serverOp = new NetServerOptions(); serverOp.setHost(config.getInboundHost()); serverOp.setPort(config.getInboundPort()); _server = _vertx.createNetServer(serverOp); _server.connectHandler(sock -> { DefaultConnectionSettings settings = new DefaultConnectionSettings(); settings.setHost(sock.remoteAddress().host()); settings.setPort(sock.remoteAddress().port()); ManagedConnection connection = new ManagedConnection(settings, this, true); connection.setNetSocket(sock); connection.write(); _inboundConnections.add(connection); connection.addDisconnectHandler(c -> { _inboundConnections.remove(c); }); }); _server.listen(result -> { if (result.failed()) { String error = format("Error {%s} Server was unable to bind to %s:%s", result.cause(), _config.getInboundHost(), _config.getInboundPort()); LOG.fatal(error, result.cause()); // We need to stop the verticle LOG.fatal("Initiating the shutdown of AMQP Service due to : %s", error); parent.stopInternal(); } }); } public void stop() { LOG.fatal("Stopping Link Manager : Closing all outgoing and incomming connections"); for (Connection con : _outboundConnections) { con.close(); } for (Connection con : _inboundConnections) { con.close(); } _server.close(); } public ConnectionSettings getConnectionSettings(String address) throws MessagingException { if (URL_CACHE.containsKey(address)) { return URL_CACHE.get(address); } else { final ConnectionSettings settings = AddressParser.parse(address); URL_CACHE.put(address, settings); return settings; } } // TODO handle reconnection. public ManagedConnection getConnection(final ConnectionSettings settings) throws MessagingException { for (ManagedConnection con : _outboundConnections) { if (con.getSettings().getHost().equals(settings.getHost()) && con.getSettings().getPort() == settings.getPort()) { if (con.getState() == State.CONNECTED) { return con; } else { LOG.info("Attempting re-connection to AMQP peer at %s:%s", settings.getHost(), settings.getPort()); break; } } } ManagedConnection connection = new ManagedConnection(settings, this, false); _client.connect(settings.getPort(), settings.getHost(), result -> { if (result.succeeded()) { connection.setNetSocket(result.result()); connection.write(); connection.addDisconnectHandler(c -> { _outboundConnections.remove(c); }); LOG.info("Connected to AMQP peer at %s:%s", connection.getSettings().getHost(), connection .getSettings().getPort()); } else { LOG.warn("Error {%s}, when connecting to AMQP peer at %s:%s", result.cause(), connection.getSettings() .getHost(), connection.getSettings().getPort()); connection.setState(State.FAILED); _outboundConnections.remove(connection); } }); LOG.info("Attempting connection to AMQP peer at %s:%s", settings.getHost(), settings.getPort()); _outboundConnections.add(connection); return connection; } // Validate if the link is connected. If not use RecoveryOptions to // determine course of action. private <T extends BaseLink> T validateLink(T link, RetryOptions options) throws MessagingException { link.checkClosed(); switch (link.getConnection().getState()) { case CONNECTED: return link; case NEW: // throw new MessagingException("Link is not ready yet", // ErrorCode.LINK_NOT_READY); return link; case RETRY_IN_PROGRESS: throw new MessagingException("Link has failed. Retry in progress", ErrorCode.LINK_RETRY_IN_PROGRESS); case FAILED: if (link.getConnection().isInbound()) { throw new MessagingException("Link created from an AMQP peer into the bridge failed", ErrorCode.LINK_FAILED); } switch (options.getRetryPolicy()) { case NO_RETRY: throw new MessagingException("Link has failed. No retry instructions specified", ErrorCode.LINK_FAILED); default: // TODO initiate failover return null; } default: throw new MessagingException("Invalid link state", ErrorCode.INTERNAL_ERROR); } } // ===================================================== // OutgoingLink // ===================================================== public OutgoingLinkImpl getSharedOutgoingLink(String amqpAddress) throws MessagingException { if (_sharedOutgoingLinks.containsKey(amqpAddress)) { String id = _sharedOutgoingLinks.get(amqpAddress); if (_outgoingLinks.containsKey(id)) { try { Outgoing outgoing = _outgoingLinks.get(id); validateLink(outgoing._link, outgoing._options.getRecoveryOptions()); return outgoing._link; } catch (MessagingException e) { throw e; } } } // Either it doesn't exist, or the link was canned. OutgoingLinkImpl link = createOutgoingLinkInternal(amqpAddress, DEFAULT_OUTGOING_LINK_OPTIONS); _sharedOutgoingLinks.put(amqpAddress, link.getName()); return link; } // Method for explicitly creating an outbound link public String createOutgoingLink(String amqpAddress, OutgoingLinkOptions options) throws MessagingException { return createOutgoingLinkInternal(amqpAddress, options).getName(); } // Internal use private OutgoingLinkImpl createOutgoingLinkInternal(String amqpAddress, OutgoingLinkOptions options) throws MessagingException { final ConnectionSettings settings = getConnectionSettings(amqpAddress); if (settings.getHost().equals(_config.getInboundHost()) && settings.getPort() == _config.getInboundPort()) { // prevent cycle. throw new MessagingException(format( "Ignoring request for connection[%s:%s] as it points to vertx-amqp-service inbound server[%s:%s]", settings.getHost(), settings.getPort(), settings.getHost(), settings.getPort()), ErrorCode.INTERNAL_ERROR); } ManagedConnection con = getConnection(settings); OutgoingLinkImpl link = con.createOutboundLink(settings.getNode(), options.getReliability()); LOG.info("Created outgoing link to AMQP peer [address=%s @ %s:%s, options=%s] ", settings.getNode(), settings.getHost(), settings.getPort(), options); _outgoingLinks.put(link.getName(), new Outgoing(link, options)); return link; } public void sendViaAddress(String amqpAddress, Message outMsg, JsonObject inMsg) throws MessagingException { send(getSharedOutgoingLink(amqpAddress), DEFAULT_OUTGOING_LINK_OPTIONS, outMsg, inMsg); } public void sendViaLink(String linkId, Message outMsg, JsonObject inMsg) throws MessagingException { Outgoing outgoing = _outgoingLinks.get(linkId); send(outgoing._link, outgoing._options, outMsg, inMsg); } private void send(OutgoingLinkImpl link, OutgoingLinkOptions options, Message outMsg, JsonObject inMsg) throws MessagingException { if (options.getReliability() == ReliabilityMode.AT_LEAST_ONCE && inMsg.containsKey(AMQPService.OUTGOING_MSG_REF)) { TrackerImpl tracker = link.send(outMsg); tracker.setContext(inMsg.getString(AMQPService.OUTGOING_MSG_REF)); } else { link.send(outMsg); } } public void closeOutgoingLink(String linkId) throws MessagingException { if (_outgoingLinks.containsKey(linkId)) { _outgoingLinks.remove(linkId)._link.close(); } // else don't bother. Link already canned } // ===================================================== // Incoming Link // ===================================================== public void setCredits(String linkId, int credits) throws MessagingException { if (_incomingLinks.containsKey(linkId)) { try { Incoming incoming = _incomingLinks.get(linkId); validateLink(incoming._link, incoming._options.getRecoveryOptions()); incoming._link.setCredits(credits); } catch (MessagingException e) { throw e; } } else { throw new MessagingException("Incoming link ref doesn't match any AMQP links", ErrorCode.INVALID_LINK_REF); } } public void settleDelivery(String msgRef, MessageDisposition disposition) throws MessagingException { if (_msgRefToSsnMap.containsKey(msgRef)) { ManagedSession ssn = _msgRefToSsnMap.remove(msgRef); ssn.checkClosed(); ssn.disposition(msgRef, disposition, SETTLE); } else { throw new MessagingException(format( "Invalid message reference : %s. Unable to find a matching AMQP message", msgRef), ErrorCode.INVALID_MSG_REF); } } // Method for explicitly creating an inbound link public String createIncomingLink(String amqpAddress, IncomingLinkOptions options) throws MessagingException { final ConnectionSettings settings = getConnectionSettings(amqpAddress); ManagedConnection con = getConnection(settings); IncomingLinkImpl link = con.createInboundLink(settings.getNode(), options.getReliability(), options.getPrefetch() > 0 ? CreditMode.AUTO : CreditMode.EXPLICT); if (options.getPrefetch() > 0) { link.setCredits(options.getPrefetch()); } _incomingLinks.put(link.getName(), new Incoming(link, options)); LOG.info("Created incoming link to AMQP peer [address=%s @ %s:%s, options=%s] ", settings.getNode(), settings.getHost(), settings.getPort(), options); return link.getName(); } public void closeIncomingLink(String linkId) throws MessagingException { if (_incomingLinks.containsKey(linkId)) { _incomingLinks.remove(linkId)._link.close(); } // else don't bother. Link already canned } // ------------ Event Handler ------------------------ @Override public void onOutgoingLinkOpen(OutgoingLinkImpl link) { boolean inbound = link.getConnection().isInbound(); String id = link.getName(); String address = link.getSource(); if (id == null || id.trim().isEmpty()) { id = address; } if (inbound) { _outgoingLinks.put(id, new Outgoing(link, DEFAULT_OUTGOING_LINK_OPTIONS)); LOG.info("Accepted an outgoing link (subscription) from AMQP peer %s", address); } _listener.outgoingLinkReady(id, address, inbound); } @Override public void onOutgoingLinkClosed(OutgoingLinkImpl link) { boolean inbound = link.getConnection().isInbound(); String id = link.getName(); String address = link.getSource(); if (id == null || id.trim().isEmpty()) { id = address; } _outgoingLinks.remove(id); _listener.outgoingLinkFinal(id, address, inbound); } @Override public void onIncomingLinkClosed(IncomingLinkImpl link) { boolean inbound = link.getConnection().isInbound(); String id = link.getName(); String address = link.getTarget(); if (id == null || id.trim().isEmpty()) { id = address; } // TODO if it's an outbound connection, then we need to notify an error if (!inbound) { _incomingLinks.remove(id); } _listener.incomingLinkFinal(id, address, inbound); } @Override public void onIncomingLinkOpen(IncomingLinkImpl link) { boolean inbound = link.getConnection().isInbound(); String id = link.getName(); String address = link.getTarget(); /* print("Local Source %s ", link.getProtocolLink().getSource() == null ? "null" : link.getProtocolLink().getSource().getAddress()); print("Remote Source %s ", link.getProtocolLink().getRemoteSource() == null ? "null" : link.getProtocolLink().getRemoteSource().getAddress()); print("Local Target %s ", link.getProtocolLink().getTarget() == null ? "null" : link.getProtocolLink().getTarget().getAddress()); print("Remote Target %s ", link.getProtocolLink().getRemoteTarget() == null ? "null" : link.getProtocolLink().getRemoteTarget().getAddress()); */ if (id == null || id.trim().isEmpty()) { id = address; } if (inbound) { _incomingLinks.put(id, new Incoming(link, new IncomingLinkOptions())); /* * try { link.setCredits(_config.getDefaultLinkCredit()); } catch * (MessagingException e) { _logger.warn( format( * "Error setting link credit for incoming link (source=%s) created via an inbound connection" * , address), e); } */ } LOG.debug("incomingLinkReady inbound=%s, id=%s , address=%s", inbound, id, address); _listener.incomingLinkReady(id, address, inbound); } @Override public void onMessage(IncomingLinkImpl link, InboundMessage msg) { ManagedSession ssn = (ManagedSession) link.getSession(); ssn.addMsgRef(msg.getMsgRef(), msg.getSequence()); _msgRefToSsnMap.put(msg.getMsgRef(), ssn); _listener.message(link.getName(), link.getAddress(), link.getReceiverMode(), msg); } @Override public void onOutgoingLinkCredit(OutgoingLinkImpl link, int credits) { _listener.outgoingLinkCreditGiven(link.getName(), credits); } @Override public void onSettled(OutgoingLinkImpl link, TrackerImpl tracker) { _listener.deliveryUpdate(link.getName(), (String) tracker.getContext(), tracker.getState(), tracker.getDisposition()); } // ---------- / Event Handler ----------------------- // ---------- Helper classes class Outgoing { OutgoingLinkImpl _link; OutgoingLinkOptions _options; String _toStr; Outgoing(OutgoingLinkImpl link, OutgoingLinkOptions options) { _link = link; _options = options; _toStr = format("[link=%s, options=%s]", _link.getAddress(), _options); } @Override public String toString() { return _toStr; } } class Incoming { IncomingLinkImpl _link; IncomingLinkOptions _options; String _toStr; Incoming(IncomingLinkImpl link, IncomingLinkOptions options) { _link = link; _options = options; _toStr = format("[link=%s, options=%s]", _link.getAddress(), _options); } @Override public String toString() { return _toStr; } } }