1

I have the following service, which gets called using quarkus's Scheduler using the @Scheduled annotation.

The basic idea is that this lists the pending tasks, and then uses the ExecutorService to run the individual tasks.

I get warning logs as follows:

[co.ar.at.arjuna] checkChildren ARJUNA012094: Commit of action id 0:ffffc0a80e70:8b9b:5f2d31f1:5b invoked while multiple threads active within it.
WARN  [co.ar.at.arjuna] check ARJUNA012107: CheckedAction::check - atomic action 0:ffffc0a80e70:8b9b:5f2d31f1:5b commiting with 2 threads active!  

What is the reason for this and what can I do to fix this? My primary goal is to get off the Scheduler thread as soon as I can and let the task updation happen in the background

@RequestScoped
    @Transactional(REQUIRED)
    public class TaskService {
     @Inject
        ManagedExecutor executor;
    
    public void runPendingTasks() {
            final List<Task> taskList = Task.list("pending=?1 ",true);
            logger.debug("Found  " + taskList.size() + "  tasks pending ");
            for (final Task task : taskList) {
                executor.execute(() -> {
                    final boolean status = doTask(task);
                });
            }
        }
    
    @Transactional(REQUIRED)
        private boolean doTask(final Task task) {
            logger.debug("Going to run task " + task.getId() );
    //Do some DB updation here
            return true;
        }

}
bobby
  • 2,629
  • 5
  • 30
  • 56
  • 2
    The answer is that manual thread management and transactions don't mix well, while transactions and multithreading mix just terribly. [This](https://docs.oracle.com/javaee/6/tutorial/doc/gkkqg.html) is what you should be using instead. As a side note, `@Transactional` is ignored for local method invocations – crizzis Aug 11 '20 at 16:43

1 Answers1

3

When you annotate the class as @Transactional as in your example, you are defining, in every method of the class, a transaction boundary.

In your example, the annotation will apply to the runPendingTasks method.

You cannot place the @Transactional annotation in private methods (related to what @crizzis suggested), it has no effect in them.

All these information explain why the warnings appear in the logs: you have a single transaction, initiated on runPendingTasks and, within it, several threads, handling the method doTask. The invocation of the method runPendingTasks ends, but these threads are still alive.

One approach to solve the problem could be finish all the database related tasks within the transaction boundary.

Here, on Stackoverflow, you have a thread that shows you how you can finish a list of tasks asynchronously.

I recommend you to read some articles of the Tomasz Nurkiewicz blog, it will give you - at least, it did for me - a great insight into asynchronous computation and Java concurrency, in general.

Following his advice, you can define your code similar to this:

@RequestScoped
@Transactional(REQUIRED)
public class TaskService {
  
  @Inject
  ManagedExecutor executor;
    
  public void runPendingTasks() {
    final List<Task> taskList = Task.list("pending=?1 ",true);
    logger.debug("Found  " + taskList.size() + "  tasks pending ");

    final List<CompletableFuture<boolean>> futures = taskList.stream().
      map(task -> CompletableFuture.supplyAsync(() -> doTask(task), executor)).
      collect(Collectors.<CompletableFuture<boolean>>toList())
    ;

    final CompletableFuture<Void> allDoneFuture =
      CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]));
    
    final CompletableFuture<List<boolean>> allDone = allDoneFuture.thenApply(v ->
      futures.stream().
        map(future -> future.join()).
        collect(Collectors.<boolean>toList())
    );

    final boolean result = allDone.thenAccept(results ->
      results.stream().
        reduce((a, b) -> a || b)
    );
  }
    
  private boolean doTask(final Task task) {
    logger.debug("Going to run task " + task.getId() );
    //Do some DB updation here
    return true;
  }

}
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • Appreciate the answer. My aim with my initial approach was to get off the scheduler thread and complete the db transaction asynchronously. I wasnt going for parallel execution of the jobs. From my understanding of your answer, trying to run the DB transaction in another context wont work, since the transaction will end with the calling function. I will leave this answer unaccepted for now, to see if any alternate solutions exists. – bobby Aug 12 '20 at 11:16
  • I understand. And, did you try [context propagation](https://quarkus.io/guides/context-propagation)? Maybe it could be of some help. – jccampanero Aug 12 '20 at 13:12