4

I want to create an exception log in the database when an @Async operation fails with an exception.

You can see the implementation for AsyncExecutorConfiguration and AsyncExceptionHandler classes below.

Inside AsyncExceptionHandler class, when I call a service that tries to access the database, I am getting: org.hibernate.HibernateException: Could not obtain transaction-synchronized Session for current thread

@Configuration
@EnableAsync
public class AsyncExecutorConfiguration implements AsyncConfigurer {

    @Autowired
    private AsyncExceptionHandler asyncExceptionHandler;

    private static final int CORE_POOL_SIZE = 3;
    private static final int MAX_POOL_SIZE = 3;
    private static final int QUEUE_CAPACITY = 24;
    private static final String THREAD_NAME_PREFIX = "AsynchThread-";

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return asyncExceptionHandler;
    }

}

@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Autowired
    private NotificationService notificationService;

    @Override
    @Transactional(rollbackFor = Exception.class, readOnly = false)
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        AsyncErrorLog log = new AsyncErrorLog(ex);
        notificationService.saveLogAndNotify(log); // throws exception "Could not obtain transaction-synchronized Session for current thread"
    }
}


@Service
public class MyServiceImpl implements MyService {

    @Override
    @Async
    @Transactional(rollbackFor = Exception.class, readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void doSomething(Long id) {
        // I can execute database operations here

    }
    ...

@Async function itself already has a valid session. What should I do to have a valid session in AsyncExceptionHandler class too?

--

UPDATE

Here is the simplified implementations for NotificationServiceImpl and LogDaoImpl.class where we get the error.

@Service
public class NotificationServiceImpl implements NotificationService {

    @Autowired
    private LogDao logDao;

    @Override
    @Transactional(rollbackFor = Exception.class, readOnly = false)
    public void saveLogAndNotify(Log log) {
        return logDao.createLog(log);
    }


@Repository
public class LogDaoImpl{

    @Autowired
    protected SessionFactory sessionFactory;


    @Override
    public void createLog(Log log) {
        sessionFactory.getCurrentSession().saveOrUpdate(log);
    }
nilgun
  • 10,460
  • 4
  • 46
  • 57
  • would you please share `NotificationServiceImpl` class – Mohsen Dec 17 '18 at 09:27
  • @Spara: `NotificationServiceImpl` calls `LogDaoImpl` where we get the error. I shared the implementation for that class. – nilgun Dec 19 '18 at 19:59
  • Did you try to add `@Transactional` top of `LogDaoImpl` class? – Mohsen Dec 19 '18 at 20:13
  • We have `@Transactional` at the service level at `NotificationServiceImpl` and it is being called from a lot of places in the application without any problems except for the `AsyncUncaughtExceptionHandler`. (Updated the code to show the service implementation) – nilgun Dec 19 '18 at 20:19
  • Yeah I see, but the error means that your service layer could not start a transaction successfully, I tried to make your situation on my project but I couldn't maybe because I used spring data, Please change 2 things and tell me does it works or not: First please change `@Component` to `@Service` in the `AsyncExceptionHandler` class and second please add `@Transactional` at the `LogDaoImpl` – Mohsen Dec 19 '18 at 20:24
  • @Spara Still same error. I suspect the problem is with how https://github.com/spring-projects/spring-framework/blob/4.2.x/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java class handles the exception. – nilgun Dec 19 '18 at 20:49
  • Maybe you should handle this by your own, I have a solution for it, I will add it as an answer, would you please check it works or not? – Mohsen Dec 19 '18 at 20:53

3 Answers3

3

Per the Hibernate exception; if you're not using Spring Data, you'll have to make sure the notification service explicitly invokes the database calls on the Hibernate session.

On another note, in my experience, the main use cases for the UncaughtExceptionHandler (in general) are used for:

  1. A simple last-resort to handle RuntimeExceptions that may be unknown to the programmer that for some reason cannot (or are not) caught in code
  2. A way to catch exceptions in code that the programmer has no control over (e.g. if you're invoking Async directly on some third party library, etc.)

The commonality between the two is that this Handler is used for something unexpected. In fact, Spring itself accounts for the "unexpectedness" in your own code and Spring Async already sets a default one for you that will log to the console (code here), letting you not have to worry about rogue exceptions killing threads and not knowing why. (Note: The message in the source code says it's catching an "unexpected" exception. Of course exceptions are unexpected, but these are one's that you really didn't know could happen. Spring Async will log it for you.)

That being the case, in your example, since you're doing Spring Database operations and should know exactly what's happening inside of #doSomething, I would just go with removing the AUEH a try-catch (and/or -finally) and handle the exception inside of #doSomething:

@Service
public class MyServiceImpl implements MyService {

    // Self autowired class to take advantage of proxied methods in the same class
    // See https://stackoverflow.com/questions/51922604/transactional-and-stream-in-spring/51923214#51923214
    private MyService myService;  

    private NotificationService notificationService;  

    @Override
    @Async
    public void doSomething(Long id) {
        // I can execute database operations here
        try {
            myService.doDatabaseOperations(...);
        } catch(DatabaseAccessException e) {
            AsyncErrorLog log = new AsyncErrorLog(ex);
            notificationService.saveLogAndNotify(log);
        }
        // Other exceptions (from DB operations or the notifications service) can be 
        // handled with other "catches" or to let the SimpleAsyncExHandler log them for you.
        // You can also use standard multithreading exception handling for this
    }

    @Transactional(rollbackFor = Exception.class, readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void doDatabaseOperations(...) {
        ...
    }

}
Dovmo
  • 8,121
  • 3
  • 30
  • 44
  • Hi @Dovmo, can you elaborate on "if you're not using Spring Data, you'll have to make sure the notification service explicitly invokes the database calls on the Hibernate session." We are not using Spring Data, how do I make sure that the notification service explicitly invokes the database calls on the Hibernate session? – nilgun Dec 19 '18 at 20:05
  • Your implementation is very close to how we solved the problem right now but we have to repeat the same try/catch block in every @Async implementation. It would be nicer if we could handle them all in one place. – nilgun Dec 19 '18 at 20:32
  • I'm envisioning two ways for you to modularize things: (1) Have an AOP/Intercepter annotation or (2) You can have `doSomething` return a `CompletableFuture` for that `@Async` call. You can have your service accept the exception from `CompletableFuture`'s `#exceptionally` and invoke the DB logger. Do either of those make sense or need explanation? – Dovmo Dec 19 '18 at 21:20
  • Yes, they make sense to me. We are still using Java7, therefore CompletableFuture is not an option. But I can see opportunities to remove the try/catch block duplication, once I remove my focus away from AsyncUncaughtExceptionHandler. Thank you very much for your input. – nilgun Dec 19 '18 at 21:51
2

You can use the applicationContext in your handler to lookup the notificationService. I had the same issue when I used @Autowired for the handler, which in turn injected my LogService. After looking at the logs I saw that the TransactionSynchronizationManager is clearing transaction synchronization after the rollback of the exception and nothing else except the no transaction for ...... error.

After using the applicationContext for looking up the logService bean and changing my handler, I saw the desired result in the logs.

  1. begin
  2. Initializing transaction synchronization
  3. Getting transaction for [....AsyncService.doAsync]
  4. Exception
  5. rolling back
  6. Clearing transaction synchronization

  7. begin

  8. Initializing transaction synchronization
  9. Getting transaction for [.....LogService.save]

Change your config to include the interface ApplicationContextAware which will give you a convenience method to access the applicationContext. Set it as a instance variable.

See my configuration class below.

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer, ApplicationContextAware {

    private static final int CORE_POOL_SIZE = 3;
    private static final int MAX_POOL_SIZE = 3;
    private static final int QUEUE_CAPACITY = 24;
    private static final String THREAD_NAME_PREFIX = "AsynchThread-";

    private ApplicationContext applicationContext;

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncExceptionHandler(this.applicationContext);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

I have removed the @Component from the handler and use it as a POJO. Every time getAsyncUncaughtExceptionHandler is called with an exception, a new handler instance is created with the applicationContext as a dependency.

public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    private final ApplicationContext applicationContext;

    public AsyncExceptionHandler(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        Log log = new Log();
        log.setEntry(ex.getMessage());
        LogService logService = this.applicationContext.getBean(LogService.class);
        logService.save(log);
    }
}

The save method on logService requires a new transaction every time it is called.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Log log)
Rentius2407
  • 1,108
  • 2
  • 11
  • 29
1

This will help you:

@Override
public void createLog(Log log) {
try {
    session = sessionFactory.getCurrentSession();
} catch (HibernateException e) {
    session = sessionFactory.openSession();
}
    session.saveOrUpdate(log);
}
Mohsen
  • 4,536
  • 2
  • 27
  • 49
  • Hi Spara, this solution does not complain about the session but does not commit the transaction either. We could go on and try to commit the transaction but I prefer Spring @Transactional handling all this process instead of manual intervention. – nilgun Dec 19 '18 at 21:09
  • @nilgun Yes you are right, this is the last solution that if you didn't know what to do you can use it... – Mohsen Dec 19 '18 at 21:11