Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java (revision 1793467) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java (working copy) @@ -17,6 +17,7 @@ package org.apache.jackrabbit.oak.plugins.document; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; import static org.apache.jackrabbit.oak.commons.PathUtils.concat; import static org.apache.jackrabbit.oak.plugins.document.util.MongoConnection.readConcernLevel; @@ -89,6 +90,7 @@ import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore; import org.apache.jackrabbit.oak.spi.blob.MemoryBlobStore; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.gc.GCMonitor; import org.apache.jackrabbit.oak.stats.Clock; import org.apache.jackrabbit.oak.stats.StatisticsProvider; import org.slf4j.Logger; @@ -593,6 +595,7 @@ new JournalPropertyHandlerFactory(); private int updateLimit = UPDATE_LIMIT; private int commitValueCacheSize = 10000; + private GCMonitor gcMonitor = GCMonitor.EMPTY; public Builder() { } @@ -1122,6 +1125,15 @@ return commitValueCacheSize; } + public Builder setGCMonitor(@Nonnull GCMonitor gcMonitor) { + this.gcMonitor = checkNotNull(gcMonitor); + return this; + } + + public GCMonitor getGCMonitor() { + return gcMonitor; + } + VersionGCSupport createVersionGCSupport() { DocumentStore store = getDocumentStore(); if (store instanceof MongoDocumentStore) { Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java (revision 1793467) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java (working copy) @@ -578,8 +578,10 @@ this.asyncDelay = builder.getAsyncDelay(); this.versionGarbageCollector = new VersionGarbageCollector( this, builder.createVersionGCSupport()); + this.versionGarbageCollector.setGCMonitor(builder.getGCMonitor()); this.journalGarbageCollector = new JournalGarbageCollector(this); - this.referencedBlobs = builder.createReferencedBlobs(this); + this.referencedBlobs = + builder.createReferencedBlobs(this); this.lastRevSeeker = builder.createMissingLastRevSeeker(); this.lastRevRecoveryAgent = new LastRevRecoveryAgent(this, lastRevSeeker); this.disableBranches = builder.isDisableBranches(); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java (revision 1793467) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java (working copy) @@ -19,6 +19,7 @@ package org.apache.jackrabbit.oak.plugins.document; import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.commons.IOUtils.closeQuietly; import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toBoolean; import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toInteger; import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toLong; @@ -34,20 +35,22 @@ import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean; import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; import javax.sql.DataSource; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.io.Closer; import com.mongodb.MongoClientURI; import org.apache.commons.io.FilenameUtils; @@ -86,11 +89,13 @@ import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore; import org.apache.jackrabbit.oak.spi.blob.stats.BlobStoreStatsMBean; import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean; +import org.apache.jackrabbit.oak.spi.gc.GCMonitorTracker; import org.apache.jackrabbit.oak.spi.state.Clusterable; import org.apache.jackrabbit.oak.spi.state.NodeStore; import org.apache.jackrabbit.oak.spi.state.NodeStoreProvider; import org.apache.jackrabbit.oak.spi.state.RevisionGC; import org.apache.jackrabbit.oak.spi.state.RevisionGCMBean; +import org.apache.jackrabbit.oak.spi.whiteboard.AbstractServiceTracker; import org.apache.jackrabbit.oak.spi.whiteboard.Registration; import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard; import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardExecutor; @@ -274,7 +279,7 @@ private final Logger log = LoggerFactory.getLogger(this.getClass()); private ServiceRegistration nodeStoreReg; - private final List registrations = new ArrayList(); + private Closer closer; private WhiteboardExecutor executor; @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, @@ -403,6 +408,7 @@ @Activate protected void activate(ComponentContext context, Map config) throws Exception { + closer = Closer.create(); this.context = context; whiteboard = new OsgiWhiteboard(context.getBundleContext()); executor = new WhiteboardExecutor(); @@ -548,6 +554,13 @@ } mkBuilder.setExecutor(executor); + + // attach GCMonitor + final GCMonitorTracker gcMonitor = new GCMonitorTracker(); + gcMonitor.start(whiteboard); + closer.register(asCloseable(gcMonitor)); + mkBuilder.setGCMonitor(gcMonitor); + nodeStore = mkBuilder.getNodeStore(); // ensure a clusterId is initialized @@ -752,10 +765,7 @@ private void unregisterNodeStore() { deactivationTimestamp = System.currentTimeMillis(); - for (Registration r : registrations) { - r.unregister(); - } - registrations.clear(); + closeQuietly(closer); if (nodeStoreReg != null) { nodeStoreReg.unregister(); @@ -784,13 +794,13 @@ private void registerJMXBeans(final DocumentNodeStore store, DocumentMK.Builder mkBuilder) throws IOException { - registrations.add( + addRegistration( registerMBean(whiteboard, CacheStatsMBean.class, store.getNodeCacheStats(), CacheStatsMBean.TYPE, store.getNodeCacheStats().getName())); - registrations.add( + addRegistration( registerMBean(whiteboard, CacheStatsMBean.class, store.getNodeChildrenCacheStats(), @@ -798,7 +808,7 @@ store.getNodeChildrenCacheStats().getName()) ); for (CacheStats cs : store.getDiffCacheStats()) { - registrations.add( + addRegistration( registerMBean(whiteboard, CacheStatsMBean.class, cs, CacheStatsMBean.TYPE, cs.getName())); @@ -806,7 +816,7 @@ DocumentStore ds = store.getDocumentStore(); if (ds.getCacheStats() != null) { for (CacheStats cacheStats : ds.getCacheStats()) { - registrations.add( + addRegistration( registerMBean(whiteboard, CacheStatsMBean.class, cacheStats, @@ -816,7 +826,7 @@ } } - registrations.add( + addRegistration( registerMBean(whiteboard, CheckpointMBean.class, new DocumentCheckpointMBean(store), @@ -824,7 +834,7 @@ "Document node store checkpoint management") ); - registrations.add( + addRegistration( registerMBean(whiteboard, DocumentNodeStoreMBean.class, store.getMBean(), @@ -833,7 +843,7 @@ ); if (mkBuilder.getBlobStoreCacheStats() != null) { - registrations.add( + addRegistration( registerMBean(whiteboard, CacheStatsMBean.class, mkBuilder.getBlobStoreCacheStats(), @@ -843,7 +853,7 @@ } if (mkBuilder.getDocumentStoreStatsCollector() instanceof DocumentStoreStatsMBean) { - registrations.add( + addRegistration( registerMBean(whiteboard, DocumentStoreStatsMBean.class, (DocumentStoreStatsMBean) mkBuilder.getDocumentStoreStatsCollector(), @@ -855,7 +865,7 @@ // register persistent cache stats Map persistenceCacheStats = mkBuilder.getPersistenceCacheStats(); for (PersistentCacheStats pcs: persistenceCacheStats.values()) { - registrations.add( + addRegistration( registerMBean(whiteboard, PersistentCacheStatsMBean.class, pcs, @@ -871,7 +881,7 @@ if (store.getBlobStore() instanceof GarbageCollectableBlobStore) { BlobGarbageCollector gc = store.createBlobGarbageCollector(blobGcMaxAgeInSecs, ClusterRepositoryInfo.getOrCreateId(nodeStore)); - registrations.add(registerMBean(whiteboard, BlobGCMBean.class, new BlobGC(gc, executor), + addRegistration(registerMBean(whiteboard, BlobGCMBean.class, new BlobGC(gc, executor), BlobGCMBean.TYPE, "Document node store blob garbage collection")); } @@ -891,13 +901,19 @@ store.getVersionGarbageCollector().cancel(); } }; - RevisionGC revisionGC = new RevisionGC(startGC, cancelGC, executor); - registrations.add(registerMBean(whiteboard, RevisionGCMBean.class, revisionGC, + Supplier status = new Supplier() { + @Override + public String get() { + return store.getVersionGarbageCollector().getStatus(); + } + }; + RevisionGC revisionGC = new RevisionGC(startGC, cancelGC, status, executor); + addRegistration(registerMBean(whiteboard, RevisionGCMBean.class, revisionGC, RevisionGCMBean.TYPE, "Document node store revision garbage collection")); BlobStoreStats blobStoreStats = mkBuilder.getBlobStoreStats(); if (!customBlobStore && blobStoreStats != null) { - registrations.add(registerMBean(whiteboard, + addRegistration(registerMBean(whiteboard, BlobStoreStatsMBean.class, blobStoreStats, BlobStoreStatsMBean.TYPE, @@ -905,7 +921,7 @@ } if (!mkBuilder.isBundlingDisabled()){ - registrations.add(registerMBean(whiteboard, + addRegistration(registerMBean(whiteboard, BackgroundObserverMBean.class, store.getBundlingConfigHandler().getMBean(), BackgroundObserverMBean.TYPE, @@ -922,7 +938,7 @@ nodeStore.getLastRevRecoveryAgent().performRecoveryIfNeeded(); } }; - registrations.add(WhiteboardUtils.scheduleWithFixedDelay(whiteboard, + addRegistration(WhiteboardUtils.scheduleWithFixedDelay(whiteboard, recoverJob, TimeUnit.MILLISECONDS.toSeconds(leaseTime), false/*runOnSingleClusterNode*/, true /*use dedicated pool*/)); } @@ -941,7 +957,7 @@ } }; - registrations.add(WhiteboardUtils.scheduleWithFixedDelay(whiteboard, + addRegistration(WhiteboardUtils.scheduleWithFixedDelay(whiteboard, journalGCJob, TimeUnit.MILLISECONDS.toSeconds(journalGCInterval), true/*runOnSingleClusterNode*/, true /*use dedicated pool*/)); } @@ -985,4 +1001,28 @@ } return result; } + + private void addRegistration(@Nonnull Registration reg) { + closer.register(asCloseable(reg)); + } + + private static Closeable asCloseable(@Nonnull final Registration reg) { + checkNotNull(reg); + return new Closeable() { + @Override + public void close() throws IOException { + reg.unregister(); + } + }; + } + + private static Closeable asCloseable(@Nonnull final AbstractServiceTracker t) { + checkNotNull(t); + return new Closeable() { + @Override + public void close() throws IOException { + t.stop(); + } + }; + } } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java (revision 1793467) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/VersionGarbageCollector.java (working copy) @@ -37,6 +37,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Stopwatch; +import com.google.common.base.Supplier; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -47,6 +48,8 @@ import org.apache.jackrabbit.oak.plugins.document.UpdateOp.Key; import org.apache.jackrabbit.oak.plugins.document.util.TimeInterval; import org.apache.jackrabbit.oak.plugins.document.util.Utils; +import org.apache.jackrabbit.oak.spi.gc.DelegatingGCMonitor; +import org.apache.jackrabbit.oak.spi.gc.GCMonitor; import org.apache.jackrabbit.oak.stats.Clock; import org.apache.jackrabbit.oak.commons.TimeDurationFormatter; import org.slf4j.Logger; @@ -66,6 +69,7 @@ import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.SplitDocType.DEFAULT_LEAF; import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.SplitDocType.DEFAULT_NO_BRANCH; import static org.apache.jackrabbit.oak.plugins.document.UpdateOp.Condition.newEqualsCondition; +import static org.slf4j.helpers.MessageFormatter.arrayFormat; public class VersionGarbageCollector { @@ -74,6 +78,8 @@ private static final int UPDATE_BATCH_SIZE = 450; private static final int PROGRESS_BATCH_SIZE = 10000; private static final Key KEY_MODIFIED = new Key(MODIFIED_IN_SECS, null); + private static final String STATUS_IDLE = "IDLE"; + private static final String STATUS_INITIALIZING = "INITIALIZING"; private static final Logger log = LoggerFactory.getLogger(VersionGarbageCollector.class); /** @@ -102,6 +108,7 @@ private final VersionGCSupport versionStore; private final AtomicReference collector = newReference(); private VersionGCOptions options; + private GCMonitor gcMonitor = GCMonitor.EMPTY; VersionGarbageCollector(DocumentNodeStore nodeStore, VersionGCSupport gcSupport) { @@ -117,7 +124,7 @@ if (options.maxDurationMs > 0) { maxRunTime = maxRunTime.startAndDuration(options.maxDurationMs); } - GCJob job = new GCJob(maxRevisionAgeInMillis, options); + GCJob job = new GCJob(maxRevisionAgeInMillis, options, gcMonitor); if (collector.compareAndSet(null, job)) { VersionGCStats overall = new VersionGCStats(); overall.active.start(); @@ -159,6 +166,19 @@ } } + public String getStatus() { + GCJob job = collector.get(); + if (job == null) { + return STATUS_IDLE; + } else { + return job.getStatus(); + } + } + + public void setGCMonitor(@Nonnull GCMonitor gcMonitor) { + this.gcMonitor = checkNotNull(gcMonitor); + } + public VersionGCOptions getOptions() { return this.options; } @@ -306,12 +326,14 @@ final VersionGCStats stats; final Stopwatch elapsed; + private final GCMonitor monitor; private final List phases = Lists.newArrayList(); private final Map watches = Maps.newHashMap(); private final AtomicBoolean canceled; - GCPhases(AtomicBoolean canceled, VersionGCStats stats) { + GCPhases(AtomicBoolean canceled, VersionGCStats stats, GCMonitor monitor) { this.stats = stats; + this.monitor = monitor; this.elapsed = Stopwatch.createStarted(); this.watches.put(GCPhase.NONE, Stopwatch.createStarted()); this.watches.put(GCPhase.COLLECTING, stats.collectDeletedDocs); @@ -337,6 +359,7 @@ } suspend(currentWatch()); this.phases.add(started); + updateStatus(); resume(currentWatch()); return true; } @@ -345,6 +368,7 @@ if (!phases.isEmpty() && phase == phases.get(phases.size() - 1)) { suspend(currentWatch()); phases.remove(phases.size() - 1); + updateStatus(); resume(currentWatch()); } } @@ -353,6 +377,7 @@ while (!phases.isEmpty()) { suspend(currentWatch()); phases.remove(phases.size() - 1); + updateStatus(); } this.elapsed.stop(); } @@ -376,6 +401,13 @@ w.stop(); } } + + private void updateStatus() { + GCPhase p = current(); + if (p != GCPhase.NONE) { + monitor.updateStatus(p.name()); + } + } } private class GCJob { @@ -382,35 +414,52 @@ private final long maxRevisionAgeMillis; private final VersionGCOptions options; - private AtomicBoolean cancel = new AtomicBoolean(); + private final AtomicBoolean cancel = new AtomicBoolean(); + private final GCMonitor monitor; + private final Supplier status; - GCJob(long maxRevisionAgeMillis, VersionGCOptions options) { + GCJob(long maxRevisionAgeMillis, + VersionGCOptions options, + GCMonitor gcMonitor) { this.maxRevisionAgeMillis = maxRevisionAgeMillis; this.options = options; + VersionGCMonitor vgcm = new VersionGCMonitor(); + this.status = vgcm; + this.monitor = new DelegatingGCMonitor(Lists.newArrayList(vgcm, gcMonitor)); + this.monitor.updateStatus(STATUS_INITIALIZING); } VersionGCStats run() throws IOException { - return gc(maxRevisionAgeMillis); + try { + return gc(maxRevisionAgeMillis); + } finally { + monitor.updateStatus(STATUS_IDLE); + } } void cancel() { - log.info("Canceling revision garbage collection."); + monitor.info("Canceling revision garbage collection."); cancel.set(true); } + String getStatus() { + return status.get(); + } + private VersionGCStats gc(long maxRevisionAgeInMillis) throws IOException { VersionGCStats stats = new VersionGCStats(); stats.active.start(); Recommendations rec = new Recommendations(maxRevisionAgeInMillis, options); - GCPhases phases = new GCPhases(cancel, stats); + GCPhases phases = new GCPhases(cancel, stats, gcMonitor); try { if (rec.ignoreDueToCheckPoint) { phases.stats.ignoredGCDueToCheckPoint = true; + monitor.skipped("Checkpoint prevented revision garbage collection"); cancel.set(true); } else { final RevisionVector headRevision = nodeStore.getHeadRevision(); final RevisionVector sweepRevisions = nodeStore.getSweepRevisions(); - log.info("Looking at revisions in {}", rec.scope); + monitor.info("Looking at revisions in {}", rec.scope); collectDeletedDocuments(phases, headRevision, rec); collectSplitDocuments(phases, sweepRevisions, rec); @@ -423,7 +472,7 @@ } rec.evaluate(stats); - log.info("Revision garbage collection finished in {}. {}", + monitor.info("Revision garbage collection finished in {}. {}", TimeDurationFormatter.forLogging().format(phases.elapsed.elapsed(MICROSECONDS), MICROSECONDS), stats); stats.active.stop(); return stats; @@ -443,7 +492,7 @@ Recommendations rec) throws IOException, LimitExceededException { int docsTraversed = 0; - DeletedDocsGC gc = new DeletedDocsGC(headRevision, cancel, options); + DeletedDocsGC gc = new DeletedDocsGC(headRevision, cancel, options, monitor); try { if (phases.start(GCPhase.COLLECTING)) { Iterable itr = versionStore.getPossiblyDeletedDocs(rec.scope.fromMs, rec.scope.toMs); @@ -459,7 +508,7 @@ // So deleting it is safe docsTraversed++; if (docsTraversed % PROGRESS_BATCH_SIZE == 0) { - log.info("Iterated through {} documents so far. {} found to be deleted", + monitor.info("Iterated through {} documents so far. {} found to be deleted", docsTraversed, gc.getNumDocuments()); } if (phases.start(GCPhase.CHECKING)) { @@ -530,14 +579,17 @@ private boolean sorted = false; private final Stopwatch timer; private final VersionGCOptions options; + private final GCMonitor monitor; public DeletedDocsGC(@Nonnull RevisionVector headRevision, @Nonnull AtomicBoolean cancel, - @Nonnull VersionGCOptions options) { + @Nonnull VersionGCOptions options, + @Nonnull GCMonitor monitor) { this.headRevision = checkNotNull(headRevision); this.cancel = checkNotNull(cancel); this.timer = Stopwatch.createUnstarted(); this.options = options; + this.monitor = monitor; this.docIdsToDelete = newStringSort(options); this.prevDocIdsToDelete = newStringSort(options); } @@ -570,7 +622,7 @@ try { Utils.getDepthFromId(id); } catch (IllegalArgumentException e) { - log.warn("Invalid GC id {} for document {}", id, doc); + monitor.warn("Invalid GC id {} for document {}", id, doc); return false; } if (doc.getNodeAtRevision(nodeStore, headRevision, null) == null) { @@ -631,12 +683,12 @@ try { docIdsToDelete.close(); } catch (IOException e) { - log.warn("Failed to close docIdsToDelete", e); + monitor.warn("Failed to close docIdsToDelete: {}", e); } try { prevDocIdsToDelete.close(); } catch (IOException e) { - log.warn("Failed to close prevDocIdsToDelete", e); + monitor.warn("Failed to close prevDocIdsToDelete: {}", e); } } @@ -744,7 +796,7 @@ private int removeDeletedDocuments(Iterator docIdsToDelete, long numDocuments, String label) throws IOException { - log.info("Proceeding to delete [{}] documents [{}]", numDocuments, label); + monitor.info("Proceeding to delete [{}] documents [{}]", numDocuments, label); Iterator> idListItr = partition(docIdsToDelete, DELETE_BATCH_SIZE); int deletedCount = 0; @@ -757,7 +809,7 @@ try { parsed = parseEntry(s); } catch (IllegalArgumentException e) { - log.warn("Invalid _modified suffix for {}", s); + monitor.warn("Invalid _modified suffix for {}", s); continue; } deletionBatch.put(parsed.getKey(), singletonMap(KEY_MODIFIED, newEqualsCondition(parsed.getValue()))); @@ -792,7 +844,7 @@ lastLoggedCount = deletedCount + recreatedCount; double progress = lastLoggedCount * 1.0 / getNumDocuments() * 100; String msg = String.format("Deleted %d (%1.2f%%) documents so far", deletedCount, progress); - log.info(msg); + monitor.info(msg); } } finally { delayOnModifications(timer.stop().elapsed(TimeUnit.MILLISECONDS)); @@ -802,7 +854,7 @@ } private int resetDeletedOnce(List resurrectedDocuments) throws IOException { - log.info("Proceeding to reset [{}] _deletedOnce flags", resurrectedDocuments.size()); + monitor.info("Proceeding to reset [{}] _deletedOnce flags", resurrectedDocuments.size()); int updateCount = 0; timer.reset().start(); @@ -819,9 +871,9 @@ updateCount += 1; } } catch (IllegalArgumentException ex) { - log.warn("Invalid _modified suffix for {}", s); + monitor.warn("Invalid _modified suffix for {}", s); } catch (DocumentStoreException ex) { - log.warn("updating {}: {}", s, ex.getMessage()); + monitor.warn("updating {}: {}", s, ex.getMessage()); } } } @@ -833,7 +885,7 @@ } private int removeDeletedPreviousDocuments() throws IOException { - log.info("Proceeding to delete [{}] previous documents", getNumPreviousDocuments()); + monitor.info("Proceeding to delete [{}] previous documents", getNumPreviousDocuments()); int deletedCount = 0; int lastLoggedCount = 0; @@ -857,7 +909,7 @@ lastLoggedCount = deletedCount; double progress = deletedCount * 1.0 / (prevDocIdsToDelete.getSize() - exclude.size()) * 100; String msg = String.format("Deleted %d (%1.2f%%) previous documents so far", deletedCount, progress); - log.info(msg); + monitor.info(msg); } } return deletedCount; @@ -1075,6 +1127,41 @@ } } + /** + * VersionGCMonitor is a partial implementation of GCMonitor because some + * methods are specific to segment-tar. We use it primarily to keep track + * of the last message issued by the GC job. + */ + private static class VersionGCMonitor + extends GCMonitor.Empty + implements Supplier { + + private volatile String lastMessage = STATUS_INITIALIZING; + + @Override + public void info(String message, Object... arguments) { + log.info(message, arguments); + lastMessage = arrayFormat(message, arguments).getMessage(); + } + + @Override + public void warn(String message, Object... arguments) { + log.warn(message, arguments); + lastMessage = arrayFormat(message, arguments).getMessage(); + } + + @Override + public void error(String message, Exception e) { + log.error(message, e); + lastMessage = message + " (" + e.getMessage() + ")"; + } + + @Override + public String get() { + return lastMessage; + } + } + private static final class LimitExceededException extends Exception { private static final long serialVersionUID = 6578586397629516408L; } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGCTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGCTest.java (revision 1793467) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/VersionGCTest.java (working copy) @@ -29,11 +29,14 @@ import javax.annotation.Nonnull; +import com.google.common.collect.Lists; + import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.plugins.document.VersionGarbageCollector.VersionGCStats; import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; import org.apache.jackrabbit.oak.spi.commit.EmptyHook; +import org.apache.jackrabbit.oak.spi.gc.GCMonitor; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.stats.Clock; import org.junit.After; @@ -49,6 +52,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.slf4j.helpers.MessageFormatter.arrayFormat; public class VersionGCTest { @@ -185,6 +189,43 @@ gc.getInfo(1, TimeUnit.HOURS); } + @Test + public void gcMonitorStatusUpdates() throws Exception { + final List statusMessages = Lists.newArrayList(); + GCMonitor monitor = new GCMonitor.Empty() { + @Override + public void updateStatus(String status) { + statusMessages.add(status); + } + }; + gc.setGCMonitor(monitor); + + gc.gc(1, TimeUnit.HOURS); + + List expected = Lists.newArrayList("INITIALIZING", + "COLLECTING", "UPDATING", "SPLITS_CLEANUP", "IDLE"); + assertEquals(expected, statusMessages); + } + + @Test + public void gcMonitorInfoMessages() throws Exception { + final List infoMessages = Lists.newArrayList(); + GCMonitor monitor = new GCMonitor.Empty() { + @Override + public void info(String message, Object[] arguments) { + infoMessages.add(arrayFormat(message, arguments).getMessage()); + } + }; + gc.setGCMonitor(monitor); + + gc.gc(1, TimeUnit.HOURS); + + assertEquals(3, infoMessages.size()); + assertTrue(infoMessages.get(0).startsWith("Looking at revisions")); + assertTrue(infoMessages.get(1).startsWith("Proceeding to reset")); + assertTrue(infoMessages.get(2).startsWith("Revision garbage collection finished")); + } + private Future gc() { // run gc in a separate thread return execService.submit(new Callable() {