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

rs.mail.queue.MailQueue Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
package rs.mail.queue;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.cowwoc.tokenbucket.Bucket;
import com.github.cowwoc.tokenbucket.ConsumptionResult;

/**
 * Abstract implementation of the queue that leaves the underlying 
 * mailing implemebtation open.
 * 
 * @param  the email message object
 * 
 * @author ralph
 *
 */
public class MailQueue {

	/** Default size of the mail queue for non-priority emails */
	public static final int DEFAULT_MAX_SIZE          = 1000;
	/** Default size of the mail queue for priority emails */
	public static final int DEFAULT_MAX_PRIORITY_SIZE = 50;
	/** Default number of retries before giving up when sending fails */
	public static final int DEFAULT_MAX_RETRIES = 3;
	/** Default period of time after a failed message will be tried again */
	public static final long DEFAULT_RETRY_PERIOD = 60000;
	/** The default period of time a queuing operation shall wait before giving up. */
	public static final long DEFAULT_QUEUING_TIMEOUT_SECONDS = 10;
	
	private Logger log = LoggerFactory.getLogger(getClass());
	
	private MailSender mailSender;
	private LinkedBlockingDeque> queue;
	private LinkedBlockingDeque> priorityQueue;
	private int maxSize;
	private int maxPrioritySize;
	private Bucket tokenBucket;
	private int maxRetries;
	private long retryPeriod;
	private Set listeners;
	
	/**
	 * Constructor.
	 * 

The queue will use the standard max capacity./p> * @param mailSender the handler that actually sends the mail * @see #DEFAULT_MAX_SIZE * @see #DEFAULT_MAX_PRIORITY_SIZE */ public MailQueue(MailSender mailSender) { this(mailSender, DEFAULT_MAX_SIZE, DEFAULT_MAX_PRIORITY_SIZE); } /** * Constructor. *

The queue will use the standard max capacity for priority messages./p> * @param mailSender the handler that actually sends the mail * @param maxSize - the maximum size of the queue for non-priority messages * @see #DEFAULT_MAX_PRIORITY_SIZE */ public MailQueue(MailSender mailSender, int maxSize) { this(mailSender, maxSize, DEFAULT_MAX_PRIORITY_SIZE); } /** * Constructor. * @param mailSender the handler that actually sends the mail * @param maxSize - the maximum size of the queue for non-priority messages * @param maxPrioritySize - the maximum size of the queue for priority messages */ public MailQueue(MailSender mailSender, int maxSize, int maxPrioritySize) { this.mailSender = mailSender; this.queue = new LinkedBlockingDeque<>(maxSize); this.priorityQueue = new LinkedBlockingDeque<>(maxPrioritySize); this.listeners = new HashSet<>(); this.maxRetries = DEFAULT_MAX_RETRIES; this.retryPeriod = DEFAULT_RETRY_PERIOD; this.maxSize = maxSize; this.maxPrioritySize = maxPrioritySize; } /** * Returns the token bucket. *

A token bucket control how many emails may be sent in a certain time window. * There will be no restrictions when the bucket is not set.

* @return the token bucket */ public Bucket getTokenBucket() { return tokenBucket; } /** * Sets the token bucket. *

A token bucket control how many emails may be sent in a certain time window. * There will be no restrictions when the bucket is not set.

* @param tokenBucket the token bucket to set */ public void setTokenBucket(Bucket tokenBucket) { this.tokenBucket = tokenBucket; } /** * Returns the number of retries before giving up when sending fails. * @return the number of retries before giving up when sending fails */ public int getMaxRetries() { return maxRetries; } /** * Sets the number of retries before giving up when sending fails. * @param maxRetries the number of retries before giving up when sending fails */ public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } /** * Returns the period in milliseconds that has to pass before a failed message will be tried again. * @return the retry period in millisecond (Default: 60000) */ public long getRetryPeriod() { return retryPeriod; } /** * Sets the period in milliseconds that has to pass before a failed message will be tried again. * @param retryPeriod the retry period in milliseconds (Default: 60000) */ public void setRetryPeriod(long retryPeriod) { this.retryPeriod = retryPeriod; } /** * Add the message to the queue for sending with normal priority. *

The message will be tried to be queued immediately without any blocking.

* @param message - message to be sent * @param referenceId - reference id for the client * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId) { return queue(message, referenceId, false, 0, 0); } /** * Add the message to the queue for sending with normal priority. * @param message - message to be sent * @param referenceId - reference id for the client * @param timeoutInSeconds the maximum waiting time to queue before giving up * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId, long timeoutInSeconds) { return queue(message, referenceId, false, 0, timeoutInSeconds); } /** * Add the message to the queue for sending. *

The message will be tried to be queued immediately without any blocking.

* @param message - message to be sent * @param referenceId - reference id for the client * @param isPriority - when the mail shall be sent with priority * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId, boolean isPriority) { return queue(message, referenceId, isPriority, 0, 0); } /** * Add the message to the queue for sending. * @param message - message to be sent * @param referenceId - reference id for the client * @param isPriority - when the mail shall be sent with priority * @param timeoutInSeconds the maximum waiting time to queue before giving up * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId, boolean isPriority, long timeoutInSeconds) { return queue(message, referenceId, isPriority, 0, timeoutInSeconds); } /** * Add the message to the queue for sending. *

This method is intended to be used when you need you re-populate the queue after * a system restart. You can pass the additional errorCount parameter to re-establish * the previous queue status.

*

The message will be tried to be queued immediately without any blocking.

* @param message - message to be sent * @param referenceId - reference id for the client * @param isPriority - when the mail shall be sent with priority * @param previousErrorCount - error count from previous attempts * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId, boolean isPriority, int previousErrorCount) { return queue(message, referenceId, isPriority, previousErrorCount, 0); } /** * Add the message to the queue for sending. *

This method is intended to be used when you need you re-populate the queue after * a system restart. You can pass the additional errorCount parameter to re-establish * the previous queue status.

* @param message - message to be sent * @param referenceId - reference id for the client * @param isPriority - when the mail shall be sent with priority * @param previousErrorCount - error count from previous attempts * @param timeoutInSeconds the maximum waiting time to queue before giving up * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ public boolean queue(T message, String referenceId, boolean isPriority, int previousErrorCount, long timeoutInSeconds) { MessageEntry entry = new MessageEntry(referenceId, message, isPriority); entry.failedAttempts = previousErrorCount; return queue(entry, isPriority ? priorityQueue : queue, timeoutInSeconds); } /** * Internal queuing implementation - queues the entry in th egiven queue using the given maximum waiting time. * @param message the message to be queued * @param queue the queue to be used * @param timeoutInSeconds the maximum waiting time to queue before giving up * @return {@code true} when the message was queued, {@code false} when it cannot be queued at this time */ protected boolean queue(MessageEntry message, LinkedBlockingDeque> queue, long timeoutInSeconds) { try { boolean rc = false; if (timeoutInSeconds > 0) { rc = queue.offer(message, timeoutInSeconds, TimeUnit.SECONDS); } else { rc = queue.offer(message); } if (rc) mailQueued(message); return rc; } catch (InterruptedException e) { log.error("Queuing interrupted on "+message.referenceId, e); } return false; } /** * Returns the current size of the queue. * @return the size of the queue */ public int size() { return size(true)+size(false); } /** * Returns the current size of the queue for normal or prioritized messages only. * @param isPriority whether the priority or normal number of messages shall be returned * @return the size of the queue */ public int size(boolean isPriority) { return isPriority ? priorityQueue.size() : queue.size(); } /** * Returns the total capacity of the normal or prioritized queue. * @param isPriority whether the capacity for priority or normal messages shall be returned * @return the total capacity */ public int capacity(boolean isPriority) { return isPriority ? maxPrioritySize : maxSize; } /** * Returns the remaining capacity of the normal or prioritized queue. * @param isPriority whether the capacity for priority or normal messages shall be returned * @return the remaining capacity */ public int remainingCapacity(boolean isPriority) { return isPriority ? priorityQueue.remainingCapacity() : queue.remainingCapacity(); } /** * Adds a listener to this queue. * @param listener the listener to add */ public void addListener(MailQueueListener listener) { listeners.add(listener); } /** * Removes a listener from this queue. * @param listener the listener to remove */ public void removeListener(MailQueueListener listener) { listeners.remove(listener); } /** * Informs listeners that a message was queued. * @param entry the message entry */ protected void mailQueued(MessageEntry entry) { for (MailQueueListener listener : listeners) { listener.onQueued(entry.referenceId); } } /** * Informs listeners that a message is being sent. * @param entry the message entry */ protected void mailSending(MessageEntry entry) { for (MailQueueListener listener : listeners) { listener.onSending(entry.referenceId); } } /** * Informs listeners that a message was sent successfully. * @param entry the message entry */ protected void mailSent(MessageEntry entry) { for (MailQueueListener listener : listeners) { listener.onSent(entry.referenceId); } } /** * Informs listeners that a message failed. * @param entry the message entry * @param reason the reson why sending failed */ protected void mailFailed(MessageEntry entry, String reason) { for (MailQueueListener listener : listeners) { listener.onFailed(entry.referenceId, entry.failedAttempts, reason); } } /** * Process the queue once. *

The method must be called periodically. The method stops when

*
    *
  • The queue is empty and no more messages to be sent, or
  • *
  • The rate limit has been reached and we need to wait before sending
  • *
* @throws Exception when the processing caused a severe faiulure */ public void run() throws Exception { if (log.isDebugEnabled()) log.debug("I have "+size()+" messages queued"); MessageEntry candidate = getNext(); while (candidate != null) { if (getBucketToken()) { mailSending(candidate); try { mailSender.sendMessage(candidate.message, candidate.referenceId); mailSent(candidate); remove(candidate); } catch (Throwable t) { candidate.failedAttempts++; mailFailed(candidate, t.getMessage()); log.error("Cannot send message", t); if (candidate.failedAttempts > getMaxRetries()) { remove(candidate); } else { candidate.notBeforeTimeInMillis = System.currentTimeMillis()+retryPeriod; } } // Try next candidate = getNext(); } else { candidate = null; } } } /** * Picks the next message for sending if available. *

The method does not yet remove the message from the queue.

* @return a message cleared to be sent */ protected MessageEntry getNext() { MessageEntry rc = getNext(true); if (rc == null) rc = getNext(false); return rc; } /** * Picks the next message from the given queue. *

The method does not yet remove the message from the queue.

* @param isPriority the queue to check (priority or normal) * @return a message cleared to be sent */ protected MessageEntry getNext(boolean isPriority) { long now = System.currentTimeMillis(); BlockingQueue> queue = isPriority ? this.priorityQueue : this.queue; Object list[] = queue.toArray(); for (Object o : list) { @SuppressWarnings("unchecked") MessageEntry entry = (MessageEntry)o; if (entry.notBeforeTimeInMillis < now) return entry; } return null; } /** * Removes the message from the queue * @param entry the message to be removed */ protected void remove(MessageEntry entry) { if (entry.isPriority) priorityQueue.remove(entry); else queue.remove(entry); } /** * Tries to consume a token from a bucket. *

The method will ask the token bucket if available and consume one * token if possible. If no token is available, the method will * not block and return {@code false}.

*

Always returns {@code true} when no token bucket was set.

* @return {@code true} when a token was consumed, {@code false} otherwise. */ protected boolean getBucketToken() { if (tokenBucket != null) { ConsumptionResult result = tokenBucket.tryConsume(); return result.getTokensConsumed() > 0; } return true; } /** * Helper class to store messages in the queue. * * @author ralph * */ protected static class MessageEntry { /** Reference ID of the message */ protected String referenceId; /** The message itself */ protected X message; /** A timestamp to hold the message */ protected long notBeforeTimeInMillis; /** Number of failed attempts */ protected int failedAttempts; /** Whether it is priority */ protected boolean isPriority; /** * Constructor. * @param referenceId reference ID of th emessage * @param message the message * @param isPriority whether it is prioritized */ public MessageEntry(String referenceId, X message, boolean isPriority) { this.referenceId = referenceId; this.message = message; this.isPriority = isPriority; this.notBeforeTimeInMillis = 0; this.failedAttempts = 0; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy