6

This critical update:

PageReference getContent() and getContentAsPDF() Methods Treated as Callouts

will be auto-activated in January 2016.

We have some code that uses a queue of custom event objects where listener classes can be registered that both consume those events (in one case that means making getContentAsPDF calls) and can insert additional events. With the critical update turned on that can result in the:

You have uncommitted work pending. Please commit or rollback before calling out.

error because:

  • a PDF can be generated (a callout)
  • then some events inserted (DML)
  • then another PDF can be generated (a callout) that produces the error.

Future calls can't be chained so they are out.

With the encouragement of Make Calls to PageReference.getContent() in Asynchronous Apex that mentions Queueable a chained Queueable looked like the way to go:

public static void fire() {
    String requestId = insertEvents();
    if (requestId != null) System.enqueueJob(new FireQueueable(requestId));
}

public class FireQueueable implements Queueable, Database.AllowsCallouts {
    private String requestId;
    public FireQueueable(String requestId) {
        this.requestId = requestId;
    }
    public void execute(QueueableContext context) {
        if (fireFor(requestId)) {
            String nextRequestId = insertEvents();
            if (nextRequestId != null) System.enqueueJob(new FireQueueable(nextRequestId));
        }
    }
}

but that generates:

System.AsyncException: Maximum callout depth has been reached.

as described in Can we callout and chain a Queueable class? and Allow callouts from chained queueable Apex jobs.

I'm starting to think there is no solution to this problem. Can anyone suggest a solution?

1
  • 1
    Looks like Queueable is broken in yet more ways than I'd imagined. You're better off with a Batchable process. I found something that should probably do what you're looking for.
    – sfdcfox
    Nov 20, 2015 at 22:45

2 Answers 2

3

You're allowed a single queueable call per database execute method, so this code should technically work:

// Must use Database.AllowsCallouts for PDF generation
public with sharing class GenerateOnePDF implements Queueable, Database.AllowsCallouts {
    public void execute(QueueableContext context) {
        Blob v = Page.myPDFPage.getContentAsPDF();
        // Do your DML here.
    }
}

// Limit one enqueueJob per execute.
public with sharing class GeneratePDFFiles implements Database.Batchable<integer> {
    public Iterable<Integer> start(Database.BatchableContext context) {
        return new Integer[] { 1, 2, 3 }; // Change this logic as you like.
    }
    public void execute(Database.BatchableContext context, integer[] scope) {
        System.enqueueJob(new GenerateOnePDF()); // You'd pass your request ID, I suppose.
    }
    public void finish(Database.BatchableContext context) {

    }
}

However, in both my Developer Edition and Sandbox orgs, the Queueable calls froze. They were stuck in "processing" for an absurd amount of time, given the following code for myPDFPage:

<apex:page>
</apex:page>

I think you're going to have to open a case with technical support. I wish I had a better answer for you, but I think something's broken.


Alternatively, you should be able to just make a Batchable class and do stuff:

public with sharing class GeneratePDFS implements Database.Batchable<Integer>, Database.Stateful, Database.AllowsCallouts {
    String priorId;
    Integer counter = 0;
    String insertEvents() {
        insert new Account(Name=counter.format());
            return ++counter > 5? null: counter.format(); 
    }

    public Integer[] start(Database.BatchableContext context) {
        return new Integer[] { 1 };
    }
    public void execute(Database.BatchableContext context, Integer[] scope) {
        if(priorId != null) {
            insert new Document(Name='Test Me', FolderId=UserInfo.getUserId(), Body = Page.myPDFPage.getContentAsPDF());
        }
        priorId = insertEvents();
    }
    public void finish(Database.BatchableContext context) {
        if(priorId != null) {
            Database.executeBatch(this);
        }
    }
}

Here, I flip the problem around by generating the record after doing the callout in a batchable class, then keep chaining the batch. This seems awfully inefficient, but this does work in my developer edition org with the critical update enabled.

1
  • (Very) lateral thinking! Looks promising and I'll give it a go - thank-you.
    – Keith C
    Nov 21, 2015 at 0:24
1

Based on sfdcfox's idea and code, I've just tried this and it works:

public class FireEventsBatchable implements Database.Batchable<Integer>,
        Database.AllowsCallouts, Database.Stateful {

    // Entry point
    public static void execute(String requestId) {
        if (requestId != null) {
            Database.executeBatch(new FireEventsBatchable(requestId), 1);
        }
    }

    private String requestId;

    private FireEventsBatchable(String requestId) {
        this.requestId = requestId;
    }

    public Integer[] start(Database.BatchableContext bc) {
        // With batch size of 1 execute gets called up to 8 times
        return new Integer[] {1, 2, 3, 4, 5, 6, 7, 8};
    }

    public void execute(Database.BatchableContext bc, Integer[] scope) {
        if (requestId != null) {
            if (Events.fireFor(requestId)) {
                requestId = Events.insertEvents();
            }
        } else {
            System.abortJob(bc.getJobId());
        }
    }

    public void finish(Database.BatchableContext bc) {
        // If more to do - chain to another job
        execute(requestId);
    }
}

The only tweak is to make a single Batchable handle the common case (in my case about 5 iterations) and to only chain in the rare cases (of more than 8 iterations). I've included a System.abortJob to stop early when the work is finished.

Seems very much a convoluted work-around for something that ought to be easier...

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .