fix(audit): submit afterCommit write to executor to avoid transaction sync conflict
AuditService.logAfterCommit() called writeLog() inline inside the afterCommit() callback. At that point Spring's transaction synchronizations are still active on the thread, so SimpleJpaRepository.save() throws IllegalStateException which the catch block silently swallowed — leaving audit_log permanently empty. Fix: submit writeLog() to auditExecutor so it runs on a fresh thread with no active synchronization context. Also switch auditExecutor from CallerRunsPolicy to AbortPolicy to prevent the bug from silently recurring when the queue fills under load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package org.raddatz.familienarchiv.audit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
@@ -16,6 +18,8 @@ import java.util.UUID;
|
||||
public class AuditService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
@Qualifier("auditExecutor")
|
||||
private final TaskExecutor auditExecutor;
|
||||
|
||||
@Async("auditExecutor")
|
||||
public void log(AuditKind kind, UUID actorId, UUID documentId, Map<String, Object> payload) {
|
||||
@@ -27,7 +31,10 @@ public class AuditService {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
writeLog(kind, actorId, documentId, payload);
|
||||
// Run on a separate thread: the afterCommit() callback fires while Spring's
|
||||
// transaction synchronizations are still active on the current thread, which
|
||||
// prevents SimpleJpaRepository.save() from starting a new transaction inline.
|
||||
auditExecutor.execute(() -> writeLog(kind, actorId, documentId, payload));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,10 @@ public class AsyncConfig {
|
||||
executor.setMaxPoolSize(2);
|
||||
executor.setQueueCapacity(50);
|
||||
executor.setThreadNamePrefix("Audit-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
// AbortPolicy instead of CallerRunsPolicy: if CallerRunsPolicy ran the task on the
|
||||
// afterCommit() callback thread, Spring's transaction synchronizations would still be
|
||||
// active on that thread and SimpleJpaRepository.save() would throw IllegalStateException.
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user