[SalesForce] Queueable Job limit hit when trigger is batched

I am trying to run a very simple trigger, in concept. The purpose of the trigger is to count some items simply, then update a field on two objects based on that number. At first, I attempted to construct this trigger so that it simply does as described. This attempt, however, failed because of locked rows with competing triggers. I tried to work around this by moving my computation to a @future method, however, I still received the locked row errors.

Finally, I looked back to previous code I had written where I had also seen these errors. I noticed that I used a queueable class, enqueued from the trigger. Using a queueable bypassed the locking of the rows and allowed me to display the result correctly, no matter how the class executed (i.e. by a batch or directly).

Now, however, I am trying to use this methodology again, with various failures. The trigger works when executed for a single order, see below. However, when run by a batch, the trigger fails due to a queued job error, which can be prevented by the following snippet of code:

If( Limits.getQueueableJobs() != Limits.getLimitQueueableJobs()) {
    System.enqueueJob(new QueueProductCount(order_ids));
}

However, even still I am not able to enqueue my jobs because this IF statement turns out to be false. I don't understand because the trigger should only be executing PER object inserted, and each trigger only enqueues one job. Therefore shouldn't all objects inserted have one associated trigger and therefore one job enqueued? Even if they all are batched into one trigger, the trigger (see below) sorts them all together, and should accommodate for that.

Any ideas?

Trigger:

trigger ProductCount on Products_Purchased__c (before insert) {
    try {
        sObject settings = [SELECT Do_Product_Counting__c, Error_Reporting_Email__c FROM Tools__c LIMIT 1];
        Boolean execute = (Boolean)settings.get('Do_Product_Counting__c');

        if (execute) {
            Set<id> order_ids = new Set<id>();
            for (Products_Purchased__c o : Trigger.New){
                order_ids.add((id)o.get('Order__c'));
            }

            System.debug('Ran trigger.  Order ids are: ' + order_ids);

            If( Limits.getQueueableJobs() != Limits.getLimitQueueableJobs()) {
                System.enqueueJob(new QueueProductCount(order_ids));
            } else {
                CalloutException y = new CalloutException('QUEUEABLE JOBS IS ' + Limits.getQueueableJobs() + ' WHERE MAX IS ' + Limits.getLimitQueueableJobs());
                Util.Report report = new Util.Report('EMAIL', y);
                Util.reportError(report);
            }
        }
    } catch (Exception e) {
        Util.Report report = new Util.Report('EMAIL', e);
        Util.reportError(report);
    }
}

Queueable Class:

public class QueueProductCount implements Queueable {
    Set<id> order_ids = new Set<id>();

    public QueueProductCount(Set<id> order_ds){
        this.order_ids = order_ds;
    }

    public void execute(QueueableContext context){
        try {
            ProductPurchaseCount.updateProducts(order_ids);
        } catch (Exception e){
            // error report
        }
    }
}

Method Class:

Note: I have tried making this @future with similar failures

public class ProductPurchaseCount {
    public static void updateProducts(Set<id> order_ids){
        try {
            List<Products_Purchased__c> products = [SELECT Purchased_in_Order__c, Quantity_Purchased__c, Order__c FROM Products_Purchased__c WHERE Order__c IN: order_ids];
            List<Comm_Order__c> orders = [SELECT Purchased_in_Order__c FROM Comm_Order__c WHERE id IN: order_ids];
            System.debug('Got the following products: ' + products);
            System.debug('Got the following orders: ' + orders);

            for (Id order_id : order_ids){
                System.debug('Currently on ORDER ID: ' + order_id);

                Double num = 0;
                for (Products_Purchased__c o : products){
                    if (o.get('Order__c') == order_id && o.get('Quantity_Purchased__c') != null){
                        num += Double.valueOf(o.get('Quantity_Purchased__c'));
                    }
                }
                System.debug('NUM has processed to be: ' + num);

                for (Products_Purchased__c o : products){
                    o.put('Purchased_in_Order__c', String.valueOf(num));
                }

                for (Comm_Order__c o : orders){
                    if (o.get('Id') == order_id){
                        o.put('Purchased_in_Order__c', String.valueOf(num));
                        break;
                    }
                }
            }

            update products;
            update orders;
        } catch (Exception e){
            //Error reporting
        }
    }
}

Custom CalloutException:

QUEUEABLE JOBS IS 1 WHERE MAX IS 1

Recall that this works if it is not batched.

Update 13:15 8/5/16:
If all of the try catch blocks are removed, and the if statement, the error reported from the Apex Jobs screen is as follows:

Too many queueable jobs added to the queue: 2

Which doesn't necessarily make sense to me, because the trigger should only run once. The batches insert the objects, which causes the trigger to run.

I've looked at this thread: Can Queueable solve "Future method cannot be called from a future or batch method"? , could it be that the other, previous trigger I made is adding to the chaining limit and this trigger just pushes it over the edge?

Turns out the batch is causing both triggers to execute. Does that chain the triggers to the batch and therefore the queueables are launched from the batch in a way? And could that be why I am getting this error? If so, what are some alternative work arounds for avoiding locked records?

Best Answer

When run in Batch, you can only enqueue 1 job per execution of the batch so if your batch is causing the Trigger to enqueue more than 1, it will fail as described. I'll try to find a link to the docs describing this.

If your trigger is firing, from a batch apex process, and you have 2 Triggers firing that both enqueue a job, you will get the error because both the triggers fire in the same execution context (even though they are separate triggers).

For example:

Batch Apex
    Insert record(s)
        Trigger 1 (enqueue job 1 - ok)
        Trigger 2 (enqueue job 2 - fail)

The only way to get around that would be to ensure that only one enqueue job gets called across all triggers that fire.