Batch Jobs: Concurrencia y Experiencia de usuario (part 1)

Trabajando con mi Business Analyst  ( cuando digo mi, no es que me haya comprado un Analista de Negocio para mí sola, es que  trabajaba conmigo en el mismo equipo )  me encontré con una serie de casos en los cuales necesitábamos procesar grandes cantidades de datos… ya sabéis que el mundo contable es una fiesta!! Tuvimos una serie de problemillas intentando no tener datos procesados a la vez por diferentes usuarios, por el mismo usuario incluso, no tener resultados dobles… y obviamente queríamos mantener al usuario informado del estado en que el proceso Batch que estaba procesando sus datos se encontraba. Me imagino lo que estáis pensando… bueno quizás lo que pensaba yo… Sabemos que a veces es un poco “doloroso” trabajar con procesos batch, por una serie de razones: tiempo de procesamiento,  solapamiento de procesos que pueden usar los mismos registros,  mantener al usuario informado cuando los resultados deben de mostrarse en UI,  como mantener una traza de los registros procesados y creados en el Batch Job…. En este post voy a intentar dar una solución al solapamiento de procesos que usen mismos registros y tienen como resultado otros diferentes, y además una forma de mantener al usuario informado de los registros a procesar y ya procesados. Como os habréis dado cuenta esta es la primera parte, en la segunda parte tratare de dar una solución para trazar los registros procesados y recuperación en caso de que ocurran problemas en dicho procesamiento. Una serie aburrida sobre procesos batch!! 😛 Para ello tengo dos objetos:

  •  Batch Control :  Usaré este objeto para mantener a mi usuario informado del estado del proceso batch, si esta en cola si se esta procesando y  lo mas importante de los registros procesados y los registros a procesar
  • Batch Record Tracking : Este objeto lo usare para guardar la traza de aquellos registros que están siendo procesados por el proceso batch para no permitir que otros usuarios los usen, o el mismo usuario.  El caso que describiré aquí es el caso simple de bloqueo de concurrencia de registros entre procesos batch, pero tú quizás podrías extenderlo a : Bloqueo de registros por un criterio determinado , no solamente a  usuarios, solamente tendrías que modificar el objeto y quizás añadir un trigger.
  • Record Processed in Batch: Este objeto me dirá exactamente que objeto esta siendo procesado , tiene una relación master-detail a Batch Record Tracking , lo cual me permitirá el borrado de forma mucho mas sencilla.

Una cosita a tener en cuenta, es que los registros de estos objetos serán borrados una vez que el proceso batch termine para liberar espacio. Cómo son estos objetos? Pues muy bonitos, con sus campos , sus etiquetas …  aquí os dejo una fotito: Screenshot 2014-03-31 23.11.54 Aparece un objeto más, éste es el que se va a procesar en el batch job. Luego vamos a ver que tienen estos objetos:

  • Batch Control:

                     Batch Class: campo de texto que contendrá el nombre de la clase batch que contiene el código para el proceso batch. Batch Id: Campo de texto, de 18 caracteres , único y que diferencia entre mayúsculas y minúsculas. Representara el Id correspondiente al proceso batch el cual esta en ejecución en este momento. Error: es un campo de texto para guardar los errores que puedan ocurrir. Sin embargo se podría extender a otro objeto y creando una relación master-detail con Batch Control. Records Already Processed : este campo será usado para mostrar al usuario cuantos registros han sido procesados ya.

  • Batch Record Tracking:

Batch Class: campo de texto que contendrá el nombre de la clase batch que contiene el código para el proceso batch. Batch Id: Campo de texto, de 18 caracteres , único y que diferencia entre mayúsculas y minúsculas. Representara el Id correspondiente al proceso batch el cual esta en ejecución en este momento. Records Processed: Roll- up que ira contando los registros que están y han sido procesados. A esta altura seguramente te estarás preguntando, ¡¿Estos dos objetos son muy parecidos?!… vale es más bien una afirmación. Tienes toda la razón, son muy parecidos casi iguales, sin embargo he creado diferentes objetos ya que para el borrado es mas fácil.  ¿Por qué? Ya que Salesforce recomienda no tener relaciones master-detail con mas de 50K registros, puede que se necesite crear mas de un registro del mismo tipo que contendrá a los hijos, si es así recuerda Quitar el “único” del campo Batch Id.

  • Record Processed In Batch:

Batch RecordTracking: Master – Detail a Batch Record Tracking MyTestObject / Project: es un Lookup al objeto que se procesa en el batch, para poder navegar directamente a él. Processed Record Id: campo de texto el que representara  al id del objeto procesado ( no es una buena opción usar campos de texto para representar Ids , pero sin embargo si quieres hacer el seguimiento de mas de 25 tipos de objetos diferentes deberás usarlo) Y ahora la clase!


public with sharing class MyBatchClass implements Database.Batchable, Database.Stateful
{

    private BatchControl__c m_batchControl ;
    private BatchRecordTracking__c m_batchRecordsTracking;
    private MyNewTestObject__c m_myNewTestobject;
    private Id m_batchId;

    public Integer BATCHSIZE = 200 ; // you can always make it dynamic using a custom setting? 

    public MyBatchClass()
    {

    }

    public Id run()
    {

      // If you like to do some validations
      // or any previous prep for batch
      /// remember try - catch 😉
      return database.executeBatch(new MyBatchClass(), BATCHSIZE);

      }

      public Database.QueryLocator start(Database.BatchableContext batchableContext)
      {
         //initialize batch id to use it later
         m_batchId = batchableContext.getJobId();

         //PLEASE NOTE THAT THE QUERY IS SO GENERIC ONLY FOR PROTOTYPING -- YOU'LL HAVE A MORE RESTRICTING ONE
         String query = 'SELECT Id, Name FROM MyTestObject__c' ;
         return Database.getQueryLocator(query );
      }

      public void execute(Database.BatchableContext batchableContext, List<MyTestObject__c> scope)
      {

         SObjectUnitOfWork uow = new SObjectUnitOfWork( new List{ BatchControl__c.SObjectType, BatchRecordTracking__c.SObjectType, RecordCreatedfromBatch__c.SObjectType, RecordProcessedInBatch__c.SObjectType, MyNewTestObject__c.SObjectType, MyNewTestObjectDetail__c.SObjectType } );

         try
         {
            //record for Batch Control
            //query to AsyncApexJob to get the number of batches and get an aproximation of records to process
            List aaj = [Select Id, TotalJobItems from AsyncApexJob where Id=:m_batchId];
            //Obviously if we are here the batchId exists and the list will have an item --- BUT PLEASE CHECKIT !!
            //if(aaj!=null && !aaj.isEmpty())

            if(m_batchControl== null && m_batchRecordsTracking == null)
            {
                //now we can calculate the aprox
                Integer aproxRecordsToProcess = Integer.valueOf(aaj[0].TotalJobItems)* BATCHSIZE;
                m_batchControl = new BatchControl__c( BatchClass__c= 'MyBatchClass',BatchId__c=batchableContext.getJobId() , BatchStatus__c='Processing', RecordsAlreadyProcess__c=0,TotalRecordstoProcess__c=aproxRecordsToProcess);
                uow.registerNew(m_batchControl);

                // in part 2 of this post the object BatchRecordTracking will be linked to BatchControl
                m_batchRecordsTracking = new BatchRecordTracking__c(BatchClass__c= 'MyBatchClass',BatchId__c=m_batchId);
                uow.registerNew(m_batchRecordsTracking);
                uow.commitWork();
            }

            // Now I CREATE LINES FOR THE RECORDS THAT WILL BE PROCESS THIS WAY THEY WILL BE LOCK FOR OTHER BATCHS
            // OR FOR OTHER USERS --- YOU CAN MAKE THIS DEPENDENT ON A CRITERIA
            List<RecordProcessedInBatch__c> recordsWillBeProcessed = new List<RecordProcessedInBatch__c>();
            for(MyTestObject__c mto : scope)
            {
                RecordProcessedInBatch__c rpb = new RecordProcessedInBatch__c(BatchRecordTracking__c=m_batchRecordsTracking.Id,ProcessedRecordId__c =mto.Id, MyTestObject__c= mto.Id); // if you want more than 25 objects you will need to use text field to save the id
                recordsWillBeProcessed.add(rpb);
            }
            //if(!recordsWillBeProcessed.isEmpty())
            List saveResults = Database.insert(recordsWillBeProcessed,false);//--> this will insert only the ones that don't cause failure
            // because ProcessedRecordId__c is unique
            //if you would like all or none -->Database.insert(recordsWillBeProcessed,true);

             /*****************************************************
             Here is where the code for the batch goes.
             The records to be processed will be that ones that were inserted using Database.insert
             The records that will be excluded will be the ones that were not inserted

             How to check which ones where inserted properly?
             You can go throw the list records and check if they have id or no, if they don't have id it means they were not inserted.

             Note: Assuming that the records that were not inserted failed because the id was duplicated, if no the error should be check just in case 🙂
             *****************************************************/

             SObjectUnitOfWork uow2 = new SObjectUnitOfWork( new List{ BatchControl__c.SObjectType, MyNewTestObject__c.SObjectType } );

             if(m_myNewTestobject==null)
             {
                 //it is the master object that will contain the result objects
                 m_myNewTestobject = new MyNewTestObject__c(Status__c='Generating'); // --> this is my transaction Master object
                 uow2.registerNew(m_myNewTestobject);
             }

             /*********
             Once all the process is done, the main record is updated --- BATCH CONTROL
             **********/
            // we update the record with the number of records that have been processed here
            m_batchControl.RecordsAlreadyProcess__c = Integer.valueOf(m_batchControl.RecordsAlreadyProcess__c)+scope.size();
            uow2.registerDirty(m_batchControl); 

            uow2.commitWork();

       }
       catch(Exception e)
       {
           /****************************
           Any of the code the needs to be added to inform about the exception
           *****************************/

          //if there is any errors the New Object that is the result of the batch is set in error status
          if(m_batchControl==null)
          {
              m_batchControl = new BatchControl__c( Error__c = 'There is an error: '+ e, BatchClass__c= 'MyBatchClass',BatchId__c=batchableContext.getJobId() , BatchStatus__c='Failed', RecordsAlreadyProcess__c=0,TotalRecordstoProcess__c=0);
              uow.registerNew(m_batchControl);

              uow.commitWork();
          }
          else
          {
              // the batch control table is updated
              m_batchControl.BatchStatus__c= 'Failed';
              m_batchControl.Error__c= 'There is an error: '+ e;
              uow.registerDirty(m_batchControl);
           }
           if(m_myNewTestobject== null)
           {
              m_myNewTestobject = new MyNewTestObject__c(Status__c='Error', Error__c= 'There is an error in the "execute": '+ e); // --> this is my transaction Master object
              uow.registerNew(m_myNewTestobject);
           }
           else
           {
              m_myNewTestobject.Error__c= 'There is an error in the "execute": '+ e;
              m_myNewTestobject.Status__c='Error';
              uow.registerDirty(m_myNewTestobject);
           }

           /****************
           the batch record tracking is deleted in order to get the space back
           WHY IS IT DELETED??
           IT IS ONLY AN OPTION, YOU CAN ALWAYS KEEP IT AND RE-USE IN ORDER TO DONT PROCESS AGAIN THOSE RECORDS THAT WERE CORRECT...
           HOWEVER THIS CODE WILL DELETE THEM AND ALLOW THE USER TO REVIEW THE ERROR FIELD AND DELETE THE RESULT OR KEEP IT 

           --- >> NOTE THAT THE DELETE WILL DELETE 10K RECORDS IF YOU NEED MORE YOU WILL NEED TO CREATE THE WAY TO DO IT
           AT THIS MOMENT THE PROTOTYPE USE MASTER -DETAIL THEREFORE WILL BE EASIER TO DELETE BUT
           REMEMBER THAT SALESFORCE DON'T RECOMEND TO HAVE MORE THAN 50K CHILDS , THEN SHOULD BE GOOD TO CREATE MORE MASTERS IF YOU NEED IT
           *****************/
           uow.registerDeleted(m_batchRecordsTracking);

           uow.commitWork();

       }
       finally
       {
          // you can add here all that you would like to set or check
          // THIS CODE WILL BE ALWAYS EXECUTED!! EVEN WHEN THE JOB IS ABORTED
       }

   }

   public void finish(Database.BatchableContext batchableContext)
   { 

       SObjectUnitOfWork uow = new SObjectUnitOfWork( new List{ BatchControl__c.SObjectType, BatchRecordTracking__c.SObjectType ,MyNewTestObject__c.SObjectType } );
       try
       {
          /********************************
          Any code will be here
          *********************************/

       if(m_batchControl!=null)
       {

          //IF WE ARE HERE THIS IS THE LAST TO DO, THEN LET'S UPDATE THE RECORDS TO THE CORRECT STATUS
          m_myNewTestobject.Status__c= 'Completed';
          uow.registerDirty(m_myNewTestobject);
          m_batchControl.BatchStatus__c= 'Completed';
          uow.registerDirty(m_batchControl);

          // I dont need any more the tracking records , then let's get the space again
          uow.registerDeleted(m_batchRecordsTracking);

          /**********IMPORTANT***********
          ALL RECORDS SHOULD BE DELETED BECAUSE THE BATCH IS FINISH SUCCESSFULLY, THEREFORE THE USER WON'T NEED THEM ANY MORE.
          AT THE MOMENT THEY ARE NOT DELETED ONLY FOR REPORTING PUPOSES
          ******************************/
        }

     }
     catch(Exception e)
     {
     if(m_batchControl!=null)
     {

       //if there is any errors the New Object that is the result of the batch is set in error status
       m_myNewTestobject.Error__c= 'There is an error in the "finish" : '+ e;
       m_myNewTestobject.Status__c='Error';
       uow.registerDirty(m_myNewTestobject);
       // the batch control table is updated
       m_batchControl.BatchStatus__c= 'Failed';
       m_batchControl.Error__c= 'There is an error in the "finish": '+ e;
       uow.registerDirty(m_batchControl);

      /*******************************
      the batch record tracking is deleted in order to get the space back
      WHY IS IT DELETED??
      IT IS ONLY AN OPTION, YOU CAN ALWAYS KEEP IT AND RE-USE IN ORDER TO DONT PROCESS AGAIN THOSE RECORDS THAT WERE CORRECT...
      HOWEVER THIS CODE WILL DELETE THEM AND ALLOW THE USER TO REVIEW THE ERROR FIELD AND DELETE THE RESULT OR KEEP IT
      --- >> NOTE THAT THE DELETE WILL DELETE 10K RECORDS IF YOU NEED MORE YOU WILL NEED TO CREATE THE WAY TO DO IT
      AT THIS MOMENT THE PROTOTYPE USE MASTER -DETAIL THEREFORE WILL BE EASIER TO DELETE BUT
      REMEMBER THAT SALESFORCE DON'T RECOMEND TO HAVE MORE THAN 50K CHILDS , THEN SHOULD BE GOOD TO CREATE MORE MASTERS IF YOU NEED IT
      *******************************/

      uow.registerDeleted(m_batchRecordsTracking);

      uow.commitWork();
    }
   }
   finally
   {

     // you can add here all that you would like to set or check
     // THIS CODE WILL BE ALWAYS EXECUTED!! EVEN WHEN THE JOB IS ABORTED
   }
  }

}
Cómo funciona?
Integer aproxRecordsToProcess = Integer.valueOf(aaj[0].TotalJobItems)* BATCHSIZE;

m_batchControl = new BatchControl__c( BatchClass__c= 'MyBatchClass',BatchId__c=batchableContext.getJobId() , BatchStatus__c='Processing', RecordsAlreadyProcess__c=0,TotalRecordstoProcess__c=aproxRecordsToProcess);

uow.registerNew(m_batchControl);

// in part 2 of this post the object BatchRecordTracking will be linked to BatchControl

m_batchRecordsTracking = new BatchRecordTracking__c(BatchClass__c= 'MyBatchClass',BatchId__c=m_batchId);

uow.registerNew(m_batchRecordsTracking);

uow.commitWork();

Lo primero que hago justo al entrar en la parte del Execute es crear dos registros. Un registro para BatchControl el cual iré actualizando con los registros procesados o errores en su caso. El otro registro será el “padre” de todos los registros que se crearan mas delante de tipo RecordProcessedInBatch. Ten en cuenta que por ahora solamente se crea un padre si deseas crear más , deberás cambiar un poquito el código J Lo segundo y más importante es crear los registros que son usados para bloquear a otros usuarios de procesarlos a la vez y crear datos duplicados o incorrectos en base de datos.

List<RecordProcessedInBatch__c> recordsWillBeProcessed = new List<RecordProcessedInBatch__c>();

for(MyTestObject__c mto : scope)

{

    RecordProcessedInBatch__c rpb = new RecordProcessedInBatch__c(BatchRecordTracking__c=m_batchRecordsTracking.Id,ProcessedRecordId__c =mto.Id, MyTestObject__c= mto.Id); // if you want more than 25 objects you will need to use text field to save the id

    recordsWillBeProcessed.add(rpb);

}

Database.insert(recordsWillBeProcessed,false);

Te estas preguntando por que no uso en esta parte la clase UnitOfWork verdad? Pues resulta que el “truco” esta en usar .insert(list, false), ya que si algún registro falla todos los demás serán creados. ¿Por qué? Deberán de fallar si hay algún otro registro que esté asociado con el mismo record que se desea procesar. Sin embargo no fallará con aquellos registros que no están siendo procesados, y son con los que realmente podemos trabajar. Y al final de cada Execute actualizaremos el registro creado para Batch Control. En cada update insertaremos el total de registros que han sido procesados.

m_batchControl.RecordsAlreadyProcess__c = Integer.valueOf(m_batchControl.RecordsAlreadyProcess__c)+scope.size();

uow2.registerDirty(m_batchControl);

La siguiente pregunta sería pensar, ¿qué pasará si un error ocurre? Si ocurriese un error los registros resultantes, los nuevos registros creados en el proceso batch, se quedaran en estado “Error” nunca pasarán a “Complete” . Ten en cuenta que se usará un registro como padre ( en el código será m_myNewTestObject ) y todos los demás se le añadirán como hijos, para que de esa manera con esta arquitectura podamos seguir y notificar de mejor manera al usuario si algo ocurre. De igual manera el registro de BatchControl__c será actualizado al estado de Error y se rellenara el campo erro con el dicho.

m_batchControl.BatchStatus__c= 'Failed';

m_batchControl.Error__c= 'There is an error: '+ e;

uow.registerDirty(m_batchControl);

m_myNewTestobject.Error__c= 'There is an error in the "execute": '+ e;

m_myNewTestobject.Status__c='Error';

uow.registerDirty(m_myNewTestobject);

Y no se borrara nada , se le deja la opción al usuario ( o al desarrollador ) Al final del proceso, en la parte Finish, lo que estoy haciendo es actualizar de nuevo el registro de BatchControl__c , y borrar todo lo demás, aunque claro solamente lo guardo para hacer reports. Realmente si todo va bien debería ser borrado y así liberar el espacio. Nota: voy a intentar poner todo el código en un repositorio publico de git lo antes posible. Recursos: financialforcedev/ fflib-apex-common Database class

One comment

Leave a comment