Search code examples
jakarta-eejpaglassfisheclipselinkentitymanager

Execute task before EntityManager is closed


I would like to execute a task before the EntityManager is closed (e.g. on shutdown of the AS or undelpoying the app) and looking for a Hook or Listener or something similar.

The actual problem: I want to save a lot of tiny data in the database with my application. To ease the load on the database I cached the data in a List and want to save all the data in an given interval.

This works great so far but in case the AS is shutting down the data will be lost. That's the reason why I want to save the data before the EntityManager is closed.

What I tried so far: I tried to use the @PreDestroy annotation to save the data before the bean is destroyed. Unfortunately the use of the EntityManager is not working and, as I read later, not allowed in PreDestroy-methods.

@Singleton
@Startup
@DependsOn(value = "StatisticRepository")
public class StatisticService {
    private static final Logger LOG = Logger.getLogger(StatisticService.class.getName());

    @EJB
    private StatisticRepository repository;
    private List<Statistic> stats = new ArrayList<>();

    @PreDestroy
    public void destroy() {
        LOG.log(Level.INFO, "Saving before destroying Service.");
        for (Statistic stat : stats) {
            // ---> EntityManager in Repository already destroyed
            repository.create(stat);
        }
        stats.clear();
    }
...
}

.

@Singleton
@Startup
public class StatisticRepository extends BaseRepository<Statistic>{
    public StatisticRepository() {
        super(Statistic.class);
    }

    @PersistenceContext(unitName = PERSISTENCE_UNIT_NAME)
    EntityManager em;

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }
    ...
}

.

@MappedSuperclass
public abstract class BaseRepository<T extends Serializable> {

    protected abstract EntityManager getEntityManager();
    private final Class<T> entityClass;

    public BaseRepository(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    public T create(T entity) {
        getEntityManager().persist(entity);
        getEntityManager().flush();
        return this.edit(entity);
    }
    ...
}

I get this exception on

Information:   Saving Statistics before destroying Service.
Warnung:   RAR5114 : Error allocating connection : [{ PoolInfo : (name=java:app/pool), (applicationName=AppName) }: Es ist kein Poolmetadaten-Objekt mit dem Pool { PoolInfo : (name=java:app/pool), (applicationName=AppName) } verknüpft. Stellen Sie die Anwendung erneut bereit. ]
Warnung:   Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: { PoolInfo : (name=java:app/pool), (applicationName=AppName) }: Es ist kein Poolmetadaten-Objekt mit dem Pool { PoolInfo : (name=java:app/pool), (applicationName=AppName) } verknüpft. Stellen Sie die Anwendung erneut bereit. 
Error Code: 0
...

It seems the pool(which is app-scoped) is already undeployed.

Full stacktrace:
here

I create my JNDI resource and connection pool over the glassfish-resources.xml. Thus it's not an application-server-wide resource. Maybe that's the key to reproduce the error?

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
  <!-- MySQL -->
  <jdbc-connection-pool allow-non-component-callers="false"
                        associate-with-thread="false"
                        connection-creation-retry-attempts="0"
                        connection-creation-retry-interval-in-seconds="10"
                        connection-leak-reclaim="false"
                        connection-leak-timeout-in-seconds="0"
                        connection-validation-method="auto-commit"
                        datasource-classname="com.mysql.jdbc.jdbc2.optional.MysqlDataSource"
                        fail-all-connections="false"
                        idle-timeout-in-seconds="170"
                        is-connection-validation-required="true"
                        is-isolation-level-guaranteed="true"
                        transaction-isolation-level="repeatable-read"
                        lazy-connection-association="false"
                        lazy-connection-enlistment="false"
                        match-connections="false"
                        max-connection-usage-count="0"
                        max-pool-size="100"
                        max-wait-time-in-millis="60000"
                        name="java:app/mysql_app_appPool"
                        non-transactional-connections="false"
                        ping="true"
                        pool-resize-quantity="2"
                        pooling="true"
                        res-type="javax.sql.DataSource"
                        statement-cache-size="0"
                        statement-leak-reclaim="false"
                        statement-leak-timeout-in-seconds="0"
                        statement-timeout-in-seconds="0"
                        steady-pool-size="20"
                        validate-atmost-once-period-in-seconds="0"
                        wrap-jdbc-objects="true">
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="driverClass" value="com.mysql.jdbc.Driver"/>
  </jdbc-connection-pool>
  <jdbc-resource enabled="true" jndi-name="java:app/jdbc/app" object-type="user" pool-name="java:app/mysql_app_appPool"/>

.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <!-- Persistence Unit for MySQL -->
  <persistence-unit name="com.app.web_app-webapp_war_1.0-SNAPSHOTPU" transaction-type="JTA">
    <jta-data-source>java:app/jdbc/app</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="javax.persistence.schema-generation.database.action" value="create"/>
      <property name="javax.persistence.schema-generation.create-source" value="metadata-then-script"/>
      <property name="javax.persistence.schema-generation.drop-source" value="metadata"/>
      <property name="javax.persistence.schema-generation-target" value="database"/>
    </properties>
  </persistence-unit>
</persistence>

Questions: What do you think about the mechanism in general? Does the caching make sense since JPA may handle it better on his own? Are there any listeners or hooks?


Solution

  • You have plenty of caching options on eclipselink. Have a look here.

    Also, for real life performance, I strongly recommend to use Infinspan. Have a look here.

    If your use case is really simple, then a @Singleton ejb bean annotated with @Startup should do the trick. Like this:

    @Singleton
    @Startup
    public class StartupShutdownBean {
    
        @PostConstruct
        private void startup() {
            // your startup code here
        }
    
        @PreDestroy
        private void shutdown() {
            // your shutdown code here
        }
    
    }
    

    You shouldn't have any problem injecting an EntityManager on this bean. Most likely, you cant use an entity manager in your bean because the transaction ends before you can perform a "cache update".

    EDIT:

    It seems the app configured resources are being deleted by the AS before your @PreDestroy call, therefore you shoud use the preserveAppScopedResources=true parameter in your asadmin deploy or redeploy commands in order to keep the pool alive during those operations.

    Some documentation here.