Skip to content

Commit

Permalink
Merge pull request #339 from catenax-ng/main
Browse files Browse the repository at this point in the history
Bugfixes and caching discovery finder requests
  • Loading branch information
ds-jhartmann authored Dec 13, 2023
2 parents 47e5d0b + ea00cda commit 6a24269
Show file tree
Hide file tree
Showing 42 changed files with 1,004 additions and 473 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added cache mechanism in DiscoveryFinderClientImpl for findDiscoveryEndpoints

## [4.3.0] - 2023-12-08
### Added
Expand Down
2 changes: 2 additions & 0 deletions charts/irs-helm/templates/configmap-spring-app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ data:
operator: {{ .operator | quote }}
rightOperand: {{ .rightOperand | quote }}
{{- end }}
discoveryFinderClient:
cacheTTL: {{ .Values.edc.discoveryFinderClient.cacheTTL | quote }}
connectorEndpointService:
cacheTTL: {{ .Values.edc.connectorEndpointService.cacheTTL | quote }}
ess:
Expand Down
3 changes: 3 additions & 0 deletions charts/irs-helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,11 @@ edc:
- leftOperand: "Membership"
operator: "eq"
rightOperand: "active"
discoveryFinderClient:
cacheTTL: PT24H # Time to live for DiscoveryFinderClient for findDiscoveryEndpoints method cache
connectorEndpointService:
cacheTTL: PT24H # Time to live for ConnectorEndpointService for fetchConnectorEndpoints method cache

discovery:
oAuthClientId: portal # ID of the OAuth2 client registration to use, see config spring.security.oauth2.client

Expand Down
11 changes: 9 additions & 2 deletions docs/src/docs/administration/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ include::irs-spring-config.adoc[leveloffset=+1]
[source,yaml]
----
include::../../../../charts/irs-helm/values.yaml[lines=104..287]
include::../../../../charts/irs-helm/values.yaml[lines=104..302]
----
<1> Use this to enable or disable the monitoring components
Expand Down Expand Up @@ -74,8 +74,15 @@ The hostname where Grafana will be made available.
The EDC consumer controlplane endpoint URL for data management, including the protocol.
If left empty, this defaults to the internal endpoint of the controlplane provided by the irs-edc-consumer Helm chart.
==== <discoveryFinderClient.cacheTTL>
When IRS calls the Discovery Finder URL for BPNLs, the results are cached to improve performance.
This parameter defines how long the cache is maintained before it is cleared.
Data is in ISO 8601.
==== <connectorEndpointService.cacheTTL>
When IRS calls EDC Discovery Service to fetch endpoints for BPNL's there is a cache mechanism between them, to improve performance.
When IRS calls EDC Discovery Service to fetch connector endpoints for BPNLs, the results are cached to improve performance.
This parameter define how long cache is maintained before it is cleared. Data is in ISO 8601.
== OAuth2 Configuration
Expand Down
11 changes: 11 additions & 0 deletions docs/src/docs/arc42/cross-cutting/under-the-hood.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,14 @@ Whenever a BPN is resolved via BPDM, the partner name is cached on IRS side, as
Whenever a semantic model schema is requested from the Semantic Hub, it is stored locally until the cache is evicted (configurable). The IRS can preload configured schema models on startup to reduce on demand call times.

Additionally, models can be deployed with the system as a backup to the real Semantic Hub service.

=== Discovery Service

The IRS uses the Discovery Finder in order to find the correct EDC Discovery URL for the type BPN.
This URL is cached locally.

When the EDC Discovery is requested to return the EDC connector endpoint URLs for a specific BPN, the results are cached as well.

The time to live for both caches can be configured separately as described in the Administration Guide.

Further information on Discovery Service can be found in the chapter "System scope and context".
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.eclipse.tractusx.irs.component.JobParameter;
import org.eclipse.tractusx.irs.component.PartChainIdentificationKey;
import org.eclipse.tractusx.irs.component.Tombstone;
import org.eclipse.tractusx.irs.component.assetadministrationshell.AssetAdministrationShellDescriptor;
import org.eclipse.tractusx.irs.component.enums.ProcessStep;
import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryKey;
import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryService;
Expand Down Expand Up @@ -65,9 +66,19 @@ public ItemContainer process(final ItemContainer.ItemContainerBuilder itemContai
ProcessStep.DIGITAL_TWIN_REQUEST)).build();
}
try {
itemContainerBuilder.shell(digitalTwinRegistryService.fetchShells(
List.of(new DigitalTwinRegistryKey(itemId.getGlobalAssetId(), itemId.getBpn()))
).stream().findFirst().orElseThrow());
final AssetAdministrationShellDescriptor shell = digitalTwinRegistryService.fetchShells(
List.of(new DigitalTwinRegistryKey(itemId.getGlobalAssetId(), itemId.getBpn())))
.stream()
.findFirst()
.orElseThrow();

if (expectedDepthOfTreeIsNotReached(jobData.getDepth(), aasTransferProcess.getDepth())) {
// traversal submodel descriptors are needed in next Delegate, and will be filtered out there
itemContainerBuilder.shell(shell);
} else {
// filter submodel descriptors if next delegate will not be executed
itemContainerBuilder.shell(shell.withFilteredSubmodelDescriptors(jobData.getAspects()));
}
} catch (final RestClientException | RegistryServiceException e) {
log.info("Shell Endpoint could not be retrieved for Item: {}. Creating Tombstone.", itemId);
itemContainerBuilder.tombstone(Tombstone.from(itemId.getGlobalAssetId(), null, e, retryCount, ProcessStep.DIGITAL_TWIN_REQUEST));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@
import org.eclipse.tractusx.irs.connector.job.JobOrchestrator;
import org.eclipse.tractusx.irs.connector.job.JobStore;
import org.eclipse.tractusx.irs.connector.job.JobTTL;
import org.eclipse.tractusx.irs.data.CxTestDataContainer;
import org.eclipse.tractusx.irs.edc.client.AsyncPollingService;
import org.eclipse.tractusx.irs.edc.client.ContractNegotiationService;
import org.eclipse.tractusx.irs.edc.client.EDCCatalogFacade;
import org.eclipse.tractusx.irs.edc.client.EdcConfiguration;
import org.eclipse.tractusx.irs.edc.client.EdcDataPlaneClient;
import org.eclipse.tractusx.irs.edc.client.EdcSubmodelFacade;
import org.eclipse.tractusx.irs.edc.client.EndpointDataReferenceStorage;
import org.eclipse.tractusx.irs.edc.client.edcsubmodelclient.EdcSubmodelClient;
import org.eclipse.tractusx.irs.edc.client.edcsubmodelclient.EdcSubmodelClientImpl;
import org.eclipse.tractusx.irs.edc.client.edcsubmodelclient.EdcSubmodelClientLocalStub;
import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryService;
import org.eclipse.tractusx.irs.registryclient.central.DigitalTwinRegistryClient;
import org.eclipse.tractusx.irs.registryclient.central.DigitalTwinRegistryClientLocalStub;
import org.eclipse.tractusx.irs.registryclient.discovery.ConnectorEndpointsService;
import org.eclipse.tractusx.irs.semanticshub.SemanticsHubFacade;
import org.eclipse.tractusx.irs.services.MeterRegistryService;
Expand Down Expand Up @@ -151,4 +163,29 @@ public SubmodelDelegate submodelDelegate(final EdcSubmodelFacade submodelFacade,
connectorEndpointsService);
}

@Profile({ "local",
"stubtest"
})
@Bean
public DigitalTwinRegistryClient digitalTwinRegistryClient(final CxTestDataContainer cxTestDataContainer) {
return new DigitalTwinRegistryClientLocalStub(cxTestDataContainer);
}

@Profile({ "local",
"stubtest"
})
@Bean
public EdcSubmodelClient edcLocalSubmodelClient(final CxTestDataContainer cxTestDataContainer) {
return new EdcSubmodelClientLocalStub(cxTestDataContainer);
}

@Profile({ "!local && !stubtest" })
@Bean
public EdcSubmodelClient edcSubmodelClient(final EdcConfiguration edcConfiguration,
final ContractNegotiationService contractNegotiationService, final EdcDataPlaneClient edcDataPlaneClient,
final EndpointDataReferenceStorage endpointDataReferenceStorage, final AsyncPollingService pollingService,
final RetryRegistry retryRegistry, final EDCCatalogFacade catalogFacade) {
return new EdcSubmodelClientImpl(edcConfiguration, contractNegotiationService, edcDataPlaneClient,
endpointDataReferenceStorage, pollingService, retryRegistry, catalogFacade);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.tractusx.irs.registryclient.decentral.EdcRetrieverException;
import org.eclipse.tractusx.irs.registryclient.decentral.EndpointDataForConnectorsService;
import org.eclipse.tractusx.irs.registryclient.discovery.ConnectorEndpointsService;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryFinderClient;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryFinderClientImpl;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -86,7 +87,14 @@ public DecentralDigitalTwinRegistryService decentralDigitalTwinRegistryService(
public ConnectorEndpointsService connectorEndpointsService(
@Qualifier(RestTemplateConfig.DTR_REST_TEMPLATE) final RestTemplate dtrRestTemplate,
@Value("${digitalTwinRegistry.discoveryFinderUrl:}") final String finderUrl) {
return new ConnectorEndpointsService(new DiscoveryFinderClientImpl(finderUrl, dtrRestTemplate));
return new ConnectorEndpointsService(discoveryFinderClient(dtrRestTemplate, finderUrl));
}

@Bean
public DiscoveryFinderClient discoveryFinderClient(
@Qualifier(RestTemplateConfig.DTR_REST_TEMPLATE) final RestTemplate dtrRestTemplate,
@Value("${digitalTwinRegistry.discoveryFinderUrl:}") final String finderUrl) {
return new DiscoveryFinderClientImpl(finderUrl, dtrRestTemplate);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ private void checkIfIsCompleted(final UUID batchId, final LimitedJobEventQueue q
log.info("BatchId: {} reached {} state.", batchId, batchProcessingState);
saveUpdatedBatch(batch, progressList, batchProcessingState);
queueMap.remove(batchId);
publishFinishProcessingEvent(batch, batchProcessingState);
if (isCompleted(batchProcessingState)) {
publishFinishProcessingEvent(batch, batchProcessingState);
}
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions irs-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ irs-edc-client:
- leftOperand: "Membership"
operator: "eq"
rightOperand: "active"
discoveryFinderClient:
cacheTTL: PT24H # Time to live for DiscoveryFinderClient for findDiscoveryEndpoints method cache
connectorEndpointService:
cacheTTL: PT24H # Time to live for ConnectorEndpointService for fetchConnectorEndpoints method cache

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/********************************************************************************
* Copyright (c) 2021,2022,2023
* 2022: ZF Friedrichshafen AG
* 2022: ISTOS GmbH
* 2022,2023: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 2022,2023: BOSCH AG
* Copyright (c) 2021,2022,2023 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 Apache License, Version 2.0 which is available at
* https://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.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

package org.eclipse.tractusx.irs;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;

import org.eclipse.tractusx.irs.configuration.RestTemplateConfig;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryEndpoint;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryFinderClient;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryFinderClientImpl;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryFinderRequest;
import org.eclipse.tractusx.irs.registryclient.discovery.DiscoveryResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.shaded.org.awaitility.Awaitility;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK,
properties = "irs-edc-client.discoveryFinderClient.cacheTTL=PT0.1S")
@ActiveProfiles(profiles = "test")
@Import({ TestConfig.class })
class DiscoveryFinderClientTest {

private static final DiscoveryResponse MOCKED_DISCOVERY_RESPONSE = new DiscoveryResponse(
List.of(new DiscoveryEndpoint("test-endpoint", "desc", "test-endpoint-addr", "docs", "resId")));

@Autowired
private DiscoveryFinderClient testee;

@MockBean(name = RestTemplateConfig.DTR_REST_TEMPLATE)
private RestTemplate restTemplateMock;

@Autowired
private CacheManager cacheManager;

@Test
void findDiscoveryEndpoints_WhenCalled_ResultsShouldBeCached() {

// GIVEN
final var request = new DiscoveryFinderRequest(List.of("bpn"));
final var originalResponse = MOCKED_DISCOVERY_RESPONSE;
simulateFindDiscoveryEndpointsRestRequest(request, originalResponse);

// WHEN
final var actualResult = testee.findDiscoveryEndpoints(request);

// THEN
// real endpoint must be called
verify(restTemplateMock).postForObject("", request, DiscoveryResponse.class);

// and the response must be cached
{
final var cachedResponse = requireDiscoveryEndpointsCacheValue(request);
final var cachedAddresses = extractEndpointAddresses(cachedResponse);
final var returnedAddresses = extractEndpointAddresses(actualResult);
final var originalAddresses = extractEndpointAddresses(originalResponse);

// which means that now the value in the cache must equal the original and the actual value
assertThat(cachedAddresses).isEqualTo(originalAddresses).isEqualTo(returnedAddresses);

// and subsequent calls must be answered from cache instead of calling http service again
final DiscoveryResponse subsequentResult = testee.findDiscoveryEndpoints(request);
assertThat(extractEndpointAddresses(subsequentResult)).isEqualTo(cachedAddresses);
verifyNoMoreInteractions(restTemplateMock);
}
}

@Test
void evictDiscoveryEndpointsCacheValues_WhenScheduled_ShouldEvictCache() {

// GIVEN
final var request = new DiscoveryFinderRequest(List.of("bpn"));
simulateFindDiscoveryEndpointsRestRequest(request, MOCKED_DISCOVERY_RESPONSE);
testee.findDiscoveryEndpoints(request);
final var cache = getDiscoveryEndpointsCache();
assertThat(cache.get(request)).isNotNull();

// WHEN
Awaitility.await().atLeast(Duration.ofMillis(100))
// THEN
.untilAsserted(() -> assertThat(cache.get(request)).isNull());

}

private void simulateFindDiscoveryEndpointsRestRequest(final DiscoveryFinderRequest discoveryFinderRequest,
final DiscoveryResponse discoveryResponse) {
when(restTemplateMock.postForObject("", discoveryFinderRequest, DiscoveryResponse.class)).thenReturn(
discoveryResponse);
}

private DiscoveryResponse requireDiscoveryEndpointsCacheValue(final DiscoveryFinderRequest request) {
final var cache = getDiscoveryEndpointsCache();
final var cacheValue = cache.get(request);
assertThat(cacheValue).isNotNull();
final DiscoveryResponse discoveryResponse = (DiscoveryResponse) cacheValue.get();
assertThat(discoveryResponse).isNotNull();
return discoveryResponse;
}

private Cache getDiscoveryEndpointsCache() {
return cacheManager.getCache(DiscoveryFinderClientImpl.DISCOVERY_ENDPOINTS_CACHE);
}

private List<String> extractEndpointAddresses(final DiscoveryResponse discoveryResponse) {
return discoveryResponse.endpoints().stream() //
.map(DiscoveryEndpoint::endpointAddress) //
.sorted() //
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
import io.github.resilience4j.retry.RetryRegistry;
import org.eclipse.tractusx.irs.aaswrapper.job.AASTransferProcess;
import org.eclipse.tractusx.irs.aaswrapper.job.ItemContainer;
import org.eclipse.tractusx.irs.component.JobParameter;
import org.eclipse.tractusx.irs.component.PartChainIdentificationKey;
import org.eclipse.tractusx.irs.component.enums.AspectType;
import org.eclipse.tractusx.irs.component.enums.BomLifecycle;
import org.eclipse.tractusx.irs.component.enums.Direction;
import org.eclipse.tractusx.irs.component.enums.ProcessStep;
import org.eclipse.tractusx.irs.registryclient.DigitalTwinRegistryService;
import org.eclipse.tractusx.irs.registryclient.exceptions.RegistryServiceException;
Expand All @@ -61,13 +65,24 @@ void shouldFillItemContainerWithShell() throws RegistryServiceException {
// then
assertThat(result).isNotNull();
assertThat(result.getShells()).isNotEmpty();
assertThat(result.getShells().get(0).getSubmodelDescriptors()).isNotEmpty();
}

private static PartChainIdentificationKey createKey() {
return PartChainIdentificationKey.builder().globalAssetId("itemId").bpn("bpn123").build();
}
private static PartChainIdentificationKey createKeyWithoutBpn() {
return PartChainIdentificationKey.builder().globalAssetId("itemId").build();
@Test
void shouldFillItemContainerWithShellAndFilteredSubmodelDescriptorsWhenDepthReached() throws RegistryServiceException {
// given
when(digitalTwinRegistryService.fetchShells(any())).thenReturn(
List.of(shellDescriptor(List.of(submodelDescriptorWithoutHref("any")))));
final JobParameter jobParameter = JobParameter.builder().depth(1).aspects(List.of()).build();

// when
final ItemContainer result = digitalTwinDelegate.process(ItemContainer.builder(), jobParameter,
new AASTransferProcess("id", 1), createKey());

// then
assertThat(result).isNotNull();
assertThat(result.getShells()).isNotEmpty();
assertThat(result.getShells().get(0).getSubmodelDescriptors()).isEmpty();
}

@Test
Expand Down Expand Up @@ -105,4 +120,10 @@ void shouldCreateTombstoneIfBPNEmpty() {
ProcessStep.DIGITAL_TWIN_REQUEST);
}

private static PartChainIdentificationKey createKey() {
return PartChainIdentificationKey.builder().globalAssetId("itemId").bpn("bpn123").build();
}
private static PartChainIdentificationKey createKeyWithoutBpn() {
return PartChainIdentificationKey.builder().globalAssetId("itemId").build();
}
}
Loading

0 comments on commit 6a24269

Please sign in to comment.