Saturday, September 16, 2017

Bad @Future

When writing apex methods, it is very tempting to go for @future if we want some work to be done asynchronously in separate transaction. @future is definitely easy to use but comes with a lot of limitation.


 Let's say I want expose an API to all developers and want to do some work which is quite resource intensive, hence I breakdown my code and put it in @future which is going to take a bit long time and might need higher governor limit.

1:  /**  
2:   * Created by chshah on 9/15/2017.  
3:   */  
4:  public with sharing class MyCoolApex {  

5:    /**  
6:     * I plan to provide this method to rest of the developers to consume.  
7:     */  

8:    public static void myExposedMethod() {  
9:      // do task 1  
10:      // do task 2  
11:      // do task 3  
12:      // do future task  
13:      System.debug(LoggingLevel.INFO, 'Inside myExposedMethod - Calling future task for additional work.');  
14:      doFutureTask();  
15:    }  

16:    @future  
17:    private static void doFutureTask() {  
18:      System.debug(LoggingLevel.INFO, 'Doing Future Resoruce Intensive Task');  
19:    }  

20:  }  

Here  I am exposing MyCoolApex.myExposedMethod for other developers to consume, and calling doFutureTask to delay some resource intensive processing.

It works fine most of the time (until it doesn't), e.g. if someone calling :

 MyCoolApex.myExposedMethod();  

However the problem comes if someone is trying to call this method from Batch, Schedule or even Future. If our method is called from Batch process, Scheduled process or Future (which makes nested Future), salesforce will halt the processing with error. e.g. Client code :


 /**  
  * Created by chshah on 9/16/2017.  
  */  
 public with sharing class TestInBatch implements Database.Batchable<sObject> {  
   public Database.QueryLocator start(Database.BatchableContext BC) {  
     return Database.getQueryLocator('select id from user limit 1');  
   }  
   public void execute(Database.BatchableContext BC, List<sObject> accounts) {  
     MyCoolApex.myExposedMethod();  
   }  
   public void finish(Database.BatchableContext BC) {  
   }  
 }  


 TestInBatch tb = new TestInBatch();  
 Database.executeBatch(tb);  


This would result in error. In order to solve it, we could put some catches inside our API, e.g. below, but that doesn't really solve the problem.

1:    public static void myExposedMethod() {  
2:      // do task 1  
3:      // do task 2  
4:      // do task 3  
5:      // do future task  
6:      if( System.isBatch() || System.isFuture() || System.isScheduled() ) {  
7:        System.debug(LoggingLevel.INFO, 'Inside myExposedMethod - Unable to call future method.');  
8:      } else {  
9:        System.debug(LoggingLevel.INFO, 'Inside myExposedMethod - Calling future task for additional work.');  
10:        doFutureTask();  
11:      }  
12:    }  


Correct Solution (Queueable) 

The right way to solve the problem would be to use Queueable. Queueable has least amount of restrictions, you can call Queueable from Future, Batch, Schedulable, and even Queueable. In Dev org, we can nest Queueable upto 5 times and in Enterprise Org, there is no limit on nested Queueable.


1:  /**  
2:   * Created by chshah on 9/15/2017.  
3:   */  
4:  public with sharing class MyCoolApex {  
5:    /**  
6:     * I plan to provide this method to rest of the developers to consume.  
7:     */  
8:    public static void myExposedMethod() {  
9:      // do task 1  
10:      // do task 2  
11:      // do task 3  
12:      // do future task  
13:      System.debug(LoggingLevel.INFO, 'Calling Queuable');  
14:      System.enqueueJob( new MyCoolApexQueuable() );  
15:    }  
16:  }  

1:  /**  
2:   * Created by chshah on 9/16/2017.  
3:   */  
4:  public with sharing class MyCoolApexQueuable implements Queueable, Database.AllowsCallouts {  
5:    public void execute(QueueableContext context) {  
6:      doFutureTask();  
7:    }  
8:    private static void doFutureTask() {  
9:      System.debug(LoggingLevel.INFO, 'Doing Resoruce Intevensive Task');  
10:    }  
11:  }  


In above example, the actual API method (myExposedMethod),  just calls Queue to defer the resource intensive work in asynchronous fashion. Now, we don't have to worry who (or which context) our method is called.




No comments: