diff --git a/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java b/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java index 2f4e2d62f..72f238555 100644 --- a/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java +++ b/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java @@ -84,7 +84,7 @@ public class LuceneSearchEngine implements SearchEngine, InitializingBean, Dispo private Directory directory; - public LuceneSearchEngine(Path indexRootDir) throws IOException { + public LuceneSearchEngine(Path indexRootDir) { this.indexRootDir = indexRootDir; } @@ -106,12 +106,14 @@ public class LuceneSearchEngine implements SearchEngine, InitializingBean, Dispo var writerConfig = new IndexWriterConfig(this.analyzer) .setOpenMode(CREATE_OR_APPEND); - try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { - indexWriter.updateDocuments(deleteQuery, docs); - } catch (IOException e) { - throw Exceptions.propagate(e); - } finally { - this.refreshSearcherManager(); + synchronized (this) { + try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { + indexWriter.updateDocuments(deleteQuery, docs); + } catch (IOException e) { + throw Exceptions.propagate(e); + } finally { + this.refreshSearcherManager(); + } } } @@ -122,12 +124,14 @@ public class LuceneSearchEngine implements SearchEngine, InitializingBean, Dispo var deleteQuery = new TermInSetQuery("id", terms); var writerConfig = new IndexWriterConfig(this.analyzer) .setOpenMode(CREATE_OR_APPEND); - try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { - indexWriter.deleteDocuments(deleteQuery); - } catch (IOException e) { - throw Exceptions.propagate(e); - } finally { - this.refreshSearcherManager(); + synchronized (this) { + try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { + indexWriter.deleteDocuments(deleteQuery); + } catch (IOException e) { + throw Exceptions.propagate(e); + } finally { + this.refreshSearcherManager(); + } } } @@ -135,12 +139,14 @@ public class LuceneSearchEngine implements SearchEngine, InitializingBean, Dispo public void deleteAll() { var writerConfig = new IndexWriterConfig(this.analyzer) .setOpenMode(CREATE_OR_APPEND); - try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { - indexWriter.deleteAll(); - } catch (IOException e) { - throw Exceptions.propagate(e); - } finally { - this.refreshSearcherManager(); + synchronized (this) { + try (var indexWriter = new IndexWriter(this.directory, writerConfig)) { + indexWriter.deleteAll(); + } catch (IOException e) { + throw Exceptions.propagate(e); + } finally { + this.refreshSearcherManager(); + } } } diff --git a/application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineTest.java b/application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineTest.java index a06f0026b..688750d51 100644 --- a/application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineTest.java +++ b/application/src/test/java/run/halo/app/search/lucene/LuceneSearchEngineTest.java @@ -3,12 +3,19 @@ package run.halo.app.search.lucene; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.IntStream; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.store.AlreadyClosedException; import org.junit.jupiter.api.AfterEach; @@ -76,6 +83,35 @@ class LuceneSearchEngineTest { assertEquals(0, reader.getDocCount("id")); } + @Test + void shouldAddOrUpdateDocumentConcurrently() + throws ExecutionException, InterruptedException, TimeoutException { + runConcurrently(() -> { + var haloDoc = createFakeHaloDoc(); + searchEngine.addOrUpdate(List.of(haloDoc)); + }); + } + + @Test + void shouldDeleteDocumentConcurrently() + throws ExecutionException, InterruptedException, TimeoutException { + runConcurrently(() -> { + var haloDoc = createFakeHaloDoc(); + searchEngine.addOrUpdate(List.of(haloDoc)); + searchEngine.deleteDocument(List.of(haloDoc.getId())); + }); + } + + @Test + void shouldDeleteAllConcurrently() + throws ExecutionException, InterruptedException, TimeoutException { + runConcurrently(() -> { + var haloDoc = createFakeHaloDoc(); + searchEngine.addOrUpdate(List.of(haloDoc)); + searchEngine.deleteAll(); + }); + } + @Test void shouldDestroy() throws Exception { var directory = this.searchEngine.getDirectory(); @@ -118,6 +154,17 @@ class LuceneSearchEngineTest { assertEquals("fake-content", gotHaloDoc.getContent()); } + void runConcurrently(Runnable runnable) + throws ExecutionException, InterruptedException, TimeoutException { + var executorService = Executors.newFixedThreadPool(10); + var futures = IntStream.of(0, 10) + .mapToObj(i -> CompletableFuture.runAsync(runnable, executorService)) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).get(10, TimeUnit.SECONDS); + executorService.shutdownNow(); + assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS)); + } + HaloDocument createFakeHaloDoc() { var haloDoc = new HaloDocument(); haloDoc.setId("fake-id");