All Downloads are FREE. Search and download functionalities are using the official Maven repository.

devutility.external.redis.queue.stream.JedisStreamQueueConsumer Maven / Gradle / Ivy

The newest version!
package devutility.external.redis.queue.stream;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import devutility.external.redis.com.Config;
import devutility.external.redis.com.ExceptionRetryApprover;
import devutility.external.redis.com.RedisQueueOption;
import devutility.external.redis.com.RedisType;
import devutility.external.redis.com.StatusCode;
import devutility.external.redis.com.StreamMessageType;
import devutility.external.redis.exception.JedisFatalException;
import devutility.external.redis.ext.model.ConsumerInfo;
import devutility.external.redis.queue.Acknowledger;
import devutility.external.redis.queue.JedisQueueConsumer;
import devutility.external.redis.utils.JedisUtils;
import devutility.internal.lang.StringUtils;
import devutility.internal.util.CollectionUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.StreamEntry;
import redis.clients.jedis.StreamEntryID;
import redis.clients.jedis.StreamPendingEntry;
import redis.clients.jedis.exceptions.JedisConnectionException;

/**
 * 
 * JedisStreamQueueConsumer
 * 
 * @author: Aldwin Su
 * @version: 2019-10-10 20:03:42
 */
public class JedisStreamQueueConsumer extends JedisQueueConsumer {
	/**
	 * Acknowledger object.
	 */
	private Acknowledger acknowledger;

	/**
	 * Consumed StreamEntryID strings.
	 */
	private Set consumedIds = new LinkedHashSet();

	/**
	 * Constructor
	 * @param jedis Jedis object to read data from Redis.
	 * @param redisQueueOption RedisQueueOption object.
	 * @param consumerEvent Custom consumer event implementation.
	 * @param exceptionRetryApprover ExceptionRetryApprover object.
	 */
	public JedisStreamQueueConsumer(final Jedis jedis, final RedisQueueOption redisQueueOption, final Acknowledger acknowledger, final JedisStreamQueueConsumerEvent consumerEvent, final ExceptionRetryApprover exceptionRetryApprover) {
		super(jedis, redisQueueOption, consumerEvent, exceptionRetryApprover);
		this.acknowledger = acknowledger;
		consumerEvent.setAcknowledger(acknowledger);
	}

	/**
	 * Constructor
	 * @param jedis Jedis object to read data from Redis.
	 * @param redisQueueOption RedisQueueOption object.
	 * @param consumerEvent Custom consumer event implementation.
	 */
	public JedisStreamQueueConsumer(final Jedis jedis, final RedisQueueOption redisQueueOption, final Acknowledger acknowledger, final JedisStreamQueueConsumerEvent consumerEvent) {
		this(jedis, redisQueueOption, acknowledger, consumerEvent, null);
	}

	@Override
	public void listen() throws Exception {
		RedisType type = devJedis.type(redisQueueOption.getKey());
		validate(type);
		initialize(type);

		while (isActive()) {
			try {
				processPending();
				process();
			} catch (Exception e) {
				/**
				 * Connection issue should be handled firstly.
				 */
				if (JedisUtils.isBrokenJedis(jedis)) {
					throw new JedisConnectionException(e);
				}

				if (e instanceof JedisFatalException || isExceptionRetryApproved(e)) {
					throw e;
				}

				if (!isReasonableConsumerException()) {
					throw new JedisFatalException("Exceptions count excced the setting exceptionLimit and exceptionIntervalMillis in RedisQueueOption object.", e);
				}

				log(e);
			}
		}
	}

	/**
	 * Validate parameters that caller provided.
	 */
	protected void validate(RedisType type) {
		super.validate();

		if (StringUtils.isNullOrEmpty(redisQueueOption.getGroupName())) {
			throw new IllegalArgumentException("Group name can't be empty!");
		}

		if (StringUtils.isNullOrEmpty(redisQueueOption.getConsumerName())) {
			throw new IllegalArgumentException("Consumer name can't be empty!");
		}

		if (RedisType.NONE != type && RedisType.STREAM != type) {
			throw new IllegalArgumentException(String.format("Invalid Redis type for key \"%s\"!", redisQueueOption.getKey()));
		}
	}

	/**
	 * Initialize for data consumption.
	 */
	protected void initialize(RedisType type) {
		consumedIds.clear();

		if (RedisType.NONE == type) {
			initializeGroup();
			return;
		}

		if (!devJedis.isGroupExist(redisQueueOption.getKey(), redisQueueOption.getGroupName())) {
			initializeGroup();
		}
	}

	/**
	 * Initialize consumer group.
	 */
	private void initializeGroup() {
		if (devJedis.createGroup(redisQueueOption.getKey(), redisQueueOption.getGroupName()) != StatusCode.OK) {
			throw new JedisFatalException("Create group failed!");
		}
	}

	/**
	 * Process entries in Pending list.
	 */
	private void processPending() {
		ConsumerInfo consumerInfo = devJedis.getConsumerInfo(redisQueueOption.getKey(), redisQueueOption.getGroupName(), redisQueueOption.getConsumerName());

		/**
		 * New consumer.
		 */
		if (consumerInfo == null) {
			return;
		}

		int pending = (int) consumerInfo.getPending();
		int count = redisQueueOption.getPageSizeForReadPending();
		int pageSize = pending / redisQueueOption.getPageSizeForReadPending();
		StreamEntryID startId = null;

		if (count % redisQueueOption.getPageSizeForReadPending() > 0) {
			pageSize += 1;
		}

		for (int i = 0; i < pageSize; i++) {
			List list = jedis.xpending(redisQueueOption.getKey(), redisQueueOption.getGroupName(), startId, null, count, redisQueueOption.getConsumerName());

			for (StreamPendingEntry item : list) {
				Map streamEntryMap = devJedis.xrangeOne(redisQueueOption.getKey(), item.getID());

				if (streamEntryMap != null) {
					onMessage(StreamMessageType.PENDING, item.getID(), streamEntryMap.get(Config.QUEUE_DEFAULT_ITEM_KEY));
					startId = new StreamEntryID(item.getID().getTime(), item.getID().getSequence() + 1);
				}
			}
		}
	}

	/**
	 * Main process.
	 * @throws InterruptedException
	 */
	private void process() throws InterruptedException {
		/**
		 * Key of queue in Redis, value is StreamEntryID object which indicate the beginning ID of queue items.
		 */
		Entry stream = new AbstractMap.SimpleEntry(redisQueueOption.getKey(), StreamEntryID.UNRECEIVED_ENTRY);

		@SuppressWarnings("unchecked")
		List>> list = devJedis.xreadGroup(redisQueueOption.getGroupName(), redisQueueOption.getConsumerName(), 1, redisQueueOption.getWaitMilliseconds(), redisQueueOption.isNoNeedAck(), stream);

		if (CollectionUtils.isNullOrEmpty(list)) {
			return;
		}

		List streamEntries = list.get(0).getValue();

		if (CollectionUtils.isNullOrEmpty(streamEntries)) {
			throw new JedisFatalException("Illegal StreamEntry list!");
		}

		StreamEntry streamEntry = streamEntries.get(0);
		Map streamEntryMap = streamEntry.getFields();

		if (streamEntryMap == null || !streamEntryMap.containsKey(Config.QUEUE_DEFAULT_ITEM_KEY)) {
			throw new JedisFatalException("Illegal StreamEntry map!");
		}

		onMessage(StreamMessageType.NORMAL, streamEntry.getID(), streamEntryMap.get(Config.QUEUE_DEFAULT_ITEM_KEY));
	}

	/**
	 * Callback method when new message arrived. The first parameter is StreamEntryID string, the second one is its value.
	 * @param value Message.
	 */
	private void onMessage(StreamMessageType streamMessageType, Object... values) {
		StreamEntryID streamEntryID = (StreamEntryID) values[0];

		/**
		 * Avoid duplicated consume.
		 */
		if (consumedIds.contains(streamEntryID.toString())) {
			return;
		}

		switch (streamMessageType) {
		case NORMAL:
			if (!consumerEvent.onMessage(redisQueueOption.getKey(), values)) {
				return;
			}
			break;

		case PENDING:
			if (!consumerEvent.onPendingMessage(redisQueueOption.getKey(), values)) {
				return;
			}
			break;

		default:
			return;
		}

		consumedIds.add(streamEntryID.toString());
		autoAck(streamEntryID);
	}

	/**
	 * Automatic acknowledge one message.
	 * @param streamEntryId: StreamEntryID object.
	 */
	private void autoAck(StreamEntryID streamEntryId) {
		if (redisQueueOption.isNoNeedAck() || !redisQueueOption.isAutoAck()) {
			return;
		}

		acknowledger.ack(streamEntryId);
	}

	@Override
	public void close() throws IOException {
		jedis.close();
		devJedis.close();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy