Spring TransactionTemplate joins the Transaction of unknown origin

21 hours ago 1
ARTICLE AD BOX
'org.springframework.boot' version '3.3.13'

I have a simple Task framework.

public class TaskRunner implements DelayedTaskRunner { private static final String TASK_WORKER_THREAD_NAME_PATTERN = "Task-Worker-%d"; private TaskExecutor taskExecutor; private ThreadPoolExecutor runnerThreadPool; public TaskRunner( TaskExecutor taskExecutor, Integer defaultThreadPoolSize, Integer maximumThreadPoolSize, Duration keepExtraThreadsAliveForSeconds ) { this.taskExecutor = taskExecutor; this.runnerThreadPool = new ThreadPoolExecutor( defaultThreadPoolSize, maximumThreadPoolSize, keepExtraThreadsAliveForSeconds.toMillis(), TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100), new ThreadFactoryBuilder().setNameFormat(TASK_WORKER_THREAD_NAME_PATTERN).build() ); } public TaskInitiationResult runWorker() { try { Future<?> taskRef = runnerThreadPool.submit(this::executeTask); return new TaskInitiationResult(taskRef); } catch (RejectedExecutionException e) { return new TaskInitiationResult(null); } } private void executeTask() { try { taskExecutor.executeNextTask(); } catch (Exception e) { log.error("Error while executing Task", e); } } public void shutdown() { ExecutorServiceManagementUtil.shutdown(runnerThreadPool, log); } @Override public String getAssignedTaskScope() { return "TASK_SCOPE_NAME"; } }

TaskRunner.java is a simple ThreadPool container. Outer while loop continuously triggers runWorker method, which is simply delegating executor call to a Thread. The Executor is simple as well:

public class TheTaskExecutor implements TaskExecutor { private TaskWorkerReportCollector workerReportCollector; private TaskRepository taskRepository; private TransactionTemplate transactionTemplate; private Duration sleepOnWorkNotFound; private CommandBus commandBus; private Integer amountOfTasksToSelect; public TheTaskExecutor( CommandBus commandBus, TaskWorkerReportCollector workerReportCollector, TaskRepository taskRepository, TransactionTemplate transactionTemplate, Duration sleepOnWorkNotFound, Integer amountOfTasksToSelect ) { this.commandBus = commandBus; this.amountOfTasksToSelect = amountOfTasksToSelect; this.taskRepository = taskRepository; this.transactionTemplate = transactionTemplate; this.sleepOnWorkNotFound = sleepOnWorkNotFound; this.workerReportCollector = workerReportCollector; } public void executeNextTask() { long threadId = Thread.currentThread().threadId(); Instant resumeWorkTimestamp = workerReportCollector.getResumeWorkTimestamps().get(threadId); if (resumeWorkTimestamp != null && resumeWorkTimestamp.isAfter(Instant.now())) { return; } transactionTemplate.executeWithoutResult(transactionStatus -> { List<DelayedTask> tasks = taskRepository.deleteWithSelectNextTasksToExecute(amountOfTasksToSelect, Instant.now()); if (tasks.isEmpty()) { workerReportCollector.put(threadId, Instant.now().plusMillis(sleepOnWorkNotFound.toMillis())); return; } workerReportCollector.evict(threadId); for (DelayedTask task : tasks) { try { commandBus.execute(task.getCommand()); } catch (Exception e) { throw e; } } }); } }

With some backoff protection (workerReportCollector is simple Map<Long, Instant>) it uses transactionTemplate.executeWithoutResult to start new Transaction. Here is the problem: somehow, this Task appears to join a Transaction, instead of spawning own. This conclusion is made from pg_stat_activity output (can't share it) having transaction that lasted for 4 hours and once pg_terminate_backed was issued, the Thread name, that appeared in the Exception Stack Trace was referring to the Runner's Thread.

So I'm looking for the general answer to the question: where could there Thread in a completely isolated pool get a connection with an active transaction? Does it mean, that a Connection with active transaction is staying in Hikari Pool?

Read Entire Article