[SalesForce] Error: Future method cannot be called

Getting the error below while introducing a new app. The new app is attempting to update all accounts and this is happening. Most likely because it is maxing out the callout limit. I can deactivate it temporarily, but I would prefer to make the the code more stable in order to avoid prevent this from happening again. Any help?

UNIQUE ERRORS UPDATING THE ACCOUNT RECORDS:

AccountChangeToCerbWebhook: execution of AfterUpdate caused by:
System.AsyncException: Future method cannot be called from a future or
batch method: CerbWebhookRequest.Post(String, String) () Please
Specify the Campaign for this Trade Show. Required fields are missing:
[Parent_Company__c]

We have a trigger:

    trigger AccountChangeToCerbWebhook on Account (After insert) {
  String webhook_url = 'https://HIDDEN CONTENT';

  for(Account a : Trigger.new) {
    Map<String, Object> fields = a.getPopulatedFieldsAsMap();

    string body = '_=';

    for(String fieldKey : fields.keySet()) {
      Object fieldValue = fields.get(fieldKey);
      String paramValue = '';

      if(fieldValue instanceof Datetime) {
        paramValue = String.valueOf(((DateTime)fieldValue).getTime()/1000);
      } else if(
        fieldValue instanceof Boolean
        || fieldValue instanceof Date
        || fieldValue instanceof Decimal
        || fieldValue instanceof Double
        || fieldValue instanceof ID
        || fieldValue instanceof Integer
        || fieldValue instanceof Long
        || fieldValue instanceof String
        || fieldValue instanceof Time
        ) {
        paramValue = String.valueOf(fieldValue);
      }

      body += '&' + fieldKey + '=' + EncodingUtil.urlEncode(paramValue, 'UTF-8');
    }
      //if(!Test.isRunningTest()){
          CerbWebhookRequest.Post(webhook_url, body);
      //}

  }
}

Here is the class:

    public with sharing class CerbWebhookRequest {
    @future (callout=true)
    static public void Post(String url, String body) {
        HttpRequest req = new HttpRequest();
        HttpResponse res = new HttpResponse();
        Http http = new Http();

        req.setEndpoint(url);
        req.setMethod('POST');
        req.setBody(body);

        try {
            if(!Test.isRunningTest()){
                res = http.send(req);
            }

        } catch(System.CalloutException e) {
            System.debug('Callout error: '+ e);
            System.debug(res.toString());
        }
    }
}

Best Answer

If you are using a batch to update all records, then you cannot call a @future method from within that batch. You should add a method which will do the processing synchronously instead. That way, when you are already asynchronous (such as in a batch), you can just run the synchronous code. That would look roughly like the below:

public class MyClass
{
    static Boolean shouldProcessAsync()
    {
        return !system.isFuture() && !system.isBatch() && !system.isQueueable() &&
            Limits.getLimitFutureCalls() > Limits.getFutureCalls();
    }

    public static void doStuff(List<MyObject__c> records)
    {
        if (records.isEmpty()) return;

        if (shouldProcessAsync())
        {
            doStuffAsync(new Map<Id, SObject>(records).keySet());
        }
        else
        {
            // logic
        }
    }
    @future(callout=true)
    static void doStuffAsync(Set<Id> recordIds)
    {
        doStuff([
            SELECT Name
            FROM MyObject__c
            WHERE Id IN :recordIds
        ]);
    }
}