Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JSPWIKI-1178] Address potential deadlock with JDK 21 Virtual Threads. #307

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

arturobernalg
Copy link
Member

Refactored synchronized blocks/methods containing blocking operations to prevent potential deadlocks introduced by the upcoming Virtual Threads feature in JDK 21.

@juanpablo-santos
Copy link
Contributor

Hi,

interesting PR! :-) My only comment is that I'd extract the lock behaviour on a utility class something similar to (pseudocode, surely doesn't even compile):

public class Synchronizer {

    public static < T > T synchronize( final ReentrantLock lock, final Supplier< T > supplier ) {
        lock.lock();
	try {
	    return supplier.get();
	} finally {
	    lock.unlock();
	}
    }

}

so code inside synchronized blocks can be passed as a lambda (also this method could be overloaded so it could also receive some Runnable, Function, etc.)

WDYT?

cheers,
juan pablo

@arturobernalg
Copy link
Member Author

Hi,

interesting PR! :-) My only comment is that I'd extract the lock behaviour on a utility class something similar to (pseudocode, surely doesn't even compile):

public class Synchronizer {

    public static < T > T synchronize( final ReentrantLock lock, final Supplier< T > supplier ) {
        lock.lock();
	try {
	    return supplier.get();
	} finally {
	    lock.unlock();
	}
    }

}

so code inside synchronized blocks can be passed as a lambda (also this method could be overloaded so it could also receive some Runnable, Function, etc.)

WDYT?

cheers, juan pablo

HI @juanpablo-santos

I agree that encapsulating the lock behavior in a utility class like Synchronizer would offer several advantages, particularly in terms of code Readability and Reusability.

However, this approach has limitations when it comes to working with condition variables and allowing for custom scenarios. Specifically, using a utility class for locking would make it challenging to implement more complex control flows that involve waiting for certain conditions to be met or signaling other threads to proceed.

In essence, while the utility class would make the code cleaner for basic locking and unlocking, it might not be flexible enough to handle advanced locking scenarios that require the use of conditions.

Best regards,

@juanpablo-santos
Copy link
Contributor

juanpablo-santos commented Oct 9, 2023

Hi @arturobernalg !

However, this approach has limitations when it comes to working with condition variables and allowing for custom scenarios. Specifically, using a utility class for locking would make it challenging to implement more complex control flows that involve waiting for certain conditions to be met or signaling other threads to proceed.

In essence, while the utility class would make the code cleaner for basic locking and unlocking, it might not be flexible enough to handle advanced locking scenarios that require the use of conditions.

I agree that more complex scenarios would not fit inside this utility class. However, the scope of the PR is to switch away from synchronized blocks, and for all them we have the same idiom all over the place:

lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

So abstracting it into a common method makes sense to me. If later on we want to refactor, or we want to capture more complex scenarios, we can always move away from the utility synchronize method. The utility/class method focus should be more about synchronizing than locking (hence the names).

WDYT?

@arturobernalg
Copy link
Member Author

arturobernalg commented Oct 9, 2023

Hi @arturobernalg !

However, this approach has limitations when it comes to working with condition variables and allowing for custom scenarios. Specifically, using a utility class for locking would make it challenging to implement more complex control flows that involve waiting for certain conditions to be met or signaling other threads to proceed.
In essence, while the utility class would make the code cleaner for basic locking and unlocking, it might not be flexible enough to handle advanced locking scenarios that require the use of conditions.

I agree that more complex scenarios would not fit inside this utility class. However, the scope of the PR is to switch away from synchronized blocks, and for all them we have the same idiom all over the place:

lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

So abstracting it into a common method makes sense to me. If later on we want to refactor, or we want to capture more complex scenarios, we can always move away from the utility synchronize method. The utility/class method focus should be more about synchronizing than locking (hence the names).

WDYT?

HI @juanpablo-santos
It makes sense to start with this abstraction for the sake of code cleanliness and readability.

I'll go ahead and make the changes to incorporate the new method It might take me a couple of days to complete the update, but I'll keep you posted on the progress.

TY

@arturobernalg
Copy link
Member Author

arturobernalg commented Oct 10, 2023

The utility/class method focus should be more about synchronizing than locking (hence the names).

Hi @arturobernalg !

However, this approach has limitations when it comes to working with condition variables and allowing for custom scenarios. Specifically, using a utility class for locking would make it challenging to implement more complex control flows that involve waiting for certain conditions to be met or signaling other threads to proceed.
In essence, while the utility class would make the code cleaner for basic locking and unlocking, it might not be flexible enough to handle advanced locking scenarios that require the use of conditions.

I agree that more complex scenarios would not fit inside this utility class. However, the scope of the PR is to switch away from synchronized blocks, and for all them we have the same idiom all over the place:

lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

So abstracting it into a common method makes sense to me. If later on we want to refactor, or we want to capture more complex scenarios, we can always move away from the utility synchronize method. The utility/class method focus should be more about synchronizing than locking (hence the names).
WDYT?

HI @juanpablo-santos It makes sense to start with this abstraction for the sake of code cleanliness and readability.

I'll go ahead and make the changes to incorporate the new method It might take me a couple of days to complete the update, but I'll keep you posted on the progress.

TY

HI @juanpablo-santos

I've just implemented the custom Synchronizer class to manage ReentrantLock synchronization, as initially planned. However, the scope of the changes turned out to be more extensive than I had originally anticipated. As a result, I'm not entirely confident in the current implementation. I had to create several methods to handle all possible use cases, and there were a couple of scenarios that I couldn't address. I'm looking forward to your feedback to determine if this approach is suitable for our use case.

Copy link
Contributor

@juanpablo-santos juanpablo-santos left a comment

Choose a reason for hiding this comment

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

Overall, the PR is in a great shape, congrats!

I've noted several places that might need the use of several locks, so they mimic the current syncing behaviour (I might have missed some, pls take a look overall), and there's a small nitch with how the util module is referenced on pom.xml files, but they're details within the amount of work done here. This should be very close to merge :-)

thanks,
juan pablo

// start up any post-instantiation services here
}
if (c_instance == null) {
Synchronizer.synchronize(lock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

seems that the synchronized blocks on this class do so against different objects: WikiEventManager.class, m_delegates, m_preloadCache, etc., so it would make more sense to use different locks on Synchronizer.synchronize calls?

*
* @see java.util.concurrent.locks.ReentrantLock
*/
private final ReentrantLock lock;
Copy link
Contributor

Choose a reason for hiding this comment

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

same as noted above, perhaps more locks are needed (?)

@@ -302,15 +315,15 @@ private void dumpStackTraceForWatchable() {
*/
@Override
public String toString() {
synchronized( m_stateStack ) {
return Synchronizer.synchronize(lock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

unrelated to the PR itself, but java.util.Stack is thread-safe (extends Vector) so not sure why this was synced on in the first place... Also, perhaps it could be substituted with a ConcurrentLinkedDeque. Anyway, out of PR's scope.

*
* @see java.util.concurrent.locks.ReentrantLock
*/
private static final ReentrantLock lock = new ReentrantLock();
Copy link
Contributor

Choose a reason for hiding this comment

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

again, perhaps several locks are needed

*
* @see ReentrantLock
*/
private final ReentrantLock lock;
Copy link
Contributor

Choose a reason for hiding this comment

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

syncs against m_sessions and at method level, so it should use two locks (?)

@@ -70,6 +82,7 @@ public DefaultWorkflowManager() {
m_approvers = new ConcurrentHashMap<>();
m_queue = new DecisionQueue();
WikiEventEmitter.attach( this );
// this.lock = new ReentrantLock();
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe delete this line?

* } finally {
* lock.unlock();
* }
* }
Copy link
Contributor

Choose a reason for hiding this comment

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

duplicated line, can be deleted?

public static <E extends Exception> void synchronize(final ReentrantLock lock, final ThrowingRunnable<E> throwingRunnable) {
lock.lock();
try {
throwingRunnable.run();
Copy link
Contributor

Choose a reason for hiding this comment

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

didn't see this initially, good catch!!

jspwiki-event/pom.xml Show resolved Hide resolved

<dependency>
<groupId>org.apache.jspwiki</groupId>
<artifactId>jspwiki-util</artifactId>
Copy link
Contributor

Choose a reason for hiding this comment

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

this shouldn't be needed here, jspwiki modules should be referenced on other modules using groupId:artifactId:version where groupId is always ${project.groupId} and version always is ${project.version} (see this for an example)

@arturobernalg arturobernalg force-pushed the JSPWIKI-1178 branch 2 times, most recently from 2a81421 to 62b140b Compare October 11, 2023 19:54
@arturobernalg
Copy link
Member Author

HI @juanpablo-santos
I fix all the comment. Please do another pass.
TY

Copy link
Contributor

@juanpablo-santos juanpablo-santos left a comment

Choose a reason for hiding this comment

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

apologies on the delay reviewing this PR, there's lots of code and very little time :-S

synchronized( m_preloadCache ) {
m_preloadCache.clear();
}
Synchronizer.synchronize(delegatesLockLock, m_delegates::clear);
Copy link
Contributor

Choose a reason for hiding this comment

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

m_delegates could be converted to a ConcurrentHashMap, avoiding the need of using Synchronizer.synchronize. Also m_preloadCache is a Vector, hence thread-safe, so there's no need of syncing.. don't know why it was synchronized in first place, though.

@@ -314,35 +337,35 @@ private Map< Object, WikiEventDelegate > getDelegates() {
* @param client the client Object, or alternately a Class reference
* @return the WikiEventDelegate.
*/
private WikiEventDelegate getDelegateFor( final Object client ) {
synchronized( m_delegates ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

again, converting to ConcurrentHashMap would avoid all the syncing..

@@ -406,15 +429,15 @@ public Set< WikiEventListener > getWikiEventListeners() {
* @return true if the listener was added (i.e., it was not already in the list and was added)
*/
public boolean addWikiEventListener( final WikiEventListener listener ) {
synchronized( m_listenerList ) {
return Synchronizer.synchronize(wikiEventListenerLock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't m_listener reuse the same Lock through the entire class? Different locks on different operations would imply they could execute operations on m_listenerList at the same time

@@ -136,26 +154,26 @@ private static void scrub() {
* Can be used to enable the WatchDog. Will cause a new Thread to be created, if none was existing previously.
*/
public void enable() {
synchronized( WatchDog.class ) {
Synchronizer.synchronize(enableLock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

same as before, same synchronized object should reuse the same Lock throughout the class.

@@ -186,10 +204,10 @@ public void enterState( final String state ) {
*/
public void enterState( final String state, final int expectedCompletionTime ) {
LOG.debug( "{}: Entering state {}, expected completion in {} s", m_watchable.getName(), state, expectedCompletionTime );
synchronized( m_stateStack ) {
Synchronizer.synchronize(enterStateLock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

same as before..

@@ -152,13 +180,13 @@ public void initialize( final Engine engine, final Properties props ) throws Wik

// Load all groups from the database into the cache
final Group[] groups = m_groupDatabase.groups();
synchronized( m_groups ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

converting m_groups to ConcurrentHashMap would avoid the use of Synchronizer

@@ -367,14 +395,18 @@ protected String[] extractMembers( final String memberLine ) {

/** {@inheritDoc} */
@Override
public synchronized void addWikiEventListener( final WikiEventListener listener ) {
WikiEventManager.addWikiEventListener( this, listener );
public void addWikiEventListener( final WikiEventListener listener ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should use the same lock as removeWikiEventListener

*
* @see java.util.concurrent.locks.ReentrantLock
*/
private final ReentrantLock lock;
Copy link
Contributor

Choose a reason for hiding this comment

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

better private final ReentrantLock lock = new ReentrantLock(); to avoid to declare the constructor?

*
* @see java.util.concurrent.locks.ReentrantLock
*/
private final ReentrantLock lock;
Copy link
Contributor

Choose a reason for hiding this comment

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

as noted before, perhaps better to just private final ReentrantLock lock = new ReentrantLock(); to avoid declaring the constructor? Also, seems that more than one lock is needed

@@ -389,9 +407,9 @@ protected Document luceneIndexPage( final Page page, final String text, final In
field = new Field( LUCENE_PAGE_KEYWORDS, page.getAttribute( "keywords" ).toString(), TextField.TYPE_STORED );
doc.add( field );
}
synchronized( writer ) {
Synchronizer.synchronize(lock, () -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

additional lock here?

arturobernalg and others added 2 commits December 6, 2023 09:32
Refactored synchronized blocks/methods containing blocking operations to prevent potential deadlocks introduced by the upcoming Virtual Threads feature in JDK 21.
@juanpablo-santos
Copy link
Contributor

Hi @arturobernalg !

went ahead and pushed remaining requested changes, however I've got tests errors on XMLUserDatabaseTest. They aren't happening on the master branch, and were happening before applying my commits. Don't know if they're flaky tests which fail due to previous tests setting some unwanted state or if a bug was introduced with the refactor, would you mind taking a look at it?

thx + best regards

@arturobernalg
Copy link
Member Author

Hi @arturobernalg !

went ahead and pushed remaining requested changes, however I've got tests errors on XMLUserDatabaseTest. They aren't happening on the master branch, and were happening before applying my commits. Don't know if they're flaky tests which fail due to previous tests setting some unwanted state or if a bug was introduced with the refactor, would you mind taking a look at it?

thx + best regards

Hi @juanpablo-santos

I'll take a look.

TY

@juanpablo-santos
Copy link
Contributor

hey @arturobernalg just found out about https://projectlombok.org/features/Locked on latest Lombok; same approach as your Synchronizer class, which seems definitely the way to go :-)

@arturobernalg
Copy link
Member Author

hey @arturobernalg just found out about https://projectlombok.org/features/Locked on latest Lombok; same approach as your Synchronizer class, which seems definitely the way to go :-)

Hey @juanpablo-santos thanks for sharing! I'm continuing to check the issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants