/** * 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(); } } }