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

org.apache.jackrabbit.oak.spi.commit.BackgroundObserver Maven / Gradle / Ivy

There is a newer version: 1.62.0
Show newest version
/*
 * 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.oak.spi.commit;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Queues.newArrayBlockingQueue;

import java.io.Closeable;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.google.common.base.Predicate;
import org.apache.jackrabbit.oak.commons.concurrent.NotifyingFutureTask;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An observer that uses a change queue and a background thread to forward
 * content changes to another observer. The mechanism is designed so that
 * the {@link #contentChanged(NodeState, CommitInfo)} method will never block,
 * regardless of the behavior of the other observer. If that observer blocks
 * or is too slow to consume all content changes, causing the change queue
 * to fill up, any further update will automatically be merged into just one
 * external content change, causing potential loss of local commit information.
 * To help prevent such cases, any sequential external content changes that
 * the background observer thread has yet to process are optionally
 * (see {@code alwaysCollapseExternalEvents} and {@code oak.observation.alwaysCollapseExternal})
 * automatically merged to just one change.
 */
public class BackgroundObserver implements Observer, Closeable {

    /**
     * Signal for the background thread to stop processing changes.
     */
    private static final ContentChange STOP = new ContentChange(null, null);

    /**
     * The receiving observer being notified off the background thread.
     */
    private final Observer observer;

    /**
     * Executor used to dispatch events
     */
    private final Executor executor;

    /**
     * Handler for uncaught exception on the background thread
     */
    private final UncaughtExceptionHandler exceptionHandler;

    /**
     * The queue of content changes to be processed.
     */
    private final BlockingQueue queue;

    /**
     * The max queue length used for this observer's queue
     */
    private final int maxQueueLength;

    /**
     * Whether external events should be collapsed even if queue isn't full yet.
     */
    private final boolean alwaysCollapseExternalEvents =
            Boolean.parseBoolean(System.getProperty("oak.observation.alwaysCollapseExternal", "false"));

    private static class ContentChange {
        private final NodeState root;
        private final CommitInfo info;
        ContentChange(NodeState root, CommitInfo info) {
            this.root = root;
            this.info = info;
        }
    }

    /**
     * The content change that was last added to the queue.
     * Used to compact external changes.
     */
    private ContentChange last;

    /**
     * Flag to indicate that some content changes were dropped because
     * the queue was full.
     */
    private boolean full;

    /**
     * Current background task
     */
    private volatile NotifyingFutureTask currentTask = NotifyingFutureTask.completed();

    /**
     * Completion handler: set the current task to the next task and schedules that one
     * on the background thread.
     */
    private final Runnable completionHandler = new Runnable() {
        Callable task = new Callable() {
            @Override
            public Void call() throws Exception {
                try {
                    ContentChange change = queue.poll();
                    if (change != null && change != STOP) {
                        observer.contentChanged(change.root, change.info);
                        currentTask.onComplete(completionHandler);
                    }
                } catch (Throwable t) {
                    exceptionHandler.uncaughtException(Thread.currentThread(), t);
                }
                return null;
            }
        };

        @Override
        public void run() {
            currentTask = new NotifyingFutureTask(task);
            executor.execute(currentTask);
        }
    };

    /**
     * {@code true} after this observer has been stopped
     */
    private volatile boolean stopped;

    public BackgroundObserver(
            @Nonnull Observer observer,
            @Nonnull Executor executor,
            int queueLength,
            @Nonnull UncaughtExceptionHandler exceptionHandler) {
        this.observer = checkNotNull(observer);
        this.executor = checkNotNull(executor);
        this.exceptionHandler = checkNotNull(exceptionHandler);
        this.maxQueueLength = queueLength;
        this.queue = newArrayBlockingQueue(maxQueueLength);
    }

    public BackgroundObserver(
            @Nonnull final Observer observer,
            @Nonnull Executor executor,
            int queueLength) {
        this(observer, executor, queueLength, new UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                getLogger(observer).error("Uncaught exception in " + observer, e);
            }
        });
    }

    public BackgroundObserver(
            @Nonnull Observer observer,
            @Nonnull Executor executor) {
        this(observer, executor, 1000);
    }

    /**
     * Called when ever an item has been added to the queue
     * @param queueSize  size of the queue
     */
    protected void added(int queueSize) { }

    /**
     * @return  The max queue length used for this observer's queue
     */
    public int getMaxQueueLength() {
        return maxQueueLength;
    }

    /**
     * Clears the change queue and signals the background thread to stop
     * without making any further {@link #contentChanged(NodeState, CommitInfo)}
     * calls to the background observer. If the thread is currently in the
     * middle of such a call, then that call is allowed to complete; i.e.
     * the thread is not forcibly interrupted. This method returns immediately
     * without blocking to wait for the thread to finish.
     * 

* After a call to this method further calls to {@link #contentChanged(NodeState, CommitInfo)} * will throw a {@code IllegalStateException}. */ @Override public synchronized void close() { queue.clear(); queue.add(STOP); stopped = true; } @Nonnull public BackgroundObserverMBean getMBean(){ return new BackgroundObserverMBean() { @Override public String getClassName() { return observer.getClass().getName(); } @Override public int getQueueSize() { return queue.size(); } @Override public int getMaxQueueSize() { return getMaxQueueLength(); } @Override public int getLocalEventCount() { return size(filter(queue, new Predicate() { @Override public boolean apply(@Nullable ContentChange input) { return input.info != null; } })); } @Override public int getExternalEventCount() { return size(filter(queue, new Predicate() { @Override public boolean apply(@Nullable ContentChange input) { return input.info == null; } })); } }; } //----------------------------------------------------------< Observer >-- /** * @throws IllegalStateException if {@link #close()} has already been called. */ @Override public synchronized void contentChanged(@Nonnull NodeState root, @Nullable CommitInfo info) { checkState(!stopped); checkNotNull(root); if (alwaysCollapseExternalEvents && info == null && last != null && last.info == null) { // This is an external change. If the previous change was // also external, we can drop it from the queue (since external // changes in any case can cover multiple commits) to help // prevent the queue from filling up too fast. queue.remove(last); full = false; } ContentChange change; if (full) { // If the queue is full, some commits have already been skipped // so we need to drop the possible local commit information as // only external changes can be merged together to larger chunks. change = new ContentChange(root, null); } else { change = new ContentChange(root, info); } // Try to add this change to the queue without blocking, and // mark the queue as full if there wasn't enough space full = !queue.offer(change); if (!full) { // Keep track of the last change added, so we can do the // compacting of external changes shown above. last = change; } // Set the completion handler on the currently running task. Multiple calls // to onComplete are not a problem here since we always pass the same value. // Thus there is no question as to which of the handlers will effectively run. currentTask.onComplete(completionHandler); added(queue.size()); } //------------------------------------------------------------< internal >--- private static Logger getLogger(@Nonnull Observer observer) { return LoggerFactory.getLogger(checkNotNull(observer).getClass()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy