Skip to content
Draft
113 changes: 102 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ Welcome to the Custom Async Framework for Salesforce Apex! This project provides
- [Platform Event Setup](#platform-event-setup)
- [EnqueueJobs Class](#enqueuejobs-class)
- [AsyncJobTriggerHandler Class](#asyncjobtriggerhandler-class)
- [AsyncJobUtils Class](#asyncjobutils-class)
- [Limitations](#limitations)
- [Error Handling](#error-handling)
- [Monitoring and Troubleshooting](#monitoring-and-troubleshooting)
- [Performance Best Practices](#performance-best-practices)
- [Contributing](#contributing)
- [License](#license)

Expand All @@ -31,10 +35,14 @@ Salesforce provides powerful tools for asynchronous processing like Queueable an

## Features

- Alternative to Queueable and Batch Apex.
- Utilizes Salesforce Platform Cache for storing class instances, reducing the overhead of object initialization.
- Leverages Platform Events as an event bus for executing actions in a sequence.
- Easy-to-use and intuitive API for developers.
- **Alternative to Queueable and Batch Apex** - Provides custom async processing when standard solutions don't fit.
- **Platform Cache Integration** - Stores class instances in cache, reducing overhead of object initialization.
- **Platform Events Bus** - Leverages events for executing actions in sequence with automatic chaining.
- **Cursor-Based Processing** - Uses Database.Cursor for efficient memory usage with large datasets.
- **Comprehensive Error Handling** - Built-in exception handling, logging, and error recovery.
- **Input Validation** - Validates parameters to prevent common configuration errors.
- **Stateful & Stateless Modes** - Support for both stateful and stateless batch processing.
- **Developer-Friendly API** - Easy-to-use and intuitive interface for developers.

## Installation

Expand Down Expand Up @@ -164,28 +172,111 @@ The Platform Event setup is essential for handling asynchronous events in the Cu

### EnqueueJobs Class

The `EnqueueJobs` class is responsible for enqueuing asynchronous jobs, either Queueable or Batch.

The `EnqueueJobs` class is responsible for enqueuing asynchronous jobs, either Queueable or Batch. It provides validation and error handling for all job submissions.

### AsyncJobTriggerHandler Class

The `AsyncJobTriggerHandler` class is responsible for processing Queue and Batch jobs triggered by Platform Events.
The `AsyncJobTriggerHandler` class is responsible for processing Queue and Batch jobs triggered by Platform Events. It includes comprehensive error handling and automatic cache cleanup.

### AsyncJobUtils Class

The `AsyncJobUtils` class provides utility methods for monitoring and managing async jobs:

```apex
// Check if a job exists in cache
Boolean exists = AsyncJobUtils.jobExists(jobId);

// Get detailed information about a job
Map<String, Object> jobInfo = AsyncJobUtils.getJobInfo(jobId);
System.debug('Job Type: ' + jobInfo.get('type'));
System.debug('Records Processed: ' + jobInfo.get('currentPosition'));

// Cancel a pending job (before it's picked up by event handler)
Boolean cancelled = AsyncJobUtils.cancelJob(jobId);

// Get list of all active jobs
List<String> activeJobs = AsyncJobUtils.getActiveJobIds();
System.debug('Active jobs count: ' + activeJobs.size());

// Get count of active jobs
Integer count = AsyncJobUtils.getActiveJobCount();
```

## Limitations

While the Custom Async Framework offers an alternative for asynchronous processing in Salesforce Apex, it also has many limitations that developers should be aware of:
While the Custom Async Framework offers an alternative for asynchronous processing in Salesforce Apex, it also has limitations that developers should be aware of:

1. **No Direct Callouts**: Framework does not support making callouts for obvious reasons.

2. **No Querylocator**: Framework does not support using the `Database.getQueryLocator()` method within Batch type jobs. If your use case requires this feature, please be aware that it is not compatible with the framework's current implementation.
2. **No Querylocator**: Framework does not support using the `Database.getQueryLocator()` method within Batch type jobs. Instead, it uses `Database.getCursor()` for efficient record processing.

3. **Limited Number of Records**: In Batch jobs, the number of records returned by the `start` method should not exceed 100 KB.
3. **Limited Number of Records**: In Batch jobs, the number of records returned by the `start` method should not exceed 100 KB per cursor operation.

4. **Platform Cache Limits**: The usage of Platform Cache is subject to its own limits and allocations in your Salesforce org. Ensure that you monitor the cache usage to avoid reaching the limits.

5. **Governor Limits**: As with any Salesforce Apex code, the Custom Async Framework is subject to governor limits. Ensure that your asynchronous jobs are designed to comply with these limits.

## Error Handling

The framework includes comprehensive error handling:

- **AsyncException**: Custom exception class with error type categorization (CACHE_ERROR, BATCH_EXECUTION_ERROR, QUEUE_EXECUTION_ERROR, INVALID_CONFIGURATION, CURSOR_ERROR)
- **AsyncLogger**: Structured logging for monitoring and troubleshooting with different log levels (ERROR, WARNING, INFO, DEBUG)
- **Input Validation**: All public methods validate inputs and throw descriptive exceptions for invalid parameters
- **Automatic Cleanup**: Cache entries are automatically cleaned up on errors to prevent orphaned data

### Best Practices for Error Handling

```apex
// Wrap your batch/queue execute logic in try-catch for custom error handling
public class MyRobustBatch implements Async.Batch {
public String start() {
return 'SELECT Id, Name FROM Account WHERE Status__c = \'Active\'';
}

public void execute(List<SObject> scope) {
try {
// Your processing logic
List<Account> accounts = (List<Account>)scope;
for (Account acc : accounts) {
// Process account
}
update scope;
} catch (DmlException ex) {
// Handle DML errors gracefully
System.debug(LoggingLevel.ERROR, 'DML Error: ' + ex.getMessage());
// Optionally log to custom object or send notification
}
}

public void finish() {
// Send completion notification or cleanup
}
}
```

## Monitoring and Troubleshooting

The framework provides detailed logging for all operations:

- Job enqueue operations are logged with job IDs
- Batch chunk processing logs the number of records processed
- Errors are logged with full stack traces and context
- Cache operations are logged for debugging

To monitor your async jobs, check the debug logs filtered by 'AsyncFramework' prefix.

## Performance Best Practices

1. **Batch Size Selection**: Choose appropriate batch sizes (50-200) based on your processing complexity. Smaller batches for complex operations, larger for simple updates.

2. **Query Optimization**: Ensure your SOQL queries use indexes and are optimized. The `start()` method should return efficient queries.

3. **Stateful vs Stateless**: Use stateful batches (`implements Async.Stateful`) only when you need to maintain state across chunks. Stateless batches use less memory.

4. **Cache Monitoring**: Monitor your Platform Cache usage to ensure you don't exceed org limits.

4. **Governor Limits**: As with any Salesforce Apex code, the Custom Async Framework is subject to governor limits. Ensure that your asynchronous jobs are designed to comply with these limits.
5. **Governor Limit Awareness**: Each batch chunk operates within governor limits. Design your execute() logic accordingly.

## Contributing

Expand Down
40 changes: 38 additions & 2 deletions force-app/main/default/classes/Async.cls
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
/**
* Core interfaces for the Async Framework
* Defines contracts for Queue and Batch jobs with optional state preservation
*/
public with sharing class Async {
/**
* Interface for batch processing jobs
* Implement this interface to create custom batch jobs
*/
public Interface Batch {
String start(); // MODIFIED: Was Iterable<sObject>
/**
* Start method - returns SOQL query for records to process
* @return SOQL query string
*/
String start();

/**
* Execute method - processes a batch of records
* @param scope List of SObjects to process in this batch
*/
void execute(List<SObject> scope);

/**
* Finish method - called after all batches are processed
* Use for cleanup, sending notifications, etc.
*/
void finish();
}

/**
* Interface for queue (single execution) jobs
* Implement this interface to create custom queue jobs
*/
public Interface Queue {
/**
* Execute method - performs the async operation
*/
void execute();
}

/**
* Marker interface for stateful batch jobs
* Implement this along with Batch to preserve state across execute() calls
* Without this, a new instance is created for each batch chunk
*/
public Interface Stateful {
//Marker Interface
// Marker Interface - no methods required
}
}
35 changes: 35 additions & 0 deletions force-app/main/default/classes/AsyncException.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Custom exception class for the Async Framework
* Provides better error categorization and handling
*/
public class AsyncException extends Exception {
public enum ErrorType {
CACHE_ERROR,
BATCH_EXECUTION_ERROR,
QUEUE_EXECUTION_ERROR,
INVALID_CONFIGURATION,
CURSOR_ERROR
}

private ErrorType errorType;
private String jobId;

public AsyncException(ErrorType errorType, String message) {
this(message);
this.errorType = errorType;
}

public AsyncException(ErrorType errorType, String message, String jobId) {
this(message);
this.errorType = errorType;
this.jobId = jobId;
}

public ErrorType getErrorType() {
return this.errorType;
}

public String getJobId() {
return this.jobId;
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/AsyncException.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<status>Active</status>
</ApexClass>
51 changes: 51 additions & 0 deletions force-app/main/default/classes/AsyncExceptionTest.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@isTest
private class AsyncExceptionTest {
@isTest
static void testExceptionWithErrorType() {
Test.startTest();
AsyncException ex = new AsyncException(
AsyncException.ErrorType.CACHE_ERROR,
'Test cache error'
);
Test.stopTest();

System.assertEquals(AsyncException.ErrorType.CACHE_ERROR, ex.getErrorType());
System.assertEquals('Test cache error', ex.getMessage());
}

@isTest
static void testExceptionWithJobId() {
String testJobId = 'test123';

Test.startTest();
AsyncException ex = new AsyncException(
AsyncException.ErrorType.BATCH_EXECUTION_ERROR,
'Test batch error',
testJobId
);
Test.stopTest();

System.assertEquals(AsyncException.ErrorType.BATCH_EXECUTION_ERROR, ex.getErrorType());
System.assertEquals('Test batch error', ex.getMessage());
System.assertEquals(testJobId, ex.getJobId());
}

@isTest
static void testAllErrorTypes() {
// Test that all error types can be used
List<AsyncException.ErrorType> errorTypes = new List<AsyncException.ErrorType>{
AsyncException.ErrorType.CACHE_ERROR,
AsyncException.ErrorType.BATCH_EXECUTION_ERROR,
AsyncException.ErrorType.QUEUE_EXECUTION_ERROR,
AsyncException.ErrorType.INVALID_CONFIGURATION,
AsyncException.ErrorType.CURSOR_ERROR
};

Test.startTest();
for (AsyncException.ErrorType errorType : errorTypes) {
AsyncException ex = new AsyncException(errorType, 'Test message');
System.assertEquals(errorType, ex.getErrorType());
}
Test.stopTest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading