package core.framework.impl.queue; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Address; import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import core.framework.api.log.ActionLogContext; import core.framework.api.log.Markers; import core.framework.api.util.StopWatch; import core.framework.impl.async.ThreadPools; import core.framework.impl.resource.Pool; import core.framework.impl.resource.PoolItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UncheckedIOException; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; /** * @author neo */ public final class RabbitMQImpl implements RabbitMQ { public final Pool<Channel> pool; private final Logger logger = LoggerFactory.getLogger(RabbitMQImpl.class); private final ConnectionFactory connectionFactory = new ConnectionFactory(); private final ExecutorService workerExecutor; private final ScheduledExecutorService heartbeatExecutor; private final Lock lock = new ReentrantLock(); private List<Address> addresses; private long slowOperationThresholdInNanos = Duration.ofMillis(100).toNanos(); private volatile Connection connection; public RabbitMQImpl() { workerExecutor = ThreadPools.fixedThreadPool(1, "rabbitMQ-worker-"); connectionFactory.setSharedExecutor(workerExecutor); heartbeatExecutor = ThreadPools.singleThreadScheduler("rabbitMQ-heartbeat-"); connectionFactory.setHeartbeatExecutor(heartbeatExecutor); connectionFactory.setAutomaticRecoveryEnabled(true); user("rabbitmq"); // default user/password password("rabbitmq"); pool = new Pool<>(this::createChannel, Channel::close); pool.name("rabbitmq"); pool.size(1, 50); pool.maxIdleTime(Duration.ofMinutes(30)); timeout(Duration.ofSeconds(5)); } public void close() { if (connection != null) { logger.info("close rabbitMQ client, hosts={}", addresses); try { pool.close(); connection.close(); } catch (IOException e) { throw new UncheckedIOException(e); } } workerExecutor.shutdown(); heartbeatExecutor.shutdown(); } public void user(String user) { connectionFactory.setUsername(user); } public void password(String password) { connectionFactory.setPassword(password); } public void hosts(String... hosts) { logger.info("set rabbitMQ hosts, hosts={}", Arrays.toString(hosts)); addresses = Arrays.stream(hosts).map(Address::new).collect(Collectors.toList()); } public void timeout(Duration timeout) { connectionFactory.setConnectionTimeout((int) timeout.toMillis()); pool.checkoutTimeout(timeout); } public void slowOperationThreshold(Duration threshold) { slowOperationThresholdInNanos = threshold.toNanos(); } @Override public RabbitMQConsumer consumer(String queue, int prefetchCount) { Channel channel = createChannel(); return new RabbitMQConsumer(channel, queue, prefetchCount, slowOperationThresholdInNanos); } @Override public void publish(String exchange, String routingKey, byte[] message, AMQP.BasicProperties properties) { StopWatch watch = new StopWatch(); PoolItem<Channel> item = pool.borrowItem(); try { item.resource.basicPublish(exchange, routingKey, properties, message); } catch (AlreadyClosedException e) { // rabbitmq throws AlreadyClosedException for channel error, e.g. channel is not configured correctly or not exists item.broken = true; throw e; } catch (IOException e) { item.broken = true; throw new UncheckedIOException(e); } finally { pool.returnItem(item); long elapsedTime = watch.elapsedTime(); ActionLogContext.track("rabbitMQ", elapsedTime); logger.debug("publish, exchange={}, routingKey={}, elapsedTime={}", exchange, routingKey, elapsedTime); checkSlowOperation(elapsedTime); } } private void checkSlowOperation(long elapsedTime) { if (elapsedTime > slowOperationThresholdInNanos) { logger.warn(Markers.errorCode("SLOW_RABBITMQ"), "slow rabbitMQ operation, elapsedTime={}", elapsedTime); } } public Channel createChannel() { try { if (connection == null) { createConnection(); } return connection.createChannel(); } catch (IOException | TimeoutException e) { throw new Error(e); } } private void createConnection() throws IOException, TimeoutException { if (addresses == null || addresses.isEmpty()) throw new Error("addresses must not be empty"); lock.lock(); try { if (connection == null) connection = connectionFactory.newConnection(addresses); } finally { lock.unlock(); } } }