/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.apache.jackrabbit.core.observation;
import org.apache.commons.collections.Buffer;
import org.apache.commons.collections.BufferUtils;
import org.apache.commons.collections.buffer.UnboundedFifoBuffer;
import org.apache.jackrabbit.core.state.ChangeLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Dispatcher for dispatching events to listeners within a single workspace.
*/
public final class ObservationDispatcher extends EventDispatcher
implements Runnable {
/**
* Logger instance for this class
*/
private static final Logger log
= LoggerFactory.getLogger(ObservationDispatcher.class);
/**
* Dummy DispatchAction indicating the notification thread to end
*/
private static final DispatchAction DISPOSE_MARKER = new DispatchAction(null, null);
/**
* The maximum number of queued asynchronous events. To avoid of of memory
* problems, the default value is 200'000. To change the default, set the
* system property jackrabbit.maxQueuedEvents to the required value. If more
* events are in the queue, the current thread waits, unless the current thread is
* the observation dispatcher itself (in which case only a warning is logged
* - usually observation listeners shouldn't cause new events).
*/
private static final int MAX_QUEUED_EVENTS = Integer.parseInt(System.getProperty("jackrabbit.maxQueuedEvents", "200000"));
/**
* Currently active <code>EventConsumer</code>s for notification.
*/
private Set<EventConsumer> activeConsumers = new HashSet<EventConsumer>();
/**
* Currently active synchronous <code>EventConsumer</code>s for notification.
*/
private Set<EventConsumer> synchronousConsumers = new HashSet<EventConsumer>();
/**
* Set of <code>EventConsumer</code>s for read only Set access
*/
private Set<EventConsumer> readOnlyConsumers;
/**
* Set of synchronous <code>EventConsumer</code>s for read only Set access.
*/
private Set<EventConsumer> synchronousReadOnlyConsumers;
/**
* synchronization monitor for listener changes
*/
private Object consumerChange = new Object();
/**
* Contains the pending events that will be delivered to event listeners
*/
private Buffer eventQueue
= BufferUtils.blockingBuffer(new UnboundedFifoBuffer());
private AtomicInteger eventQueueSize = new AtomicInteger();
/**
* The background notification thread
*/
private Thread notificationThread;
private long lastError;
/**
* Creates a new <code>ObservationDispatcher</code> instance
* and starts the notification thread daemon.
*/
public ObservationDispatcher() {
notificationThread = new Thread(this, "ObservationManager");
notificationThread.setDaemon(true);
notificationThread.start();
}
/**
* Disposes this <code>ObservationManager</code>. This will
* effectively stop the background notification thread.
*/
public void dispose() {
// dispatch dummy event to mark end of notification
eventQueue.add(DISPOSE_MARKER);
try {
notificationThread.join();
} catch (InterruptedException e) {
// FIXME log exception ?
}
log.info("Notification of EventListeners stopped.");
}
/**
* Returns an unmodifiable <code>Set</code> of <code>EventConsumer</code>s.
*
* @return <code>Set</code> of <code>EventConsumer</code>s.
*/
Set<EventConsumer> getAsynchronousConsumers() {
synchronized (consumerChange) {
if (readOnlyConsumers == null) {
readOnlyConsumers = Collections.unmodifiableSet(new HashSet<EventConsumer>(activeConsumers));
}
return readOnlyConsumers;
}
}
Set<EventConsumer> getSynchronousConsumers() {
synchronized (consumerChange) {
if (synchronousReadOnlyConsumers == null) {
synchronousReadOnlyConsumers = Collections.unmodifiableSet(new HashSet<EventConsumer>(synchronousConsumers));
}
return synchronousReadOnlyConsumers;
}
}
/**
* Implements the run method of the background notification
* thread.
*/
public void run() {
DispatchAction action;
while ((action = (DispatchAction) eventQueue.remove()) != DISPOSE_MARKER) {
eventQueueSize.getAndAdd(-action.getEventStates().size());
log.debug("got EventStateCollection");
log.debug("event delivery to " + action.getEventConsumers().size() + " consumers started...");
for (Iterator<EventConsumer> it = action.getEventConsumers().iterator(); it.hasNext();) {
EventConsumer c = it.next();
try {
c.consumeEvents(action.getEventStates());
} catch (Throwable t) {
log.warn("EventConsumer " +
c.getEventListener().getClass().getName() +
" threw exception", t);
// move on to the next consumer
}
}
log.debug("event delivery finished.");
}
}
/**
* {@inheritDoc}
* <p>
* Gives this observation manager the opportunity to
* prepare the events for dispatching.
*/
void prepareEvents(EventStateCollection events) {
Set<EventConsumer> consumers = new HashSet<EventConsumer>();
consumers.addAll(getSynchronousConsumers());
consumers.addAll(getAsynchronousConsumers());
for (EventConsumer c : consumers) {
c.prepareEvents(events);
}
}
/**
* {@inheritDoc}
*/
void prepareDeleted(EventStateCollection events, ChangeLog changes) {
Set<EventConsumer> consumers = new HashSet<EventConsumer>();
consumers.addAll(getSynchronousConsumers());
consumers.addAll(getAsynchronousConsumers());
for (EventConsumer c : consumers) {
c.prepareDeleted(events, changes.deletedStates());
}
}
/**
* {@inheritDoc}
* <p>
* Dispatches the {@link EventStateCollection events} to all
* registered {@link javax.jcr.observation.EventListener}s.
*/
void dispatchEvents(EventStateCollection events) {
// JCR-3426: log warning when changes are done
// with the notification thread
if (Thread.currentThread() == notificationThread) {
log.warn("Save call with event notification thread detected. This " +
"may lead to a growing event queue. Enable debug log to " +
"see the stack trace with the class calling save().");
if (log.isDebugEnabled()) {
log.debug("Stack trace:", new Exception());
}
}
// notify synchronous listeners
Set<EventConsumer> synchronous = getSynchronousConsumers();
if (log.isDebugEnabled()) {
log.debug("notifying " + synchronous.size() + " synchronous listeners.");
}
for (EventConsumer c : synchronous) {
try {
c.consumeEvents(events);
} catch (Throwable t) {
log.error("Synchronous EventConsumer threw exception.", t);
// move on to next consumer
}
}
eventQueue.add(new DispatchAction(events, getAsynchronousConsumers()));
eventQueueSize.addAndGet(events.size());
}
/**
* Checks if the observation event queue contains more than the
* configured {@link #MAX_QUEUED_EVENTS maximum number of events},
* and delays the current thread in such cases. No delay is added
* if the current thread is the observation thread, for example if
* an observation listener writes to the repository.
* <p>
* This method should only be called outside the scope of internal
* repository access locks.
*/
public void delayIfEventQueueOverloaded() {
if (eventQueueSize.get() > MAX_QUEUED_EVENTS) {
boolean logWarning = false;
long now = System.currentTimeMillis();
// log a warning at most every 5 seconds (to avoid filling the log file)
if (lastError == 0 || now > lastError + 5000) {
logWarning = true;
log.warn("More than " + MAX_QUEUED_EVENTS + " events in the queue", new Exception("Stack Trace"));
lastError = now;
}
if (Thread.currentThread() == notificationThread) {
if (logWarning) {
log.warn("Recursive notification?");
}
} else {
if (logWarning) {
log.warn("Waiting");
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.warn("Interrupted while rate-limiting writes", e);
}
}
}
}
/**
* Adds or replaces an event consumer.
* @param consumer the <code>EventConsumer</code> to add or replace.
*/
void addConsumer(EventConsumer consumer) {
synchronized (consumerChange) {
if (consumer.getEventListener() instanceof SynchronousEventListener) {
// remove existing if any
synchronousConsumers.remove(consumer);
// re-add it
synchronousConsumers.add(consumer);
// reset read only consumer set
synchronousReadOnlyConsumers = null;
} else {
// remove existing if any
activeConsumers.remove(consumer);
// re-add it
activeConsumers.add(consumer);
// reset read only consumer set
readOnlyConsumers = null;
}
}
}
/**
* Unregisters an event consumer from event notification.
* @param consumer the consumer to deregister.
*/
void removeConsumer(EventConsumer consumer) {
synchronized (consumerChange) {
if (consumer.getEventListener() instanceof SynchronousEventListener) {
synchronousConsumers.remove(consumer);
// reset read only listener set
synchronousReadOnlyConsumers = null;
} else {
activeConsumers.remove(consumer);
// reset read only listener set
readOnlyConsumers = null;
}
}
}
}