4

I have a APEX function (call this from another method) which make a SOAP web service callout every 2 seconds to check a status, when I get the status as 'Complete', then I call another method. I used a while loop to make this 2 seconds delay. Here is the code.

public void getQueryStatus(String taskId){

    String status;

    //
    //Code to get the status from a web service callout here..
    //

    if(!status.equalsIgnoreCase('Complete')){
        DateTime start = System.Now();
        while(System.Now().getTime()< start.getTime()+2000){} //Delay
        //calling again to get the status
        getQueryStatus(taskId);
    }else if(status.equalsIgnoreCase('Complete')){
        //Calling another method to get data
        retrieveDataFromDataExtensionWithFilters();
    }
}

But when the service take more time to send the 'Complete' status, I get a CPU Limit exceeds issue due to this while loop. Any idea to do this in a better way. (Good way to create a 2 seconds time delay between calls)

  • 2
    Note that there is also a 120 second "Maximum cumulative timeout for all callouts (HTTP requests or Web services calls) in a transaction" and a 100 "Total number of callouts (HTTP requests or Web services calls) in a transaction" you could run into. A bodge would be to put up your own trivial web service on Heroku that only responds after 2 seconds and call that in between your SOAP calls. – Keith C May 16 '18 at 11:02
  • 2
    I think you should look at callbacks instead of doing a callout. The pattern you have used does not seem right to me – RedDevil May 16 '18 at 11:20
  • @RedDevil Thanks a lot for the comment, can you please share me any links to learn more about callbacks and how to implement it? Thanks! – Dinesh Wickramasinghe May 16 '18 at 11:24
  • 2
    How are you calling this method? There are several possible alternatives depending on the context. – sfdcfox May 16 '18 at 11:26
  • 2
    @DineshWickramasinghe, what i meant was lets say you post a message to it once the task is created. Once the the work in marketing cloud API is complete it comes back and lets you know this task is complete(it calls back salesforce) instead of you polling it every 2 minutes. – RedDevil May 16 '18 at 11:53
4

One approach you could take (this is fairly architecture-dependent) is a chained Queueable. Design a tiny Queueable class that takes a taskId parameter and keeps it in an instance variable. Then, your Queueable body can look something like this:

public class QueueablePoller implements Queueable, Database.AllowsCallouts {
    private String taskId;

    public QueueablePoller(String taskToPoll) {
        taskId = taskToPoll;
    }

    public void execute(QueueableContext qc) {
        String status = getQueryStatus(taskId);
        if (status.equalsIgnoreCase('Complete')) {
            //Calling another method to get data
            retrieveDataFromDataExtensionWithFilters();
        } else {
            System.enqueueJob(new QueueablePoller(taskId));
        }
    }
}

If you need an actual clock-time 2 second delay, you could store a time value in each Queueable and pass it between instances along with the job Id, using that in a while loop to determine when to fire the next callout. Otherwise, you could just rely on the inherent small delay between invocations of asynchronous jobs to do the polling for you. (Either way is rather messy, I'm afraid).

You could also do limits introspection within the body of the Queueable, using the Limits class and inspecting the current time to safely include multiple polling callouts in a single invocation before firing another Queueable.

  • Hi David, I implemented this and I get below error while executing. [System.CalloutException: Callout not allowed from this future method. Please enable callout by annotating the future method. eg: @Future(callout=true)] . This error coming from [String status = getQueryStatus(taskId);] I cannot add @Future(callout=true) to this method because it returns a string. Any suggestion? – Dinesh Wickramasinghe May 16 '18 at 12:59
  • You should make the callout synchronously from the Queueable context (i.e., no future method) and add the Database.AllowsCallouts interface to the Queueable class (I've made that edit above). – David Reed May 16 '18 at 13:05
  • So would this work in the case that I need to delay the queueable from doing anything for 30 seconds? e.g a record is created, which fires a bunch of other updates, and related records. I don't want my code to execute until that is all done - so could I invoke the queueable, have it wait 30 seconds, and then fire? Or is that going to hit some limits in CPU, Queueable etc? – BritishBoyinDC May 16 '18 at 18:01
  • 1
    @BritishBoyinDC Off the top of my head, I'm not sure, but I would think using the technique mentioned above you'd get a CPU time limit for that. You could poll at a lower time threshold, use a different polling technique like sfdcfox's below, or re-architect and maybe use something like Platform Events. – David Reed May 16 '18 at 18:09
6

If your current context allows PageReference.getContent, you can get your approximate 2 second wait using a Visualforce page.


Controller

public class DelayController {
    public DelayController() {
        Integer delay = Integer.valueOf(ApexPages.currentPage().getParameters().get('delay'));
        Long start = DateTime.now().getTime();
        while(DateTime.now().getTime()-start<delay);
    }
}

Page

<apex:page controller="DelayController" />

Example

public class DelayBatch implements Database.Batchable<Object> {
    public Object[] start(Database.BatchableContext context) {
        return new Object[] { 5 };
    }
    public void execute(Database.BatchableContext context, Object[] scope) {
        PageReference delay = Page.Delay;
        delay.getParameters().put('delay','2000');
        delay.getContent();
    }
    public void finish(Database.BatchableContext context) {

    }
}

Using this method offloads the governor limits on to a separate thread. Note that, as mentioned by David, there's still a 120 second callout limit that may apply, so if you need more than 2 minutes, consider using a Queueable or Batchable class; you can do a spin loop for up to a minute per execution using Database.Batchable or Queueable, or even use this technique to give you even more spinning time.

  • Would DateTime.now().getTime()-start not return 0 always as start = DateTime.now().getTime() itself? – Ayub May 16 '18 at 11:56
  • @AyubAnsari DateTime.now() returns the current clock time, so this loop results in the calculation of how long it's been between now and the first call to DateTime.now(). – sfdcfox May 16 '18 at 12:04
  • Ok ..I understand now...however it's same statement but it returns very deeper part of clock...it would get changed at next line of code!..thanks. – Ayub May 16 '18 at 12:07

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.