33

I've got a scenario that relies on the system's "automatic rollback" behaviour to undo any DML, emails, futures and so on, in the case of an error. Basically, by allowing code to throw an unhandled exception in a batch execute, I guarantee almost everything gets rolled back. Pretty hair brained!

Except there's this one little thing I do want to survive. The e.getMessage() string.

So I've been trying to think of mechanisms for doing this. I had a couple of angles:

  • querying AsyncApexJob.ExtendedStatus (can't see anything but the first exception),
  • use an HttpRequest callout to do something with message (can't after DML or savepoint)
  • try Page.getContent() on a VF page with an action attribute (can't do this in batch)
  • courtesy crop1645, pull it the Apex Exception email notification (can't do this from inside org)

Is there any other "last ditch" measure I can use to have side effects outside the transaction?

I need only to get my paws on that error string. Or stick it somewhere so another context can!

Edit: they published it :) https://success.salesforce.com/issues_view?id=a1p300000008Y1OAAU

  • 2
    from the doc: The developer specified in the LastModifiedBy field receives the (unhandled exception) error via email with the Apex stack trace and the customer’s organization and user ID – cropredy Nov 1 '14 at 21:21
  • 1
    Thanks @crop1645, out of the box thinking! I can see it now :D an inbound email service in the packaging org, on the exception notification user, that sends an email back to a user in the exception-throwing org... that updates a log object with the error message. – bigassforce Nov 1 '14 at 21:27
  • 1
    you're kidding, surely :D – cropredy Nov 1 '14 at 21:41
  • 1
    Sorry, had flooded your question without really understanding it first. Actually said a lot of "funny things". I should get a rest. When I realized the picture of your question, plus your avatar, I swear I had a mystic vision. Good luck. – MLucci Nov 6 '14 at 20:58
  • 1
    Nice angle @JeremyNottingham! This is a really cool application of Database.Stateful IMHO. I'll have a tonk and see what happens to the in-memory properties during automatic system rollback. – bigassforce Nov 7 '14 at 14:50

10 Answers 10

22

Update, Summer '17:

Platform Events are now GA. Note the following from the docs:

Platform Events and Transactions
Unlike custom objects, platform events aren’t processed within database transactions in the Force.com platform. As a result, publishing platform events can’t be rolled back. Note the following:

  • The allOrNoneHeader API header is ignored when publishing platform events through the API.
  • The Apex setSavepoint() and rollback() Database methods aren’t supported with platform events.

So you could publish an event with the required information and have a corresponding Apex trigger subscribed to the event to store the persistent message.

I've expanded the idea out a bit in Can I store information even if the trigger throws an exception?


Wrap the contents of the batch execute method in a try catch and for any caught exceptions throw a new exception with the most important information at the start of the message property. Ideally this will show up in the AsyncApexJob.ExtendedStatus.

This will only help if the exception is of a type that can be caught.


You could abuse the logging mechanism. Use a scheduled task to keep a TraceFlag active for the user running the batch job.

In the Batch finish method pull the log contents if NumberOfErrors is greater than zero. Then painfully extract the missing information.

You could potentially combine this with the above approach and a System.debug(LoggingLevel.Error, e.getMessage()).


Better yet, use a Checkpoint in the code to capture the contents of the exception. Then extract the Exception message from the ApexExecutionOverlayResult using the ToolingAPI from Apex.

  • Very innovative angles, thanks for swinging at this one Daniel! – bigassforce Nov 5 '14 at 1:33
  • 1
    It's a good challenge, I think the Checkpoint think could be made to work. However, you will need the Apex Log Level set to at least Finer, so it might be easier to just capture the log. – Daniel Ballinger Nov 5 '14 at 1:45
14
+100

I wrote a bit on my blog last week about how I was using the Tooling api to effectively create an "eval()" method. You could replace the transaction mechanism with a similar eval mechanism. The "eval'd" transaction code would still roll everything back, but the get message would be available for the eval exception to return.

see http://codefriar.wordpress.com

MB: So instead of busting out (after DML) to handle the error, it'd bust out to handle the DML itself.

Tooling.Api.Client client = new Tooling.Api.Client();
String command = 'TheClass.thingThatDoesCalloutsAndDmlAndStuff();';

//exec anon maintains 'automatic rollback' behaviour from the uncaught exceptions
Tooling.Api.ExecuteAnonymousResult result = client.executeAnonymous(command);

//do something with message
String exception = result.exceptionMessage;
  • very cunning, thanks for weighing in! in particular am admiring the availability of ExecuteAnonymousResult.exceptionMessage after letting the uncaught exception blow up – bigassforce Nov 5 '14 at 21:26
6

The way we have resolved (or worked around) this in our package is basically by taking transaction control away from the platform.

First we set a savepoint, then we do the processing, and if an exception occurs we manually rollback, and then we process the error (store it, send an email, ...).

An example of this can be seen at work in the run method of our TDTM_TriggerHandler.

We do it that way because there really isn't any other good alternative. The way I understand it is that outbound calls, emails, chatter posts, etc, are put in queues, and if an unhandled exception occurs they all get pulled from the queue. This makes sense, since an unhandled exception means that there was an error in the transaction, and the platform doesn't have a way of knowing if the outbound call, email, or whatever, has to do with error handling. It has to treat everything as business logic that needs to be rolled back so that the transaction has no side-effects, since there was an error. Also, for completeness, using addError in one of the records has the same effect as an unhandled exception in terms of rollbacks.

  • Thanks for chipping in! Can I pick your brains? I'm wondering if the savepoint isn't used (eg no DML), you are still obstructed from HTTP callouts? – bigassforce Nov 4 '14 at 20:47
  • If no DML was performed and you are not in a batch or a future, you should be able to do callouts. – ceiroa Nov 4 '14 at 21:02
  • Actually, I think I misunderstood. You were asking if you can do a callout when there is an exception, right? I don't think you can, in that case. If you get an exception the callout request will be pulled from the callout queue. – ceiroa Nov 4 '14 at 21:13
  • You'll still have to use the manual savepoint and rollback, and only then do the callout. – ceiroa Nov 4 '14 at 21:19
  • I wasn't sure callouts work after rollback, that error message might be a lie :( – bigassforce Nov 5 '14 at 1:42
6

Interesting question with some interesting answers. I think/hope I've understood what you're asking. In some respects, this feels a bit like fighting the platform here, and I guess it'll depend on how you feel about doing callouts back into the same instance of Salesforce using UserInfo.getSessionId(), but I think you could do something using this approach to give what you need.

To illustrate, I have 2,810 Contacts in my org where I'm simply updating a custom field value Val__c to a numeric value (just so i can see the results each time).

I have a batch apex class which I'm calling with a chunk size of 500 so that I get 6 iterations. In one of those iterations (#4), I'm going to deliberately throw an Exception (i.e. one that can be caught) and then in iteration #5 I'm going to create a Limits Exception (i.e. one that cannot be caught) and all the others will succeed.

At the end of the batch, I should have updated 1,810 records and 1,000 will remain untouched/rolled back thanks to the platform rollback feature (all good). I'm going to preserve state across each batch execution and report the errors in the finish method.

Batch Apex Class

This doesn't do a whole lot of work, it basically makes the callouts (back to SF) which have their own execution context, so essentially each iteration is isolated (and will be committed/rolled back in isolation), except for the state I'm passing between them. It is also possible to retrieve the error message from the REST worker class, even if we don't catch the error in that class (see makeDMLCallout)

global class MainBatch implements Database.Batchable<sObject>, Database.Stateful, Database.AllowsCallouts
{
    private String sessionId;
    private Decimal val;
    private Integer iteration = 0;
    private List<String> errors = new List<String>();

    global MainBatch(Decimal val, String sessionId) 
    {
        this.val = val;
        this.sessionId = sessionId;
    }

    global Database.QueryLocator start(Database.BatchableContext BC) 
    {
        return Database.getQueryLocator([select Val__c From Contact]);
    }

    global void execute(Database.BatchableContext BC, List<SObject> scope) 
    {
        // create some state to pass to REST service
        BatchData bd = new BatchData();
        bd.val = this.val;
        bd.recordIds = new Map<Id,SObject>(scope).keySet();
        bd.iteration = this.iteration++;

        // make first callout
        makeBenignCallout(bd);

        // make second one
        makeDMLCallout(bd);
    }

    global void finish(Database.BatchableContext BC) 
    {
        System.debug('Iteration = ' + this.iteration);
        System.debug('Errors='+JSON.serializePretty(errors));
    }

    global class BatchData
    {
        public String sessionId {get;set;}
        public Decimal Val {get;set;}
        public Set<Id> recordIds {get;set;}
        public Integer iteration {get;set;}
    }


    private void makeDMLCallout(BatchData bd)
    {
        String addr = URL.getSalesforceBaseUrl().toExternalForm() + '/services/apexrest/RestWorker';

        HttpRequest req = new HttpRequest();
        req.setTimeout(60000); 
        req.setEndpoint( addr );
        req.setMethod('POST');
        req.setHeader('Authorization', 'OAuth ' + bd.sessionId);
        req.setHeader('Content-Type','application/json; charset=UTF-8');
        req.setHeader('Accept','application/json');
        req.setHeader('Accept-Encoding','gzip');
        req.setBodyAsBlob( Blob.valueOf( JSON.serialize(bd) ) );

        Http http = new Http();

        HttpResponse response = http.send(req);    

        String body = response.getBody();

        if(response.getStatus()=='OK')
        {
            // celebrate
        }
        else
        {
            List<Object> results = (List<Object>)JSON.deserializeUntyped(body);
            Map<String,Object> result = (Map<String,Object>)results[0];
            String message = (String)result.get('message');
            errors.add('Preserved Error Message: '+message);
        }
    }

    private static void makeBenignCallout(MainBatch.BatchData bd)
    {
        String addr = URL.getSalesforceBaseUrl().toExternalForm() + '/services/apexrest/RestWorker';

        HttpRequest req = new HttpRequest();
        req.setTimeout(60000); 
        req.setEndpoint( addr );
        req.setMethod('GET');
        req.setHeader('Authorization', 'OAuth ' + bd.sessionId);
        req.setHeader('Content-Type','application/json; charset=UTF-8');
        req.setHeader('Accept','application/json');
        req.setHeader('Accept-Encoding','gzip');

        Http http = new Http();

        HttpResponse response = http.send(req);    
    }   

}

REST service class

This is basically a faux implementation of the batch apex's execute method, called via the REST api. The platform is kind enough to return us a HttpResponse which includes the error message, whether it be a catchable, or non-catchable exception + rolling back etc. as it should:

@RestResource(urlMapping='/RestWorker')
global class RestWorker 
{
    @HttpGet
    global static void doBenignUnitOfWork()
    {
        System.debug('nothing to see here');
    }

    @HttpPost
    global static WorkerResponse doUnitOfWork()
    {
        // deserialize the JSON passed from MainBatch
        String jsonString = System.RestContext.request.requestBody.toString();
        MainBatch.BatchData bd = (MainBatch.BatchData)JSON.deserialize(jsonString, MainBatch.BatchData.class);

        // query the dataset based on the record id's in the batch
        List<Contact> contacts = [Select Val__c From Contact Where Id IN :bd.recordIds];

        // do something
        for(Contact contact : contacts)
        {
            contact.Val__c = bd.Val;
        }

        update contacts;

        // simulate a failure we can control
        if(bd.iteration == 3)
        {
            throw new DeliberateException('Deliberate Exception Thrown in Iteration ' + bd.iteration);
        }

        // similate a limits exception
        if(bd.iteration == 4)
        {
            causeLimitsException();
        }

        return new WorkerResponse();
    }

    private static void causeLimitsException()
    {
        for(Integer i = 0 ; i <= 100 ; i++)
        {
            List<Contact> contacts = [Select Val__c From Contact limit 1];
        }
    }

    global class WorkerResponse
    {
        public String result {get;set;}
    }

    public class DeliberateException extends Exception{}
}

The results

After running this test against my 2,810 i get the expected results:

  • 1,810 Contacts updated with the correct numeric value
  • 1,000 Contacts rolled back by platform
  • 2 x Error messages (one RestWorker.DeliberateException and one System.LimitException: Too many SOQL queries: 101 exception) output in the finish method
  • iteration value of 6

Obviously, needs some work but could potentially be made to work unless i've missed something obvious.

  • 1
    Nice, phil! Another great angle that busts out of the transaction to do the "work" at the expense of an API call, instead of fighting to jettison the exception itself - and renders the execute method immune to the automatic rollback that happens in the REST service. – bigassforce Nov 11 '14 at 22:38
  • 1
    Always robbing Peter to pay Paul on this platform ;) – Phil Hawthorn Nov 11 '14 at 22:43
6

I've been blasting away at this, trying to best apply some of the contributions, notably:

  • JeremyNottingham with the exemplary Database.Stateful pattern,
  • MLucci's technique to 'empty out' the successful list members, leaving the failures known,
  • Phil Hawthorn and Kevin P who immunize the execute by busting out of it to do the work,

However, I believe I have found an underlying platform inconsistency that undermines the very premise on which I asked this question. It relates to how Database.Stateful class members are persisted (or not persisted) in the face of fatal errors (eg assert / limits).

QueryLocator batch implementation can leave side effects in memory.

Here's an example implementation anyone can save:

public class StatefulQueryLocator implements Database.Stateful, Database.Batchable<SObject> {

    public String someStatefulProperty = 'set';

    public Database.QueryLocator start(Database.BatchableContext context) {
        return Database.getQueryLocator([SELECT Id FROM User LIMIT 1]);
    }

    public void execute(Database.BatchableContext context, List<SObject> scopes) {
        this.someStatefulProperty = 'unset';
        System.assert(false, 'something uncatchable');
    }

    public void finish(Database.BatchableContext context) {
        //leave some indication of what happened
        insert new Account(Name = this.someStatefulProperty);
    }

}

and run in Developer Console:

Database.executeBatch(new StatefulQueryLocator());

stateful querylocator result

Iterable batch implementation cannot.

Now try this one! For all intents and purposes the code is the same.

public class StatefulIterator implements Database.Stateful, Database.Batchable<SObject> {

    public String someStatefulProperty = 'set';

    public Iterable<SObject> start(Database.BatchableContext context) {
        return [SELECT Id FROM User LIMIT 1];
    }

    public void execute(Database.BatchableContext context, List<SObject> scopes) {
        this.someStatefulProperty = 'unset';
        System.assert(false, 'something uncatchable');
    }

    public void finish(Database.BatchableContext context) {
        //leave some indication of what happened
        insert new Account(Name = this.someStatefulProperty);
    }

}

and execute like so:

Database.executeBatch(new StatefulIterator());

enter image description here

Essentially the same code (bar the start method). Different results.

  • I have raised this with Salesforce. Case Number 11236834 – bigassforce Nov 17 '14 at 16:43
  • 1
    Thanks for updating on this, absolutely very interesting finding! Salesforce Engineers should thank you as well :) – MLucci Nov 17 '14 at 18:50
  • 1
    R&D have logged bug W-2443023! A different code path is taken depending if Iterable or QueryLocator is used, and how those exceptions are handled is different. @MLucci – bigassforce Dec 4 '14 at 11:00
  • 1
    It's not every day that you face an issue and upon googling for help come across your own reference to your own crazy ass workaround for this bug. Thanks for doing this side by side comparison, as it helped keep me sane. – Kevin P Apr 2 '15 at 21:44
2

You could use a try catch block around the operation you are doing, Then if you do hit the roll back catch the exception and create a Error log object to place the information in. I.E.

 Try
    {
       system.assert(false, 'Hi');
    }
catch(Exception e)
{ 
      roll back
      New Error__c err ( e.getMessage)
}

You could then report to any users error and there appropriate resolve.

If you do this approach i would recommend a parent obj containing Apex job Id and other batch info Then a child object per error (As you might have more then one) containing the errors.

  • I'm wondering if the second block gets reached given an uncatchable exception - guess this is a neat thing about automatic rollback, message is saved in AsyncApexJob.ExtendedStatus even if uncaught. – bigassforce Nov 5 '14 at 15:16
  • And rollback necessitates a Savepoint, which prevents try {} from doing any callouts :( – bigassforce Nov 5 '14 at 15:21
  • If you segment your batch operations into a engine calling smaller methods from the execute each doing less, You can have more control over what is going on. Then you can use your own assertions within your code creating custom labels for errors and making sure all errors at all points are catchable. – TimChadwick Nov 5 '14 at 15:21
  • You should only do the Savepoint just before you do your insert. Then you can reduce the possibility of errors, Also from this point you know that it is going to be a DML Exception Or Custom Validation Exception with a after insert trigger. – TimChadwick Nov 5 '14 at 15:28
  • 4
    uncatchable exceptions won't be caught by the catch{..} – cropredy Nov 5 '14 at 18:24
2
+200

I love this kind of challenges, they're our everyday bread. Anyway, what I'm going to expose could be the worse (non)solution exposed here, as well as the solution to your problem. It's hellish, but this is how it works on the platform.

DISAMBIGUATION

The following is my first answer, which assumes the question is only about exceptions raising during a Batch processing. After writing it, I thought to have misunderstood the question, and that it was about all force.com transaction contexts. At first I removed the answer, but then I thought it was worth re-adding it (editing it a bit). See note at the end.

Solution:

  • Apply the Stateful interface to the Batch class
  • Add a List< Exception > property to the Batch class
  • Before the TRY/CATCH: If there are Exceptions in the List, dump them to an sObject, or whatever (leave list intact).
  • TRY/CATCH for Batch logic: When an exception is caught in here, it's added to the List, then thrown up and the transaction voided (including any Exception Log inserted before). End.
  • After the TRY/CATCH: If we are here, we assume that no exception raised during this execute() logic, and that any previously execute() exception has been already logged somewhere somehow before the TRY/CATCH. Therefore we can empty the Exceptions list and hope it's not going to rain next.

Comment:

This solution does not solve the problem completely. What if a series of exception strikes through the Batch end? And what about exceptions happening in the last Job Item? And is anyway left open the question about how you can store the exceptions (sobject, callout, 3.5" floppys, etc.), answer also higly dependant on the Batch logic itself, how much resource consuming it is, etc.

But still could make (part of the) the job.

NOTE: But... Is the question philosophical?

At a certain point I realized/started thinking the question was about any transaction (within Batch, Future, Apex, API, etc.). That's when I deleted my (confused and non yet valid) answer.

But thinking more...

  1. The Author of the Question had been mentioning of trying reading AsyncApexJob status message (so +1 for the Batch related question)
  2. If anyway his plans are to stay behind any uncaught exception everywhere around, would be a useless to look for a unique solution to the "problem", since it could be "solved" only putting together all different pieces as the above tailored to each context/case.

...so my answer's is back now :-)

  • All great insight. Very relevant to the use case which is indeed batch! – bigassforce Nov 6 '14 at 23:52
  • This was within a hair of a perfect solution, but I think something is too good to be true... when there is Database.Stateful on the batch class, the in-memory changes to those variables are indeed 'rolled back' if the execute() context does not succeed; like when we catch-and-re-throw the exception. Ultimately correct behaviour! But would have been oh-so-inconvenient when I really want to leave that memory tainted in this case :D – bigassforce Nov 7 '14 at 1:18
  • 1
    Acc! Fair enough, and good (not for this question context) to see they made it soooo well. – MLucci Nov 7 '14 at 9:32
  • I think this is very creative; instead of starting with an empty list and adding to it, to assume failure and "empty the Exceptions list" to leave behind a side-effect in the next execution context. Thank you. – bigassforce Nov 10 '14 at 19:14
  • 1
    If the start method populates a Database.Stateful list of potential exception items, and each successful execute method empties its item from the list as you say, then the finish method will see what remains: the exceptional items. Maybe we don't know "what" the problem is, but the "who" survives, at least. And survives even if uncatchable, like limits or asserts. – bigassforce Nov 10 '14 at 20:19
2

What if you move the batch execute() logic inside a Trigger for an example object ExecuteMyBatch__c.

That way you could make the logic being executed as a whole within a single DML from the execute() context.

What would happen:

  • You get in the execute method with a scope of 200 records
  • You create (insert) an object ExecuteMyBatch passing over the ids of those records in the scope from within a try/catch
  • The execute() logic is executed in the ExecuteMyBatch Trigger
  • If no exception... well
  • If exception - no side-effect (anything happened as consequence of your ExecuteMyBatch record insert is reverted as being part of a single DML thanks to allOrNone) - you can catch it as a whole and get the message you are looking for
  • Collect the exceptions and use them in the finish method

All Batch chunks would appear as successful in this way, but we know all is fine (should be) regarding the desired no-side-effects.

Unfortunately I guess Callouts would be prevented because of the initial ExecuteMyBatch DML. You could eventually defer them to be executed by the "next" chunk, before the ExecuteMyBatch insertion, but that depends on what the Callouts are meant to do and if they need to be done in the middle of the logic, etc. etc. For the last Batch Chunk (you can understand by querying AsyncApexJob when you are executing it), process the callouts it requires in the batch finish() method or in a future called from the finish() method - don't remember if you can make callouts from there at the moment.

2

If your Batch class implements the Database.Stateful() interface, you can have a class-level variable that is available from any of the methods in the class. Then email it to yourself at the end of the job.

global class Batch_myBatch implements 
    Database.Batchable<sObject>, 
    Database.Stateful 
{

    //this will build up over the course of several executes. Could be a list<String> also.
    private String debuglog = ''; 
    ...

    global void execute(Database.BatchableContext BC, List<sObject> scope)
    {
        Savepoint sp = Database.setSavePoint();
        try {

            //do your stuff that might break here
        } catch (Exception e)
        {
            Database.rollBack(sp);
            //record exception message and any other debugging info you want.
            debuglog += '\nException on execute: ' + e.getMessage();
        }
    }

    global void finish(Database.BatchableContext BC) {
        //send the debuglog to somebody in an email 
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

        String[] toAddresses = new String[] { 'email@example.com'};

        mail.setToAddresses(toAddresses);
        mail.setReplyTo('noreply@salesforce.com');
        mail.setSenderDisplayName('Batch Job completed'); 
        mail.setSubject('Batch Job completed');

        mail.setPlainTextBody(debuglog);
        mail.setHtmlBody(debuglog);

        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });

    }
}
  • Any DML that occurs anywhere within the try{} will be rolled back in case of an exception. But only that one execute() will be rolled back. The finish() will execute no matter what. – Jeremy Nottingham Nov 6 '14 at 23:39
  • I should really get a sleep. No words. – MLucci Nov 6 '14 at 23:45
  • 1
    Anyway, if you catch the exception, the DML happened before, in try block or not, are not rolled back. This is completely wrong (unless you forgot in your code to re-throw the exception up to break the batch execute() context up and actually rollback). – MLucci Nov 6 '14 at 23:55
  • OP was how to handle uncaught exceptions - that is, those that can't be caught in a catch block – cropredy Nov 7 '14 at 0:00
  • 1
    Run the following in the dev console: try { insert new Account(Name = 'Mario'); Integer x = 0; if (42 / x > 0) { system.debug('Found the Answer'); } } catch (Exception e) {}. Then go and meet your new account ;-) – MLucci Nov 7 '14 at 9:23
2

If the Exception is one that can be caught, it appears that static variables persist after a rollback. Perhaps I've got the wrong end of the stick, but this seems to be what you want.

Here's some code to demonstrate this:

Savepoint sp = Database.setSavepoint();
static String errorVal;
try {
    Integer i = 5/0;
}catch (Exception e){
   errorVal = '' + e;
   Database.rollback(sp); 
}
System.debug(errorVal);

Here are the docs that confirm this.

  • One limitation of this is it removes the option of using Callouts after Database.setSavepoint() is called. Apparently this expands to after calling rollback as well. – Daniel Ballinger May 24 '15 at 20:20
  • I wonder if you could create a Queuable object after rollback and perform your callout then? – Caspar Harmer May 24 '15 at 20:38
  • Maybe, I was thinking more along the lines of needing to make callouts and then act on the responses. These would all need to be make before calling setSavepoint(). After rollback in may also be possible to do a future method invocation, which will get a separate context. Another issue would be handling uncatchable exceptions. Manual transactions can't catch a LimitException. – Daniel Ballinger May 24 '15 at 20:42
  • 1
    Yes, there are definitely limitations - I mainly put this here because noone else seemed to have noticed that static variables persist and I figured that it was probably going to be useful for some situations. – Caspar Harmer May 24 '15 at 21:05
  • You can also now dispatch a platform event and it'll persist. – Caspar Harmer Apr 10 at 20:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.