From c297c655a2f56bfb54641aba5c077088688e1f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Wed, 24 Jan 2024 10:25:04 +0100 Subject: [PATCH] review commit: * added a cache to SearchIndexingSignalEnrichmentFacade in order to only evaluate "patterns" once for a given namespace * removed copy&pasted unit tests in SearchIndexingSignalEnrichmentFacadeTest by adding another abstract test class AbstractCachingSignalEnrichmentFacadeTest * added a unit test testing selection of JsonFieldSelectors based on different namespaces * minor cleanup and formatting * enhanced documentation about how to configure the indexed namespaces via system properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Jäckle --- .../pages/ditto/installation-operating.md | 44 +- .../DittoCachingSignalEnrichmentFacade.java | 8 +- .../SearchIndexingSignalEnrichmentFacade.java | 39 +- ...ractCachingSignalEnrichmentFacadeTest.java | 498 +++++++++++++++++ .../AbstractSignalEnrichmentFacadeTest.java | 6 +- ...ittoCachingSignalEnrichmentFacadeTest.java | 470 +--------------- ...rchIndexingSignalEnrichmentFacadeTest.java | 502 ++---------------- ...ndexingSignalEnrichmentFacadeProvider.java | 35 +- ...DefaultNamespaceSearchIndexConfigTest.java | 26 +- .../namespace-search-index-test.conf | 15 +- 10 files changed, 650 insertions(+), 993 deletions(-) create mode 100644 internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractCachingSignalEnrichmentFacadeTest.java diff --git a/documentation/src/main/resources/pages/ditto/installation-operating.md b/documentation/src/main/resources/pages/ditto/installation-operating.md index 594ce9cabc..c9f352b19d 100644 --- a/documentation/src/main/resources/pages/ditto/installation-operating.md +++ b/documentation/src/main/resources/pages/ditto/installation-operating.md @@ -330,24 +330,31 @@ The default behavior of Ditto is to index the complete JSON of a thing, which in * Increased load on the search database, leading to performance degradation and increased database cost. * Only a few fields are ever used for searching. -In Ditto *3.5.0*, there is now configuration to specify, by a namespace pattern, which fields will be included in the search database. +Since Ditto *3.5.0*, there is a configuration to specify, by a namespace pattern, which fields will be included in the search database. To enable this functionality, there are two new options in the `thing-search.conf` configuration: -``` +```hocon ditto { - ... + //... caching-signal-enrichment-facade-provider = org.eclipse.ditto.thingsearch.service.persistence.write.streaming.SearchIndexingSignalEnrichmentFacadeProvider - ... + //... search { namespace-search-include-fields = [ { - namespace-pattern = "org.eclipse", - search-include-fields = [ "attributes", "features/info" ] + namespace-pattern = "org.eclipse.test" + search-include-fields = [ + "attributes", + "features/info/properties", + "features/info/other" + ] }, { - namespace-pattern = "org.eclipse.test", - search-include-fields = [ "attributes", "features/info/properties/", "features/info/other" ] + namespace-pattern = "org.eclipse*" + search-include-fields = [ + "attributes", + "features/info" + ] } ] } @@ -356,10 +363,11 @@ ditto { There is a new implementation of the caching signal enrichment facade provider that must be configured to enable this functionality. -For each namespace pattern, only the selected fields are included in the search database. In the example above, for -things in the "org.eclipse" namespace, only the "attributes" and "features/info" paths will be the only fields indexed -in the search database. For things in the "org.eclipse.test" namespace, the fields indexed in the search database will -only be "attributes", "features/info/properties", and "features/info/other". +For each namespace pattern, only the selected fields are included in the search database. In the example above, for +things in the "org.eclipse.test" namespace, the fields indexed in the search database will +only be "attributes", "features/info/properties", and "features/info/other". +Things matching the "org.eclipse*" namespace, only the "attributes" and "features/info" paths will be the only fields +indexed in the search database. Important notes: * Ditto will use the namespace of the thing and match the FIRST namespace-pattern it encounters. So make sure any @@ -367,6 +375,18 @@ Important notes: * Ditto will automatically add the system-level fields it needs to operate, so no manual configuration of these is necessary. +Example for configuring the same configuration via system properties for the `things-search` service: + +```shell +-Dditto.search.namespace-search-include-fields.0.namespace-pattern=org.eclipse.test +-Dditto.search.namespace-search-include-fields.0.search-include-fields.0=attributes +-Dditto.search.namespace-search-include-fields.0.search-include-fields.1=features/info/properties +-Dditto.search.namespace-search-include-fields.0.search-include-fields.2=features/info/other +-Dditto.search.namespace-search-include-fields.1.namespace-pattern=org.eclipse* +-Dditto.search.namespace-search-include-fields.1.search-include-fields.0=attributes +-Dditto.search.namespace-search-include-fields.1.search-include-fields.1=features/info +``` + ## Logging Gathering logs for a running Ditto installation can be achieved by: diff --git a/internal/models/signalenrichment/src/main/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacade.java b/internal/models/signalenrichment/src/main/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacade.java index 0ba232f33b..0e17e8114f 100644 --- a/internal/models/signalenrichment/src/main/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacade.java +++ b/internal/models/signalenrichment/src/main/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacade.java @@ -28,11 +28,11 @@ import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.base.model.signals.Signal; import org.eclipse.ditto.base.model.signals.WithResource; -import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; -import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; import org.eclipse.ditto.internal.utils.cache.Cache; import org.eclipse.ditto.internal.utils.cache.CacheFactory; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; +import org.eclipse.ditto.internal.utils.pekko.logging.DittoLoggerFactory; +import org.eclipse.ditto.internal.utils.pekko.logging.ThreadSafeDittoLogger; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonFieldSelector; import org.eclipse.ditto.json.JsonObject; @@ -97,7 +97,7 @@ public CompletionStage retrieveThing(final ThingId thingId, final Li final DittoHeaders dittoHeaders = DittoHeaders.empty(); - JsonFieldSelector fieldSelector = determineSelector(thingId.getNamespace()); + final JsonFieldSelector fieldSelector = determineSelector(thingId.getNamespace()); if (minAcceptableSeqNr < 0) { final var cacheKey = @@ -450,7 +450,7 @@ private JsonObject enhanceJsonObject(final JsonObject jsonObject, final List> selectedIndexes; + private final Map selectedIndexesCache; private SearchIndexingSignalEnrichmentFacade( final List> selectedIndexes, @@ -49,7 +42,8 @@ private SearchIndexingSignalEnrichmentFacade( super(cacheLoaderFacade, cacheConfig, cacheLoaderExecutor, cacheNamePrefix); - this.selectedIndexes = Collections.unmodifiableList(selectedIndexes); + this.selectedIndexes = List.copyOf(selectedIndexes); + selectedIndexesCache = new HashMap<>(); } /** @@ -78,16 +72,15 @@ public static SearchIndexingSignalEnrichmentFacade newInstance( } @Override - protected JsonFieldSelector determineSelector(String namespace) { + protected JsonFieldSelector determineSelector(final String namespace) { - // We iterate through the list and return the first JsonFieldSelector that matches the namespace pattern. - for (final Pair pair : selectedIndexes) { - - if (pair.first().matcher(namespace).matches()) { - return pair.second(); - } + if (!selectedIndexesCache.containsKey(namespace)) { + // We iterate through the list and return the first JsonFieldSelector that matches the namespace pattern. + selectedIndexes.stream() + .filter(pair -> pair.first().matcher(namespace).matches()) + .findFirst() + .ifPresent(pair -> selectedIndexesCache.put(namespace, pair.second())); } - - return super.determineSelector(namespace); + return selectedIndexesCache.get(namespace); } } diff --git a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractCachingSignalEnrichmentFacadeTest.java b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractCachingSignalEnrichmentFacadeTest.java new file mode 100644 index 0000000000..2d51c3cc96 --- /dev/null +++ b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractCachingSignalEnrichmentFacadeTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.internal.models.signalenrichment; + +import java.time.Duration; +import java.util.concurrent.CompletionStage; + +import org.apache.pekko.actor.ActorSelection; +import org.apache.pekko.testkit.javadsl.TestKit; +import org.assertj.core.api.JUnitSoftAssertions; +import org.eclipse.ditto.base.model.auth.AuthorizationContext; +import org.eclipse.ditto.base.model.auth.AuthorizationSubject; +import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; +import org.eclipse.ditto.base.model.entity.metadata.Metadata; +import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory; +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.json.FieldType; +import org.eclipse.ditto.base.model.signals.DittoTestSystem; +import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; +import org.eclipse.ditto.internal.utils.cache.config.DefaultCacheConfig; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonFieldSelector; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingId; +import org.eclipse.ditto.things.model.ThingsModelFactory; +import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing; +import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse; +import org.eclipse.ditto.things.model.signals.events.AttributeDeleted; +import org.eclipse.ditto.things.model.signals.events.ThingMerged; +import org.junit.Rule; +import org.junit.Test; + +import com.typesafe.config.ConfigFactory; + +/** + * Abstract base test for different {@link SignalEnrichmentFacade} implementations providing caching. + */ +abstract class AbstractCachingSignalEnrichmentFacadeTest extends AbstractSignalEnrichmentFacadeTest { + + private static final String ISSUER_PREFIX = "test:"; + private static final String CACHE_CONFIG_KEY = "my-cache"; + private static final String CACHE_CONFIG = CACHE_CONFIG_KEY + """ + { + maximum-size = 10 + expire-after-create = 2m + } + """; + + private static final JsonObject THING_RESPONSE_JSON = JsonObject.of(""" + { + "_revision": 3, + "policyId": "policy:id", + "attributes": {"x": 5}, + "features": {"y": {"properties": {"z": true}}}, + "_metadata": {"attributes": {"x": {"type": "x attribute"}}} + }"""); + + + @Rule + public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); + + @Test + public void alreadyLoadedCacheEntryIsReused() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, + getThingEvent().setRevision(getThingEvent().getRevision() + 1)); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached.toCompletableFuture().join(); + softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); + }); + } + + @Test + public void alreadyLoadedCacheEntryIsReusedForMergedEvent() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead + final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/attributes"), + JsonObject.newBuilder() + .set("x", 42) + .set("foo", "bar") + .build(), + getThingEvent().getRevision() + 1, + null, + DittoHeaders.empty(), + null + ); + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached.toCompletableFuture().join(); + // AND: the resulting thing JSON includes the with the merge update updated value: + final JsonObject expectedThingJson = getExpectedThingJson().toBuilder() + .set("/attributes/x", 42) + .build(); + softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); + + // WHEN: then the attribute "x" is modified with a merge: + final ThingMerged mergeAttributeX = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), + JsonValue.of(1337), + mergeAttributes.getRevision() + 1, + null, + DittoHeaders.empty(), + null + ); + final CompletionStage askResultCached2 = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributeX); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached2.toCompletableFuture().join(); + // AND: the resulting thing JSON includes the with the merge update updated value: + final JsonObject expectedThingJson2 = getExpectedThingJson().toBuilder() + .set("/attributes/x", 1337) + .build(); + softly.assertThat(askResultCached2).isCompletedWithValue(expectedThingJson2); + }); + } + + @Test + public void alreadyLoadedCacheEntryIsReusedForMergedEventOnRootLevel() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead + final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/"), + JsonObject.newBuilder() + .set("attributes", + JsonObject.newBuilder() + .set("x", 42) + .set("foo", "bar") + .build()) + .build(), + getThingEvent().getRevision() + 1, + null, + DittoHeaders.empty(), + null + ); + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached.toCompletableFuture().join(); + }); + } + + @Test + public void alreadyLoadedCacheEntryIsInvalidatedForUnexpectedEventRevision() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final DittoHeaders headers = DittoHeaders.newBuilder().randomCorrelationId().build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector with event with 2 revisions ahead + final DittoHeaders headers2 = DittoHeaders.newBuilder().randomCorrelationId().build(); + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, + getThingEvent().setRevision(getThingEvent().getRevision() + 2)); // notice +2 here + + // THEN: do another cache lookup after invalidation + final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing2.getSelectedFields()) + .contains(actualSelectedFields(getJsonFieldSelector())); + final Thing thing2 = ThingsModelFactory.newThing(getThingResponseThingJson()); + final Thing thing2WithUpdatedRev = thing2.toBuilder() + .setRevision(thing2.getRevision().get().increment().increment()) + .build(); + kit.reply(RetrieveThingResponse.of(thingId, thing2WithUpdatedRev.toJson( + thing2WithUpdatedRev.getImplementedSchemaVersion(), FieldType.all()), headers2)); + askResultCached.toCompletableFuture().join(); + softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); + }); + } + + @Test + public void differentAuthSubjectsLeadToCacheRetrievals() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId1 = ISSUER_PREFIX + "user1"; + final String userId2 = ISSUER_PREFIX + "user2"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId1))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId1); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead but other auth subjects + final DittoHeaders headers2 = headers.toBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(ISSUER_PREFIX + "user2") + )) + .build(); + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, + getThingEvent().setRevision(getThingEvent().getRevision() + 1)); + + // THEN: a cache lookup should be done containing the other auth subject header + final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId2); + softly.assertThat(retrieveThing2.getSelectedFields()) + .contains(actualSelectedFields(getJsonFieldSelector())); + }); + } + + @Test + public void differentFieldSelectorsLeadToCacheRetrievals() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user1"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + final JsonFieldSelector selector2 = JsonFieldSelector.newInstance("attributes", "features"); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with different selector for an event with one revision ahead + underTest.retrievePartialThing(thingId, selector2, headers, + getThingEvent().setRevision(getThingEvent().getRevision() + 1)); + + // THEN: a cache lookup should be done using the other selector + final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(selector2)); + }); + } + + @Test + public void metadataIsUpdatedForMergedEvent() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead + final ThingMerged mergeAttribute = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), + JsonFactory.newValue(6), + getThingEvent().getRevision() + 1, + null, + DittoHeaders.empty(), + Metadata.newMetadata(JsonObject.newBuilder() + .set("type", "x is now y attribute") + .build() + ) + ); + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttribute); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached.toCompletableFuture().join(); + // AND: the resulting thing JSON includes the with the merged metadata updated value: + final JsonObject expectedThingJson = getExpectedThingJson().toBuilder() + .set("attributes", JsonObject.newBuilder() + .set("x", 6) + .build()) + .set("_metadata", JsonObject.newBuilder() + .set("attributes", JsonObject.newBuilder() + .set("x", JsonObject.newBuilder() + .set("type", "x is now y attribute") + .build()) + .build()) + .build()) + .build(); + + softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); + }); + } + + @Test + public void metadataIsDeletedForDeletedEvent() { + DittoTestSystem.run(this, kit -> { + // GIVEN: SignalEnrichmentFacade.retrievePartialThing() + final SignalEnrichmentFacade underTest = + createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); + final ThingId thingId = ThingId.generateRandom(); + final String userId = ISSUER_PREFIX + "user"; + final DittoHeaders headers = DittoHeaders.newBuilder() + .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, + AuthorizationSubject.newInstance(userId))) + .randomCorrelationId() + .build(); + final CompletionStage askResult = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); + + // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse + final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); + softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) + .contains(userId); + softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); + // WHEN: response is handled so that it is also added to the cache + kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); + askResult.toCompletableFuture().join(); + + softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + + // WHEN: same thing is asked again with same selector for an event with one revision ahead + final AttributeDeleted attributeDeleted = + AttributeDeleted.of(thingId, JsonPointer.of("/x"), + getThingEvent().getRevision() + 1, + null, + DittoHeaders.empty(), + MetadataModelFactory.nullMetadata()); + + final CompletionStage askResultCached = + underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, attributeDeleted); + + // THEN: no cache lookup should be done + kit.expectNoMessage(Duration.ofSeconds(1)); + askResultCached.toCompletableFuture().join(); + // AND: the resulting thing JSON includes the with the merged metadata updated value: + final JsonObject expectedThingJson = getExpectedThingJson().toBuilder() + .remove("attributes") + .set("_metadata", JsonObject.newBuilder() + .set("attributes", JsonObject.newBuilder().build()) + .build()) + .build(); + + softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); + }); + } + + @Override + protected JsonFieldSelector actualSelectedFields(final JsonFieldSelector selector) { + return JsonFactory.newFieldSelectorBuilder() + .addPointers(selector) + .addFieldDefinition(Thing.JsonFields.REVISION) // additionally always select the revision + .build(); + } + + @Override + protected JsonObject getThingResponseThingJson() { + return THING_RESPONSE_JSON; + } + + @Override + protected SignalEnrichmentFacade createSignalEnrichmentFacadeUnderTest(final TestKit kit, final Duration duration) { + final CacheConfig cacheConfig = + DefaultCacheConfig.of(ConfigFactory.parseString(CACHE_CONFIG), CACHE_CONFIG_KEY); + final ActorSelection commandHandler = ActorSelection.apply(kit.getRef(), ""); + final ByRoundTripSignalEnrichmentFacade cacheLoaderFacade = + ByRoundTripSignalEnrichmentFacade.of(commandHandler, Duration.ofSeconds(10L)); + return createCachingSignalEnrichmentFacade(kit, cacheLoaderFacade, cacheConfig); + } + + protected abstract CachingSignalEnrichmentFacade createCachingSignalEnrichmentFacade(TestKit kit, + ByRoundTripSignalEnrichmentFacade cacheLoaderFacade, CacheConfig cacheConfig); +} diff --git a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractSignalEnrichmentFacadeTest.java b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractSignalEnrichmentFacadeTest.java index dc1dd866ff..d3b81ce0db 100644 --- a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractSignalEnrichmentFacadeTest.java +++ b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/AbstractSignalEnrichmentFacadeTest.java @@ -19,6 +19,8 @@ import java.util.UUID; import java.util.concurrent.CompletionStage; +import org.apache.pekko.pattern.AskTimeoutException; +import org.apache.pekko.testkit.javadsl.TestKit; import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.base.model.signals.DittoTestSystem; @@ -34,9 +36,6 @@ import org.eclipse.ditto.things.model.signals.events.ThingDeleted; import org.junit.Test; -import org.apache.pekko.pattern.AskTimeoutException; -import org.apache.pekko.testkit.javadsl.TestKit; - /** * Abstract base test for different {@link SignalEnrichmentFacade} implementations. */ @@ -197,4 +196,5 @@ public void enrichThingDeleted() { } protected abstract SignalEnrichmentFacade createSignalEnrichmentFacadeUnderTest(TestKit kit, Duration duration); + } diff --git a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacadeTest.java b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacadeTest.java index d0dc71cc2f..8b54abe56e 100644 --- a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacadeTest.java +++ b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacadeTest.java @@ -12,62 +12,15 @@ */ package org.eclipse.ditto.internal.models.signalenrichment; -import java.time.Duration; -import java.util.concurrent.CompletionStage; - -import org.assertj.core.api.JUnitSoftAssertions; -import org.eclipse.ditto.base.model.auth.AuthorizationContext; -import org.eclipse.ditto.base.model.auth.AuthorizationSubject; -import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; -import org.eclipse.ditto.base.model.entity.metadata.Metadata; -import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.json.FieldType; -import org.eclipse.ditto.base.model.signals.DittoTestSystem; +import org.apache.pekko.testkit.javadsl.TestKit; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; -import org.eclipse.ditto.internal.utils.cache.config.DefaultCacheConfig; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonFieldSelector; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonPointer; -import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.things.model.ThingsModelFactory; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse; -import org.eclipse.ditto.things.model.signals.events.AttributeDeleted; -import org.eclipse.ditto.things.model.signals.events.ThingMerged; -import org.junit.Rule; -import org.junit.Test; - -import com.typesafe.config.ConfigFactory; - -import org.apache.pekko.actor.ActorSelection; -import org.apache.pekko.testkit.javadsl.TestKit; /** * Unit tests for {@link DittoCachingSignalEnrichmentFacade}. */ -public final class DittoCachingSignalEnrichmentFacadeTest extends AbstractSignalEnrichmentFacadeTest { +public final class DittoCachingSignalEnrichmentFacadeTest extends AbstractCachingSignalEnrichmentFacadeTest { - private static final String CACHE_CONFIG_KEY = "my-cache"; - private static final String CACHE_CONFIG = CACHE_CONFIG_KEY + """ - { - maximum-size = 10 - expire-after-create = 2m - } - """; - private static final String ISSUER_PREFIX = "test:"; - - private static final JsonObject THING_RESPONSE_JSON = JsonObject.of(""" - { - "_revision": 3, - "policyId": "policy:id", - "attributes": {"x": 5}, - "features": {"y": {"properties": {"z": true}}}, - "_metadata": {"attributes": {"x": {"type": "x attribute"}}} - }"""); private static final JsonObject EXPECTED_THING_JSON = JsonObject.of(""" { "policyId": "policy:id", @@ -76,16 +29,9 @@ public final class DittoCachingSignalEnrichmentFacadeTest extends AbstractSignal "_metadata": {"attributes": {"x": {"type": "x attribute"}}} }"""); - @Rule - public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); - @Override - protected SignalEnrichmentFacade createSignalEnrichmentFacadeUnderTest(final TestKit kit, final Duration duration) { - final CacheConfig cacheConfig = - DefaultCacheConfig.of(ConfigFactory.parseString(CACHE_CONFIG), CACHE_CONFIG_KEY); - final ActorSelection commandHandler = ActorSelection.apply(kit.getRef(), ""); - final ByRoundTripSignalEnrichmentFacade cacheLoaderFacade = - ByRoundTripSignalEnrichmentFacade.of(commandHandler, Duration.ofSeconds(10L)); + protected CachingSignalEnrichmentFacade createCachingSignalEnrichmentFacade(final TestKit kit, + final ByRoundTripSignalEnrichmentFacade cacheLoaderFacade, final CacheConfig cacheConfig) { return DittoCachingSignalEnrichmentFacade.newInstance( cacheLoaderFacade, cacheConfig, @@ -93,418 +39,10 @@ protected SignalEnrichmentFacade createSignalEnrichmentFacadeUnderTest(final Tes "test"); } - @Override - protected JsonObject getThingResponseThingJson() { - return THING_RESPONSE_JSON; - } - @Override protected JsonObject getExpectedThingJson() { return EXPECTED_THING_JSON; } - @Override - protected JsonFieldSelector actualSelectedFields(final JsonFieldSelector selector) { - return JsonFactory.newFieldSelectorBuilder() - .addPointers(selector) - .addFieldDefinition(Thing.JsonFields.REVISION) // additionally always select the revision - .build(); - } - - @Test - public void alreadyLoadedCacheEntryIsReused() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); - }); - } - - @Test - public void alreadyLoadedCacheEntryIsReusedForMergedEvent() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/attributes"), - JsonObject.newBuilder() - .set("x", 42) - .set("foo", "bar") - .build(), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merge update updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .set("/attributes/x", 42) - .build(); - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); - - // WHEN: then the attribute "x" is modified with a merge: - final ThingMerged mergeAttributeX = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), - JsonValue.of(1337), - mergeAttributes.getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached2 = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributeX); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached2.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merge update updated value: - final JsonObject expectedThingJson2 = EXPECTED_THING_JSON.toBuilder() - .set("/attributes/x", 1337) - .build(); - softly.assertThat(askResultCached2).isCompletedWithValue(expectedThingJson2); - }); - } - - @Test - public void alreadyLoadedCacheEntryIsReusedForMergedEventOnRootLevel() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/"), - JsonObject.newBuilder() - .set("attributes", - JsonObject.newBuilder() - .set("x", 42) - .set("foo", "bar") - .build()) - .build(), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - }); - } - - @Test - public void alreadyLoadedCacheEntryIsInvalidatedForUnexpectedEventRevision() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final DittoHeaders headers = DittoHeaders.newBuilder().randomCorrelationId().build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector with event with 2 revisions ahead - final DittoHeaders headers2 = DittoHeaders.newBuilder().randomCorrelationId().build(); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, - getThingEvent().setRevision(getThingEvent().getRevision() + 2)); // notice +2 here - - // THEN: do another cache lookup after invalidation - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - final Thing thing2 = ThingsModelFactory.newThing(getThingResponseThingJson()); - final Thing thing2WithUpdatedRev = thing2.toBuilder() - .setRevision(thing2.getRevision().get().increment().increment()) - .build(); - kit.reply(RetrieveThingResponse.of(thingId, thing2WithUpdatedRev.toJson( - thing2WithUpdatedRev.getImplementedSchemaVersion(), FieldType.all()), headers2)); - askResultCached.toCompletableFuture().join(); - softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); - }); - } - - @Test - public void differentAuthSubjectsLeadToCacheRetrievals() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId1 = ISSUER_PREFIX + "user1"; - final String userId2 = ISSUER_PREFIX + "user2"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId1))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId1); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead but other auth subjects - final DittoHeaders headers2 = headers.toBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(ISSUER_PREFIX + "user2") - )) - .build(); - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: a cache lookup should be done containing the other auth subject header - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId2); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - }); - } - - @Test - public void differentFieldSelectorsLeadToCacheRetrievals() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user1"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - final JsonFieldSelector selector2 = JsonFieldSelector.newInstance("attributes", "features"); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with different selector for an event with one revision ahead - underTest.retrievePartialThing(thingId, selector2, headers, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: a cache lookup should be done using the other selector - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(selector2)); - }); - } - - @Test - public void metadataIsUpdatedForMergedEvent() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttribute = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), - JsonFactory.newValue(6), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - Metadata.newMetadata(JsonObject.newBuilder() - .set("type", "x is now y attribute") - .build() - ) - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttribute); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merged metadata updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .set("attributes", JsonObject.newBuilder() - .set("x", 6) - .build()) - .set("_metadata", JsonObject.newBuilder() - .set("attributes", JsonObject.newBuilder() - .set("x", JsonObject.newBuilder() - .set("type", "x is now y attribute") - .build()) - .build()) - .build()) - .build(); - - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); - }); - } - - @Test - public void metadataIsDeletedForDeletedEvent() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = ThingId.generateRandom(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final AttributeDeleted attributeDeleted = - AttributeDeleted.of(thingId, JsonPointer.of("/x"), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - MetadataModelFactory.nullMetadata()); - - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, attributeDeleted); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merged metadata updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .remove("attributes") - .set("_metadata", JsonObject.newBuilder() - .set("attributes", JsonObject.newBuilder().build()) - .build()) - .build(); - - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); - }); - } } diff --git a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/SearchIndexingSignalEnrichmentFacadeTest.java b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/SearchIndexingSignalEnrichmentFacadeTest.java index 72dfc93193..f91fa362b7 100644 --- a/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/SearchIndexingSignalEnrichmentFacadeTest.java +++ b/internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/SearchIndexingSignalEnrichmentFacadeTest.java @@ -12,64 +12,37 @@ */ package org.eclipse.ditto.internal.models.signalenrichment; -import com.typesafe.config.ConfigFactory; -import org.apache.pekko.actor.ActorSelection; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + import org.apache.pekko.japi.Pair; import org.apache.pekko.testkit.javadsl.TestKit; import org.assertj.core.api.JUnitSoftAssertions; -import org.eclipse.ditto.base.model.auth.AuthorizationContext; -import org.eclipse.ditto.base.model.auth.AuthorizationSubject; -import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; -import org.eclipse.ditto.base.model.entity.metadata.Metadata; +import org.eclipse.ditto.base.model.common.LikeHelper; import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.json.FieldType; import org.eclipse.ditto.base.model.signals.DittoTestSystem; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; -import org.eclipse.ditto.internal.utils.cache.config.DefaultCacheConfig; -import org.eclipse.ditto.json.*; -import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.json.JsonFieldSelector; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.things.model.ThingsModelFactory; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse; -import org.eclipse.ditto.things.model.signals.events.AttributeDeleted; import org.eclipse.ditto.things.model.signals.events.AttributeModified; -import org.eclipse.ditto.things.model.signals.events.ThingMerged; import org.junit.Rule; import org.junit.Test; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.regex.Pattern; - /** * Unit tests for {@link SearchIndexingSignalEnrichmentFacade}. */ -public final class SearchIndexingSignalEnrichmentFacadeTest extends AbstractSignalEnrichmentFacadeTest { +public final class SearchIndexingSignalEnrichmentFacadeTest extends AbstractCachingSignalEnrichmentFacadeTest { - private static final String CACHE_CONFIG_KEY = "my-cache"; - private static final String CACHE_CONFIG = CACHE_CONFIG_KEY + """ - { - maximum-size = 10 - expire-after-create = 2m - } - """; - private static final String ISSUER_PREFIX = "test:"; - private static final String NAMESPACE = "org.eclipse.test"; - - private static final JsonObject THING_RESPONSE_JSON = JsonObject.of(""" - { - "_revision": 3, - "policyId": "policy:id", - "attributes": {"x": 5}, - "features": {"y": {"properties": {"z": true}}}, - "_metadata": {"attributes": {"x": {"type": "x attribute"}}} - }"""); private static final JsonObject EXPECTED_THING_JSON = JsonObject.of(""" { "policyId": "policy:id", @@ -77,44 +50,48 @@ public final class SearchIndexingSignalEnrichmentFacadeTest extends AbstractSign "_metadata": {"attributes": {"x": {"type": "x attribute"}}} }"""); - private static final JsonFieldSelector SELECTED_INDEXES = JsonFieldSelector.newInstance("policyId", "attributes/x", "_metadata"); + private static final JsonFieldSelector SELECTED_INDEXES_WILDCARD_NS = + JsonFieldSelector.newInstance("attributes/wild"); + private static final AttributeModified THING_EVENT = AttributeModified.of( - ThingId.generateRandom(NAMESPACE), + ThingId.generateRandom("org.eclipse.test"), JsonPointer.of("x"), JsonValue.of(5), 3L, Instant.EPOCH, DittoHeaders.empty(), MetadataModelFactory.newMetadataBuilder() - .set("type", "x attribute") - .build()); + .set("type", "x attribute") + .build()); @Rule public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); @Override - protected SignalEnrichmentFacade createSignalEnrichmentFacadeUnderTest(final TestKit kit, final Duration duration) { - final CacheConfig cacheConfig = - DefaultCacheConfig.of(ConfigFactory.parseString(CACHE_CONFIG), CACHE_CONFIG_KEY); - final ActorSelection commandHandler = ActorSelection.apply(kit.getRef(), ""); - final ByRoundTripSignalEnrichmentFacade cacheLoaderFacade = - ByRoundTripSignalEnrichmentFacade.of(commandHandler, Duration.ofSeconds(10L)); + protected CachingSignalEnrichmentFacade createCachingSignalEnrichmentFacade(final TestKit kit, + final ByRoundTripSignalEnrichmentFacade cacheLoaderFacade, final CacheConfig cacheConfig) { return SearchIndexingSignalEnrichmentFacade.newInstance( - List.of(Pair.create(Pattern.compile("org.eclipse.test"), SELECTED_INDEXES)), + List.of( + Pair.create( + Pattern.compile( + Objects.requireNonNull(LikeHelper.convertToRegexSyntax("org.eclipse.test"))), + SELECTED_INDEXES + ), + Pair.create( + Pattern.compile( + Objects.requireNonNull(LikeHelper.convertToRegexSyntax("org.eclipse*"))), + SELECTED_INDEXES_WILDCARD_NS + ) + ), cacheLoaderFacade, cacheConfig, kit.getSystem().getDispatcher(), "test"); } - @Override - protected JsonObject getThingResponseThingJson() { - return THING_RESPONSE_JSON; - } - @Override protected JsonObject getExpectedThingJson() { return EXPECTED_THING_JSON; @@ -130,413 +107,24 @@ protected AttributeModified getThingEvent() { return THING_EVENT; } - @Override - protected JsonFieldSelector actualSelectedFields(final JsonFieldSelector selector) { - return JsonFactory.newFieldSelectorBuilder() - .addPointers(selector) - .addFieldDefinition(Thing.JsonFields.REVISION) // additionally always select the revision - .build(); - } - - @Test - public void alreadyLoadedCacheEntryIsReused() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); - }); - } - @Test - public void alreadyLoadedCacheEntryIsReusedForMergedEvent() { + public void determineRightSelectorForMultipleNamespacesConfigured() { DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); + final SearchIndexingSignalEnrichmentFacade underTest = + (SearchIndexingSignalEnrichmentFacade) createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/attributes"), - JsonObject.newBuilder() - .set("x", 42) - .set("foo", "bar") - .build(), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); + assertThat(underTest.determineSelector("org.eclipse.test")) + .isEqualTo(SELECTED_INDEXES); - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merge update updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .set("/attributes/x", 42) - .build(); - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); + assertThat(underTest.determineSelector("org.eclipse")) + .isEqualTo(SELECTED_INDEXES_WILDCARD_NS); - // WHEN: then the attribute "x" is modified with a merge: - final ThingMerged mergeAttributeX = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), - JsonValue.of(1337), - mergeAttributes.getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached2 = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributeX); + assertThat(underTest.determineSelector("org.eclipsefoo")) + .isEqualTo(SELECTED_INDEXES_WILDCARD_NS); - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached2.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merge update updated value: - final JsonObject expectedThingJson2 = EXPECTED_THING_JSON.toBuilder() - .set("/attributes/x", 1337) - .build(); - softly.assertThat(askResultCached2).isCompletedWithValue(expectedThingJson2); + assertThat(underTest.determineSelector("org.eclipse.wild")) + .isEqualTo(SELECTED_INDEXES_WILDCARD_NS); }); } - @Test - public void alreadyLoadedCacheEntryIsReusedForMergedEventOnRootLevel() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttributes = ThingMerged.of(thingId, JsonPointer.of("/"), - JsonObject.newBuilder() - .set("attributes", - JsonObject.newBuilder() - .set("x", 42) - .set("foo", "bar") - .build()) - .build(), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - null - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttributes); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - }); - } - - @Test - public void alreadyLoadedCacheEntryIsInvalidatedForUnexpectedEventRevision() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final DittoHeaders headers = DittoHeaders.newBuilder().randomCorrelationId().build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector with event with 2 revisions ahead - final DittoHeaders headers2 = DittoHeaders.newBuilder().randomCorrelationId().build(); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, - getThingEvent().setRevision(getThingEvent().getRevision() + 2)); // notice +2 here - - // THEN: do another cache lookup after invalidation - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - final Thing thing2 = ThingsModelFactory.newThing(getThingResponseThingJson()); - final Thing thing2WithUpdatedRev = thing2.toBuilder() - .setRevision(thing2.getRevision().get().increment().increment()) - .build(); - kit.reply(RetrieveThingResponse.of(thingId, thing2WithUpdatedRev.toJson( - thing2WithUpdatedRev.getImplementedSchemaVersion(), FieldType.all()), headers2)); - askResultCached.toCompletableFuture().join(); - softly.assertThat(askResultCached).isCompletedWithValue(getExpectedThingJson()); - }); - } - - @Test - public void differentAuthSubjectsLeadToCacheRetrievals() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId1 = ISSUER_PREFIX + "user1"; - final String userId2 = ISSUER_PREFIX + "user2"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId1))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId1); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead but other auth subjects - final DittoHeaders headers2 = headers.toBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(ISSUER_PREFIX + "user2") - )) - .build(); - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers2, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: a cache lookup should be done containing the other auth subject header - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId2); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - }); - } - - @Test - public void differentFieldSelectorsLeadToCacheRetrievals() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user1"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - final JsonFieldSelector selector2 = JsonFieldSelector.newInstance("attributes", "features"); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with different selector for an event with one revision ahead - underTest.retrievePartialThing(thingId, selector2, headers, - getThingEvent().setRevision(getThingEvent().getRevision() + 1)); - - // THEN: a cache lookup should be done using the other selector - final RetrieveThing retrieveThing2 = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing2.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing2.getSelectedFields()).contains(actualSelectedFields(selector2)); - }); - } - - @Test - public void metadataIsUpdatedForMergedEvent() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final ThingMerged mergeAttribute = ThingMerged.of(thingId, JsonPointer.of("/attributes/x"), - JsonFactory.newValue(6), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - Metadata.newMetadata(JsonObject.newBuilder() - .set("type", "x is now y attribute") - .build() - ) - ); - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, mergeAttribute); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merged metadata updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .set("attributes", JsonObject.newBuilder() - .set("x", 6) - .build()) - .set("_metadata", JsonObject.newBuilder() - .set("attributes", JsonObject.newBuilder() - .set("x", JsonObject.newBuilder() - .set("type", "x is now y attribute") - .build()) - .build()) - .build()) - .build(); - - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); - }); - } - - @Test - public void metadataIsDeletedForDeletedEvent() { - DittoTestSystem.run(this, kit -> { - // GIVEN: SignalEnrichmentFacade.retrievePartialThing() - final SignalEnrichmentFacade underTest = - createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L)); - final ThingId thingId = buildThingId(); - final String userId = ISSUER_PREFIX + "user"; - final DittoHeaders headers = DittoHeaders.newBuilder() - .authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED, - AuthorizationSubject.newInstance(userId))) - .randomCorrelationId() - .build(); - final CompletionStage askResult = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, getThingEvent()); - - // WHEN: Command handler receives expected RetrieveThing and responds with RetrieveThingResponse - final RetrieveThing retrieveThing = kit.expectMsgClass(RetrieveThing.class); - softly.assertThat(retrieveThing.getDittoHeaders().getAuthorizationContext().getAuthorizationSubjectIds()) - .contains(userId); - softly.assertThat(retrieveThing.getSelectedFields()).contains(actualSelectedFields(getJsonFieldSelector())); - // WHEN: response is handled so that it is also added to the cache - kit.reply(RetrieveThingResponse.of(thingId, getThingResponseThingJson(), headers)); - askResult.toCompletableFuture().join(); - - softly.assertThat(askResult).isCompletedWithValue(getExpectedThingJson()); - - // WHEN: same thing is asked again with same selector for an event with one revision ahead - final AttributeDeleted attributeDeleted = - AttributeDeleted.of(thingId, JsonPointer.of("/x"), - getThingEvent().getRevision() + 1, - null, - DittoHeaders.empty(), - MetadataModelFactory.nullMetadata()); - - final CompletionStage askResultCached = - underTest.retrievePartialThing(thingId, getJsonFieldSelector(), headers, attributeDeleted); - - // THEN: no cache lookup should be done - kit.expectNoMessage(Duration.ofSeconds(1)); - askResultCached.toCompletableFuture().join(); - // AND: the resulting thing JSON includes the with the merged metadata updated value: - final JsonObject expectedThingJson = EXPECTED_THING_JSON.toBuilder() - .remove("attributes") - .set("_metadata", JsonObject.newBuilder() - .set("attributes", JsonObject.newBuilder().build()) - .build()) - .build(); - - softly.assertThat(askResultCached).isCompletedWithValue(expectedThingJson); - }); - } - - private static ThingId buildThingId() { - return ThingId.generateRandom(NAMESPACE); - } - } - diff --git a/thingsearch/service/src/main/java/org/eclipse/ditto/thingsearch/service/persistence/write/streaming/SearchIndexingSignalEnrichmentFacadeProvider.java b/thingsearch/service/src/main/java/org/eclipse/ditto/thingsearch/service/persistence/write/streaming/SearchIndexingSignalEnrichmentFacadeProvider.java index 8884d0bd41..9c880008eb 100644 --- a/thingsearch/service/src/main/java/org/eclipse/ditto/thingsearch/service/persistence/write/streaming/SearchIndexingSignalEnrichmentFacadeProvider.java +++ b/thingsearch/service/src/main/java/org/eclipse/ditto/thingsearch/service/persistence/write/streaming/SearchIndexingSignalEnrichmentFacadeProvider.java @@ -10,37 +10,42 @@ * * SPDX-License-Identifier: EPL-2.0 */ - package org.eclipse.ditto.thingsearch.service.persistence.write.streaming; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import java.util.regex.Pattern; +import org.apache.pekko.actor.ActorSystem; import org.apache.pekko.japi.Pair; import org.eclipse.ditto.base.model.common.LikeHelper; import org.eclipse.ditto.internal.models.signalenrichment.CachingSignalEnrichmentFacade; import org.eclipse.ditto.internal.models.signalenrichment.SearchIndexingSignalEnrichmentFacade; import org.eclipse.ditto.internal.models.signalenrichment.SignalEnrichmentFacade; import org.eclipse.ditto.internal.utils.cache.config.CacheConfig; - -import com.typesafe.config.Config; - -import org.apache.pekko.actor.ActorSystem; import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; -import org.eclipse.ditto.json.*; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonFieldDefinition; +import org.eclipse.ditto.json.JsonFieldSelector; +import org.eclipse.ditto.json.JsonParseOptions; +import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.things.model.Thing; - import org.eclipse.ditto.thingsearch.service.common.config.DittoSearchConfig; import org.eclipse.ditto.thingsearch.service.common.config.NamespaceSearchIndexConfig; import org.eclipse.ditto.thingsearch.service.common.config.SearchConfig; +import com.typesafe.config.Config; + /** * Default {@link SearchIndexingSignalEnrichmentFacadeProvider} who provides a {@link org.eclipse.ditto.internal.models.signalenrichment.SearchIndexingSignalEnrichmentFacade}. */ public final class SearchIndexingSignalEnrichmentFacadeProvider implements CachingSignalEnrichmentFacadeProvider { - private static final Set REQUIRED_INDEXED_FIELDS = Set.of( + private static final Set> REQUIRED_INDEXED_FIELDS = Set.of( Thing.JsonFields.ID, Thing.JsonFields.POLICY_ID, Thing.JsonFields.NAMESPACE, @@ -48,6 +53,7 @@ public final class SearchIndexingSignalEnrichmentFacadeProvider implements Cachi /** * Instantiate this provider. Called by reflection. + * * @param actorSystem the actor system in which to load the extension. * @param config the configuration for this extension. */ @@ -75,12 +81,17 @@ public CachingSignalEnrichmentFacade getSignalEnrichmentFacade( if (!namespaceConfig.getSearchIncludeFields().isEmpty()) { // Ensure the constructed JsonFieldSelector has the required fields needed for the search to work. - final Set set = new HashSet<>(namespaceConfig.getSearchIncludeFields()); - set.addAll(REQUIRED_INDEXED_FIELDS.stream().map(JsonFieldDefinition::getPointer).map(JsonPointer::toString).toList()); + final Set set = new LinkedHashSet<>(); + set.addAll(REQUIRED_INDEXED_FIELDS.stream() + .map(JsonFieldDefinition::getPointer) + .map(JsonPointer::toString) + .toList()); + set.addAll(namespaceConfig.getSearchIncludeFields()); final List searchIncludeFields = new ArrayList<>(set); - JsonFieldSelector indexedFields = JsonFactory.newFieldSelector(searchIncludeFields, JsonParseOptions.newBuilder().build()); + final JsonFieldSelector indexedFields = + JsonFactory.newFieldSelector(searchIncludeFields, JsonParseOptions.newBuilder().build()); // Build a Pattern from the namespace value. final Pattern namespacePattern = Pattern.compile( diff --git a/thingsearch/service/src/test/java/org/eclipse/ditto/thingsearch/service/common/config/DefaultNamespaceSearchIndexConfigTest.java b/thingsearch/service/src/test/java/org/eclipse/ditto/thingsearch/service/common/config/DefaultNamespaceSearchIndexConfigTest.java index 888edd25aa..89b771ed17 100644 --- a/thingsearch/service/src/test/java/org/eclipse/ditto/thingsearch/service/common/config/DefaultNamespaceSearchIndexConfigTest.java +++ b/thingsearch/service/src/test/java/org/eclipse/ditto/thingsearch/service/common/config/DefaultNamespaceSearchIndexConfigTest.java @@ -13,20 +13,22 @@ package org.eclipse.ditto.thingsearch.service.common.config; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import nl.jqno.equalsverifier.EqualsVerifier; +import static org.mutabilitydetector.unittesting.AllowedReason.assumingFields; +import static org.mutabilitydetector.unittesting.AllowedReason.provided; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import java.util.List; + import org.assertj.core.api.JUnitSoftAssertions; import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig; import org.junit.Rule; import org.junit.Test; -import java.util.List; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; -import static org.mutabilitydetector.unittesting.AllowedReason.assumingFields; -import static org.mutabilitydetector.unittesting.AllowedReason.provided; -import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; -import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; +import nl.jqno.equalsverifier.EqualsVerifier; public final class DefaultNamespaceSearchIndexConfigTest { @@ -77,19 +79,19 @@ public void underTestReturnsValuesOfConfigFile() { NamespaceSearchIndexConfig second = underTest.getNamespaceSearchIncludeFields().get(1); // First config - softly.assertThat(first.getNamespacePattern()).isEqualTo("org.eclipse"); + softly.assertThat(first.getNamespacePattern()).isEqualTo("org.eclipse.test"); softly.assertThat(first.getSearchIncludeFields()) .as(NamespaceSearchIndexConfig.NamespaceSearchIndexConfigValue.SEARCH_INCLUDE_FIELDS.getConfigPath()) .isEqualTo( - List.of("attributes", "features/info")); + List.of("attributes", "features/info/properties", "features/info/other")); // Second config - softly.assertThat(second.getNamespacePattern()).isEqualTo("org.eclipse.test"); + softly.assertThat(second.getNamespacePattern()).isEqualTo("org.eclipse*"); softly.assertThat(second.getSearchIncludeFields()) .as(NamespaceSearchIndexConfig.NamespaceSearchIndexConfigValue.SEARCH_INCLUDE_FIELDS.getConfigPath()) .isEqualTo( - List.of("attributes", "features/info/properties/", "features/info/other")); + List.of("attributes", "features/info")); } } diff --git a/thingsearch/service/src/test/resources/namespace-search-index-test.conf b/thingsearch/service/src/test/resources/namespace-search-index-test.conf index c3f5ae7460..487545146a 100644 --- a/thingsearch/service/src/test/resources/namespace-search-index-test.conf +++ b/thingsearch/service/src/test/resources/namespace-search-index-test.conf @@ -11,12 +11,19 @@ ditto { namespace-search-include-fields = [ # The list of thing paths that are included in the search index. { - namespace-pattern = "org.eclipse", - search-include-fields = [ "attributes", "features/info" ] + namespace-pattern = "org.eclipse.test" + search-include-fields = [ + "attributes", + "features/info/properties", + "features/info/other" + ] }, { - namespace-pattern = "org.eclipse.test", - search-include-fields = [ "attributes", "features/info/properties/", "features/info/other" ] + namespace-pattern = "org.eclipse*" + search-include-fields = [ + "attributes", + "features/info" + ] } ] }