From 159498e56af7783b32d8ce6c00023363ada0511f Mon Sep 17 00:00:00 2001
From: Chetan Mehrotra <chetanm@apache.org>
Date: Thu, 20 Jun 2013 15:59:42 +0530
Subject: [PATCH] OAK-863 - Enable stats for various caches used in Oak by
 default

-- Enabling the stats collection by default
-- Added new CacheStatsMBean class for exposing the cache stats via JMX
-- Using the Whiteboard to register the JMX beans
---
 .../main/java/org/apache/jackrabbit/oak/Oak.java   |   71 ++-------
 .../jackrabbit/oak/api/jmx/CacheStatsMBean.java    |  113 ++++++++++++++
 .../jackrabbit/oak/kernel/KernelNodeStore.java     |   45 +++++-
 .../org/apache/jackrabbit/oak/osgi/Activator.java  |   19 ++-
 .../oak/plugins/mongomk/MongoDocumentStore.java    |   20 ++-
 .../jackrabbit/oak/plugins/mongomk/MongoMK.java    |   84 +++++++++--
 .../plugins/mongomk/MongoMicroKernelService.java   |    7 +-
 .../oak/plugins/segment/SegmentCache.java          |   29 +++-
 .../plugins/segment/SegmentNodeStoreService.java   |    4 +-
 .../oak/plugins/segment/mongo/MongoStore.java      |    1 +
 .../oak/spi/whiteboard/DefaultWhiteboard.java      |   26 ++--
 .../oak/spi/whiteboard/SimpleWhiteboard.java       |  117 ++++++++++++++
 .../jackrabbit/oak/spi/whiteboard/Whiteboard.java  |    5 +
 .../jackrabbit/oak/util/GuavaCacheStats.java       |  159 ++++++++++++++++++++
 .../apache/jackrabbit/oak/NodeStoreFixture.java    |    3 +-
 15 files changed, 599 insertions(+), 104 deletions(-)
 create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/CacheStatsMBean.java
 create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/SimpleWhiteboard.java
 create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/util/GuavaCacheStats.java

diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
index 0861365..85bc3c5 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
@@ -69,6 +69,7 @@ import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.SimpleWhiteboard;
 import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
 
@@ -87,8 +88,6 @@ public class Oak {
      */
     public static final String DEFAULT_WORKSPACE_NAME = "default";
 
-    private final NodeStore store;
-
     private final List<RepositoryInitializer> initializers = newArrayList();
 
     private final List<QueryIndexProvider> queryIndexProviders = newArrayList();
@@ -105,6 +104,10 @@ public class Oak {
 
     private MBeanServer mbeanServer;
 
+    private NodeStore store;
+
+    private MicroKernel kernel;
+
     private String defaultWorkspaceName = DEFAULT_WORKSPACE_NAME;
 
     @SuppressWarnings("unchecked")
@@ -123,63 +126,7 @@ public class Oak {
         return getValue(properties, name, type, null);
     }
 
-    private Whiteboard whiteboard = new Whiteboard() {
-        @Override
-        public <T> Registration register(
-                Class<T> type, T service, Map<?, ?> properties) {
-            Future<?> future = null;
-            if (executor != null && type == Runnable.class) {
-                Runnable runnable = (Runnable) service;
-                Long period =
-                        getValue(properties, "scheduler.period", Long.class);
-                if (period != null) {
-                    Boolean concurrent = getValue(
-                            properties, "scheduler.concurrent",
-                            Boolean.class, Boolean.FALSE);
-                    if (concurrent) {
-                        future = executor.scheduleAtFixedRate(
-                                runnable, period, period, TimeUnit.SECONDS);
-                    } else {
-                        future = executor.scheduleWithFixedDelay(
-                                runnable, period, period, TimeUnit.SECONDS);
-                    }
-                }
-            }
-
-            ObjectName objectName = null;
-            Object name = properties.get("jmx.objectname");
-            if (mbeanServer != null && name != null) {
-                try {
-                    if (name instanceof ObjectName) {
-                        objectName = (ObjectName) name;
-                    } else {
-                        objectName = new ObjectName(String.valueOf(name));
-                    }
-                    mbeanServer.registerMBean(service, objectName);
-                } catch (JMException e) {
-                    // ignore
-                }
-            }
-
-            final Future<?> f = future;
-            final ObjectName on = objectName;
-            return new Registration() {
-                @Override
-                public void unregister() {
-                    if (f != null) {
-                        f.cancel(false);
-                    }
-                    if (on != null) {
-                        try {
-                            mbeanServer.unregisterMBean(on);
-                        } catch (JMException e) {
-                            // ignore
-                        }
-                    }
-                }
-            };
-        }
-    };
+    private Whiteboard whiteboard = new SimpleWhiteboard();
 
     /**
      * Flag controlling the asynchronous indexing behavior. If false (default)
@@ -193,7 +140,7 @@ public class Oak {
     }
 
     public Oak(MicroKernel kernel) {
-        this(new KernelNodeStore(checkNotNull(kernel)));
+        this.kernel = checkNotNull(kernel);
     }
 
     public Oak() {
@@ -365,6 +312,10 @@ public class Oak {
     }
 
     public ContentRepository createContentRepository() {
+        if(store == null){
+            store = new KernelNodeStore(kernel,KernelNodeStore.DEFAULT_CACHE_SIZE,whiteboard);
+        }
+
         IndexEditorProvider indexEditors = CompositeIndexEditorProvider.compose(indexEditorProviders);
         OakInitializer.initialize(store, new CompositeInitializer(initializers), indexEditors);
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/CacheStatsMBean.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/CacheStatsMBean.java
new file mode 100644
index 0000000..fdccd8f
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/api/jmx/CacheStatsMBean.java
@@ -0,0 +1,113 @@
+package org.apache.jackrabbit.oak.api.jmx;
+
+public interface CacheStatsMBean {
+    String TYPE = "CacheStats";
+
+    /**
+     * Returns the number of times {@link com.google.common.cache.Cache} lookup methods have returned either a cached or
+     * uncached value. This is defined as {@code getHitCount + getMissCount}.
+     */
+    long getRequestCount();
+
+    /**
+     * Returns the number of times {@link com.google.common.cache.Cache} lookup methods have returned a cached value.
+     */
+    long getHitCount();
+
+    /**
+     * Returns the ratio of cache requests which were hits. This is defined as
+     * {@code getHitCount / getRequestCount}, or {@code 1.0} when {@code getRequestCount == 0}.
+     * Note that {@code getHitRate + getMissRate =~ 1.0}.
+     */
+    double getHitRate();
+    /**
+     * Returns the number of times {@link com.google.common.cache.Cache} lookup methods have returned an uncached (newly
+     * loaded) value, or null. Multiple concurrent calls to {@link com.google.common.cache.Cache} lookup methods on an absent
+     * value can result in multiple misses, all returning the results of a single cache load
+     * operation.
+     */
+    long getMissCount();
+
+    /**
+     * Returns the ratio of cache requests which were misses. This is defined as
+     * {@code getMissCount / getRequestCount}, or {@code 0.0} when {@code getRequestCount == 0}.
+     * Note that {@code getHitRate + getMissRate =~ 1.0}. Cache misses include all requests which
+     * weren't cache hits, including requests which resulted in either successful or failed loading
+     * attempts, and requests which waited for other threads to finish loading. It is thus the case
+     * that {@code getMissCount &gt;= getLoadSuccessCount + getLoadExceptionCount}. Multiple
+     * concurrent misses for the same key will result in a single load operation.
+     */
+    double getMissRate();
+
+    /**
+     * Returns the total number of times that {@link com.google.common.cache.Cache} lookup methods attempted to load new
+     * values. This includes both successful load operations, as well as those that threw
+     * exceptions. This is defined as {@code getLoadSuccessCount + getLoadExceptionCount}.
+     */
+    long getLoadCount();
+
+    /**
+     * Returns the number of times {@link com.google.common.cache.Cache} lookup methods have successfully loaded a new value.
+     * This is always incremented in conjunction with {@link #getMissCount}, though {@code getMissCount}
+     * is also incremented when an exception is encountered during cache loading (see
+     * {@link #getLoadExceptionCount}). Multiple concurrent misses for the same key will result in a
+     * single load operation.
+     */
+    long getLoadSuccessCount();
+
+    /**
+     * Returns the number of times {@link com.google.common.cache.Cache} lookup methods threw an exception while loading a
+     * new value. This is always incremented in conjunction with {@code getMissCount}, though
+     * {@code getMissCount} is also incremented when cache loading completes successfully (see
+     * {@link #getLoadSuccessCount}). Multiple concurrent misses for the same key will result in a
+     * single load operation.
+     */
+    long getLoadExceptionCount();
+
+    /**
+     * Returns the ratio of cache loading attempts which threw exceptions. This is defined as
+     * {@code getLoadExceptionCount / (getLoadSuccessCount + getLoadExceptionCount)}, or
+     * {@code 0.0} when {@code getLoadSuccessCount + getLoadExceptionCount == 0}.
+     */
+    double getLoadExceptionRate();
+
+    /**
+     * Returns the total number of nanoseconds the cache has spent loading new values. This can be
+     * used to calculate the miss penalty. This value is increased every time
+     * {@code getLoadSuccessCount} or {@code getLoadExceptionCount} is incremented.
+     */
+    long getTotalLoadTime();
+
+    /**
+     * Returns the average time spent loading new values. This is defined as
+     * {@code getTotalLoadTime / (getLoadSuccessCount + getLoadExceptionCount)}.
+     */
+    double getAverageLoadPenalty();
+
+    /**
+     * Returns the number of times an entry has been evicted. This count does not include manual
+     * {@linkplain com.google.common.cache.Cache#invalidate invalidations}.
+     */
+    long getEvictionCount();
+
+    /**
+     * Get the number of elements/objects in the cache.
+     * @return the number of elements
+     */
+    long getElementCount();
+
+
+    /**
+     * Total weight of the complete cache. Depending on implementation it might be the amount
+     * of RAM taken by the cache
+     * @return to weight of the cache
+     */
+    long getTotalWeight();
+
+    long getMaxTotalWeight();
+
+    /**
+     * Gathers the stats of the cache for logging.
+     */
+    String getCacheInfoAsString();
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java
index 354da72..8356e9e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/kernel/KernelNodeStore.java
@@ -32,21 +32,26 @@ import com.google.common.util.concurrent.SettableFuture;
 
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.mk.api.MicroKernelException;
+import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.spi.commit.EmptyObserver;
 import org.apache.jackrabbit.oak.spi.commit.Observer;
 import org.apache.jackrabbit.oak.spi.state.AbstractNodeStore;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.util.GuavaCacheStats;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
 
 /**
  * {@code NodeStore} implementations against {@link MicroKernel}.
  */
 public class KernelNodeStore extends AbstractNodeStore {
 
-    private static final long DEFAULT_CACHE_SIZE = 16 * 1024 * 1024;
+    public static final long DEFAULT_CACHE_SIZE = 16 * 1024 * 1024;
 
     /**
      * The {@link MicroKernel} instance used to store the content tree.
@@ -61,21 +66,26 @@ public class KernelNodeStore extends AbstractNodeStore {
 
     private final LoadingCache<String, KernelNodeState> cache;
 
+    private final Registration jmxReg;
+
     /**
      * State of the current root node.
      */
     private KernelNodeState root;
 
-    public KernelNodeStore(final MicroKernel kernel, long cacheSize) {
+    public KernelNodeStore(final MicroKernel kernel, long cacheSize, Whiteboard whiteboard) {
         this.kernel = checkNotNull(kernel);
+
+        Weigher<String, KernelNodeState> weigher = new Weigher<String, KernelNodeState>() {
+            @Override
+            public int weigh(String key, KernelNodeState state) {
+                return state.getMemory();
+            }
+        };
         this.cache = CacheBuilder.newBuilder()
                 .maximumWeight(cacheSize)
-                .weigher(new Weigher<String, KernelNodeState>() {
-                    @Override
-                    public int weigh(String key, KernelNodeState state) {
-                        return state.getMemory();
-                    }
-                }).build(new CacheLoader<String, KernelNodeState>() {
+                .weigher(weigher)
+                .build(new CacheLoader<String, KernelNodeState>() {
                     @Override
                     public KernelNodeState load(String key) {
                         int slash = key.indexOf('/');
@@ -83,6 +93,7 @@ public class KernelNodeStore extends AbstractNodeStore {
                         String path = key.substring(slash);
                         return new KernelNodeState(kernel, path, revision, cache);
                     }
+
                     @Override
                     public ListenableFuture<KernelNodeState> reload(
                             String key, KernelNodeState oldValue) {
@@ -95,6 +106,14 @@ public class KernelNodeStore extends AbstractNodeStore {
                     }
                 });
 
+        jmxReg = registerMBean(
+                    whiteboard,
+                    CacheStatsMBean.class,
+                    new GuavaCacheStats(cache, weigher, cacheSize),
+                    CacheStatsMBean.TYPE,
+                    "KernelNodeState"
+        );
+
         try {
             this.root = cache.get(kernel.getHeadRevision() + '/');
         } catch (Exception e) {
@@ -102,6 +121,10 @@ public class KernelNodeStore extends AbstractNodeStore {
         }
     }
 
+    public KernelNodeStore(final MicroKernel kernel, long cacheSize){
+        this(kernel, cacheSize, Whiteboard.DEFAULT);
+    }
+
     public KernelNodeStore(MicroKernel kernel) {
         this(kernel, DEFAULT_CACHE_SIZE);
     }
@@ -162,6 +185,12 @@ public class KernelNodeStore extends AbstractNodeStore {
         }
     }
 
+    public void close(){
+        if(jmxReg != null){
+            jmxReg.unregister();
+        }
+    }
+
     //-----------------------------------------------------------< internal >---
 
     @Nonnull
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java
index 01d06e2..4c96130 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/osgi/Activator.java
@@ -32,6 +32,8 @@ import org.apache.jackrabbit.oak.security.SecurityProviderImpl;
 import org.apache.jackrabbit.oak.spi.lifecycle.OakInitializer;
 import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.whiteboard.OsgiWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
@@ -45,7 +47,9 @@ public class Activator implements BundleActivator, ServiceTrackerCustomizer, Rep
 
     private ServiceTracker microKernelTracker;
 
-    // see OAK-795 for a reason why the nodeStore tracker is disabled 
+    private Whiteboard whiteboard;
+
+    // see OAK-795 for a reason why the nodeStore tracker is disabled
     // private ServiceTracker nodeStoreTracker;
 
     private final OsgiIndexProvider indexProvider = new OsgiIndexProvider();
@@ -58,12 +62,14 @@ public class Activator implements BundleActivator, ServiceTrackerCustomizer, Rep
 
     private final Map<ServiceReference, ServiceRegistration> services = new HashMap<ServiceReference, ServiceRegistration>();
 
+    private final List<KernelNodeStore> kernelNodeStores = new ArrayList<KernelNodeStore>();
+
     //----------------------------------------------------< BundleActivator >---
 
     @Override
     public void start(BundleContext bundleContext) throws Exception {
         context = bundleContext;
-
+        whiteboard = new OsgiWhiteboard(bundleContext);
         indexProvider.start(bundleContext);
         indexEditorProvider.start(bundleContext);
         validatorProvider.start(bundleContext);
@@ -85,6 +91,10 @@ public class Activator implements BundleActivator, ServiceTrackerCustomizer, Rep
         indexEditorProvider.stop();
         validatorProvider.stop();
         repositoryInitializerTracker.stop();
+
+        for(KernelNodeStore store : kernelNodeStores){
+            store.close();
+        }
     }
 
     //-------------------------------------------< ServiceTrackerCustomizer >---
@@ -94,10 +104,12 @@ public class Activator implements BundleActivator, ServiceTrackerCustomizer, Rep
         Object service = context.getService(reference);
         if (service instanceof MicroKernel) {
             MicroKernel kernel = (MicroKernel) service;
+            KernelNodeStore store = new KernelNodeStore(kernel,KernelNodeStore.DEFAULT_CACHE_SIZE,whiteboard);
             services.put(reference, context.registerService(
                     NodeStore.class.getName(),
-                    new KernelNodeStore(kernel),
+                    store,
                     new Properties()));
+            kernelNodeStores.add(store);
         } else if (service instanceof NodeStore) {
             NodeStore store = (NodeStore) service;
             OakInitializer.initialize(store, repositoryInitializerTracker, indexEditorProvider);
@@ -106,6 +118,7 @@ public class Activator implements BundleActivator, ServiceTrackerCustomizer, Rep
                 .with(new SecurityProviderImpl())
                 .with(validatorProvider)
                 .with(indexProvider)
+                .with(whiteboard)
                 .with(indexEditorProvider);
             services.put(reference, context.registerService(
                     ContentRepository.class.getName(),
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoDocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoDocumentStore.java
index dd6926f..adfe99a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoDocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoDocumentStore.java
@@ -28,8 +28,11 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
 
 import org.apache.jackrabbit.mk.api.MicroKernelException;
+import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.plugins.mongomk.UpdateOp.Operation;
 import org.apache.jackrabbit.oak.plugins.mongomk.util.Utils;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.util.GuavaCacheStats;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,6 +48,8 @@ import com.mongodb.QueryBuilder;
 import com.mongodb.WriteConcern;
 import com.mongodb.WriteResult;
 
+import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
+
 /**
  * A document store that uses MongoDB as the backend.
  */
@@ -63,6 +68,8 @@ public class MongoDocumentStore implements DocumentStore {
     private long timeSum;
     
     private final Cache<String, CachedDocument> nodesCache;
+    
+    private final Registration jmxReg;
 
     public MongoDocumentStore(DB db, MongoMK.Builder builder) {
         nodes = db.getCollection(
@@ -82,9 +89,17 @@ public class MongoDocumentStore implements DocumentStore {
         // TODO expire entries if the parent was changed
         nodesCache = CacheBuilder.newBuilder()
                 .weigher(builder.getWeigher())
-                //.recordStats() FIXME: OAK-863
+                .recordStats() 
                 .maximumWeight(builder.getDocumentCacheSize())
                 .build();
+
+        jmxReg = registerMBean(
+                    builder.getWhiteboard(),
+                    CacheStatsMBean.class,
+                    new GuavaCacheStats(nodesCache, builder.getWeigher(),
+                        builder.getDocumentCacheSize()), CacheStatsMBean.TYPE,
+                    "MongoMk-Documents"
+                );
         
     }
     
@@ -445,6 +460,9 @@ public class MongoDocumentStore implements DocumentStore {
             LOG.debug("MongoDB time: " + timeSum);
         }
         nodes.getDB().getMongo().close();
+        if(jmxReg != null){
+            jmxReg.unregister();
+        }
     }
     
     private static void log(String message, Object... args) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java
index d192ef7..1d6893c 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMK.java
@@ -39,6 +39,7 @@ import javax.annotation.Nullable;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.Weigher;
+import com.google.common.collect.Lists;
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.mk.api.MicroKernelException;
 import org.apache.jackrabbit.mk.blobs.BlobStore;
@@ -47,6 +48,7 @@ import org.apache.jackrabbit.mk.json.JsopReader;
 import org.apache.jackrabbit.mk.json.JsopStream;
 import org.apache.jackrabbit.mk.json.JsopTokenizer;
 import org.apache.jackrabbit.mk.json.JsopWriter;
+import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
 import org.apache.jackrabbit.oak.plugins.mongomk.DocumentStore.Collection;
 import org.apache.jackrabbit.oak.plugins.mongomk.Node.Children;
 import org.apache.jackrabbit.oak.plugins.mongomk.Revision.RevisionComparator;
@@ -54,11 +56,16 @@ import org.apache.jackrabbit.oak.plugins.mongomk.blob.MongoBlobStore;
 import org.apache.jackrabbit.oak.plugins.mongomk.util.TimingDocumentStoreWrapper;
 import org.apache.jackrabbit.oak.plugins.mongomk.util.Utils;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.util.GuavaCacheStats;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.mongodb.DB;
 
+import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
+
 /**
  * A MicroKernel implementation that stores the data in a MongoDB.
  */
@@ -195,6 +202,8 @@ public class MongoMK implements MicroKernel {
     private final UnmergedBranches branches;
 
     private boolean stopBackground;
+
+    private List<Registration> registrations = Lists.newArrayListWithCapacity(5);
     
     MongoMK(Builder builder) {
         DocumentStore s = builder.getDocumentStore();
@@ -226,19 +235,23 @@ public class MongoMK implements MicroKernel {
         nodeCache = CacheBuilder.newBuilder()
                         .weigher(builder.getWeigher())
                         .maximumWeight(builder.getNodeCacheSize())
-                        // .recordStats() FIXME: OAK-863
+                        .recordStats()
                         .build();
 
+
         nodeChildrenCache =  CacheBuilder.newBuilder()
                         .weigher(builder.getWeigher())
-                        //.recordStats() FIXME: OAK-863
+                        .recordStats()
                         .maximumWeight(builder.getChildrenCacheSize())
                         .build();
-        
+
         diffCache = CacheBuilder.newBuilder()
                 .maximumSize(CACHE_DIFF)
+                .recordStats()
                 .build();
-        
+
+        registerJMXBeans(builder.getWhiteboard(),builder);
+
         init();
         // initial reading of the revisions of other cluster nodes
         backgroundRead();
@@ -246,7 +259,7 @@ public class MongoMK implements MicroKernel {
         headRevision = newRevision();
         LOG.info("Initialized MongoMK with clusterNodeId: {}", clusterId);
     }
-    
+
     void init() {
         headRevision = newRevision();
         Node n = readNode("/", headRevision);
@@ -417,6 +430,11 @@ public class MongoMK implements MicroKernel {
                 clusterNodeInfo.dispose();
             }
             store.dispose();
+
+            for(Registration reg : registrations){
+                reg.unregister();
+            }
+            registrations.clear();
             LOG.info("Disposed MongoMK with clusterNodeId: {}", clusterId);
         }
     }
@@ -1514,6 +1532,30 @@ public class MongoMK implements MicroKernel {
             nodeChildrenCache.put(key, c2);
         }
     }
+
+    private void registerJMXBeans(Whiteboard wb, Builder b){
+        registrations.add(
+                registerMBean(wb,
+                        CacheStatsMBean.class,
+                        new GuavaCacheStats(nodeCache, b.getWeigher(), b.getNodeCacheSize()),
+                        CacheStatsMBean.TYPE,
+                        "MongoMk-Node")
+        );
+        registrations.add(
+                registerMBean(wb,
+                        CacheStatsMBean.class,
+                        new GuavaCacheStats(nodeChildrenCache, b.getWeigher(), b.getChildrenCacheSize()),
+                        CacheStatsMBean.TYPE,
+                        "MongoMk-NodeChildren")
+        );
+        registrations.add(
+                registerMBean(wb,
+                        CacheStatsMBean.class,
+                        new GuavaCacheStats(diffCache),
+                        CacheStatsMBean.TYPE,
+                        "MongoMk-DiffCache")
+        );
+    }
     
     /**
      * A background thread.
@@ -1562,6 +1604,8 @@ public class MongoMK implements MicroKernel {
         private long nodeCacheSize;
         private long childrenCacheSize;
         private long documentCacheSize;
+        private Whiteboard whiteboard = Whiteboard.DEFAULT;
+        private DB db;
 
         public Builder() {
             memoryCacheSize(DEFAULT_MEMORY_CACHE_SIZE);
@@ -1574,10 +1618,7 @@ public class MongoMK implements MicroKernel {
          * @return this
          */
         public Builder setMongoDB(DB db) {
-            if (db != null) {
-                this.documentStore = new MongoDocumentStore(db, this);
-                this.blobStore = new MongoBlobStore(db);
-            }
+            this.db = db;
             return this;
         }
         
@@ -1608,9 +1649,6 @@ public class MongoMK implements MicroKernel {
         }
         
         public DocumentStore getDocumentStore() {
-            if (documentStore == null) {
-                documentStore = new MemoryDocumentStore();
-            }
             return documentStore;
         }
 
@@ -1626,9 +1664,6 @@ public class MongoMK implements MicroKernel {
         }
 
         public BlobStore getBlobStore() {
-            if (blobStore == null) {
-                blobStore = new MemoryBlobStore();
-            }
             return blobStore;
         }
 
@@ -1668,7 +1703,16 @@ public class MongoMK implements MicroKernel {
             return weigher;
         }
 
-        public Builder weigher(Weigher<String, Object> weigher) {
+        public Builder with(Whiteboard whiteboard){
+            this.whiteboard = whiteboard;
+            return this;
+        }
+
+        public Whiteboard getWhiteboard() {
+            return whiteboard;
+        }
+
+        public Builder withWeigher(Weigher<String, Object> weigher) {
             this.weigher = weigher;
             return this;
         }
@@ -1698,6 +1742,14 @@ public class MongoMK implements MicroKernel {
          * @return the MongoMK instance
          */
         public MongoMK open() {
+            if(this.documentStore == null){
+                this.documentStore = (db != null) ? new MongoDocumentStore(db,this) : new MemoryDocumentStore();
+            }
+
+            if(blobStore == null){
+                this.blobStore = (db != null) ? new MongoBlobStore(db): new MemoryBlobStore();
+            }
+
             return new MongoMK(this);
         }
     }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMicroKernelService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMicroKernelService.java
index cddd1f3..2d8c338 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMicroKernelService.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/mongomk/MongoMicroKernelService.java
@@ -28,6 +28,7 @@ import org.apache.felix.scr.annotations.ConfigurationPolicy;
 import org.apache.felix.scr.annotations.Property;
 import org.apache.jackrabbit.mk.api.MicroKernel;
 import org.apache.jackrabbit.oak.plugins.mongomk.util.MongoConnection;
+import org.apache.jackrabbit.oak.spi.whiteboard.OsgiWhiteboard;
 import org.apache.sling.commons.osgi.PropertiesUtil;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceRegistration;
@@ -86,7 +87,11 @@ public class MongoMicroKernelService {
 
         logger.info("Connected to database {}", mongoDB);
 
-        mk = new MongoMK.Builder().memoryCacheSize(cacheSize * MB).setMongoDB(mongoDB).open();
+        mk = new MongoMK.Builder()
+                        .memoryCacheSize(cacheSize * MB)
+                        .with(new OsgiWhiteboard(context))
+                        .setMongoDB(mongoDB)
+                        .open();
 
         Properties props = new Properties();
         props.setProperty("oak.mk.type", "mongo");
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentCache.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentCache.java
index 71f39af..c2431e3 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentCache.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentCache.java
@@ -22,6 +22,12 @@ import java.util.concurrent.ExecutionException;
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.util.GuavaCacheStats;
+
+import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
 
 /**
  * Combined memory and disk cache for segments.
@@ -32,11 +38,13 @@ public class SegmentCache {
 
     private final Cache<UUID, Segment> memoryCache;
 
+    private final Registration jmxReg;
+
     // private final Cache<UUID, File> diskCache;
 
     // private final File diskCacheDirectory;
 
-    public SegmentCache(long memoryCacheSize) {
+    public SegmentCache(long memoryCacheSize, Whiteboard wb) {
 //        this.diskCacheDirectory = diskCacheDirectory;
 //        this.diskCache = CacheBuilder.newBuilder()
 //                .maximumWeight(diskCacheSize)
@@ -48,6 +56,7 @@ public class SegmentCache {
 //                }).build();
         this.memoryCache = CacheBuilder.newBuilder()
                 .maximumWeight(memoryCacheSize)
+                .recordStats()
                 .weigher(Segment.WEIGHER)
 //                .removalListener(new RemovalListener<UUID, Segment>() {
 //                    @Override
@@ -57,6 +66,18 @@ public class SegmentCache {
 //                    }
 //                })
                 .build();
+
+        jmxReg = registerMBean(
+                    wb,
+                    CacheStatsMBean.class,
+                    new GuavaCacheStats(memoryCache, Segment.WEIGHER, memoryCacheSize),
+                    CacheStatsMBean.TYPE,
+                    "Segment"
+                );
+    }
+
+    public SegmentCache(long memoryCacheSize){
+        this(memoryCacheSize, Whiteboard.DEFAULT);
     }
 
     public SegmentCache() {
@@ -80,4 +101,10 @@ public class SegmentCache {
         memoryCache.invalidate(segmentId);
     }
 
+    public void close(){
+        if(jmxReg != null){
+            jmxReg.unregister();
+        }
+    }
+
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
index 10c70f3..945fd53 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
@@ -36,6 +36,7 @@ import org.apache.jackrabbit.oak.spi.state.AbstractNodeStore;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch;
+import org.apache.jackrabbit.oak.spi.whiteboard.OsgiWhiteboard;
 import org.osgi.service.component.ComponentContext;
 
 import com.mongodb.Mongo;
@@ -95,7 +96,8 @@ public class SegmentNodeStoreService extends AbstractNodeStore {
             int cache = Integer.parseInt(String.valueOf(properties.get(CACHE)));
 
             mongo = new Mongo(host, port);
-            store = new MongoStore(mongo.getDB(db), cache * MB);
+            SegmentCache segmentCache = new SegmentCache(cache * MB,new OsgiWhiteboard(context.getBundleContext()));
+            store = new MongoStore(mongo.getDB(db), segmentCache);
         }
 
         delegate = new SegmentNodeStore(store);
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/mongo/MongoStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/mongo/MongoStore.java
index 134575f..44912f5 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/mongo/MongoStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/mongo/MongoStore.java
@@ -77,6 +77,7 @@ public class MongoStore implements SegmentStore {
 
     @Override
     public void close() {
+        cache.close();
     }
 
     @Override
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java
index 875feea..0f466c0 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java
@@ -1,18 +1,20 @@
 /*
- * 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
+ * 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
+ *   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.
+ * 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.whiteboard;
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/SimpleWhiteboard.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/SimpleWhiteboard.java
new file mode 100644
index 0000000..91352d1
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/SimpleWhiteboard.java
@@ -0,0 +1,117 @@
+/*
+ * 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.whiteboard;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import javax.management.JMException;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+
+public class SimpleWhiteboard implements Whiteboard {
+    private ScheduledExecutorService executor = newScheduledThreadPool(0);
+    private MBeanServer mbeanServer;
+
+    public SimpleWhiteboard() {
+    }
+
+    public SimpleWhiteboard(ScheduledExecutorService executor, MBeanServer mbeanServer) {
+        this.executor = executor;
+        this.mbeanServer = mbeanServer;
+    }
+
+    @Override
+    public <T> Registration register(
+            Class<T> type, T service, Map<?, ?> properties) {
+        Future<?> future = null;
+        if (executor != null && type == Runnable.class) {
+            Runnable runnable = (Runnable) service;
+            Long period =
+                    getValue(properties, "scheduler.period", Long.class);
+            if (period != null) {
+                Boolean concurrent = getValue(
+                        properties, "scheduler.concurrent",
+                        Boolean.class, Boolean.FALSE);
+                if (concurrent) {
+                    future = executor.scheduleAtFixedRate(
+                            runnable, period, period, TimeUnit.SECONDS);
+                } else {
+                    future = executor.scheduleWithFixedDelay(
+                            runnable, period, period, TimeUnit.SECONDS);
+                }
+            }
+        }
+
+        ObjectName objectName = null;
+        Object name = properties.get("jmx.objectname");
+        if (mbeanServer != null && name != null) {
+            try {
+                if (name instanceof ObjectName) {
+                    objectName = (ObjectName) name;
+                } else {
+                    objectName = new ObjectName(String.valueOf(name));
+                }
+                mbeanServer.registerMBean(service, objectName);
+            } catch (JMException e) {
+                // ignore
+            }
+        }
+
+        final Future<?> f = future;
+        final ObjectName on = objectName;
+        return new Registration() {
+            @Override
+            public void unregister() {
+                if (f != null) {
+                    f.cancel(false);
+                }
+                if (on != null) {
+                    try {
+                        mbeanServer.unregisterMBean(on);
+                    } catch (JMException e) {
+                        // ignore
+                    }
+                }
+            }
+        };
+    }
+
+    public void close(){
+        executor.shutdown();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T getValue(
+            Map<?, ?> properties, String name, Class<T> type, T def) {
+        Object value = properties.get(name);
+        if (type.isInstance(value)) {
+            return (T) value;
+        } else {
+            return def;
+        }
+    }
+
+    private static <T> T getValue(
+            Map<?, ?> properties, String name, Class<T> type) {
+        return getValue(properties, name, type, null);
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java
index 66992d6..d11812a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java
@@ -19,6 +19,11 @@ package org.apache.jackrabbit.oak.spi.whiteboard;
 import java.util.Map;
 
 public interface Whiteboard {
+    /**
+     * Default Whiteboard instance providing support for scheduling and
+     * JMX registration
+     */
+    Whiteboard DEFAULT = new DefaultWhiteboard();
 
     /**
      * Publishes the given service to the whiteboard. Use the returned
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/util/GuavaCacheStats.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/GuavaCacheStats.java
new file mode 100644
index 0000000..e48c08b
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/util/GuavaCacheStats.java
@@ -0,0 +1,159 @@
+/*
+ * 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.util;
+
+import java.util.Map;
+
+import com.google.common.base.Objects;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.Weigher;
+import org.apache.jackrabbit.oak.api.jmx.CacheStatsMBean;
+
+public class GuavaCacheStats implements CacheStatsMBean{
+    private final Cache<Object,Object> cache;
+    private final Weigher weigher;
+    private final long maxWeight;
+
+    public GuavaCacheStats(Cache cache) {
+        this(cache,null,-1);
+    }
+
+    public GuavaCacheStats(Cache cache, Weigher weigher,long maxWeight) {
+        this.cache = cache;
+        this.weigher = weigher;
+        this.maxWeight = maxWeight;
+    }
+
+    @Override
+    public long getRequestCount() {
+        return stats().requestCount();
+    }
+
+    @Override
+    public long getHitCount() {
+        return stats().hitCount();
+    }
+
+    @Override
+    public double getHitRate() {
+        return stats().hitRate();
+    }
+
+    @Override
+    public long getMissCount() {
+        return stats().missCount();
+    }
+
+    @Override
+    public double getMissRate() {
+        return stats().missRate();
+    }
+
+    @Override
+    public long getLoadCount() {
+        return stats().loadCount();
+    }
+
+    @Override
+    public long getLoadSuccessCount() {
+        return stats().loadSuccessCount();
+    }
+
+    @Override
+    public long getLoadExceptionCount() {
+        return stats().loadExceptionCount();
+    }
+
+    @Override
+    public double getLoadExceptionRate() {
+        return stats().loadExceptionRate();
+    }
+
+    @Override
+    public long getTotalLoadTime() {
+        return stats().totalLoadTime();
+    }
+
+    @Override
+    public double getAverageLoadPenalty() {
+        return stats().averageLoadPenalty();
+    }
+
+    @Override
+    public long getEvictionCount() {
+        return stats().evictionCount();
+    }
+
+    @Override
+    public long getElementCount() {
+        return cache.size();
+    }
+
+    @Override
+    public long getTotalWeight() {
+        if(weigher == null){
+            return -1;
+        }
+        long size = 0;
+        for(Map.Entry e : cache.asMap().entrySet()){
+            size += weigher.weigh(e.getKey(),e.getValue());
+        }
+        return size;
+    }
+
+    @Override
+    public long getMaxTotalWeight() {
+        return maxWeight;
+    }
+
+    @Override
+    public String getCacheInfoAsString() {
+        return Objects.toStringHelper("CacheStats")
+                .add("hitCount", getHitCount())
+                .add("missCount", getMissCount())
+                .add("loadSuccessCount", getLoadSuccessCount())
+                .add("lLoadExceptionCount", getLoadExceptionCount())
+                .add("totalLoadTime", getTotalLoadTime())
+                .add("evictionCount", getEvictionCount())
+                .add("elementCount", getElementCount())
+                .add("totalWeight", humanReadableByteCount(getTotalWeight(), true))
+                .add("maxWeight", humanReadableByteCount(getMaxTotalWeight(),true))
+                .toString();
+    }
+
+    private CacheStats stats() {
+        return cache.stats();
+    }
+
+    /**
+     * Based on http://stackoverflow.com/a/3758880/1035417
+     */
+    private static String humanReadableByteCount(long bytes, boolean si) {
+        if(bytes < 0){
+            return "0";
+        }
+        int unit = si ? 1000 : 1024;
+        if (bytes < unit) return bytes + " B";
+        int exp = (int) (Math.log(bytes) / Math.log(unit));
+        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
+        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+    }
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixture.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixture.java
index d1686fa..80014fd 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixture.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/NodeStoreFixture.java
@@ -88,7 +88,8 @@ public abstract class NodeStoreFixture {
         }
 
         @Override
-        public void close() throws IOException {
+        public void close() {
+            super.close();
             kernel.dispose();
         }
     }
-- 
1.7.9.5

