/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 org.apereo.portal.events.handlers;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apereo.portal.spring.context.FilteringApplicationListener;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationEvent;
/**
* Queues PortalEvents in a local {@link ConcurrentLinkedQueue} and flushes the events to the
* configured {@link BatchingEventHandler} when {@link #flush()} is called. This class must be used
* with some external timer that will call {@link #flush()} at regular intervals
*
*/
public abstract class QueueingEventHandler<E extends ApplicationEvent>
extends FilteringApplicationListener<E> implements DisposableBean {
private final Queue<E> eventQueue = new ConcurrentLinkedQueue<E>();
private final Lock flushLock = new ReentrantLock();
private int batchSize = 25;
//Used to hold events to flush, MUST only be read/written from within the flushLock
private List<E> eventBuffer = new ArrayList<E>(this.batchSize);
/** The maximum number of events to be flushed to the {@link BatchingEventHandler} per call. */
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
eventBuffer = new ArrayList<E>(this.batchSize);
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
@Override
public final void destroy() throws Exception {
this.flush();
}
/* (non-Javadoc)
* @see org.apereo.portal.spring.context.FilteringApplicationListener#onFilteredApplicationEvent(org.springframework.context.ApplicationEvent)
*/
@Override
protected final void onFilteredApplicationEvent(E event) {
this.eventQueue.offer(event);
}
/**
* Handle a batch of application events, these events have been filtered by the parent {@link
* FilteringApplicationListener}
*
* @param events Events to handle
*/
protected abstract void onApplicationEvents(Iterable<E> events);
/**
* Flushes the queued PortalEvents to the configured {@link BatchingEventHandler}. If <code>
* force</code> is false flushing only happens if there are enough events in the queue and a
* flush isn't already under way. If <code>force</code> is true all queued events will be
* flushed and the calling thread will wait until any previously executing flush call completes
* before flushing
*
* @param force Forces flushing events to the {@link BatchingEventHandler} even if there are
* fewer than <code>flushCount</code> PortalEvents in the queue.
*/
public final void flush() {
if (eventQueue.isEmpty()) {
//No events to flush
logger.trace("No events to flush, returning.");
return;
}
//Only one thread should be flushing at a time, try to get the flush lock and if it
//is already held just return.
if (!this.flushLock.tryLock()) {
logger.trace("FlushLock already held, returning.");
return;
}
try {
while (!this.eventQueue.isEmpty()) {
//Clear the buffer for re-use
eventBuffer.clear();
//Pop events off the queue into the buffer
while (!this.eventQueue.isEmpty() && eventBuffer.size() < this.batchSize) {
final E event = eventQueue.poll();
eventBuffer.add(event);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Flushing " + eventBuffer.size() + " events");
}
//Write events out to batching listener
try {
this.onApplicationEvents(eventBuffer);
} catch (Throwable t) {
this.logger.error(
"An exception was thrown while trying to flush "
+ eventBuffer.size()
+ " events",
t);
final StringBuilder failedEvents = new StringBuilder();
failedEvents.append(
"The following events that were being flushed, some may have been persisted correctly");
for (final E portalEvent : eventBuffer) {
failedEvents.append("\n\t");
try {
failedEvents.append(portalEvent.toString());
} catch (Exception e) {
failedEvents
.append("toString failed on a PortalEvent of type '")
.append(portalEvent.getClass())
.append("': ")
.append(e);
}
}
this.logger.error(failedEvents.toString(), t);
}
}
} finally {
//Clear the buffer to avoid memory leaks
eventBuffer.clear();
this.flushLock.unlock();
}
}
}