In my application I am providing some application wide data in a @Singleton
EJB. The preperation of the data is a long running task. However, the underlying source of this outcome is changing frequently, so I have to recalculated the data which is populated by the Singleton frequently as well.
The task now is to ensure a quick access to the data, even if it is currently recalculated. It does not matter if I am exposing the old status, while the preparation is ongoing.
My current approach looks like this:
@Singleton
@Startup
@Lock(LockType.READ)
public class MySingleton {
@Inject private SomeService service;
@Getter private SomeData currentVersion;
@PostConstruct
private void init() {
update();
}
private void update() {
currentVersion = service.longRunningTask();
}
@Lock(LockType.WRITE)
@AccessTimeout(value = 1, unit = TimeUnit.MINUTES)
@Asynchronous
public void catchEvent(
@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event) {
this.update();
}
}
The idea behind this is to prepare the data once on startup. Afterwards the clients are able to access the current version of my data (consider this data as immutable) concurrently.
Now, if some action happens that may cause a recalculation of the data, I am fireing a CDI event and in the Singleton I am observing this event and triggering the recalculation again. Those actions may happen quite often in a short period of time (but also there might be none of those actions for a long time).
The Problem is obvious:
LockType.WRITE
then the access to the getter methods is locked as well, during the recalculation.Is it possible to lock the access to this single method only instead of locking access to the whole instance of my singleton? Would this solve my problem? Any other approaches how I may handle my problem?
How to deal with this?
Option A: Never have more than 1 thread waiting:
.
@Singleton
@Startup
@Lock(LockType.READ)
public class MySingleton {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// ...
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
try{
if(!lock.writeLock().tryLock()){
// Unable to immediately obtain lock
if(lock.hasQueuedThreads()){
// there is *probably* at least 1 thread waiting on
// the lock already, don't have more than 1 thread waiting
return;
} else {
// There are no other threads waiting. Wait to acquire lock
lock.writeLock().lock();
this.update();
}
} else {
// obtained the lock on first try
this.update();
}
} finally {
try {
lock.writeLock().unlock();
} catch (IllegalMonitorStateException ignore){}
}
}
}
Note: Brett Kail pointed out that there could be a race condition here where two threads go through the tryLock()
and lock.hasQueuedThreads()
at the same time and one of them gets the write lock while the other blocks for the full duration of this.update()
. This could potentially happen, but I think this is rare enough that the advantage of not waiting at all most of the time outweighs waiting 1 minute most of the time.
Option B: Never have any threads waiting:
I'm not sure how steadily your events come in, but it would simplify the logic much more if you just aborted if tryLock()
returned false, however you wouldn't get as many updates.
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
if(!lock.writeLock().tryLock())
return; // there is already an update processing
try{
this.update();
} finally {
lock.writeLock().unlock();
}
}
Option C: Wait for up to X amount of time for a lock (mimmic @AccessTimeout)
Brett pointed out in the comments that option A has a race condition and could potentially have a thread block for the full duration of this.update()
, and suggested this approach as a safer alternative. The only drawback is that tryLock()
will end up timing out most of the time, so setting the timeout as small as possible is advantageous.
@Asynchronous
public void catchEvent(@Observes(during = TransactionPhase.AFTER_SUCCESS) MyEvent event)
{
if(lock.writeLock().tryLock(1, TimeUnit.MINUTES)) {
try{
this.update();
} finally {
lock.writeLock().unlock();
}
}
}