InMemPagedTRS handles concurrency.#855
Conversation
|
@berezovskyi See it working with oslc-op/refimpl#473 on the rm-server |
There was a problem hiding this comment.
Pull request overview
This PR adds concurrency support to the InMemPagedTRS implementation by transitioning from List-based to ConcurrentHashMap-based storage for base resources and change logs, and using CopyOnWriteArrayList for thread-safe collections in Base and ChangeLog classes.
Key changes:
- Converted
InmemPagedTrsinternal storage fromListtoConcurrentHashMapwith URI-based indexing - Added thread-safe collections (
CopyOnWriteArrayList) toBaseandChangeLogclasses - Synchronized critical methods (
onHistoryData,initBase,findOrCreateBase, etc.) to prevent race conditions during concurrent updates - Added new navigation methods to the
PagedTrsinterface (getBaseResource(URI),getBaseFirst(),getNext(),getChangeLog(URI),getPrevious()) - Added comprehensive concurrency test (
InmemPagedTrsThreadSafetyTest) to verify thread-safety under concurrent access
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| trs/server/src/main/java/org/eclipse/lyo/oslc4j/trs/server/InmemPagedTrs.java | Refactored to use ConcurrentHashMap for storage, added synchronized methods, implemented URI-based lookups |
| trs/server/src/main/java/org/eclipse/lyo/oslc4j/trs/server/PagedTrs.java | Added new interface methods for URI-based lookups and navigation |
| trs/server/src/test/java/org/eclipse/lyo/oslc4j/trs/server/InmemPagedTrsThreadSafetyTest.java | New concurrency test validating thread-safe behavior under concurrent updates |
| trs/server/src/test/java/org/eclipse/lyo/oslc4j/trs/server/InmemPagedTrsTest.java | Updated tests to use new URI-based API methods |
| core/oslc-trs/src/main/java/org/eclipse/lyo/core/trs/Base.java | Changed members list to CopyOnWriteArrayList for thread-safety, updated setter to be defensive |
| core/oslc-trs/src/main/java/org/eclipse/lyo/core/trs/ChangeLog.java | Changed change list to CopyOnWriteArrayList for thread-safety, updated setter to be defensive, fixed spacing in annotation |
| trs/client/trs-client/src/main/java/org/eclipse/lyo/trs/client/util/ProviderUtil.java | Wrapped subList() results with ArrayList to prevent issues with list views |
| trs/server/pom.xml | Added junit-vintage-engine dependency for test compatibility |
| CHANGELOG.md | Added entry documenting concurrency support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (TRSConstants.RDF_NIL.equals(base.getNextPage().getNextPage().toString())) { | ||
| return null; | ||
| } | ||
| return getBaseResource(base.getNextPage().getNextPage()); |
There was a problem hiding this comment.
This method will throw a NullPointerException if base.getNextPage() or base.getNextPage().getNextPage() returns null. Add null checks before calling toString() and accessing nested properties to handle these edge cases safely.
| if (TRSConstants.RDF_NIL.equals(base.getNextPage().getNextPage().toString())) { | |
| return null; | |
| } | |
| return getBaseResource(base.getNextPage().getNextPage()); | |
| if (base == null) { | |
| return null; | |
| } | |
| Page nextPage = base.getNextPage(); | |
| if (nextPage == null) { | |
| return null; | |
| } | |
| URI nextNextPage = nextPage.getNextPage(); | |
| if (nextNextPage == null) { | |
| return null; | |
| } | |
| if (TRSConstants.RDF_NIL.equals(nextNextPage.toString())) { | |
| return null; | |
| } | |
| return getBaseResource(nextNextPage); |
| private Base getLastBaseResource() { | ||
| return this.baseResources.get(this.baseResources.size() - 1); | ||
| private URI lastBasePageUri() { | ||
| int pageId = this.baseResources.size(); |
There was a problem hiding this comment.
The lastBasePageUri() method can fail when baseResources.size() is 0, as basePageUriForPage(0) will throw an IllegalArgumentException (page id must be >= 1). This method is called by getLastBaseResource() which is used in synchronized methods. Add a check to handle the empty base resources case.
| int pageId = this.baseResources.size(); | |
| int pageId = this.baseResources.size(); | |
| if (pageId == 0) { | |
| return null; | |
| } |
| .path(this.baseRelativePath) | ||
| .path(String.valueOf(pageId)) |
There was a problem hiding this comment.
Use consistent indentation: lines 382-383 use tab characters while the surrounding code uses spaces. This affects code readability and consistency.
| .path(this.baseRelativePath) | |
| .path(String.valueOf(pageId)) | |
| .path(this.baseRelativePath) | |
| .path(String.valueOf(pageId)) |
|
|
||
| /** | ||
| * Get the previous ChangeLog of the given changeLog. | ||
| * |
There was a problem hiding this comment.
The @param tag is missing for the changeLog parameter. The JavaDoc should document this parameter: @param changeLog the current ChangeLog page
| * | |
| * | |
| * @param changeLog the current ChangeLog page |
| final URI uri = getUriBuilder().path(this.baseRelativePath).build(); | ||
| return uri; | ||
| private URI nextBasePageUri() { | ||
| int pageId = this.baseResources.size() + 1; |
There was a problem hiding this comment.
Use consistent indentation: this line uses a tab character while the surrounding code uses spaces. This affects code readability and consistency.
| int pageId = this.baseResources.size() + 1; | |
| int pageId = this.baseResources.size() + 1; |
| //There is always an entry with the "nil" changeLog page. | ||
| return changelogUriForPage(this.changelogResources.size()); |
There was a problem hiding this comment.
Use consistent indentation: this line uses a tab character while the surrounding code uses spaces. This affects code readability and consistency.
| //There is always an entry with the "nil" changeLog page. | |
| return changelogUriForPage(this.changelogResources.size()); | |
| //There is always an entry with the "nil" changeLog page. | |
| return changelogUriForPage(this.changelogResources.size()); |
| } | ||
|
|
||
| exec.shutdown(); | ||
| exec.awaitTermination(10, TimeUnit.SECONDS); |
There was a problem hiding this comment.
The test doesn't verify that the executor service completed successfully. If awaitTermination returns false (meaning timeout occurred), the test should fail. Consider checking the return value: Assert.assertTrue("Executor did not complete in time", exec.awaitTermination(10, TimeUnit.SECONDS));
| private int pageIdToListIdx(final Integer pageId) { | ||
| return pageId - 1; | ||
| @Override | ||
| public Base getBaseFirst() { |
There was a problem hiding this comment.
The getBaseFirst() method will throw a NoSuchElementException if baseResources is empty. This can occur if the TRS is initialized with an empty collection or before any base resources are added. Consider adding a null check or isEmpty() check to return null when there are no base resources.
| public Base getBaseFirst() { | |
| public Base getBaseFirst() { | |
| if (baseResources.isEmpty()) { | |
| return null; | |
| } |
| : (totalEvents % changelogPageLimit == 0 | ||
| ? changelogPageLimit | ||
| : totalEvents % changelogPageLimit); |
There was a problem hiding this comment.
Test is always false.
| : (totalEvents % changelogPageLimit == 0 | |
| ? changelogPageLimit | |
| : totalEvents % changelogPageLimit); | |
| : totalEvents % changelogPageLimit; |
| @OslcDescription("A member Resource of the Resource Set.") | ||
| @OslcPropertyDefinition(RDFS_MEMBER) | ||
| @OslcTitle("Member") | ||
| public List<URI> getMembers() { |
There was a problem hiding this comment.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
getMembers exposes the internal representation stored in field members. The value may be modified after this call to getMembers.
berezovskyi
left a comment
There was a problem hiding this comment.
Will need to have a closer look
| if (members == null) { | ||
| throw new IllegalArgumentException("Members list must not be null"); | ||
| } | ||
| this.members.clear(); |
There was a problem hiding this comment.
This does not seem thread safe. As no lock is used, there could be a case where a reader would observe an empty list while this update is going on. Can we not just introduce an api to add an item? Otherwise it's best to mark this method synchronised.
| @OslcDescription("A member Resource of the Resource Set.") | ||
| @OslcPropertyDefinition(RDFS_MEMBER) | ||
| @OslcTitle("Member") | ||
| public List<URI> getMembers() { |
There was a problem hiding this comment.
Can we update this signature to return a readonly list? To stop the pattern of getting a list and then modifying it by hand.
|
|
||
| firstChangelogEvents = firstChangelogEvents.subList(indexOfSync + 1, | ||
| firstChangelogEvents.size()); | ||
| firstChangelogEvents = new ArrayList<>(firstChangelogEvents.subList(indexOfSync + 1, |
There was a problem hiding this comment.
I don’t like this, it's really prone to misuse. I can see how a user may forget to do this and get occasional concurrency issues.
There was a problem hiding this comment.
I also think that copy on write list AND a new arraylist are quite wasteful when used together?
|
@Jad-el-khoury, see my feedback inside but if you are happy to merge as is, let's do it. We can convert some comments into new issues. |
InMemPagedTRS handles concurrency.
Checklist
@oslc-bot /test-allif not sure) or adds unit/integration tests.mvn package org.openrewrite.maven:rewrite-maven-plugin:run spotless:apply -DskipTests -P'!enforcer'if not, commit & push)