Tag: Apex

  • Beyond “Try-Catch”: Building Self-Healing Apex with Transaction Finalizers

    We’ve all been there as developers. You build a complex Queueable job. You bulk-test it in the sandbox. Everything looks perfect. Then, production reality hits. A row lock here, a CPU timeout there, and suddenly your process dies a silent death.

    As an Architect, the “silent failure” is my nightmare. In the past, we tried to wrap everything in try-catch blocks, but let’s be honest—you can’t try-catch a Limit Exception. When you hit 10.1 seconds of CPU time, the transaction just… ends.

    That’s why I’ve become an advocate for the System.Finalizer interface. It’s the closest thing we have to a “safety net” for the asynchronous world.

    The Architecture: A “Manager-Worker” Relationship

    Think of a Finalizer as a supervisor who stands outside the factory floor. Even if the factory (your Queueable) collapses, the supervisor is still standing there with a clipboard, ready to log the incident and call for help.

    The Glue: The IRetryable Interface

    To ensure our Finalizer can talk to any Queueable job without knowing its specific business logic, we define an interface. This allows the Finalizer to ask the job, “Are you allowed to try again?” and “What is your current retry count?”

    The Implementation

    Here is how I structure this pattern to ensure resiliency. We are going to build a Self-Healing Worker that can detect its own failure and attempt a retry.

    Architect’s Warning: Salesforce limits successive re-queuing from a Finalizer to 5 consecutive attempts. If the job fails 5 times in a row, the chain stops to prevent infinite loops.

    1. The Interface

    /**
     * @description Interface to enable self-healing capabilities.
     */
    public interface IRetryable {
        Boolean canRetry();
        void incrementRetryCount();
        Integer getRetryCount();
    }

    2. The Supervisor (The Finalizer)

    /**
     * @description Architect Pattern: Transactional Safety Net
     */
    public class QueueableSafetyNet implements System.Finalizer {
        private Object parentJob; 
    
        public QueueableSafetyNet(Object job) {
            this.parentJob = job;
        }
    
        public void execute(System.FinalizerContext ctx) {
            if (ctx.getResult() != ParentJobResult.SUCCESS) {
                handleFailure(ctx);
            }
        }
    
        private void handleFailure(System.FinalizerContext ctx) {
            Exception ex = ctx.getException();
            System.debug('Async failure detected: ' + ex?.getMessage());
            // 1. Log to your custom error framework
            // insert new Error_Log__c(...);
    
            if (parentJob instanceof IRetryable) {
                IRetryable retryableJob = (IRetryable)parentJob;
                
                if (retryableJob.canRetry()) {
                    retryableJob.incrementRetryCount();
                    System.debug('Self-healing: Retry #' + retryableJob.getRetryCount());
                    System.enqueueJob(parentJob); 
                }
            }
        }
    }

    3. The Worker (The Queueable)

    public class DataSyncJob implements Queueable, IRetryable {
        private List<Id> recordIds;
        private Integer retryCount = 0;
        private static final Integer MAX_RETRIES = 3;
    
        public DataSyncJob(List<Id> ids) { this.recordIds = ids; }
    
        public void execute(QueueableContext qbc) {
            // ATTACH FIRST: Ensure the net is under you before you start walking the wire
            System.attachFinalizer(new QueueableSafetyNet(this));
    
            // Business Logic: High-risk processing goes here
        }
    
        public Boolean canRetry() { return retryCount < MAX_RETRIES; }
        public void incrementRetryCount() { this.retryCount++; }
        public Integer getRetryCount() { return this.retryCount; }
    }

    Comparison: Traditional Try-Catch vs. Finalizers

    ScenarioTry-Catch BlockTransaction Finalizer
    Logic Errors (Null Pointer, etc.)✅ Can catch✅ Can catch
    Governor Limits (CPU/Heap)Cannot catchCan catch
    Assertion FailuresCannot catchCan catch
    ScopeOnly the code inside the blockThe entire execute method

    Why this changes your “Architectural DNA”

    • Resiliency over Rigidity: Instead of just failing on a row lock, your code now says, “I’ll try again in a minute.”
    • True Error Visibility: You can finally report on why things failed in the background without digging through raw Trace Logs.
    • Governance: You’re respecting the platform. Finalizers allow you to fail gracefully rather than leaving data in a partial or “zombie” state.

    The Trade-offs (Architect’s Reality Check)

    • Chain Limits: You can only chain 5 jobs in a row. If your job is fundamentally broken (logic error), retrying won’t help. Use your retry count wisely.
    • State Management: Ensure your Queueable class is serializable. Everything you need to “restart” the job must be stored in the class variables.

    Final Thought

    We’re moving toward a world of “Autonomous Salesforce.” Our systems should be smart enough to detect a hiccup. They should correct it without an admin having to manually click a button. Transaction Finalizers are the foundation of that autonomy.

  • Eclipse/Class save error – This Apex class has batch or future jobs pending or in progress

    I was constantly getting an error “This Apex class has batch or future jobs pending or in progress” whenever I was saving Apex class written for batch Apex. This was happening in the developer org where we develop a test managed package for the AppExchange product. There was no way I can throw out this org and start with a new one.

    For debugging started with following steps:

    1. Check Schedule Jobs – No schedule jobs (If any are running and related to your class somehow you can just delete the schedule job)
    2. Check Apex jobs – No Apex is job is running or in Queued state. (If are running you could just click “Abort”)
    3. Google – Found some known issue and checked workarounds which were of nouse.

    As nothing worked logged a case through Salesforce Partner portal as job needed to be deleted from the backend. But case got categorized as a developer support case. Not having the premium support it Case got closed 😐

     

    Then started research –

    • Tried deleting all running schedule jobs through Apex in case any is stuck in the background and not visible on UI.

    https://gist.github.com/prasannadeshpande/4fea1278051f169e4bf8f5b1a437a24d

    No Luck!! 🙁

    • Making query on the AsyncApexJob object – SELECT Id, Status, JobItemsProcessed, TotalJobItems, ParentJobId, NumberOfErrors FROM AsyncApexJob Where Status = 'Queued' et voila!! returns the job stuck in Queued status which was not visible through UI. I thought my job is over I will just copy Id from the query result and execute “System.abortJob(jobid);” But that didn’t work. It needs a ParentJobId which was missing from this entry. – No Luck 🙁
    • Then came across a tiny line in the Salesforce Article. If you want to abort a job using Job Id use API version 32.0 or earlier. Login to workbench using v32.0 and from “Execute anonymous” execute System.abortjob(). This time it worked… Finally!!! 🙂

  • Salesforce: Trigger on Attachment – Restricting user from attaching files with specific extensions

    By default, Salesforce doesn’t allow admin to configure or restrict a user from adding a specific type of files. Admin may need that user should not be able to attach files of type exe, dll which can have the virus in them. Also, there is no virus check is done when a file is getting uploaded to the Salesforce.

    But Salesforce does allow writing trigger on the Attachment Object by which you can implement such restrictions. Following is the snippet of the code which will restrict the user from adding the files with extension mentioned in the set list. You can also edit the code and do the reverse by checking if extension exists in the set then only allow attaching the file.

    https://gist.github.com/prasannadeshpande/7ad6f5e49c83ab5a84e628e1096c24f8

    Once trigger code is up and running, whenever Salesforce tries to attach any file with extension exe or dll he will come across following error message.

    AttachmentException