Skip to content

Commit 423c7b9

Browse files
committed
Harden registry synchronization consistency
Signed-off-by: Aaron Zielstorff <aaron.zielstorff@iese.fraunhofer.de>
1 parent 2b2fbcb commit 423c7b9

4 files changed

Lines changed: 398 additions & 51 deletions

File tree

basyx.aasrepository/basyx.aasrepository-feature-registry-integration/Readme.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AssetAdministrationShell Repository - Registry Integration
2-
This feature automatically integrates the Descriptor with the Registry while creation of the Shell at Repository. <br>
3-
It also automatically removes the Descriptor from the Registry when the Shell is removed from the Repository.
2+
This feature automatically creates and updates the Descriptor in the Registry when a Shell or its Asset Information changes in the Repository. <br>
3+
It also automatically removes the Descriptor from the Registry when the Shell is removed from the Repository.
44

55
To enable this feature, the following two properties should be configured:
66

@@ -31,4 +31,14 @@ basyx.aasrepository.feature.registryintegration.authorization.client-secret = <c
3131
basyx.aasrepository.feature.registryintegration.authorization.username=test
3232
basyx.aasrepository.feature.registryintegration.authorization.password=test
3333
basyx.aasrepository.feature.registryintegration.authorization.scopes=[]
34-
```
34+
```
35+
36+
The Registry permissions required by the integration are:
37+
38+
- `CREATE` when a Shell is created
39+
- `READ` and `UPDATE` when a Shell or its Asset Information is updated
40+
- `DELETE` when a Shell is deleted
41+
42+
`READ` is required during updates because the Registry API replaces complete descriptors. The integration reads the existing descriptor first so that Registry-managed endpoint metadata and Submodel Descriptors are retained.
43+
44+
The Registry API currently provides neither conditional updates nor a partial update operation for Shell Descriptors. Consequently, descriptor changes made directly in the Registry by another client must not run concurrently with an AAS update if both changes need to be retained.

basyx.aasrepository/basyx.aasrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasrepository/feature/registry/integration/RegistryIntegrationAasRepository.java

Lines changed: 107 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626

2727
import java.io.File;
2828
import java.io.InputStream;
29+
import java.util.Arrays;
2930
import java.util.List;
31+
import java.util.Objects;
3032

3133
import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell;
3234
import org.eclipse.digitaltwin.aas4j.v3.model.AssetInformation;
@@ -54,17 +56,20 @@
5456
*
5557
*/
5658
public class RegistryIntegrationAasRepository implements AasRepository {
59+
private static final int SHELL_LOCK_COUNT = 64;
5760
private static Logger logger = LoggerFactory.getLogger(RegistryIntegrationAasRepository.class);
5861

5962
private AasRepository decorated;
6063

6164
private AasRepositoryRegistryLink aasRepositoryRegistryLink;
6265
private AttributeMapper attributeMapper;
66+
private final Object[] shellMutationLocks = new Object[SHELL_LOCK_COUNT];
6367

6468
public RegistryIntegrationAasRepository(AasRepository decorated, AasRepositoryRegistryLink aasRepositoryRegistryLink, AttributeMapper attributeMapper) {
6569
this.decorated = decorated;
6670
this.aasRepositoryRegistryLink = aasRepositoryRegistryLink;
6771
this.attributeMapper = attributeMapper;
72+
Arrays.setAll(shellMutationLocks, index -> new Object());
6873
}
6974

7075
@Override
@@ -79,40 +84,45 @@ public AssetAdministrationShell getAas(String shellId) throws ElementDoesNotExis
7984

8085
@Override
8186
public void createAas(AssetAdministrationShell shell) throws CollidingIdentifierException {
82-
AssetAdministrationShellDescriptor descriptor = createDescriptor(shell);
87+
synchronized (getShellMutationLock(shell.getId())) {
88+
AssetAdministrationShellDescriptor descriptor = createDescriptor(shell);
8389

84-
decorated.createAas(shell);
90+
decorated.createAas(shell);
8591

86-
boolean registrationSuccessful = false;
92+
boolean registrationSuccessful = false;
8793

88-
try {
89-
registerAas(descriptor);
90-
registrationSuccessful = true;
91-
} finally {
92-
if (!registrationSuccessful)
93-
decorated.deleteAas(shell.getId());
94+
try {
95+
registerAas(descriptor);
96+
registrationSuccessful = true;
97+
} finally {
98+
if (!registrationSuccessful)
99+
decorated.deleteAas(shell.getId());
100+
}
94101
}
95102
}
96103

97104
@Override
98105
public void updateAas(String shellId, AssetAdministrationShell shell) {
99-
AssetAdministrationShell previousShell = decorated.getAas(shellId);
106+
synchronized (getShellMutationLock(shellId)) {
107+
AssetAdministrationShell previousShell = decorated.getAas(shellId);
100108

101-
decorated.updateAas(shellId, shell);
109+
decorated.updateAas(shellId, shell);
102110

103-
try {
104-
updateDescriptor(shellId, shell);
105-
} catch (RuntimeException e) {
106-
rollbackAasUpdate(shellId, previousShell, e);
107-
throw e;
111+
try {
112+
updateDescriptor(shellId, shell);
113+
} catch (RuntimeException e) {
114+
rollbackAasUpdate(shellId, previousShell, e);
115+
throw e;
116+
}
108117
}
109118
}
110119

111120
@Override
112121
public void deleteAas(String shellId) {
113-
deleteFromRegistry(shellId);
114-
115-
decorated.deleteAas(shellId);
122+
synchronized (getShellMutationLock(shellId)) {
123+
deleteFromRegistry(shellId);
124+
decorated.deleteAas(shellId);
125+
}
116126
}
117127

118128
@Override
@@ -127,25 +137,31 @@ public CursorResult<List<Reference>> getSubmodelReferences(String shellId, Pagin
127137

128138
@Override
129139
public void addSubmodelReference(String shellId, Reference submodelReference) {
130-
decorated.addSubmodelReference(shellId, submodelReference);
140+
synchronized (getShellMutationLock(shellId)) {
141+
decorated.addSubmodelReference(shellId, submodelReference);
142+
}
131143
}
132144

133145
@Override
134146
public void removeSubmodelReference(String shellId, String submodelId) {
135-
decorated.removeSubmodelReference(shellId, submodelId);
147+
synchronized (getShellMutationLock(shellId)) {
148+
decorated.removeSubmodelReference(shellId, submodelId);
149+
}
136150
}
137151

138152
@Override
139153
public void setAssetInformation(String shellId, AssetInformation shellInfo) throws ElementDoesNotExistException {
140-
AssetInformation previousAssetInformation = decorated.getAssetInformation(shellId);
154+
synchronized (getShellMutationLock(shellId)) {
155+
AssetInformation previousAssetInformation = decorated.getAssetInformation(shellId);
141156

142-
decorated.setAssetInformation(shellId, shellInfo);
157+
decorated.setAssetInformation(shellId, shellInfo);
143158

144-
try {
145-
updateDescriptor(shellId, decorated.getAas(shellId));
146-
} catch (RuntimeException e) {
147-
rollbackAssetInformationUpdate(shellId, previousAssetInformation, e);
148-
throw e;
159+
try {
160+
updateDescriptor(shellId, decorated.getAas(shellId));
161+
} catch (RuntimeException e) {
162+
rollbackAssetInformationUpdate(shellId, previousAssetInformation, e);
163+
throw e;
164+
}
149165
}
150166
}
151167

@@ -168,25 +184,75 @@ private void registerAas(AssetAdministrationShellDescriptor descriptor) {
168184

169185
private void updateDescriptor(String shellId, AssetAdministrationShell shell) {
170186
RegistryAndDiscoveryInterfaceApi registryApi = aasRepositoryRegistryLink.getRegistryApi();
187+
AssetAdministrationShellDescriptor existingDescriptor;
171188

172189
try {
173-
AssetAdministrationShellDescriptor existingDescriptor = registryApi.getAssetAdministrationShellDescriptorById(shellId);
174-
AssetAdministrationShellDescriptor updatedDescriptor = createDescriptor(shell);
175-
updatedDescriptor.setSubmodelDescriptors(existingDescriptor.getSubmodelDescriptors());
190+
existingDescriptor = registryApi.getAssetAdministrationShellDescriptorById(shellId);
191+
} catch (ApiException e) {
192+
throw createRegistryLinkException(shellId, "update", e);
193+
}
176194

195+
AssetAdministrationShellDescriptor updatedDescriptor = createDescriptor(shell);
196+
preserveRegistryManagedAttributes(existingDescriptor, updatedDescriptor);
197+
198+
try {
177199
registryApi.putAssetAdministrationShellDescriptorById(shellId, updatedDescriptor);
178200

179201
logger.info("Shell descriptor '{}' has been automatically updated in the Registry", shellId);
180202
} catch (ApiException e) {
203+
if (descriptorMatchesAasDerivedState(shellId, updatedDescriptor, registryApi, e)) {
204+
logger.warn("Registry update for shell descriptor '{}' returned an error, but a read-back confirmed the requested AAS-derived state.", shellId);
205+
return;
206+
}
207+
181208
throw createRegistryLinkException(shellId, "update", e);
182209
}
183210
}
184211

212+
private void preserveRegistryManagedAttributes(AssetAdministrationShellDescriptor existingDescriptor, AssetAdministrationShellDescriptor updatedDescriptor) {
213+
if (existingDescriptor.getEndpoints() != null)
214+
updatedDescriptor.setEndpoints(existingDescriptor.getEndpoints());
215+
216+
updatedDescriptor.setSubmodelDescriptors(existingDescriptor.getSubmodelDescriptors());
217+
}
218+
219+
private boolean descriptorMatchesAasDerivedState(String shellId, AssetAdministrationShellDescriptor expectedDescriptor, RegistryAndDiscoveryInterfaceApi registryApi, ApiException updateException) {
220+
try {
221+
AssetAdministrationShellDescriptor actualDescriptor = registryApi.getAssetAdministrationShellDescriptorById(shellId);
222+
return aasDerivedAttributesEqual(expectedDescriptor, actualDescriptor);
223+
} catch (ApiException verificationException) {
224+
updateException.addSuppressed(verificationException);
225+
return false;
226+
}
227+
}
228+
229+
private boolean aasDerivedAttributesEqual(AssetAdministrationShellDescriptor expected, AssetAdministrationShellDescriptor actual) {
230+
return actual != null
231+
&& Objects.equals(expected.getAdministration(), actual.getAdministration())
232+
&& Objects.equals(expected.getAssetKind(), actual.getAssetKind())
233+
&& Objects.equals(expected.getAssetType(), actual.getAssetType())
234+
&& Objects.equals(expected.getDescription(), actual.getDescription())
235+
&& Objects.equals(expected.getDisplayName(), actual.getDisplayName())
236+
&& Objects.equals(expected.getExtensions(), actual.getExtensions())
237+
&& Objects.equals(expected.getGlobalAssetId(), actual.getGlobalAssetId())
238+
&& Objects.equals(expected.getId(), actual.getId())
239+
&& Objects.equals(expected.getIdShort(), actual.getIdShort())
240+
&& Objects.equals(expected.getSpecificAssetIds(), actual.getSpecificAssetIds());
241+
}
242+
185243
private AssetAdministrationShellDescriptor createDescriptor(AssetAdministrationShell shell) {
186244
return new AasDescriptorFactory(aasRepositoryRegistryLink.getAasRepositoryBaseURLs(), attributeMapper).create(shell);
187245
}
188246

189247
private RepositoryRegistryLinkException createRegistryLinkException(String shellId, String operation, ApiException cause) {
248+
return new RepositoryRegistryLinkException(shellId, createRegistryFailureDetails(operation, cause), cause);
249+
}
250+
251+
private RepositoryRegistryUnlinkException createRegistryUnlinkException(String shellId, ApiException cause) {
252+
return new RepositoryRegistryUnlinkException(shellId, createRegistryFailureDetails("deletion", cause), cause);
253+
}
254+
255+
private String createRegistryFailureDetails(String operation, ApiException cause) {
190256
StringBuilder details = new StringBuilder("Registry descriptor ").append(operation).append(" failed");
191257

192258
if (cause.getCode() > 0)
@@ -199,7 +265,7 @@ private RepositoryRegistryLinkException createRegistryLinkException(String shell
199265
if (responseDetails != null && !responseDetails.isBlank())
200266
details.append(": ").append(responseDetails);
201267

202-
return new RepositoryRegistryLinkException(shellId, details.toString(), cause);
268+
return details.toString();
203269
}
204270

205271
private void rollbackAasUpdate(String shellId, AssetAdministrationShell previousShell, RuntimeException updateException) {
@@ -222,30 +288,23 @@ private void rollbackAssetInformationUpdate(String shellId, AssetInformation pre
222288

223289
private void deleteFromRegistry(String shellId) {
224290
RegistryAndDiscoveryInterfaceApi registryApi = aasRepositoryRegistryLink.getRegistryApi();
225-
226-
if (!shellExistsOnRegistry(shellId, registryApi)) {
227-
logger.error("Unable to un-link the AAS descriptor '{}' from the Registry because it does not exist on the Registry.", shellId);
228-
229-
return;
230-
}
231291

232292
try {
233293
registryApi.deleteAssetAdministrationShellDescriptorById(shellId);
234294

235295
logger.info("Shell '{}' has been automatically un-linked from the Registry.", shellId);
236296
} catch (ApiException e) {
237-
throw new RepositoryRegistryUnlinkException(shellId, e);
297+
if (e.getCode() == 404) {
298+
logger.info("Shell descriptor '{}' was already absent from the Registry.", shellId);
299+
return;
300+
}
301+
302+
throw createRegistryUnlinkException(shellId, e);
238303
}
239304
}
240-
241-
private boolean shellExistsOnRegistry(String shellId, RegistryAndDiscoveryInterfaceApi registryApi) {
242-
try {
243-
registryApi.getAssetAdministrationShellDescriptorById(shellId);
244-
245-
return true;
246-
} catch (ApiException e) {
247-
return false;
248-
}
305+
306+
private Object getShellMutationLock(String shellId) {
307+
return shellMutationLocks[Math.floorMod(Objects.hashCode(shellId), shellMutationLocks.length)];
249308
}
250309

251310
@Override

0 commit comments

Comments
 (0)