7 Replies Latest reply on Mar 18, 2010 3:49 AM by kapitanpetko

    QuartzTriggerHandle set to null in database on second job run

    ssarver
      After upgrading to Quartz 1.7.3 on jboss-5.1.0.GA, I changed

      org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

      in jboss-seam-2.2.0.GA/examples/quartz/resources/seam.quartz.properties
      to use the following new configuration that uses a persistent job store,
      I noticed that calling handle.cancel() in PaymentHome always throws an exception, because the
      QuartzTriggerHandle handle is set to null in the row of the Payment table in the database when Quartz runs the corresponding job the second time.
      So, when the cancel() method is called in PaymentHome, it retrieves a null handle. 
      Why is the LOB handle set to null on the second job run?  Does the handle need a scope qualifier, so it will
      not behave in this transient way?
      What is a good alternate approach to use in this example to cancel a recurring payment?

      jboss-seam-2.2.0.GA/examples/quartz/src/org/jboss/seam/example/quartz/PaymentHome.java
      /////////////////////////////////////////////////////////////////
      @Name("paymentHome")
      public class PaymentHome
          extends EntityHome<Payment>
      {
          @RequestParameter Long paymentId;
          @In PaymentProcessor processor;
         
          @Logger Log log;

          @Transactional
          public void cancel() {
              Payment payment = getInstance();
             
              QuartzTriggerHandle handle = payment.getQuartzTriggerHandle();
              payment.setQuartzTriggerHandle(null);
              payment.setActive(false);

              try
              {
                  handle.cancel();
              }
              catch (Exception nsole)
              {
                  FacesMessages.instance().add("Payment already processed");
              }
          }
      ...
      /////////////////////////////////////////////////////////////////


      jboss-seam-2.2.0.GA/examples/quartz/src/org/jboss/seam/example/quartz/PaymentProcessor.java
      /////////////////////////////////////////////////////////////////
      @Name("processor")
      @AutoCreate
      public class PaymentProcessor {
         
          @In
          EntityManager entityManager;

          @Logger Log log;

          @Asynchronous
          @Transactional
          public QuartzTriggerHandle schedulePayment(@Expiration Date when,
                                       @IntervalDuration Long interval,
                                       @FinalExpiration Date stoptime,
                                       Payment payment)
          {
              payment = entityManager.merge(payment);
             
              log.info("[#0] Processing payment #1", System.currentTimeMillis(), payment.getId());

              if (payment.getActive()) {
                  BigDecimal balance = payment.getAccount().adjustBalance(payment.getAmount().negate());
                  log.info(":: balance is now #0", balance);
                  payment.setLastPaid(new Date());

                  if (payment.getPaymentFrequency().equals(Payment.Frequency.ONCE)) {
                      payment.setActive(false);
                  }
              }

              return null;
          }
      ...
      /////////////////////////////////////////////////////////////////


      New:

      jboss-seam-2.2.0.GA/examples/quartz/resources/seam.quartz.properties
      /////////////////////////////////////////////////////////////////
      org.quartz.scheduler.instanceName = Sched1
      org.quartz.scheduler.rmi.export = false
      org.quartz.scheduler.rmi.proxy = false
      org.quartz.scheduler.xaTransacted = false
      org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
      org.quartz.threadPool.threadCount = 5
      org.quartz.threadPool.threadPriority = 4
      org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
      org.quartz.jobStore.driverDelegateClass =  org.quartz.impl.jdbcjobstore.StdJDBCDelegate
      org.quartz.jobStore.tablePrefix = QRTZ_
      org.quartz.jobStore.dataSource = QUARTZ
      org.quartz.dataSource.QUARTZ.jndiURL = java:/QuartzDS
      org.quartz.jobStore.nonManagedTXDataSource = QUARTZ_NO_TX
      org.quartz.dataSource.QUARTZ_NO_TX.jndiURL = java:/QuartzNoTxDS
      ///////////////////////////////////////////////////////////////


      quartz-ds.xml
      //////////////////////////////////////////////////////////////////////
      <?xml version="1.0" encoding="UTF-8"?>

      <datasources>
        <local-tx-datasource>
          <jndi-name>QuartzDS</jndi-name>
          <connection-url>jdbc:mysql://localhost:3306/quartz</connection-url>
          <driver-class>com.mysql.jdbc.Driver</driver-class>
          <user-name>******</user-name>
          <password>*******</password>
          <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
          <metadata>
             <type-mapping>mySQL</type-mapping>
          </metadata>
        </local-tx-datasource>
        <no-tx-datasource>
          <jndi-name>QuartzNoTxDS</jndi-name>
          <connection-url>jdbc:mysql://localhost:3306/quartz</connection-url>
          <driver-class>com.mysql.jdbc.Driver</driver-class>
          <user-name>*****</user-name>
          <password>******</password>
          <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name>
          <metadata>
             <type-mapping>mySQL</type-mapping>
          </metadata>
        </no-tx-datasource>


      </datasources>
      //////////////////////////////////////////////////////////////////////////////////


      jboss-seam-2.2.0.GA/examples/quartz/resources/META-INF/persistence.xml
      /////////////////////////////////////////////////////////////////////////
      <?xml version="1.0" encoding="UTF-8"?>
      <persistence xmlns="http://java.sun.com/xml/ns/persistence"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
                   version="1.0">
          <persistence-unit name="default">
              <provider>org.hibernate.ejb.HibernatePersistence</provider>
              <jta-data-source>java:/DefaultDS</jta-data-source>
              <properties>
                  <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
                  <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
                  <property name="hibernate.show_sql" value="true"/>
                  <property name="jboss.entity.manager.factory.jndi.name"
                            value="java:/seampayEntityManagerFactory"/>
              </properties>
          </persistence-unit>
          <persistence-unit name="local-tx-ds">
              <provider>org.hibernate.ejb.HibernatePersistence</provider>
              <jta-data-source>java:/QuartzDS</jta-data-source>
              <properties>
                  <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
                  <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
                  <property name="hibernate.show_sql" value="true"/>
                  <property name="jboss.entity.manager.factory.jndi.name"
                            value="java:/seampayEntityManagerFactory"/>
              </properties>
          </persistence-unit>
          <persistence-unit name="no-tx-ds">
              <provider>org.hibernate.ejb.HibernatePersistence</provider>
              <jta-data-source>java:/QuartzNoTxDS</jta-data-source>
              <properties>
                  <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
                  <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
                  <property name="hibernate.show_sql" value="true"/>
                  <property name="jboss.entity.manager.factory.jndi.name"
                            value="java:/seampayEntityManagerFactory"/>
              </properties>
          </persistence-unit>
      </persistence>
      /////////////////////////////////////////////////////////////////////////

      Database table in MySQL:

      CREATE TABLE  `quartz`.`Payment` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT,
        `active` bit(1) NOT NULL,
        `amount` decimal(19,2) NOT NULL,
        `createdDate` datetime NOT NULL,
        `lastPaid` datetime DEFAULT NULL,
        `payee` varchar(255) NOT NULL,
        `paymentCron` varchar(255) DEFAULT NULL,
        `paymentDate` datetime NOT NULL,
        `paymentEndDate` datetime DEFAULT NULL,
        `paymentFrequency` int(11) DEFAULT NULL,
        `quartzTriggerHandle` longblob,
        `account_id` bigint(20) NOT NULL,
        PRIMARY KEY (`id`),
        KEY `FK3454C9E6A3DBB9FA` (`account_id`),
        CONSTRAINT `FK3454C9E6A3DBB9FA` FOREIGN KEY (`account_id`) REFERENCES `Account` (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1
        • 1. Re: QuartzTriggerHandle set to null in database on second job run
          kapitanpetko

          How/when are you saving the handle? Show that part of your code.Are you sure it gets saved in the first place?

          • 2. Re: QuartzTriggerHandle set to null in database on second job run
            ssarver
            After I create a job, I can see the non-null LOB handle in the database.
            The call to the saveAndSchedule() method stores the serializable QuartzTriggerHandle handle to the Payment table.
            using the following statement:

                    payment.setQuartzTriggerHandle( handle );

            I also noticed that even though the method calls use @Transactional, and the call to:

                    payment.setActive(false);

            succeeds, and I verify that the row in the Payment table has been set to false,
            the next run of the job somehow sets the active flag to true in the Payment table.
            So, there is no way to delete the job from Quartz, and there is no way to mark it
            inactive, so that the payment operation will not be performed.

            jboss-seam-2.2.0.GA/examples/quartz/src/org/jboss/seam/example/quartz/PaymentHome.java
            /////////////////////////////////////////////////////////////////
            @Name("paymentHome")
            public class PaymentHome
                extends EntityHome<Payment>
            {
                @RequestParameter Long paymentId;
                @In PaymentProcessor processor;
               
                @Logger Log log;

                public String saveAndSchedule()
                {
                    String result = persist();
                   
                    Payment payment = getInstance();
                   
                    log.info("scheduling instance #0", payment);
                    QuartzTriggerHandle handle = processor.schedulePayment(payment.getPaymentDate(),
                                                            payment.getPaymentFrequency().getInterval(),
                                                            payment.getPaymentEndDate(),
                                                            payment);
                   
                    payment.setQuartzTriggerHandle( handle );

                    return result;
                }

                @Override
                public Object getId() {
                    return paymentId;
                }

                @Transactional
                public void cancel() {
                    Payment payment = getInstance();
                   
                    QuartzTriggerHandle handle = payment.getQuartzTriggerHandle();
                    payment.setQuartzTriggerHandle(null);
                    payment.setActive(false);
                   
                    try
                    {
                        handle.cancel();
                    }
                    catch (Exception nsole)
                    {
                        FacesMessages.instance().add("Payment already processed");
                    }
                }
               
            }
            /////////////////////////////////////////////////////////////////

            `

            ``

            `

            • 3. Re: QuartzTriggerHandle set to null in database on second job run
              kapitanpetko

              Ah, your calling merge:


               payment = entityManager.merge(payment);
              



              That would reset things, of course. Pass just the id, look it up in your async method and do your work. Are you creating a job (calling an async method) for every payment?

              • 4. Re: QuartzTriggerHandle set to null in database on second job run
                ssarver
                Note: All of the source code I am listing is the original seam quartz example source code.
                If you have a proposed change, then the change needs to be propagated to revision control.

                If the following statement is NOT executed:

                   payment = entityManager.merge(payment);

                then, no write to the database occurs.

                Using source code, how would you accomplish your proposed change?
                • 5. Re: QuartzTriggerHandle set to null in database on second job run
                  ssarver

                  Nikolay Elenkov wrote on Mar 18, 2010 03:19:


                  Ah, your calling merge:

                   payment = entityManager.merge(payment);
                  



                  That would reset things, of course. Pass just the id, look it up in your async method and do your work. Are you creating a job (calling an async method) for every payment?


                  The definition is in the source code in the first blog entry listed under:
                  jboss-seam-2.2.0.GA/examples/quartz/src/org/jboss/seam/example/quartz/PaymentProcessor.java

                  • 6. Re: QuartzTriggerHandle set to null in database on second job run
                    ssarver
                    <blockquote>
                    _Nikolay Elenkov wrote on Mar 18, 2010 03:19:_<br/>

                    Ah, your calling |merge|:

                    `
                    payment = entityManager.merge(payment);
                    `

                    That would reset things, of course. Pass just the id, look it up in your async method and do your work. Are you creating a job (calling an async method) for every payment?
                    </blockquote>

                    For every new payment that is scheduled (using the UI), saveAndSchedule() is called,
                    which in turn calls the @Asynchronous schedulePayment method in PaymentProcessor:


                    jboss-seam-2.2.0.GA/examples/quartz/src/org/jboss/seam/example/quartz/PaymentProcessor.java
                    /////////////////////////////////////////////////////////////////
                    @Name("processor")
                    @AutoCreate
                    public class PaymentProcessor {
                             @In
                        EntityManager entityManager;

                        @Logger Log log;

                        @Asynchronous
                        @Transactional
                        public QuartzTriggerHandle schedulePayment(@Expiration Date when,
                                                     @IntervalDuration Long interval,
                                                     @FinalExpiration Date stoptime,
                                                     Payment payment)
                        {
                            payment = entityManager.merge(payment);
                                     log.info("[#0] Processing payment #1", System.currentTimeMillis(), payment.getId());

                            if (payment.getActive()) {
                                BigDecimal balance = payment.getAccount().adjustBalance(payment.getAmount().negate());
                                log.info(":: balance is now #0", balance);
                                payment.setLastPaid(new Date());

                                if (payment.getPaymentFrequency().equals(Payment.Frequency.ONCE)) {
                                    payment.setActive(false);
                                }
                            }

                            return null;
                        }
                    ...
                    /////////////////////////////////////////////////////////////////
                    • 7. Re: QuartzTriggerHandle set to null in database on second job run
                      kapitanpetko

                      If you find the example is buggy, file a JIRA. I don't have the time to test is, so can't help you there. The way async works in Seam is this:



                      1. saves all your arguments in AsynchronousInvocations (serializable)

                      2. saves that as part of Quartz's JobDetail

                      3. creates a job/trigger



                      So next time the job runs, it uses a 'snapshot' of Payment which is old, calling merge overwrites other changes. If it had a Version field, Hibernate would throw an OptimisticLockException, but since it doesn't have one, your entity is simply overwritten. So I think this the example is pretty wrong. To get it to work, as I said, pass the id, find your entity, do work, then call merge.