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: 2024.11.18751.20241128T090041Z-241100
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 java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.guava.common.base.Preconditions.checkState;
import static org.apache.jackrabbit.guava.common.collect.Iterables.filter;
import static org.apache.jackrabbit.guava.common.collect.Iterables.size;
import static org.apache.jackrabbit.guava.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 java.util.concurrent.TimeUnit;

import org.apache.jackrabbit.oak.commons.concurrent.NotifyingFutureTask;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
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 {

    public final static int DEFAULT_QUEUE_SIZE = 10000;

    /**
     * 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;
        private final long created = System.currentTimeMillis();
        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;

    /**
     * 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);
                        removed(queue.size(), change.created);
                        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(
            @NotNull Observer observer,
            @NotNull Executor executor,
            int queueLength,
            @NotNull UncaughtExceptionHandler exceptionHandler) {
        this.observer = requireNonNull(observer);
        this.executor = requireNonNull(executor);
        this.exceptionHandler = requireNonNull(exceptionHandler);
        this.maxQueueLength = queueLength;
        this.queue = newArrayBlockingQueue(maxQueueLength);
    }

    public BackgroundObserver(
            @NotNull final Observer observer,
            @NotNull 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(
            @NotNull Observer observer,
            @NotNull Executor executor) {
        this(observer, executor, DEFAULT_QUEUE_SIZE);
    }

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

    /**
     * Called when ever an item has been removed from the queue.
     *
     * @param queueSize the size of the queue after the item was removed.
     * @param created the time in milliseconds when the removed item was put
     *                into the queue.
     */
    protected void removed(int queueSize, long created) { }

    /**
     * @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; } @NotNull 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, input -> !input.info.isExternal())); } @Override public int getExternalEventCount() { return size(filter(queue, input -> input.info.isExternal())); } }; } //----------------------------------------------------------< Observer >-- /** * @throws IllegalStateException if {@link #close()} has already been called. */ @Override public synchronized void contentChanged(@NotNull NodeState root, @NotNull CommitInfo info) { checkState(!stopped); requireNonNull(root); requireNonNull(info); if (alwaysCollapseExternalEvents && info.isExternal() && last != null && last.info.isExternal()) { // 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. //TODO - Support for merging ChangeSet for external changes queue.remove(last); } ContentChange change = new ContentChange(root, info); // Try to add this change to the queue without blocking boolean full = !queue.offer(change); if (full && last != null) { // last is only null at the beginning // queue is full. // when the change can't be added to the queue because it's full // remove the last entry and add an explicit overflow entry instead. queue.remove(last); // by removing the last entry we have to drop the possible // local commit information of the current change, // as we're doing collapsing here and the commit information // no longer represents an individual commit change = new ContentChange(root, CommitInfo.EMPTY_EXTERNAL); queue.offer(change); } // 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(@NotNull Observer observer) { return LoggerFactory.getLogger(requireNonNull(observer).getClass()); } /** FOR TESTING ONLY * @throws InterruptedException **/ boolean waitUntilStopped(int timeout, TimeUnit unit) throws InterruptedException { long done = System.currentTimeMillis() + unit.toMillis(timeout); boolean added = false; while(done > System.currentTimeMillis()) { synchronized(this) { if (!added) { added = queue.offer(STOP); if (added) { currentTask.onComplete(completionHandler); } } if (added && queue.size() == 0) { return true; } wait(1); } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy