Merge pull request #4791 from apache/nouveau-indexmanager-improvements

Nouveau indexmanager improvements
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
index 3039214..89b2b85 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java
@@ -13,10 +13,12 @@
 
 package org.apache.couchdb.nouveau;
 
+import com.github.benmanes.caffeine.cache.Scheduler;
 import io.dropwizard.core.Application;
 import io.dropwizard.core.setup.Environment;
 import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
 import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ScheduledExecutorService;
 import org.apache.couchdb.nouveau.core.IndexManager;
 import org.apache.couchdb.nouveau.health.AnalyzeHealthCheck;
 import org.apache.couchdb.nouveau.health.IndexHealthCheck;
@@ -25,7 +27,6 @@
 import org.apache.couchdb.nouveau.resources.AnalyzeResource;
 import org.apache.couchdb.nouveau.resources.IndexResource;
 import org.apache.couchdb.nouveau.tasks.CloseAllIndexesTask;
-import org.apache.lucene.search.SearcherFactory;
 
 public class NouveauApplication extends Application<NouveauApplicationConfiguration> {
 
@@ -40,17 +41,20 @@
 
     @Override
     public void run(NouveauApplicationConfiguration configuration, Environment environment) throws Exception {
+
         // configure index manager
         final IndexManager indexManager = new IndexManager();
         indexManager.setCommitIntervalSeconds(configuration.getCommitIntervalSeconds());
         indexManager.setIdleSeconds(configuration.getIdleSeconds());
         indexManager.setMaxIndexesOpen(configuration.getMaxIndexesOpen());
         indexManager.setMetricRegistry(environment.metrics());
-        indexManager.setScheduler(environment
+        final ScheduledExecutorService schedulerExecutorService = environment
                 .lifecycle()
                 .scheduledExecutorService("index-manager-%d")
-                .threads(5)
-                .build());
+                .threads(configuration.getSchedulerThreadCount())
+                .build();
+        indexManager.setScheduler(Scheduler.forScheduledExecutorService(schedulerExecutorService));
+        indexManager.setSearcherFactory(new ParallelSearcherFactory(ForkJoinPool.commonPool()));
         indexManager.setObjectMapper(environment.getObjectMapper());
         indexManager.setRootDir(configuration.getRootDir());
         environment.lifecycle().manage(indexManager);
@@ -63,8 +67,7 @@
         environment.jersey().register(analyzeResource);
 
         // IndexResource
-        final SearcherFactory searcherFactory = new ParallelSearcherFactory(ForkJoinPool.commonPool());
-        final IndexResource indexResource = new IndexResource(indexManager, searcherFactory);
+        final IndexResource indexResource = new IndexResource(indexManager);
         environment.jersey().register(indexResource);
 
         // Health checks
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
index 50d3201..dce6fe6 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplicationConfiguration.java
@@ -33,6 +33,9 @@
     @NotNull
     private Path rootDir = null;
 
+    @Min(2)
+    private int schedulerThreadCount = 5;
+
     @JsonProperty
     public void setMaxIndexesOpen(int maxIndexesOpen) {
         this.maxIndexesOpen = maxIndexesOpen;
@@ -68,4 +71,12 @@
     public Path getRootDir() {
         return rootDir;
     }
+
+    public int getSchedulerThreadCount() {
+        return schedulerThreadCount;
+    }
+
+    public void setSchedulerThreadCount(int schedulerThreadCount) {
+        this.schedulerThreadCount = schedulerThreadCount;
+    }
 }
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
index 740e7ea..bc56cd7 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/Index.java
@@ -18,7 +18,6 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
 import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
 import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.IndexInfo;
@@ -40,8 +39,6 @@
     private long updateSeq;
     private long purgeSeq;
     private boolean deleteOnClose = false;
-    private long lastCommit = now();
-    private volatile boolean closed;
     private final Semaphore permits = new Semaphore(Integer.MAX_VALUE);
 
     protected Index(final long updateSeq, final long purgeSeq) {
@@ -50,25 +47,7 @@
     }
 
     public final boolean tryAcquire() {
-        if (permits.tryAcquire() == false) {
-            return false;
-        }
-        if (closed) {
-            permits.release();
-            return false;
-        }
-        return true;
-    }
-
-    public final boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
-        if (permits.tryAcquire(timeout, unit) == false) {
-            return false;
-        }
-        if (closed) {
-            permits.release();
-            return false;
-        }
-        return true;
+        return permits.tryAcquire();
     }
 
     public final void release() {
@@ -117,17 +96,13 @@
         final long updateSeq;
         final long purgeSeq;
         synchronized (this) {
+            if (deleteOnClose) {
+                return false;
+            }
             updateSeq = this.updateSeq;
             purgeSeq = this.purgeSeq;
         }
-        final boolean result = doCommit(updateSeq, purgeSeq);
-        if (result) {
-            final long now = now();
-            synchronized (this) {
-                this.lastCommit = now;
-            }
-        }
-        return result;
+        return doCommit(updateSeq, purgeSeq);
     }
 
     protected abstract boolean doCommit(final long updateSeq, final long purgeSeq) throws IOException;
@@ -154,28 +129,24 @@
 
     @Override
     public final void close() throws IOException {
-        synchronized (this) {
-            closed = true;
-        }
         // Ensures exclusive access to the index before closing.
         permits.acquireUninterruptibly(Integer.MAX_VALUE);
-        try {
-            doClose();
-        } finally {
-            permits.release(Integer.MAX_VALUE);
-        }
+        doClose();
+        // Never release permits.
     }
 
     protected abstract void doClose() throws IOException;
 
-    public boolean isDeleteOnClose() {
+    public synchronized boolean isDeleteOnClose() {
         return deleteOnClose;
     }
 
-    public void setDeleteOnClose(final boolean deleteOnClose) {
-        synchronized (this) {
-            this.deleteOnClose = deleteOnClose;
-        }
+    public synchronized void setDeleteOnClose(final boolean deleteOnClose) {
+        this.deleteOnClose = deleteOnClose;
+    }
+
+    public final boolean isActive() {
+        return permits.availablePermits() < Integer.MAX_VALUE || permits.hasQueuedThreads();
     }
 
     protected final void assertUpdateSeqProgress(final long matchSeq, final long updateSeq)
@@ -211,15 +182,4 @@
         assertPurgeSeqProgress(matchSeq, purgeSeq);
         this.purgeSeq = purgeSeq;
     }
-
-    public boolean needsCommit(final long duration, final TimeUnit unit) {
-        final long commitNeededSince = now() - unit.toNanos(duration);
-        synchronized (this) {
-            return this.lastCommit < commitNeededSince;
-        }
-    }
-
-    private long now() {
-        return System.nanoTime();
-    }
 }
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java
deleted file mode 100644
index b5def16..0000000
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexLoader.java
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// Licensed 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.couchdb.nouveau.core;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import org.apache.couchdb.nouveau.api.IndexDefinition;
-
-@FunctionalInterface
-public interface IndexLoader {
-
-    Index apply(final Path path, final IndexDefinition indexDefinition) throws IOException;
-}
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
index fb8cf2d..5d10c59 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/IndexManager.java
@@ -17,13 +17,14 @@
 
 import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.caffeine.MetricsStatsCounter;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
+import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.RemovalListener;
 import com.github.benmanes.caffeine.cache.Scheduler;
+import com.github.benmanes.caffeine.cache.Weigher;
 import io.dropwizard.lifecycle.Managed;
 import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.core.Response.Status;
@@ -34,12 +35,23 @@
 import java.nio.file.StandardCopyOption;
 import java.time.Duration;
 import java.util.List;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
 import java.util.concurrent.locks.Lock;
 import java.util.stream.Stream;
 import org.apache.couchdb.nouveau.api.IndexDefinition;
-import org.eclipse.jetty.io.RuntimeIOException;
+import org.apache.couchdb.nouveau.lucene9.Lucene9AnalyzerFactory;
+import org.apache.couchdb.nouveau.lucene9.Lucene9Index;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.misc.store.DirectIODirectory;
+import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.checkerframework.checker.index.qual.NonNegative;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,55 +80,27 @@
 
     private MetricRegistry metricRegistry;
 
-    private ScheduledExecutorService scheduler;
+    private Scheduler scheduler;
 
-    private Cache<String, Index> cache;
+    private SearcherFactory searcherFactory;
+
+    private AsyncLoadingCache<String, Index> cache;
 
     private StripedLock<String> lock;
 
-    public <R> R with(final String name, final IndexLoader loader, final IndexFunction<Index, R> indexFun)
+    public <R> R with(final String name, final IndexFunction<Index, R> indexFun)
             throws IOException, InterruptedException {
         while (true) {
             if (!exists(name)) {
                 throw new WebApplicationException("Index does not exist", Status.NOT_FOUND);
             }
 
-            final Index index;
-            try {
-                index = cache.get(name, (n) -> {
-                    LOGGER.info("opening {}", n);
-                    final Path path = indexPath(n);
-                    try {
-                        final IndexDefinition indexDefinition = loadIndexDefinition(n);
-                        return loader.apply(path, indexDefinition);
-                    } catch (final IOException e) {
-                        throw new RuntimeIOException(e);
-                    }
-                });
-            } catch (final RuntimeIOException e) {
-                throw (IOException) e.getCause();
-            }
+            final CompletableFuture<Index> future = cache.get(name);
+            final Index index = future.join();
 
-            if (index.tryAcquire(1, TimeUnit.SECONDS)) {
+            if (index.tryAcquire()) {
                 try {
-                    final R result = indexFun.apply(index);
-                    if (index.needsCommit(commitIntervalSeconds, TimeUnit.SECONDS)) {
-                        scheduler.execute(() -> {
-                            if (index.tryAcquire()) {
-                                try {
-                                    LOGGER.debug("committing {}", name);
-                                    try {
-                                        index.commit();
-                                    } catch (final IOException e) {
-                                        LOGGER.warn("I/O exception while committing " + name, e);
-                                    }
-                                } finally {
-                                    index.release();
-                                }
-                            }
-                        });
-                    }
-                    return result;
+                    return indexFun.apply(index);
                 } finally {
                     index.release();
                 }
@@ -193,7 +177,11 @@
     }
 
     private void deleteIndex(final String name) throws IOException {
-        final Index index = cache.asMap().remove(name);
+        final CompletableFuture<Index> future = cache.asMap().remove(name);
+        if (future == null) {
+            return;
+        }
+        final Index index = future.getNow(null);
         if (index != null) {
             index.setDeleteOnClose(true);
             close(name, index);
@@ -202,39 +190,18 @@
         }
     }
 
-    @JsonProperty
-    public int getMaxIndexesOpen() {
-        return maxIndexesOpen;
-    }
-
     public void setMaxIndexesOpen(int maxIndexesOpen) {
         this.maxIndexesOpen = maxIndexesOpen;
     }
 
-    public int getCommitIntervalSeconds() {
-        return commitIntervalSeconds;
-    }
-
     public void setCommitIntervalSeconds(int commitIntervalSeconds) {
         this.commitIntervalSeconds = commitIntervalSeconds;
     }
 
-    public int getIdleSeconds() {
-        return idleSeconds;
-    }
-
     public void setIdleSeconds(int idleSeconds) {
         this.idleSeconds = idleSeconds;
     }
 
-    public void setScheduler(ScheduledExecutorService scheduler) {
-        this.scheduler = scheduler;
-    }
-
-    public Path getRootDir() {
-        return rootDir;
-    }
-
     public void setRootDir(Path rootDir) {
         this.rootDir = rootDir;
     }
@@ -247,16 +214,26 @@
         this.metricRegistry = metricRegistry;
     }
 
+    public void setScheduler(final Scheduler scheduler) {
+        this.scheduler = scheduler;
+    }
+
+    public void setSearcherFactory(final SearcherFactory searcherFactory) {
+        this.searcherFactory = searcherFactory;
+    }
+
     @Override
     public void start() throws IOException {
         cache = Caffeine.newBuilder()
                 .recordStats(() -> new MetricsStatsCounter(metricRegistry, name(IndexManager.class, "cache")))
                 .initialCapacity(maxIndexesOpen)
-                .maximumSize(maxIndexesOpen)
+                .maximumWeight(maxIndexesOpen)
+                .weigher(new IndexWeigher())
                 .expireAfterAccess(Duration.ofSeconds(idleSeconds))
-                .scheduler(Scheduler.systemScheduler())
+                .refreshAfterWrite(Duration.ofSeconds(commitIntervalSeconds))
+                .scheduler(scheduler)
                 .evictionListener(new IndexEvictionListener())
-                .build();
+                .buildAsync(new AsyncIndexLoader());
         lock = new StripedLock<String>(100);
     }
 
@@ -307,12 +284,91 @@
         }
     }
 
+    private class IndexWeigher implements Weigher<String, Index> {
+
+        @Override
+        public @NonNegative int weigh(String key, Index value) {
+            // Pin active indexes
+            return value.isActive() ? 0 : 1;
+        }
+    }
+
+    private class AsyncIndexLoader implements AsyncCacheLoader<String, Index> {
+
+        @Override
+        public CompletableFuture<? extends Index> asyncLoad(String name, Executor executor) throws Exception {
+            final CompletableFuture<Index> future = new CompletableFuture<Index>();
+
+            executor.execute(() -> {
+                LOGGER.info("opening {}", name);
+                final Path path = indexPath(name);
+                Index result;
+                try {
+                    final IndexDefinition indexDefinition = loadIndexDefinition(name);
+                    final Analyzer analyzer = Lucene9AnalyzerFactory.fromDefinition(indexDefinition);
+                    final Directory dir = new DirectIODirectory(FSDirectory.open(path.resolve("9")));
+                    final IndexWriterConfig config = new IndexWriterConfig(analyzer);
+                    config.setUseCompoundFile(false);
+                    final IndexWriter writer = new IndexWriter(dir, config);
+                    final long updateSeq = getSeq(writer, "update_seq");
+                    final long purgeSeq = getSeq(writer, "purge_seq");
+                    final SearcherManager searcherManager = new SearcherManager(writer, searcherFactory);
+                    result = new Lucene9Index(analyzer, writer, updateSeq, purgeSeq, searcherManager);
+                    future.complete(result);
+                } catch (IOException e) {
+                    future.completeExceptionally(e);
+                }
+            });
+
+            return future;
+        }
+
+        @Override
+        public CompletableFuture<? extends Index> asyncReload(String name, Index index, Executor executor)
+                throws Exception {
+            executor.execute(() -> {
+                if (index.tryAcquire()) {
+                    try {
+                        if (index.commit()) {
+                            LOGGER.info("committed {}", name);
+                        }
+                    } catch (final IOException e) {
+                        LOGGER.warn("I/O exception while committing " + name, e);
+                    } finally {
+                        index.release();
+                    }
+                }
+            });
+            return CompletableFuture.completedFuture(index);
+        }
+
+        private long getSeq(final IndexWriter writer, final String key) throws IOException {
+            final Iterable<Map.Entry<String, String>> commitData = writer.getLiveCommitData();
+            if (commitData == null) {
+                return 0L;
+            }
+            for (Map.Entry<String, String> entry : commitData) {
+                if (entry.getKey().equals(key)) {
+                    return Long.parseLong(entry.getValue());
+                }
+            }
+            return 0L;
+        }
+    }
+
+    private void close(final String name, final CompletableFuture<Index> future) throws IOException {
+        final Index index = future.getNow(null);
+        if (index != null) {
+            close(name, index);
+        }
+    }
+
     private void close(final String name, final Index index) throws IOException {
         IOUtils.runAll(
                 () -> {
                     if (index.tryAcquire()) {
                         try {
-                            if (!index.isDeleteOnClose() && index.commit()) {
+                            if (index.commit()) {
                                 LOGGER.debug("committed {} before close", name);
                             }
                         } finally {
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
index 9c7a100..a6ca2c4 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/resources/IndexResource.java
@@ -29,7 +29,6 @@
 import jakarta.ws.rs.core.MediaType;
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import org.apache.couchdb.nouveau.api.DocumentDeleteRequest;
 import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
@@ -38,18 +37,7 @@
 import org.apache.couchdb.nouveau.api.IndexInfoRequest;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
-import org.apache.couchdb.nouveau.core.IndexLoader;
 import org.apache.couchdb.nouveau.core.IndexManager;
-import org.apache.couchdb.nouveau.lucene9.Lucene9AnalyzerFactory;
-import org.apache.couchdb.nouveau.lucene9.Lucene9Index;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.misc.store.DirectIODirectory;
-import org.apache.lucene.search.SearcherFactory;
-import org.apache.lucene.search.SearcherManager;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.store.FSDirectory;
 
 @Path("/index/{name}")
 @Metered
@@ -60,11 +48,9 @@
 public final class IndexResource {
 
     private final IndexManager indexManager;
-    private final SearcherFactory searcherFactory;
 
-    public IndexResource(final IndexManager indexManager, final SearcherFactory searcherFactory) {
+    public IndexResource(final IndexManager indexManager) {
         this.indexManager = Objects.requireNonNull(indexManager);
-        this.searcherFactory = Objects.requireNonNull(searcherFactory);
     }
 
     @PUT
@@ -80,7 +66,7 @@
             @PathParam("docId") String docId,
             @NotNull @Valid DocumentDeleteRequest request)
             throws Exception {
-        indexManager.with(name, indexLoader(), (index) -> {
+        indexManager.with(name, (index) -> {
             index.delete(docId, request);
             return null;
         });
@@ -93,7 +79,7 @@
 
     @GET
     public IndexInfo getIndexInfo(@PathParam("name") String name) throws Exception {
-        return indexManager.with(name, indexLoader(), (index) -> {
+        return indexManager.with(name, (index) -> {
             return index.info();
         });
     }
@@ -101,7 +87,7 @@
     @POST
     public void setIndexInfo(@PathParam("name") String name, @NotNull @Valid IndexInfoRequest request)
             throws Exception {
-        indexManager.with(name, indexLoader(), (index) -> {
+        indexManager.with(name, (index) -> {
             if (request.getMatchUpdateSeq().isPresent()
                     && request.getUpdateSeq().isPresent()) {
                 index.setUpdateSeq(
@@ -121,7 +107,7 @@
     @Path("/search")
     public SearchResults searchIndex(@PathParam("name") String name, @NotNull @Valid SearchRequest request)
             throws Exception {
-        return indexManager.with(name, indexLoader(), (index) -> {
+        return indexManager.with(name, (index) -> {
             return index.search(request);
         });
     }
@@ -133,36 +119,9 @@
             @PathParam("docId") String docId,
             @NotNull @Valid DocumentUpdateRequest request)
             throws Exception {
-        indexManager.with(name, indexLoader(), (index) -> {
+        indexManager.with(name, (index) -> {
             index.update(docId, request);
             return null;
         });
     }
-
-    private IndexLoader indexLoader() {
-        return (path, indexDefinition) -> {
-            final Analyzer analyzer = Lucene9AnalyzerFactory.fromDefinition(indexDefinition);
-            final Directory dir = new DirectIODirectory(FSDirectory.open(path.resolve("9")));
-            final IndexWriterConfig config = new IndexWriterConfig(analyzer);
-            config.setUseCompoundFile(false);
-            final IndexWriter writer = new IndexWriter(dir, config);
-            final long updateSeq = getSeq(writer, "update_seq");
-            final long purgeSeq = getSeq(writer, "purge_seq");
-            final SearcherManager searcherManager = new SearcherManager(writer, searcherFactory);
-            return new Lucene9Index(analyzer, writer, updateSeq, purgeSeq, searcherManager);
-        };
-    }
-
-    private static long getSeq(final IndexWriter writer, final String key) throws IOException {
-        final Iterable<Map.Entry<String, String>> commitData = writer.getLiveCommitData();
-        if (commitData == null) {
-            return 0L;
-        }
-        for (Map.Entry<String, String> entry : commitData) {
-            if (entry.getKey().equals(key)) {
-                return Long.parseLong(entry.getValue());
-            }
-        }
-        return 0L;
-    }
 }
diff --git a/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java b/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
index c71c281..7ee3223 100644
--- a/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
+++ b/nouveau/src/test/java/org/apache/couchdb/nouveau/health/IndexHealthCheckTest.java
@@ -17,8 +17,8 @@
 
 import com.codahale.metrics.MetricRegistry;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.benmanes.caffeine.cache.Scheduler;
 import java.nio.file.Path;
-import java.util.concurrent.Executors;
 import org.apache.couchdb.nouveau.core.IndexManager;
 import org.apache.couchdb.nouveau.resources.IndexResource;
 import org.apache.lucene.search.SearcherFactory;
@@ -29,19 +29,20 @@
 
     @Test
     public void testIndexHealthCheck(@TempDir final Path tempDir) throws Exception {
-        var scheduler = Executors.newSingleThreadScheduledExecutor();
         var manager = new IndexManager();
+        manager.setCommitIntervalSeconds(1);
         manager.setObjectMapper(new ObjectMapper());
         manager.setMetricRegistry(new MetricRegistry());
         manager.setRootDir(tempDir);
-        manager.setScheduler(scheduler);
+        manager.setScheduler(Scheduler.systemScheduler());
+        manager.setSearcherFactory(new SearcherFactory());
         manager.start();
+
         try {
-            var resource = new IndexResource(manager, new SearcherFactory());
+            var resource = new IndexResource(manager);
             var check = new IndexHealthCheck(resource);
             assertTrue(check.check().isHealthy());
         } finally {
-            scheduler.shutdown();
             manager.stop();
         }
     }
diff --git a/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java b/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
index eaaad17..ece5fb3 100644
--- a/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
+++ b/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
@@ -33,7 +33,6 @@
 import org.apache.couchdb.nouveau.api.SearchResults;
 import org.apache.couchdb.nouveau.api.StringField;
 import org.apache.couchdb.nouveau.core.Index;
-import org.apache.couchdb.nouveau.core.IndexLoader;
 import org.apache.couchdb.nouveau.core.UpdatesOutOfOrderException;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.IndexWriter;
@@ -50,12 +49,17 @@
     protected final Index setup(final Path path) throws IOException {
         final IndexDefinition indexDefinition = new IndexDefinition();
         indexDefinition.setDefaultAnalyzer("standard");
-        final Index index = indexLoader().apply(path, indexDefinition);
-        index.setDeleteOnClose(true);
-        return index;
+        final Analyzer analyzer = Lucene9AnalyzerFactory.fromDefinition(indexDefinition);
+        final Directory dir = new DirectIODirectory(FSDirectory.open(path));
+        final IndexWriterConfig config = new IndexWriterConfig(analyzer);
+        config.setUseCompoundFile(false);
+        final IndexWriter writer = new IndexWriter(dir, config);
+        final SearcherManager searcherManager = new SearcherManager(writer, null);
+        return new Lucene9Index(analyzer, writer, 0L, 0L, searcherManager);
     }
 
     protected final void cleanup(final Index index) throws IOException {
+        index.setDeleteOnClose(true);
         index.close();
     }
 
@@ -236,16 +240,4 @@
             cleanup(index);
         }
     }
-
-    protected IndexLoader indexLoader() {
-        return (path, indexDefinition) -> {
-            final Analyzer analyzer = Lucene9AnalyzerFactory.fromDefinition(indexDefinition);
-            final Directory dir = new DirectIODirectory(FSDirectory.open(path));
-            final IndexWriterConfig config = new IndexWriterConfig(analyzer);
-            config.setUseCompoundFile(false);
-            final IndexWriter writer = new IndexWriter(dir, config);
-            final SearcherManager searcherManager = new SearcherManager(writer, null);
-            return new Lucene9Index(analyzer, writer, 0L, 0L, searcherManager);
-        };
-    }
 }