Author: thomasm
Date: Thu Oct  6 10:29:16 2016
New Revision: 1763550

URL: http://svn.apache.org/viewvc?rev=1763550&view=rev
Log:
OAK-4522 Improve CommitRateLimiter to optionally block some commits

Added:
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/observation/SlowObservationIT.java
Modified:
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiter.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiterTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/observation/ChangeProcessor.java

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiter.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiter.java?rev=1763550&r1=1763549&r2=1763550&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiter.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiter.java
 Thu Oct  6 10:29:16 2016
@@ -21,6 +21,8 @@ package org.apache.jackrabbit.oak.plugin
 
 import static org.apache.jackrabbit.oak.api.CommitFailedException.OAK;
 
+import java.util.concurrent.atomic.AtomicInteger;
+
 import javax.annotation.Nonnull;
 
 import org.apache.jackrabbit.oak.api.CommitFailedException;
@@ -40,6 +42,15 @@ public class CommitRateLimiter implement
     private volatile boolean blockCommits;
     private volatile long delay;
 
+    // the observation call depth of the current thread
+    // (only updated by the current thread, so technically isn't necessary that
+    // this is an AtomicInteger, but it's simpler to use it)
+    private static ThreadLocal<AtomicInteger> NON_BLOCKING_LEVEL = 
+            new ThreadLocal<AtomicInteger>();
+    
+    private static boolean EXCEPTION_ON_BLOCK = 
+            Boolean.getBoolean("oak.commitRateLimiter.exceptionOnBlock");
+
     /**
      * Block any further commits until {@link #unblockCommits()} is called.
      */
@@ -54,10 +65,14 @@ public class CommitRateLimiter implement
         blockCommits = false;
     }
 
+    public boolean getBlockCommits() {
+        return blockCommits;
+    }
+
     /**
-     * Number of milli seconds to delay commits going through this hook.
+     * Number of milliseconds to delay commits going through this hook.
      * If {@code 0}, any currently blocked commit will be unblocked.
-     * @param delay  milli seconds
+     * @param delay  milliseconds
      */
     public void setDelay(long delay) {
         if (LOG.isTraceEnabled()) {
@@ -78,15 +93,33 @@ public class CommitRateLimiter implement
     @Override
     public NodeState processCommit(NodeState before, NodeState after, 
CommitInfo info)
             throws CommitFailedException {
-        if (blockCommits) {
-            throw new CommitFailedException(OAK, 1, "System busy. Try again 
later.");
+        if (blockCommits && isThreadBlocking()) {
+            blockCommit();
+        } else {
+            delay();
         }
-        delay();
         return after;
     }
+    
+    public void blockCommit() throws CommitFailedException {
+        if (EXCEPTION_ON_BLOCK) {
+            throw new CommitFailedException(OAK, 1, "System busy. Try again 
later.");
+        }
+        synchronized (this) {
+            try {
+                while (getBlockCommits()) {
+                    wait(1000);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new CommitFailedException(OAK, 2,
+                        "Interrupted while waiting to commit", e);
+            }
+        }
+    }
 
-    private void delay() throws CommitFailedException {
-        if (delay > 0) {
+    protected void delay() throws CommitFailedException {
+        if (delay > 0 && isThreadBlocking()) {
             synchronized (this) {
                 try {
                     long t0 = Clock.ACCURATE.getTime();
@@ -97,9 +130,50 @@ public class CommitRateLimiter implement
                         dt = dt - Clock.ACCURATE.getTime() + t0;
                     }
                 } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
                     throw new CommitFailedException(OAK, 2, "Interrupted while 
waiting to commit", e);
                 }
             }
         }
     }
+
+    /**
+     * The current thread will now run code that must not be throttled or
+     * blocked, such as processing events (EventListener.onEvent is going to be
+     * called).
+     */
+    public void beforeNonBlocking() {
+        AtomicInteger value = NON_BLOCKING_LEVEL.get();
+        if (value == null) {
+            value = new AtomicInteger(1);
+            NON_BLOCKING_LEVEL.set(value);
+        } else {
+            value.incrementAndGet();
+        }
+    }
+
+    /**
+     * The current thread finished running code that must not be throttled or
+     * blocked.
+     */
+    public void afterNonBlocking() {
+        AtomicInteger value = NON_BLOCKING_LEVEL.get();
+        if (value == null) {
+            // TODO should not happen (log an error?)
+        } else {
+            value.decrementAndGet();
+        }        
+    }
+    
+    /**
+     * Check whether the current thread is non-blocking.
+     * 
+     * @return whether thread thread is non-blocking
+     */
+    public boolean isThreadBlocking() {
+        AtomicInteger value = NON_BLOCKING_LEVEL.get();
+        // no delay while processing events
+        return value == null || value.get() == 0;
+    }
+
 }

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiterTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiterTest.java?rev=1763550&r1=1763549&r2=1763550&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiterTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/observation/CommitRateLimiterTest.java
 Thu Oct  6 10:29:16 2016
@@ -59,9 +59,27 @@ public class CommitRateLimiterTest {
     }
 
     @Test(expected = CommitFailedException.class)
-    public void blockCommits() throws CommitFailedException {
+    public void blockCommits() throws CommitFailedException, 
InterruptedException {
         limiter.blockCommits();
-        limiter.processCommit(EMPTY_NODE, AFTER, null);
+        final Thread mainThread = Thread.currentThread();
+        Thread t = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+                mainThread.interrupt();
+            }
+        };
+        t.start();
+        try {
+            limiter.processCommit(EMPTY_NODE, AFTER, null);
+        } finally {
+            t.join();
+            assertTrue(Thread.interrupted());
+        }
     }
 
     @Test

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/observation/ChangeProcessor.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/observation/ChangeProcessor.java?rev=1763550&r1=1763549&r2=1763550&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/observation/ChangeProcessor.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/observation/ChangeProcessor.java
 Thu Oct  6 10:29:16 2016
@@ -131,7 +131,7 @@ class ChangeProcessor implements Observe
     private final AtomicReference<FilterProvider> filterProvider;
     private final MeterStats eventCount;
     private final TimerStats eventDuration;
-    private final TimeSeriesMax maxQueueLength;
+    private final TimeSeriesMax maxQueueLengthRecorder;
     private final int queueLength;
     private final CommitRateLimiter commitRateLimiter;
 
@@ -162,7 +162,7 @@ class ChangeProcessor implements Observe
         filterProvider = new AtomicReference<FilterProvider>(filter);
         this.eventCount = statisticManager.getMeter(OBSERVATION_EVENT_COUNTER);
         this.eventDuration = 
statisticManager.getTimer(OBSERVATION_EVENT_DURATION);
-        this.maxQueueLength = statisticManager.maxQueLengthRecorder();
+        this.maxQueueLengthRecorder = statisticManager.maxQueLengthRecorder();
         this.queueLength = queueLength;
         this.commitRateLimiter = commitRateLimiter;
     }
@@ -226,11 +226,19 @@ class ChangeProcessor implements Observe
             private volatile boolean blocking;
 
             @Override
-            protected void added(int queueSize) {
-                maxQueueLength.recordValue(queueSize);
-                tracker.recordQueueLength(queueSize);
-
-                if (queueSize == queueLength) {
+            protected void added(int newQueueSize) {
+                queueSizeChanged(newQueueSize);
+            }
+            
+            @Override
+            protected void removed(int newQueueSize, long created) {
+                queueSizeChanged(newQueueSize);
+            }
+            
+            private void queueSizeChanged(int newQueueSize) {
+                maxQueueLengthRecorder.recordValue(newQueueSize);
+                tracker.recordQueueLength(newQueueSize);
+                if (newQueueSize >= queueLength) {
                     if (commitRateLimiter != null) {
                         if (!blocking) {
                             LOG.warn("Revision queue is full. Further commits 
will be blocked.");
@@ -241,7 +249,7 @@ class ChangeProcessor implements Observe
                     }
                     blocking = true;
                 } else {
-                    double fillRatio = (double) queueSize / queueLength;
+                    double fillRatio = (double) newQueueSize / queueLength;
                     if (fillRatio > DELAY_THRESHOLD) {
                         if (commitRateLimiter != null) {
                             if (delay == 0) {
@@ -274,11 +282,6 @@ class ChangeProcessor implements Observe
                 }
             }
 
-            @Override
-            protected void removed(int queueSize, long created) {
-                maxQueueLength.recordValue(queueSize);
-                tracker.recordQueueLength(queueSize, created);
-            }
         };
     }
 
@@ -347,11 +350,17 @@ class ChangeProcessor implements Observe
                     boolean hasEvents = events.hasNext();
                     tracker.recordProducerTime(System.nanoTime() - time, 
TimeUnit.NANOSECONDS);
                     if (hasEvents && runningMonitor.enterIf(running)) {
+                        if (commitRateLimiter != null) {
+                            commitRateLimiter.beforeNonBlocking();
+                        }
                         try {
                             CountingIterator countingEvents = new 
CountingIterator(events);
                             eventListener.onEvent(countingEvents);
                             countingEvents.updateCounters(eventCount, 
eventDuration);
                         } finally {
+                            if (commitRateLimiter != null) {
+                                commitRateLimiter.afterNonBlocking();
+                            }
                             runningMonitor.leave();
                         }
                     }

Added: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/observation/SlowObservationIT.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/observation/SlowObservationIT.java?rev=1763550&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/observation/SlowObservationIT.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/observation/SlowObservationIT.java
 Thu Oct  6 10:29:16 2016
@@ -0,0 +1,331 @@
+/*
+ * 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.jcr.observation;
+
+import static javax.jcr.observation.Event.NODE_ADDED;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import org.apache.jackrabbit.oak.plugins.observation.CommitRateLimiter;
+import org.h2.util.Profiler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An slow test case that that tries to test the commit rate limiter.
+ */
+@RunWith(Parameterized.class)
+// Don't run "Parallelized" as this causes tests to timeout in "weak" 
environments
+public class SlowObservationIT extends AbstractRepositoryTest {
+    
+    static final Logger LOG = LoggerFactory.getLogger(SlowObservationIT.class);
+    
+    private static final int OBSERVER_COUNT = 3;
+    
+    private static final boolean NO_DELAY_JUST_BLOCK = false;
+
+    private static final boolean PROFILE = Boolean.getBoolean("oak.profile");
+    
+    private static final String TEST_NODE = "test_node";
+    private static final String TEST_PATH = '/' + TEST_NODE;
+    private static final String TEST2_NODE = "test_node2";
+    private static final String TEST2_PATH = '/' + TEST2_NODE;
+    
+    public SlowObservationIT(NodeStoreFixture fixture) {
+        super(fixture);
+    }
+    
+    @Override
+    protected Jcr initJcr(Jcr jcr) {
+        CommitRateLimiter limiter = new CommitRateLimiter() {
+            
+            long lastLog;
+            
+            @Override
+            public void setDelay(long delay) {
+                long now = System.currentTimeMillis();
+                if (now > lastLog + 1000) {
+                    log("Delay " + delay);
+                    lastLog = now;
+                }
+                super.setDelay(delay);
+            }
+            
+            @Override
+            protected void delay() throws CommitFailedException {
+                if (!NO_DELAY_JUST_BLOCK) {
+                    // default behavior
+                    super.delay();
+                    return;
+                }
+                if (getBlockCommits() && isThreadBlocking()) {
+                    synchronized (this) {           
+                        try {
+                            while (getBlockCommits()) {
+                                wait(1000);
+                            }
+                        } catch (InterruptedException e) {
+                            throw new CommitFailedException(
+                                    CommitFailedException.OAK, 2, "Interrupted 
while waiting to commit", e);
+                        }
+                    }
+                }
+            }
+            
+        };
+        return super.initJcr(jcr).with(limiter);
+    }
+
+    @Before
+    public void setup() throws RepositoryException {
+        if (!isDocumentNodeStore()) {
+            return;
+        }
+        Session session = getAdminSession();
+        
+        Node nodetypeIndex = 
session.getRootNode().getNode("oak:index").getNode("nodetype");
+        nodetypeIndex.remove();
+
+        Node testNode;
+        testNode = session.getRootNode().addNode(TEST_NODE, 
"oak:Unstructured");
+        testNode.setProperty("test", 0);
+        testNode = session.getRootNode().addNode(TEST2_NODE, 
"oak:Unstructured");
+        testNode.setProperty("test", 0);
+        session.save();
+    }
+
+    private boolean isDocumentNodeStore() {
+        // SegmentNodeStore can result in deadlocks, because
+        // the CommitRateLimiter may be blocking inside a sychronized block
+        return fixture.toString().indexOf("DocumentNodeStore") >= 0;
+    }
+
+    @Test
+    public void observation() throws Exception {
+        if (!isDocumentNodeStore()) {
+            return;
+        }
+        AtomicBoolean saveInObservation = new AtomicBoolean();
+        saveInObservation.set(true);
+        ArrayList<MyListener> listeners = new ArrayList<MyListener>();
+        for (int i = 0; i < OBSERVER_COUNT; i++) {
+            Session observingSession = createAdminSession();
+            MyListener listener = new MyListener(i, observingSession, 
saveInObservation);
+            listener.open();
+            listeners.add(listener);
+        }
+        log("Starting...");
+        Profiler prof = null;
+        long start = System.currentTimeMillis();
+        for (int i = 1;; i++) {
+            if (prof == null && PROFILE) {
+                // prof = new Profiler().startCollecting();
+            }
+            long time = System.currentTimeMillis() - start;
+            if (time > 20 * 1000) {
+                if (saveInObservation.get()) {
+                    log("Disable saves in observation now");
+                    saveInObservation.set(false);
+                }
+            }
+            if (time > 30 * 1000) {
+                break;
+            }
+            Node testNode;
+            if (i % 100 < 52) {
+                // in 52% of the cases, use testNode
+                testNode = getNode(TEST_PATH);
+            } else {
+                // in 48% of the cases, use testNode2
+                testNode = getNode(TEST2_PATH);
+            }
+            String a = "c-" + (i / 40);
+            String b = "c-" + (i % 40);
+            Node x;
+            if (testNode.hasNode(a)) {
+                x = testNode.getNode(a);
+            } else {
+                x = testNode.addNode(a, "oak:Unstructured");
+            }
+            Node t = x.addNode(b, "oak:Unstructured");
+            for (int j = 0; j < 10; j++) {
+                t.addNode("c-" + j, "oak:Unstructured");
+            }
+            long saveTime = System.currentTimeMillis();
+            getAdminSession().save();
+            saveTime = System.currentTimeMillis() - saveTime;
+            if (saveTime > 100 || i % 200 == 0) {
+                if (prof != null) {
+                    log(prof.getTop(1));
+                    prof = null;
+                }
+                log("Save #" + i + " took " + saveTime + " ms");
+            }
+        }
+        log("Stopping...");
+        for (MyListener listener : listeners) {
+            listener.stop();
+            listener.waitUntilDone();
+            listener.close();
+        }
+        log("Done");
+        if (PROFILE) {
+            printFullThreadDump();
+        }
+    }
+
+    //------------------------------------------------------------< private 
>---
+
+    private Node getNode(String path) throws RepositoryException {
+        return getAdminSession().getNode(path);
+    }
+    
+    /**
+     * A simple listener that writes in 10% of the cases.
+     */
+    private static class MyListener implements EventListener {
+        
+        private final AtomicBoolean saveInObservation;
+        private final int id;
+        private final Session session;
+        private ObservationManager observationManager;
+        private volatile boolean stopped;
+        private volatile boolean done;
+        private Exception exception;
+        
+        MyListener(int id, Session session, AtomicBoolean saveInObservation) {
+            this.id = id;
+            this.session = session;
+            this.saveInObservation = saveInObservation;
+        }
+        
+        public void open() throws RepositoryException {
+            observationManager = 
session.getWorkspace().getObservationManager();
+            observationManager.addEventListener(this, NODE_ADDED, "/" + 
TEST_NODE, true, null, null, false);
+        }
+
+        public void close() throws RepositoryException {
+            observationManager.removeEventListener(this);
+        }
+
+        public void stop() {
+            stopped = true;
+        }
+        
+        public void waitUntilDone() throws Exception {
+            while (!done) {
+                synchronized (this) {
+                    wait(1000);
+                }
+            }
+            if (exception != null) {
+                throw exception;
+            }
+        }
+
+        @Override
+        public void onEvent(EventIterator events) {
+            while (!stopped && events.hasNext()) {
+                Event event = events.nextEvent();
+                try {
+                    String path = event.getPath();
+                    // Thread.currentThread().setName("Observer path " + path);
+                    // System.out.println("observer " + path);
+                    if (Math.abs(path.hashCode() % OBSERVER_COUNT) != id) {
+                        // if it's not for "my" observer, ignore
+                        continue;
+                    }
+                    if (Math.random() > 0.5) {
+                        // do nothing 50% of the time
+                        continue;
+                    }
+                    // else save something as well: 5 times setProperty
+                    if (saveInObservation.get()) {
+                        if (session.getRootNode().hasNode(path.substring(1))) {
+                            Node n = session.getNode(path);
+                            // System.out.println("observer save "+ path);     
                       
+                            for (int i = 0; i < 5; i++) {
+                                n.setProperty("x", i);
+                                session.save();
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log("Error " + e);
+                    LOG.error("Observation listener error", e);
+                    exception = e;
+                }
+            }
+            done = true;
+            synchronized (this) {
+                notifyAll();
+            }
+        }
+    }
+    
+    static void log(String message) {
+        if (PROFILE) {
+            System.out.println(message);
+        }
+        LOG.info(message);
+    }
+    
+    public static void printFullThreadDump() {
+        log(new Timestamp(System.currentTimeMillis()).toString()
+                .substring(0, 19));
+        log("Full thread dump " +
+                System.getProperty("java.vm.name") + 
+                " (" + System.getProperty("java.vm.version") + "):");
+        log("");
+        for (Entry<Thread, StackTraceElement[]> e : Thread.getAllStackTraces()
+                .entrySet()) {
+            Thread t = e.getKey();
+            log(String.format("\"%s\"%s prio=%d tid=0x%x", 
+                    t.getName(), 
+                    t.isDaemon() ? " daemon" : "",
+                    t.getPriority(),
+                    t.getId()));
+            log("    java.lang.Thread.State: " + t.getState());
+            for (StackTraceElement s : e.getValue()) {
+                log("\tat " + s);
+            }
+            log("");
+        }
+    }
+
+}


Reply via email to