/** * RabbitQueueFactory * Copyright 14.01.2017 by Michael Peter Christen, @0rb1t3r * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package net.yacy.grid.io.messages; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.GetResponse; import com.rabbitmq.client.MessageProperties; import net.yacy.grid.mcp.Data; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmCallback; /** * to monitor the rabbitMQ queue, open the admin console at * http://127.0.0.1:15672/ * and log with admin/admin */ public class RabbitQueueFactory implements QueueFactory<byte[]> { private static int DEFAULT_PORT = 5672; private static String DEFAULT_EXCHANGE = ""; public static String PROTOCOL_PREFIX = "amqp://"; private final String server, username, password; private final int port; private Connection connection; private Map<String, Queue<byte[]>> queues; private final AtomicBoolean lazy; private final AtomicInteger queueLimit; /** * create a queue factory for a rabbitMQ message server * @param server the host name of the rabbitMQ server * @param port a port for the access to the rabbitMQ server. If given -1, then the default port will be used * @param username * @param password * @param lazy * @param queueLimit maximum number of entries for the queue, 0 = unlimited * @throws IOException */ public RabbitQueueFactory(final String server, final int port, final String username, final String password, final boolean lazy, final int queueLimit) throws IOException { this.server = server; this.port = port; this.username = username; this.password = password; this.lazy = new AtomicBoolean(lazy); this.queueLimit = new AtomicInteger(queueLimit); this.connection = null; this.queues = new ConcurrentHashMap<>(); } private Connection getConnection() throws IOException { if (this.connection != null && this.connection.isOpen()) return this.connection; ConnectionFactory factory = new ConnectionFactory(); factory.setAutomaticRecoveryEnabled(true); factory.setHost(this.server); if (this.port > 0) factory.setPort(this.port); if (this.username != null && this.username.length() > 0) factory.setUsername(this.username); if (this.password != null && this.password.length() > 0) factory.setPassword(this.password); try { this.connection = factory.newConnection(); //Map<String, Object> map = this.connection.getServerProperties(); if (!this.connection.isOpen()) throw new IOException("no connection"); return this.connection; } catch (TimeoutException e) { throw new IOException(e.getMessage()); } } private Channel getChannel() throws IOException { getConnection(); Channel channel = connection.createChannel(); if (!channel.isOpen()) throw new IOException("no channel"); return channel; } @Override public String getConnectionURL() { return PROTOCOL_PREFIX + (this.username != null && this.username.length() > 0 ? username + (this.password != null && this.password.length() > 0 ? ":" + this.password : "") + "@" : "") + this.getHost() + ((this.hasDefaultPort() ? "" : ":" + this.getPort())); } @Override public String getHost() { return this.server; } @Override public boolean hasDefaultPort() { return this.port == -1 || this.port == DEFAULT_PORT; } @Override public int getPort() { return hasDefaultPort() ? DEFAULT_PORT : this.port; } @Override public Queue<byte[]> getQueue(String queueName) throws IOException { Queue<byte[]> queue = queues.get(queueName); if (queue != null) return queue; synchronized (this) { queue = queues.get(queueName); if (queue != null) return queue; queue = new RabbitMessageQueue(queueName); this.queues.put(queueName, queue); return queue; } } private class RabbitMessageQueue extends AbstractQueue<byte[]> implements Queue<byte[]> { private final String queueName; private final SortedMap<Long, BlockingQueue<Boolean>> unconfirmedSet; private Channel channel; public RabbitMessageQueue(String queueName) throws IOException { this.queueName = queueName; this.unconfirmedSet = Collections.synchronizedSortedMap(new TreeMap<>()); connect(); } private void connect() throws IOException { Map<String, Object> arguments = new HashMap<>(); arguments.put("x-queue-mode", lazy.get() ? "lazy" : "default"); // we want to minimize memory usage; see http://www.rabbitmq.com/lazy-queues.html if (RabbitQueueFactory.this.queueLimit.get() > 0) { arguments.put("x-max-length", RabbitQueueFactory.this.queueLimit.get()); arguments.put("x-overflow", "reject-publish"); } this.channel = RabbitQueueFactory.this.getChannel(); try { this.channel.queueDeclare(this.queueName, true, false, false, arguments); } catch (Throwable e) { // we first try to delete the old queue, but only if it is not used and if empty try { channel = connection.createChannel(); this.channel.queueDelete(this.queueName, true, true); } catch (Throwable ee) {} // try again try { channel = connection.createChannel(); this.channel.queueDeclare(this.queueName, true, false, false, arguments); } catch (Throwable ee) { // that did not work. Try to modify the call to match with the previous queueDeclare String ec = ee.getCause() == null ? ee.getMessage() : ee.getCause().getMessage(); if (ec != null && ec.contains("'signedint' but current is none")) { arguments.remove("x-max-length"); arguments.remove("x-overflow"); } //arguments.put("x-queue-mode", lazy.get() ? "default" : "lazy"); try { channel = connection.createChannel(); this.channel.queueDeclare(this.queueName, true, false, false, arguments); } catch (Throwable eee) { throw new IOException(eee.getMessage()); } } } this.channel.confirmSelect(); // declare that the channel sends confirmations this.channel.addConfirmListener( new ConfirmCallback() { // ack @Override public void handle(long seqNo, boolean multiple) throws IOException { if (multiple) { Map<Long, BlockingQueue<Boolean>> m = unconfirmedSet.headMap(seqNo + 1); m.forEach((s, b) -> b.add(Boolean.TRUE)); m.clear(); } else { BlockingQueue<Boolean> b = unconfirmedSet.remove(seqNo); assert b != null; if (b != null) b.add(Boolean.TRUE); } }}, new ConfirmCallback() { // nack @Override public void handle(long seqNo, boolean multiple) throws IOException { if (multiple) { Map<Long, BlockingQueue<Boolean>> m = unconfirmedSet.headMap(seqNo + 1); m.forEach((s, b) -> b.add(Boolean.FALSE)); m.clear(); } else { BlockingQueue<Boolean> b = unconfirmedSet.remove(seqNo); assert b != null; if (b != null) b.add(Boolean.FALSE); } }} ); } @Override public void checkConnection() throws IOException { available(); } @Override public Queue<byte[]> send(byte[] message) throws IOException { try { return sendInternal(message); } catch (IOException e) { if (e.getMessage().equals(GridBroker.TARGET_LIMIT_MESSAGE)) throw e; // try again Data.logger.warn("RabbitQueueFactory.send: re-connecting broker"); connect() ; return sendInternal(message); } } private Queue<byte[]> sendInternal(byte[] message) throws IOException { BlockingQueue<Boolean> semaphore = new ArrayBlockingQueue<>(1); long seqNo = channel.getNextPublishSeqNo(); unconfirmedSet.put(seqNo, semaphore); channel.basicPublish(DEFAULT_EXCHANGE, this.queueName, MessageProperties.PERSISTENT_BASIC, message); // wait for confirmation try { Boolean delivered = semaphore.poll(10, TimeUnit.SECONDS); if (delivered == null) throw new IOException("message sending timeout"); if (delivered) return this; throw new IOException(GridBroker.TARGET_LIMIT_MESSAGE); } catch (InterruptedException x) { unconfirmedSet.remove(seqNo); // prevent a memory leak throw new IOException("message sending interrupted"); } } @Override public MessageContainer<byte[]> receive(long timeout, boolean autoAck) throws IOException { if (timeout <= 0) timeout = Long.MAX_VALUE; long termination = timeout <= 0 || timeout == Long.MAX_VALUE ? Long.MAX_VALUE : System.currentTimeMillis() + timeout; Throwable ee = null; while (System.currentTimeMillis() < termination) { ee = null; try { GetResponse response = channel.basicGet(this.queueName, autoAck); if (response != null) { Envelope envelope = response.getEnvelope(); long deliveryTag = envelope.getDeliveryTag(); //channel.basicAck(deliveryTag, false); return new MessageContainer<byte[]>(RabbitQueueFactory.this, response.getBody(), deliveryTag); } //Data.logger.warn("receive failed: response empty"); } catch (Throwable e) { Data.logger.warn("receive failed: " + e.getMessage(), e); connect() ; ee = e; //autoAck = ! autoAck; } try {Thread.sleep(1000);} catch (InterruptedException e) {return null;} } if (ee == null) return null; throw new IOException(ee.getMessage()); } @Override public void acknowledge(long deliveryTag) throws IOException { try { channel.basicAck(deliveryTag, false); } catch (IOException e) { // try again Data.logger.warn("RabbitQueueFactory.acknowledge: re-connecting broker"); connect() ; channel.basicAck(deliveryTag, false); } } @Override public void reject(long deliveryTag) throws IOException { try { channel.basicReject(deliveryTag, true); } catch (IOException e) { // try again Data.logger.warn("RabbitQueueFactory.reject: re-connecting broker"); connect() ; channel.basicReject(deliveryTag, false); } } @Override public void recover() throws IOException { try { channel.basicRecover(true); } catch (IOException e) { // try again Data.logger.warn("RabbitQueueFactory.recover: re-connecting broker"); connect() ; channel.basicRecover(true); } } @Override public long available() throws IOException { try { return availableInternal(); } catch (IOException e) { // try again Data.logger.warn("RabbitQueueFactory.available: re-connecting broker"); connect() ; return availableInternal(); } } private int availableInternal() throws IOException { //int a = channel.queueDeclarePassive(this.queueName).getMessageCount(); int b = (int) channel.messageCount(this.queueName); //assert a == b; return b; } } @Override public void close() { this.queues.clear(); try { this.connection.close(); } catch (IOException e) {} this.queues = null; } public static void main(String[] args) { RabbitQueueFactory qc; try { qc = new RabbitQueueFactory("127.0.0.1", -1, null, null, true, 0); qc.getQueue("test").send("Hello World".getBytes()); System.out.println(qc.getQueue("test2").receive(60000, true)); qc.close(); } catch (IOException e) { Data.logger.warn("", e); } } }