From 474f4e6f4a7324d390799856a13fc325e90526e0 Mon Sep 17 00:00:00 2001 From: Tushar Naik Date: Sat, 25 Feb 2023 00:15:30 +0530 Subject: [PATCH 1/4] Added a field validator to handle issues with null indexing + better logging --- forage-core/pom.xml | 2 +- forage-dropwizard-bundle/pom.xml | 2 +- .../dropwizard/bundle/ForageBundle.java | 3 +- .../dropwizard/bundle/ForageBundleTest.java | 18 +++++++++-- forage-models/pom.xml | 2 +- .../models/result/field/FloatField.java | 12 ++++++++ .../forage/models/result/field/IntField.java | 12 ++++++-- .../models/result/field/StringField.java | 7 +++-- .../forage/models/result/field/TextField.java | 7 +++-- forage-search-engine/pom.xml | 2 +- .../SearchEngineSwapReferenceHandler.java | 4 ++- .../engine/lucene/LuceneDocumentHandler.java | 5 +++- .../search/engine/lucene/LuceneIndex.java | 2 -- .../engine/lucene/LuceneIndexInstance.java | 27 ++++++++++------- ...mpedLockCloseForageLuceneSearchEngine.java | 2 +- .../lucene/field/LuceneFieldValidator.java | 30 +++++++++++++++++++ ...ion.java => ExceptionWrappedExecutor.java} | 2 +- .../forage/search/engine/util/Utils.java | 1 + ...chEngineTest.java => ForageQueryTest.java} | 2 +- .../LuceneSearchEngineIndexingTest.java | 22 +++++++++++++- pom.xml | 2 +- 21 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java rename forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/{ExceptionExecution.java => ExceptionWrappedExecutor.java} (96%) rename forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/{LuceneSearchEngineTest.java => ForageQueryTest.java} (99%) diff --git a/forage-core/pom.xml b/forage-core/pom.xml index 8f3b954..2adf297 100644 --- a/forage-core/pom.xml +++ b/forage-core/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.7 + 1.0.8-SNAPSHOT 4.0.0 diff --git a/forage-dropwizard-bundle/pom.xml b/forage-dropwizard-bundle/pom.xml index 990d893..a654365 100644 --- a/forage-dropwizard-bundle/pom.xml +++ b/forage-dropwizard-bundle/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.7 + 1.0.8-SNAPSHOT 4.0.0 diff --git a/forage-dropwizard-bundle/src/main/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundle.java b/forage-dropwizard-bundle/src/main/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundle.java index 29b3058..cf7d0e7 100644 --- a/forage-dropwizard-bundle/src/main/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundle.java +++ b/forage-dropwizard-bundle/src/main/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundle.java @@ -97,7 +97,8 @@ public void start() { updateEngineRef.set(updateEngine); updateEngineRef.get().start(); - log.info("[forage][startup] .. Done starting engine and setting up periodic updates"); + log.info("[forage][startup] .. Done starting engine and setting up periodic updates, every {}s", + forageConfiguration(configuration).getRefreshIntervalInSeconds()); } @Override diff --git a/forage-dropwizard-bundle/src/test/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundleTest.java b/forage-dropwizard-bundle/src/test/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundleTest.java index fa66a12..9f1401c 100644 --- a/forage-dropwizard-bundle/src/test/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundleTest.java +++ b/forage-dropwizard-bundle/src/test/java/com/livetheoogway/forage/dropwizard/bundle/ForageBundleTest.java @@ -19,10 +19,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.livetheoogway.forage.core.Bootstrapper; import com.livetheoogway.forage.dropwizard.bundle.model.Book; +import com.livetheoogway.forage.models.query.util.QueryBuilder; import com.livetheoogway.forage.models.result.ForageQueryResult; import com.livetheoogway.forage.search.engine.exception.ForageErrorCode; import com.livetheoogway.forage.search.engine.exception.ForageSearchError; -import com.livetheoogway.forage.models.query.util.QueryBuilder; import com.livetheoogway.forage.search.engine.model.index.ForageDocument; import com.livetheoogway.forage.search.engine.model.index.IndexableDocument; import com.livetheoogway.forage.search.engine.store.Store; @@ -33,6 +33,7 @@ import io.dropwizard.lifecycle.setup.LifecycleEnvironment; import io.dropwizard.setup.AdminEnvironment; import io.dropwizard.setup.Environment; +import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -43,11 +44,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@Slf4j class ForageBundleTest { private final HealthCheckRegistry healthChecks = mock(HealthCheckRegistry.class); private final MetricRegistry metricRegistry = new MetricRegistry(); @@ -56,6 +60,8 @@ class ForageBundleTest { private final Environment environment = mock(Environment.class); private final AdminEnvironment adminEnvironment = mock(AdminEnvironment.class); + private final CountDownLatch countDownLatch = new CountDownLatch(1); + @BeforeEach public void setUp() { when(jerseyEnvironment.getResourceConfig()).thenReturn(new DropwizardResourceConfig()); @@ -68,7 +74,7 @@ public void setUp() { } @Test - void testBundleExecution() throws ForageSearchError { + void testBundleExecution() throws ForageSearchError, InterruptedException { final BookStore store = new BookStore(); final ForageBundle bundle = new ForageBundle<>() { @@ -119,6 +125,14 @@ public ForageConfiguration forageConfiguration(final SampleConfig configuration) return false; } }); + + log.info("Pausing for 3 seconds..."); + final boolean await = countDownLatch.await(3, TimeUnit.SECONDS); + if (await) { + Assertions.fail(); + } + + /* query should work even after a few more refreshes */ final ForageQueryResult results = bundle.searchEngine().search( QueryBuilder.matchQuery("author", "rowling").buildForageQuery()); Assertions.assertEquals("id1", results.getMatchingResults().get(0).getId()); diff --git a/forage-models/pom.xml b/forage-models/pom.xml index 565d6ad..772ceeb 100644 --- a/forage-models/pom.xml +++ b/forage-models/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.7 + 1.0.8-SNAPSHOT 4.0.0 diff --git a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/FloatField.java b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/FloatField.java index 0afc039..2da064d 100644 --- a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/FloatField.java +++ b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/FloatField.java @@ -18,6 +18,8 @@ import lombok.ToString; import lombok.Value; +import java.util.Arrays; + @Value @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -35,4 +37,14 @@ public FloatField(final String name, final float[] points) { public T accept(final FieldVisitor fieldVisitor) { return fieldVisitor.visit(this); } + + @Override + public String toString() { + return new StringBuilder().append("FLOAT[") + .append(name) + .append(":") + .append(Arrays.toString(points)) + .append("]") + .toString(); + } } diff --git a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/IntField.java b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/IntField.java index ca3d5f3..3e35661 100644 --- a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/IntField.java +++ b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/IntField.java @@ -15,13 +15,13 @@ package com.livetheoogway.forage.models.result.field; import lombok.EqualsAndHashCode; -import lombok.ToString; import lombok.Value; +import java.util.Arrays; + @Value @EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IntField extends Field{ +public class IntField extends Field { String name; int[] points; @@ -35,4 +35,10 @@ public IntField(final String name, final int[] points) { public T accept(final FieldVisitor fieldVisitor) { return fieldVisitor.visit(this); } + + @Override + public String toString() { + return new StringBuilder().append("INT[").append(name) + .append(":").append(Arrays.toString(points)).append("]").toString(); + } } diff --git a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/StringField.java b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/StringField.java index 93c9754..4332678 100644 --- a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/StringField.java +++ b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/StringField.java @@ -15,7 +15,6 @@ package com.livetheoogway.forage.models.result.field; import lombok.EqualsAndHashCode; -import lombok.ToString; import lombok.Value; /** @@ -23,7 +22,6 @@ */ @Value @EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class StringField extends Field { String name; String value; @@ -38,4 +36,9 @@ public StringField(final String name, final String value) { public T accept(final FieldVisitor fieldVisitor) { return fieldVisitor.visit(this); } + + @Override + public String toString() { + return new StringBuilder().append("STRING[").append(name).append(":").append(value).append("]").toString(); + } } diff --git a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/TextField.java b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/TextField.java index bd5c8c9..0408736 100644 --- a/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/TextField.java +++ b/forage-models/src/main/java/com/livetheoogway/forage/models/result/field/TextField.java @@ -15,7 +15,6 @@ package com.livetheoogway.forage.models.result.field; import lombok.EqualsAndHashCode; -import lombok.ToString; import lombok.Value; /** @@ -23,7 +22,6 @@ */ @Value @EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class TextField extends Field { String name; String value; @@ -38,4 +36,9 @@ public TextField(String name, String value) { public T accept(FieldVisitor fieldVisitor) { return fieldVisitor.visit(this); } + + @Override + public String toString() { + return new StringBuilder().append("TEXT[").append(name).append(":").append(value).append("]").toString(); + } } diff --git a/forage-search-engine/pom.xml b/forage-search-engine/pom.xml index 967ad18..ea640bc 100644 --- a/forage-search-engine/pom.xml +++ b/forage-search-engine/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.7 + 1.0.8-SNAPSHOT 4.0.0 diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java index fed6fd6..6b009d8 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java @@ -84,10 +84,12 @@ public void consume(final T indexableDocument) throws Exception { @Override public void finish() throws IOException, ForageSearchError { val writeStamp = lock.writeLock(); - try (DocumentIndexer ignored = liveReference.get()) { + try { + final DocumentIndexer referenceToBeSwapped = liveReference.get(); newReferenceBeingBuilt.get().flush(); liveReference.set(newReferenceBeingBuilt.get()); log.info("[forage] reference successfully swapped. Indexed: {}", counter.get()); + referenceToBeSwapped.close(); } finally { newReferenceBeingBuilt.set(null); lock.unlockWrite(writeStamp); diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneDocumentHandler.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneDocumentHandler.java index 5f04ec4..9209a39 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneDocumentHandler.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneDocumentHandler.java @@ -14,8 +14,9 @@ package com.livetheoogway.forage.search.engine.lucene; -import com.livetheoogway.forage.search.engine.model.index.DocumentVisitor; import com.livetheoogway.forage.search.engine.lucene.field.LuceneFieldHandler; +import com.livetheoogway.forage.search.engine.lucene.field.LuceneFieldValidator; +import com.livetheoogway.forage.search.engine.model.index.DocumentVisitor; import com.livetheoogway.forage.search.engine.model.index.ForageDocument; import com.livetheoogway.forage.search.engine.model.index.LuceneDocument; import lombok.val; @@ -26,12 +27,14 @@ public class LuceneDocumentHandler implements DocumentVisitor { private static final String ID = "__ID__"; private final LuceneFieldHandler fieldGenerator = new LuceneFieldHandler(); + private final LuceneFieldValidator fieldValidator = new LuceneFieldValidator(); @Override public Document visit(final ForageDocument forageDocument) { val document = new Document(); forageDocument.getFields() .stream() + .filter(field -> field.accept(fieldValidator)) .map(field -> field.accept(fieldGenerator)) .forEach(document::add); document.add(new StringField(ID, forageDocument.id(), Field.Store.YES)); diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndex.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndex.java index 5aa1c86..c1b2482 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndex.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndex.java @@ -28,6 +28,4 @@ public interface LuceneIndex extends Closeable { void flush() throws ForageSearchError; DocRetriever docRetriever(); - - LuceneIndex freshIndex() throws ForageSearchError; } diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java index 41772bf..af1f2a3 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java @@ -16,7 +16,7 @@ import com.livetheoogway.forage.search.engine.exception.ForageErrorCode; import com.livetheoogway.forage.search.engine.exception.ForageSearchError; -import com.livetheoogway.forage.search.engine.util.ExceptionExecution; +import com.livetheoogway.forage.search.engine.util.ExceptionWrappedExecutor; import com.livetheoogway.forage.search.engine.util.Utils; import lombok.extern.slf4j.Slf4j; import org.apache.lucene.analysis.Analyzer; @@ -49,26 +49,30 @@ public LuceneIndexInstance(Analyzer analyzer) { indexReaderReference = new AtomicReference<>(null); } - public static Directory newInMemoryIndex() { + private static Directory newInMemoryIndex() { return new ByteBuffersDirectory(); } @Override public void close() { + log.info("Closing all references.."); if (indexReaderReference.get() != null) { Utils.closeSafe(indexReaderReference.get().getIndexReader(), "IndexReader"); } Utils.closeSafe(indexWriterReference.get(), "IndexWriter"); Utils.closeSafe(memoryIndex, "MemoryIndex"); + log.info("All references closed successfully.."); } @Override public IndexSearcher searcher() throws ForageSearchError { if (indexWriterReferenceChanged.get()) { - synchronized (indexWriterReferenceChanged) { + synchronized (indexWriterReference) { if (indexWriterReferenceChanged.get()) { - final IndexReader indexReader = ExceptionExecution.get(() -> DirectoryReader.open(memoryIndex), - ForageErrorCode.INDEX_READER_IO_ERROR); + log.info("[forage] indexReader is being initialized"); + final IndexReader indexReader = ExceptionWrappedExecutor.get( + () -> DirectoryReader.open(memoryIndex), + ForageErrorCode.INDEX_READER_IO_ERROR); final IndexSearcher searcher = new IndexSearcher(indexReader); final DocRetriever docRetriever = new DocRetriever(indexReader, searcher); final DocRetriever docRetrieverToBeClosed = indexReaderReference.get(); @@ -80,6 +84,9 @@ public IndexSearcher searcher() throws ForageSearchError { } } } + log.info("[forage] returning reference to searcher id:{}, numDocs:{}", this.hashCode(), + indexReaderReference.get().getIndexReader().numDocs()); + return indexReaderReference.get().getSearcher(); } @@ -91,13 +98,16 @@ public IndexWriter indexWriter() throws ForageSearchError { current writer */ if (indexWriterReference.get() == null) { IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); - final IndexWriter indexWriter = ExceptionExecution.get( + final IndexWriter indexWriter = ExceptionWrappedExecutor.get( () -> new IndexWriter(memoryIndex, indexWriterConfig), ForageErrorCode.INDEX_WRITER_IO_ERROR); indexWriterReference.set(indexWriter); } } } + log.info("[forage] returning reference to writer id:{}, ramDocs:{} pendingDocs:{}", this.hashCode(), + indexWriterReference.get().numRamDocs(), indexWriterReference.get().getPendingNumDocs()); + return indexWriterReference.get(); } @@ -118,9 +128,4 @@ public synchronized void flush() throws ForageSearchError { public DocRetriever docRetriever() { return indexReaderReference.get(); } - - @Override - public LuceneIndex freshIndex() { - return new LuceneIndexInstance(this.analyzer); - } } diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/StampedLockCloseForageLuceneSearchEngine.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/StampedLockCloseForageLuceneSearchEngine.java index b6f6249..82b91e1 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/StampedLockCloseForageLuceneSearchEngine.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/StampedLockCloseForageLuceneSearchEngine.java @@ -28,7 +28,7 @@ /** * This class uses a {@link StampedLock} to ensure that the {@link StampedLockCloseForageLuceneSearchEngine#close()} - * happens only after taking a write lock, and no more new search queries are accepted + * happens only after taking a write-lock, and no more new search queries are accepted */ public class StampedLockCloseForageLuceneSearchEngine extends ForageLuceneSearchEngine { diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java new file mode 100644 index 0000000..405b8d4 --- /dev/null +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java @@ -0,0 +1,30 @@ +package com.livetheoogway.forage.search.engine.lucene.field; + +import com.google.common.base.Strings; +import com.livetheoogway.forage.models.result.field.FieldVisitor; +import com.livetheoogway.forage.models.result.field.FloatField; +import com.livetheoogway.forage.models.result.field.IntField; +import com.livetheoogway.forage.models.result.field.StringField; +import com.livetheoogway.forage.models.result.field.TextField; + +public class LuceneFieldValidator implements FieldVisitor { + @Override + public Boolean visit(final TextField textField) { + return !Strings.isNullOrEmpty(textField.getName()) && !Strings.isNullOrEmpty(textField.getValue()); + } + + @Override + public Boolean visit(final StringField stringField) { + return !Strings.isNullOrEmpty(stringField.getName()) && !Strings.isNullOrEmpty(stringField.getValue()); + } + + @Override + public Boolean visit(final FloatField floatField) { + return !Strings.isNullOrEmpty(floatField.getName()) && floatField.getPoints().length > 0; + } + + @Override + public Boolean visit(final IntField intField) { + return !Strings.isNullOrEmpty(intField.getName()) && intField.getPoints().length > 0; + } +} diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionExecution.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java similarity index 96% rename from forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionExecution.java rename to forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java index b26aa85..e3feb39 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionExecution.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java @@ -19,7 +19,7 @@ import lombok.experimental.UtilityClass; @UtilityClass -public class ExceptionExecution { +public class ExceptionWrappedExecutor { public T get(final ESupplier supplier, final ForageErrorCode errorCode) throws ForageSearchError { try { return supplier.get(); diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/Utils.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/Utils.java index 73cb790..04caf6e 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/Utils.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/Utils.java @@ -30,6 +30,7 @@ public void closeSafe(Closeable closeable, String type) { } try { closeable.close(); + log.info("[forage] Safely closed {}", type); } catch (IOException e) { log.error("[forage] Error closing {}", type, e); } diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageQueryTest.java similarity index 99% rename from forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineTest.java rename to forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageQueryTest.java index b24386a..df955a3 100644 --- a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineTest.java +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageQueryTest.java @@ -36,7 +36,7 @@ import java.util.stream.Collectors; @Slf4j -class LuceneSearchEngineTest { +class ForageQueryTest { private static ForageLuceneSearchEngine searchEngine; @BeforeAll diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineIndexingTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineIndexingTest.java index b234edf..539c80a 100644 --- a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineIndexingTest.java +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/LuceneSearchEngineIndexingTest.java @@ -40,7 +40,7 @@ void setUp() throws ForageSearchError { } @Test - void simpleIndexedSearch() throws ForageSearchError { + void simpleIndexedSearchShouldWorkOnValidFields() throws ForageSearchError { store.store(new SomeObject("Some data")); searchEngine.index(ForageDocument.builder() .id("ID1") @@ -59,6 +59,26 @@ void simpleIndexedSearch() throws ForageSearchError { Assertions.assertEquals("Some data", representation); } + @Test + void indexingFieldsWithNullValuesShouldContinueToWork() throws ForageSearchError { + store.store(new SomeObject("Some data")); + searchEngine.index(ForageDocument.builder() + .id("ID1") + .field(new TextField("pod", "nexus")) + .field(new TextField("app", null)) + .build()); + searchEngine.flush(); + + + final ForageQueryResult result + = searchEngine.search(QueryBuilder.matchQuery("pod", "nexus").buildForageQuery()); + Assertions.assertEquals(1, result.getMatchingResults().size()); + final String representation = ResultUtil.getRepresentation(result, + data -> data.getData().getData(), + (a, b) -> a + "\n" + b); + Assertions.assertEquals("Some data", representation); + } + @AllArgsConstructor private static class SomeObject implements DataId { diff --git a/pom.xml b/pom.xml index 9e90482..d3d09b6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.livetheoogway.forage forage pom - 1.0.7 + 1.0.8-SNAPSHOT forage From d8766507c3a20acf84d579faa8267f183a47ced4 Mon Sep 17 00:00:00 2001 From: Tushar Naik Date: Sat, 25 Feb 2023 00:36:43 +0530 Subject: [PATCH 2/4] Ensuring that only 1 bootstrap runs at any given point in time using a simple atomic boolean --- .../forage/core/AsyncQueuedConsumer.java | 1 + .../forage/core/UpdateEngine.java | 35 +++++++++---- .../SearchEngineSwapReferenceHandler.java | 3 +- .../lucene/ForageLuceneSearchEngineTest.java | 2 +- ...odicallyUpdatedForageSearchEngineTest.java | 50 ++++++++++++++++++- 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/forage-core/src/main/java/com/livetheoogway/forage/core/AsyncQueuedConsumer.java b/forage-core/src/main/java/com/livetheoogway/forage/core/AsyncQueuedConsumer.java index d6c0110..dcdc4e5 100644 --- a/forage-core/src/main/java/com/livetheoogway/forage/core/AsyncQueuedConsumer.java +++ b/forage-core/src/main/java/com/livetheoogway/forage/core/AsyncQueuedConsumer.java @@ -141,6 +141,7 @@ private void finishListener() { consumer.finish(); } catch (Exception e) { log.error("[forage] Error while finishing the listener", e); + itemConsumptionErrorHandler.handleError(null, e); } } diff --git a/forage-core/src/main/java/com/livetheoogway/forage/core/UpdateEngine.java b/forage-core/src/main/java/com/livetheoogway/forage/core/UpdateEngine.java index 539821a..89f426a 100644 --- a/forage-core/src/main/java/com/livetheoogway/forage/core/UpdateEngine.java +++ b/forage-core/src/main/java/com/livetheoogway/forage/core/UpdateEngine.java @@ -17,6 +17,8 @@ import com.livetheoogway.forage.models.DataId; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.atomic.AtomicBoolean; + /** * This is the main update engine that is responsible for exposing the boostrap function * @@ -28,6 +30,7 @@ public abstract class UpdateEngine { private final Bootstrapper bootstrapper; private final ItemConsumer itemConsumer; private final ErrorHandler errorHandler; + private final AtomicBoolean alreadyRunning; protected UpdateEngine(final Bootstrapper bootstrapper, final ItemConsumer itemConsumer, @@ -35,23 +38,33 @@ protected UpdateEngine(final Bootstrapper bootstrapper, this.bootstrapper = bootstrapper; this.itemConsumer = itemConsumer; this.errorHandler = errorHandler; + this.alreadyRunning = new AtomicBoolean(false); } /** * the primary function that is supposed to bootstrap all items into the consumer */ public void bootstrap() throws Exception { - log.info("[forage] Bootstrapping forage ..."); - itemConsumer.init(); - bootstrapper.bootstrap(item -> { - try { - itemConsumer.consume(item); - } catch (Exception e) { - errorHandler.handleError(item, e); - } - }); - itemConsumer.finish(); - log.info("[forage] ... Bootstrapping forage done"); + if (alreadyRunning.get()) { + log.warn("A bootstrap is already running, not bootstrapping again.."); + return; + } + alreadyRunning.set(true); + try { + log.info("[forage] Bootstrapping forage ..."); + itemConsumer.init(); + bootstrapper.bootstrap(item -> { + try { + itemConsumer.consume(item); + } catch (Exception e) { + errorHandler.handleError(item, e); + } + }); + itemConsumer.finish(); + log.info("[forage] ... Bootstrapping forage done"); + } finally { + alreadyRunning.set(false); + } } /** diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java index 6b009d8..3dc9008 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/core/SearchEngineSwapReferenceHandler.java @@ -17,6 +17,7 @@ import com.livetheoogway.forage.core.ItemConsumer; import com.livetheoogway.forage.search.engine.DocumentIndexer; import com.livetheoogway.forage.search.engine.exception.ForageSearchError; +import com.livetheoogway.forage.search.engine.util.Utils; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -89,7 +90,7 @@ public void finish() throws IOException, ForageSearchError { newReferenceBeingBuilt.get().flush(); liveReference.set(newReferenceBeingBuilt.get()); log.info("[forage] reference successfully swapped. Indexed: {}", counter.get()); - referenceToBeSwapped.close(); + Utils.closeSafe(referenceToBeSwapped, "referenceToBeSwapped"); } finally { newReferenceBeingBuilt.set(null); lock.unlockWrite(writeStamp); diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageLuceneSearchEngineTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageLuceneSearchEngineTest.java index dae50b7..8f95ae2 100644 --- a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageLuceneSearchEngineTest.java +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/ForageLuceneSearchEngineTest.java @@ -11,7 +11,7 @@ class ForageLuceneSearchEngineTest { @Test - void testScenarioWhereThereIsNothingToIndex() throws ForageSearchError { + void testThatThereAreNoErrorsWhenThereIsNothingToIndex() throws ForageSearchError { ForageLuceneSearchEngine searchEngine; InMemoryHashStore dataStore = new InMemoryHashStore<>(); searchEngine = ForageSearchEngineBuilder.builder() diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/PeriodicallyUpdatedForageSearchEngineTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/PeriodicallyUpdatedForageSearchEngineTest.java index f40462f..fd5ec7f 100644 --- a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/PeriodicallyUpdatedForageSearchEngineTest.java +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/PeriodicallyUpdatedForageSearchEngineTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; class PeriodicallyUpdatedForageSearchEngineTest { @@ -102,6 +103,52 @@ void testPeriodicallyUpdatedQueryEngine() throws Exception { periodicUpdateEngine.stop(); } + @Test + void testErrorsWhenBootstrappingSimultaneously() throws Exception { + /* This test is being written because we observe an NPE when parallel bootstrap runs happen */ + + final BookDataStore dataStore = new BookDataStore(); + final ForageEngineIndexer luceneQueryEngineContainer = new ForageEngineIndexer<>( + ForageSearchEngineBuilder.builder() + .withDataStore(dataStore) + .withObjectMapper(TestUtils.mapper())); + + dataStore.addBooks(1); + + AtomicBoolean wasErrorTrapped = new AtomicBoolean(false); + + /* we will set up a trap using the error handler in AsyncQueuedConsumer */ + final PeriodicUpdateEngine periodicUpdateEngine = + new PeriodicUpdateEngine<>( + dataStore, new AsyncQueuedConsumer<>(luceneQueryEngineContainer, 10, + (indexableDocument, e) -> wasErrorTrapped.set(true)), + 1, TimeUnit.SECONDS + ); + + /* while the periodic update happens in background (and calls the bootstrap), we will invoke bootstrap + forcefully */ + periodicUpdateEngine.start(); + periodicUpdateEngine.bootstrap(); + + Awaitility.await().atMost(Duration.of(50, ChronoUnit.SECONDS)) + .with() + .pollInterval(Duration.of(100, ChronoUnit.MILLIS)) + .ignoreExceptionsMatching(throwable -> throwable instanceof ForageSearchError + && ((ForageSearchError) throwable).getForageErrorCode() + == ForageErrorCode.QUERY_ENGINE_NOT_INITIALIZED_YET) + .until(() -> { + final ForageQueryResult query = luceneQueryEngineContainer.search( + new ForageSearchQuery(new RangeQuery("numPage", new IntRange(0, 100000)), 10)); + return query.getTotal().getTotal() == 1; + }); + + if (wasErrorTrapped.get()) { + Assertions.fail("There was an error in the async handler"); + } + + periodicUpdateEngine.stop(); + } + @Test void testPeriodicallyUpdatedQueryEngineWithFrequentQueries() throws Exception { final BookDataStore dataStore = new BookDataStore(); @@ -113,7 +160,8 @@ void testPeriodicallyUpdatedQueryEngineWithFrequentQueries() throws Exception { performParallelSearchExecutions(dataStore, luceneQueryEngineContainer); } - private void performParallelSearchExecutions(final BookDataStore dataStore, final ForageEngineIndexer luceneQueryEngineContainer) + private void performParallelSearchExecutions(final BookDataStore dataStore, + final ForageEngineIndexer luceneQueryEngineContainer) throws Exception { dataStore.addBooks(1); From 4a447c5fc22252bc9589ea77b5820a79ef25a99a Mon Sep 17 00:00:00 2001 From: Tushar Naik Date: Sat, 25 Feb 2023 00:42:15 +0530 Subject: [PATCH 3/4] bumping version to 1.0.8 for release and cleaned up logs --- forage-core/pom.xml | 2 +- forage-dropwizard-bundle/pom.xml | 2 +- forage-models/pom.xml | 2 +- forage-search-engine/pom.xml | 2 +- .../forage/search/engine/lucene/LuceneIndexInstance.java | 4 ++-- pom.xml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/forage-core/pom.xml b/forage-core/pom.xml index 2adf297..a674341 100644 --- a/forage-core/pom.xml +++ b/forage-core/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.8-SNAPSHOT + 1.0.8 4.0.0 diff --git a/forage-dropwizard-bundle/pom.xml b/forage-dropwizard-bundle/pom.xml index a654365..d3368f6 100644 --- a/forage-dropwizard-bundle/pom.xml +++ b/forage-dropwizard-bundle/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.8-SNAPSHOT + 1.0.8 4.0.0 diff --git a/forage-models/pom.xml b/forage-models/pom.xml index 772ceeb..766a80e 100644 --- a/forage-models/pom.xml +++ b/forage-models/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.8-SNAPSHOT + 1.0.8 4.0.0 diff --git a/forage-search-engine/pom.xml b/forage-search-engine/pom.xml index ea640bc..bf3b0a2 100644 --- a/forage-search-engine/pom.xml +++ b/forage-search-engine/pom.xml @@ -5,7 +5,7 @@ forage com.livetheoogway.forage - 1.0.8-SNAPSHOT + 1.0.8 4.0.0 diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java index af1f2a3..b198faf 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/LuceneIndexInstance.java @@ -84,7 +84,7 @@ public IndexSearcher searcher() throws ForageSearchError { } } } - log.info("[forage] returning reference to searcher id:{}, numDocs:{}", this.hashCode(), + log.debug("[forage] returning reference to searcher id:{}, numDocs:{}", this.hashCode(), indexReaderReference.get().getIndexReader().numDocs()); return indexReaderReference.get().getSearcher(); @@ -105,7 +105,7 @@ public IndexWriter indexWriter() throws ForageSearchError { } } } - log.info("[forage] returning reference to writer id:{}, ramDocs:{} pendingDocs:{}", this.hashCode(), + log.debug("[forage] returning reference to writer id:{}, ramDocs:{} pendingDocs:{}", this.hashCode(), indexWriterReference.get().numRamDocs(), indexWriterReference.get().getPendingNumDocs()); return indexWriterReference.get(); diff --git a/pom.xml b/pom.xml index d3d09b6..81d22d7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.livetheoogway.forage forage pom - 1.0.8-SNAPSHOT + 1.0.8 forage From 1109644470ee4b408381653be482288cb53f9492 Mon Sep 17 00:00:00 2001 From: Tushar Naik Date: Sat, 25 Feb 2023 00:58:04 +0530 Subject: [PATCH 4/4] Added few tests on new code --- .../lucene/field/LuceneFieldValidator.java | 48 +++++++++++++++++-- .../engine/util/ExceptionWrappedExecutor.java | 3 ++ .../field/LuceneFieldValidatorTest.java | 47 ++++++++++++++++++ .../util/ExceptionWrappedExecutorTest.java | 33 +++++++++++++ 4 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidatorTest.java create mode 100644 forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutorTest.java diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java index 405b8d4..66086c4 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidator.java @@ -1,3 +1,17 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * 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 com.livetheoogway.forage.search.engine.lucene.field; import com.google.common.base.Strings; @@ -6,25 +20,51 @@ import com.livetheoogway.forage.models.result.field.IntField; import com.livetheoogway.forage.models.result.field.StringField; import com.livetheoogway.forage.models.result.field.TextField; +import lombok.extern.slf4j.Slf4j; +/** + * Lucene has a bug where, if the value of a field is null, the entire document does not get indexed, without any + * errors or logs. This Validator silently ignores such fields with a warning + */ +@Slf4j public class LuceneFieldValidator implements FieldVisitor { + @Override public Boolean visit(final TextField textField) { - return !Strings.isNullOrEmpty(textField.getName()) && !Strings.isNullOrEmpty(textField.getValue()); + return executeIfFalseAndReturn( + !Strings.isNullOrEmpty(textField.getName()) && !Strings.isNullOrEmpty(textField.getValue()), + () -> log.warn("Null values/name for TextField: {}", textField.getName())); } @Override public Boolean visit(final StringField stringField) { - return !Strings.isNullOrEmpty(stringField.getName()) && !Strings.isNullOrEmpty(stringField.getValue()); + return executeIfFalseAndReturn( + !Strings.isNullOrEmpty(stringField.getName()) && !Strings.isNullOrEmpty(stringField.getValue()), + () -> log.warn("Null values/name for StringField: {}", stringField.getName())); } @Override public Boolean visit(final FloatField floatField) { - return !Strings.isNullOrEmpty(floatField.getName()) && floatField.getPoints().length > 0; + return executeIfFalseAndReturn( + !Strings.isNullOrEmpty(floatField.getName()) + && floatField.getPoints() != null + && floatField.getPoints().length > 0, + () -> log.warn("Null values/name for FloatField: {}", floatField.getName())); } @Override public Boolean visit(final IntField intField) { - return !Strings.isNullOrEmpty(intField.getName()) && intField.getPoints().length > 0; + return executeIfFalseAndReturn( + !Strings.isNullOrEmpty(intField.getName()) + && intField.getPoints() != null + && intField.getPoints().length > 0, + () -> log.warn("Null values/name for IntField: {}", intField.getName())); + } + + private boolean executeIfFalseAndReturn(final boolean check, final Runnable runnable) { + if (!check) { + runnable.run(); + } + return check; } } diff --git a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java index e3feb39..d08a78c 100644 --- a/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java +++ b/forage-search-engine/src/main/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutor.java @@ -24,6 +24,9 @@ public T get(final ESupplier supplier, final ForageErrorCode errorCode) t try { return supplier.get(); } catch (Exception e) { + if (e instanceof ForageSearchError) { + throw (ForageSearchError) e; + } throw new ForageSearchError(errorCode, e); } } diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidatorTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidatorTest.java new file mode 100644 index 0000000..2840bc4 --- /dev/null +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/lucene/field/LuceneFieldValidatorTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * 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 com.livetheoogway.forage.search.engine.lucene.field; + +import com.livetheoogway.forage.models.result.field.FloatField; +import com.livetheoogway.forage.models.result.field.IntField; +import com.livetheoogway.forage.models.result.field.StringField; +import com.livetheoogway.forage.models.result.field.TextField; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LuceneFieldValidatorTest { + @Test + void testCorrectFieldValidation() { + final LuceneFieldValidator luceneFieldValidator = new LuceneFieldValidator(); + + assertFalse(new TextField("", null).accept(luceneFieldValidator)); + assertFalse(new TextField("name", null).accept(luceneFieldValidator)); + assertTrue(new TextField("name", "value").accept(luceneFieldValidator)); + + assertFalse(new StringField("", null).accept(luceneFieldValidator)); + assertFalse(new StringField("name", null).accept(luceneFieldValidator)); + assertTrue(new StringField("name", "value").accept(luceneFieldValidator)); + + assertFalse(new IntField("", null).accept(luceneFieldValidator)); + assertFalse(new IntField("name", null).accept(luceneFieldValidator)); + assertTrue(new IntField("name", new int[]{1}).accept(luceneFieldValidator)); + + assertFalse(new FloatField("", null).accept(luceneFieldValidator)); + assertFalse(new FloatField("name", null).accept(luceneFieldValidator)); + assertTrue(new FloatField("name", new float[]{1.1f}).accept(luceneFieldValidator)); + } +} \ No newline at end of file diff --git a/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutorTest.java b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutorTest.java new file mode 100644 index 0000000..63e21cc --- /dev/null +++ b/forage-search-engine/src/test/java/com/livetheoogway/forage/search/engine/util/ExceptionWrappedExecutorTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * 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 com.livetheoogway.forage.search.engine.util; + +import com.livetheoogway.forage.search.engine.exception.ForageErrorCode; +import com.livetheoogway.forage.search.engine.exception.ForageSearchError; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ExceptionWrappedExecutorTest { + @Test + void testThatExceptionWrappedExecutorDoesItsJob() { + assertThrows(ForageSearchError.class, () -> ExceptionWrappedExecutor.get(() -> { + throw ForageSearchError.raise(ForageErrorCode.SOMETHING_WENT_WRONG, ""); + }, ForageErrorCode.SOMETHING_WENT_WRONG)); + assertThrows(ForageSearchError.class, () -> ExceptionWrappedExecutor.get(() -> { + throw new RuntimeException(); + }, ForageErrorCode.SOMETHING_WENT_WRONG)); + } +} \ No newline at end of file