-
Notifications
You must be signed in to change notification settings - Fork 102
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
base: master
Are you sure you want to change the base?
Conversation
ace15eb
to
0af7dcf
Compare
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, |
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, |
Hi @arturobernalg !
I agree that more complex scenarios would not fit inside this utility class. However, the scope of the PR is to switch away from
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 WDYT? |
HI @juanpablo-santos 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 |
0af7dcf
to
5b7704b
Compare
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. |
There was a problem hiding this 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, () -> { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, () -> { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); | ||
* } | ||
* } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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!!
|
||
<dependency> | ||
<groupId>org.apache.jspwiki</groupId> | ||
<artifactId>jspwiki-util</artifactId> |
There was a problem hiding this comment.
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)
2a81421
to
62b140b
Compare
HI @juanpablo-santos |
c1edd8a
to
b9b3a38
Compare
b9b3a38
to
659b8a5
Compare
There was a problem hiding this 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); |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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, () -> { |
There was a problem hiding this comment.
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, () -> { |
There was a problem hiding this comment.
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, () -> { |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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, () -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
additional lock here?
Refactored synchronized blocks/methods containing blocking operations to prevent potential deadlocks introduced by the upcoming Virtual Threads feature in JDK 21.
659b8a5
to
3a83d94
Compare
+ some format edits
…which is not happening here
Hi @arturobernalg ! went ahead and pushed remaining requested changes, however I've got tests errors on thx + best regards |
I'll take a look. TY |
hey @arturobernalg just found out about https://projectlombok.org/features/Locked on latest Lombok; same approach as your |
Hey @juanpablo-santos thanks for sharing! I'm continuing to check the issues. |
Refactored synchronized blocks/methods containing blocking operations to prevent potential deadlocks introduced by the upcoming Virtual Threads feature in JDK 21.