Skip to content

Comments

InMemPagedTRS handles concurrency.#855

Merged
berezovskyi merged 3 commits intomasterfrom
trs_concurrency
Dec 13, 2025
Merged

InMemPagedTRS handles concurrency.#855
berezovskyi merged 3 commits intomasterfrom
trs_concurrency

Conversation

@Jad-el-khoury
Copy link
Contributor

InMemPagedTRS handles concurrency.

Checklist

  • This PR adds an entry to the CHANGELOG. See https://keepachangelog.com/en/1.0.0/ for instructions. Minor edits are exempt.
  • This PR was tested on at least one Lyo OSLC server (comment @oslc-bot /test-all if not sure) or adds unit/integration tests.
  • This PR does NOT break the API
  • Lint checks pass (run mvn package org.openrewrite.maven:rewrite-maven-plugin:run spotless:apply -DskipTests -P'!enforcer' if not, commit & push)

@Jad-el-khoury
Copy link
Contributor Author

@berezovskyi See it working with oslc-op/refimpl#473 on the rm-server

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 InmemPagedTrs internal storage from List to ConcurrentHashMap with URI-based indexing
  • Added thread-safe collections (CopyOnWriteArrayList) to Base and ChangeLog classes
  • Synchronized critical methods (onHistoryData, initBase, findOrCreateBase, etc.) to prevent race conditions during concurrent updates
  • Added new navigation methods to the PagedTrs interface (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.

Comment on lines +140 to +143
if (TRSConstants.RDF_NIL.equals(base.getNextPage().getNextPage().toString())) {
return null;
}
return getBaseResource(base.getNextPage().getNextPage());
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
private Base getLastBaseResource() {
return this.baseResources.get(this.baseResources.size() - 1);
private URI lastBasePageUri() {
int pageId = this.baseResources.size();
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
int pageId = this.baseResources.size();
int pageId = this.baseResources.size();
if (pageId == 0) {
return null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +382 to +383
.path(this.baseRelativePath)
.path(String.valueOf(pageId))
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistent indentation: lines 382-383 use tab characters while the surrounding code uses spaces. This affects code readability and consistency.

Suggested change
.path(this.baseRelativePath)
.path(String.valueOf(pageId))
.path(this.baseRelativePath)
.path(String.valueOf(pageId))

Copilot uses AI. Check for mistakes.

/**
* Get the previous ChangeLog of the given changeLog.
*
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @param tag is missing for the changeLog parameter. The JavaDoc should document this parameter: @param changeLog the current ChangeLog page

Suggested change
*
*
* @param changeLog the current ChangeLog page

Copilot uses AI. Check for mistakes.
final URI uri = getUriBuilder().path(this.baseRelativePath).build();
return uri;
private URI nextBasePageUri() {
int pageId = this.baseResources.size() + 1;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistent indentation: this line uses a tab character while the surrounding code uses spaces. This affects code readability and consistency.

Suggested change
int pageId = this.baseResources.size() + 1;
int pageId = this.baseResources.size() + 1;

Copilot uses AI. Check for mistakes.
Comment on lines +388 to +389
//There is always an entry with the "nil" changeLog page.
return changelogUriForPage(this.changelogResources.size());
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistent indentation: this line uses a tab character while the surrounding code uses spaces. This affects code readability and consistency.

Suggested change
//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());

Copilot uses AI. Check for mistakes.
}

exec.shutdown();
exec.awaitTermination(10, TimeUnit.SECONDS);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));

Copilot uses AI. Check for mistakes.
private int pageIdToListIdx(final Integer pageId) {
return pageId - 1;
@Override
public Base getBaseFirst() {
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
public Base getBaseFirst() {
public Base getBaseFirst() {
if (baseResources.isEmpty()) {
return null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +99
: (totalEvents % changelogPageLimit == 0
? changelogPageLimit
: totalEvents % changelogPageLimit);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is always false.

Suggested change
: (totalEvents % changelogPageLimit == 0
? changelogPageLimit
: totalEvents % changelogPageLimit);
: totalEvents % changelogPageLimit;

Copilot uses AI. Check for mistakes.
@OslcDescription("A member Resource of the Resource Set.")
@OslcPropertyDefinition(RDFS_MEMBER)
@OslcTitle("Member")
public List<URI> getMembers() {
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@berezovskyi berezovskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need to have a closer look

if (members == null) {
throw new IllegalArgumentException("Members list must not be null");
}
this.members.clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think that copy on write list AND a new arraylist are quite wasteful when used together?

@berezovskyi
Copy link
Contributor

@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.

@berezovskyi berezovskyi merged commit ba6a796 into master Dec 13, 2025
10 checks passed
@berezovskyi berezovskyi added this to the 7.0 milestone Dec 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants