Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ global with sharing class LogBatchPurger implements Database.Batchable<SObject>,
LoggerBatchableContext input = new LoggerBatchableContext(batchableContext, this.currentSObjectType);
this.executePlugins(BatchableMethod.EXECUTE, input, scopeRecords);

LoggerDataStore.getDatabase().hardDeleteRecords(scopeRecords);
this.hardDeleteRecords(scopeRecords);
Logger.saveLog();
} catch (System.Exception apexException) {
if (LoggerParameter.ENABLE_SYSTEM_MESSAGES) {
Expand Down Expand Up @@ -246,6 +246,42 @@ global with sharing class LogBatchPurger implements Database.Batchable<SObject>,
return queryLocator;
}

private void hardDeleteRecords(List<SObject> scopeRecords) {
try {
LoggerDataStore.getDatabase().hardDeleteRecords(scopeRecords);
} catch (System.DmlException dmlException) {
this.hardDeleteRecordsWithPartialDeletes(scopeRecords);
}
}

private void hardDeleteRecordsWithPartialDeletes(List<SObject> scopeRecords) {
// A batch delete can fail when the DML cascades to too many related records at once
// (e.g., DELETE_OPERATION_TOO_LARGE on LogEntry__c or FeedItem records). Retry with partial
// deletes so that records that can be deleted are still purged, and log any records that fail
List<Database.DeleteResult> deleteResults = LoggerDataStore.getDatabase().deleteRecords(scopeRecords, false);
List<SObject> deletedRecords = new List<SObject>();
List<Database.DeleteResult> failedDeleteResults = new List<Database.DeleteResult>();
for (Integer i = 0; i < deleteResults.size(); i++) {
Database.DeleteResult deleteResult = deleteResults.get(i);
if (deleteResult.isSuccess()) {
deletedRecords.add(scopeRecords.get(i));
} else {
failedDeleteResults.add(deleteResult);
}
}

if (deletedRecords.isEmpty() == false) {
LoggerDataStore.getDatabase().emptyRecycleBin(deletedRecords);
}

if (failedDeleteResults.isEmpty() == false && LoggerParameter.ENABLE_SYSTEM_MESSAGES) {
Logger.error(
new LogMessage('Nebula Logger - Unable to delete {0} {1} record(s)', failedDeleteResults.size(), this.currentSObjectType),
failedDeleteResults
);
}
}

private void executePlugins(BatchableMethod method, LoggerBatchableContext input, List<SObject> scopeRecords) {
for (LoggerPlugin__mdt pluginConfiguration : this.pluginConfigurations) {
if (String.isNotBlank(pluginConfiguration.BatchPurgerApexClass__c)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ public without sharing class LoggerDataStore {
return System.Database.delete(records, allOrNone);
}

/**
* @description Executes an `emptyRecycleBin` DML operation on the provided list of `SObject` records
* @param records The list of `SObject` records to hard delete
* @return The instance of `List<Database.EmptyRecycleBinResult>`, generated by the platform when hard deleting the records
*/
public virtual List<Database.EmptyRecycleBinResult> emptyRecycleBin(List<SObject> records) {
return System.Database.emptyRecycleBin(records);
}

/**
* @description Executes a `delete` DML operation for the `SObject` record, followed by hard deleting the
* record using `Database.emptyRecycleBin(record)`
Expand All @@ -148,7 +157,7 @@ public without sharing class LoggerDataStore {
public virtual List<Database.DeleteResult> hardDeleteRecords(List<SObject> records) {
List<Database.DeleteResult> results = this.deleteRecords(records);
if (records.isEmpty() == false) {
System.Database.emptyRecycleBin(records);
this.emptyRecycleBin(records);
}
return results;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ private class LogBatchPurger_Tests {
System.Assert.isNotNull(thrownNullPointerException);
}

@IsTest
static void it_should_partially_delete_records_when_batch_delete_throws_dml_exception() {
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
Log__c deletableLog = new Log__c(Id = LoggerMockDataCreator.createId(Schema.Log__c.SObjectType));
Log__c undeletableLog = new Log__c(Id = LoggerMockDataCreator.createId(Schema.Log__c.SObjectType));
List<Log__c> scopeRecords = new List<Log__c>{ deletableLog, undeletableLog };
MockPartiallyFailingDatabase mockDatabase = new MockPartiallyFailingDatabase();
mockDatabase.undeletableRecordIds.add(undeletableLog.Id);
LoggerDataStore.setMock(mockDatabase);
LogBatchPurger batchJobInstance = new LogBatchPurger();
batchJobInstance.currentSObjectType = Schema.Log__c.SObjectType;
Database.BatchableContext mockBatchableContext = new LoggerMockDataCreator.MockBatchableContext();

batchJobInstance.execute(mockBatchableContext, scopeRecords);

System.Assert.areEqual(1, mockDatabase.allOrNoneDeleteCallCount, 'The all-or-none delete should have been attempted once');
System.Assert.areEqual(1, mockDatabase.partialDeleteCallCount, 'The partial delete should have been attempted once');
System.Assert.areEqual(1, mockDatabase.emptyRecycleBinCallCount, 'Deleted records should have been removed from the recycle bin');
System.Assert.areEqual(
new List<SObject>{ deletableLog },
mockDatabase.hardDeletedRecords,
'Only the records that were successfully deleted should have been removed from the recycle bin'
);
LoggerDataStore.setMock((LoggerDataStore.Database) null);
}

@IsTest
static void it_should_default_batch_size_for_chained_job_batch_size() {
LogBatchPurger batchJobInstance = new LogBatchPurger();
Expand Down Expand Up @@ -435,4 +461,38 @@ private class LogBatchPurger_Tests {
batchInput = input;
}
}

public class MockPartiallyFailingDatabase extends LoggerDataStore.Database {
public final Set<Id> undeletableRecordIds = new Set<Id>();
public Integer allOrNoneDeleteCallCount = 0;
public Integer emptyRecycleBinCallCount = 0;
public List<SObject> hardDeletedRecords;
public Integer partialDeleteCallCount = 0;

public override List<Database.DeleteResult> deleteRecords(List<SObject> records) {
this.allOrNoneDeleteCallCount++;
System.DmlException dmlException = new System.DmlException();
dmlException.setMessage('Cascade deleting too many records: DELETE_OPERATION_TOO_LARGE');
throw dmlException;
}

public override List<Database.DeleteResult> deleteRecords(List<SObject> records, Boolean allOrNone) {
this.partialDeleteCallCount++;
List<Database.DeleteResult> deleteResults = new List<Database.DeleteResult>();
for (SObject record : records) {
deleteResults.add(LoggerMockDataCreator.createDatabaseDeleteResult(this.undeletableRecordIds.contains(record.Id) == false, record.Id));
}
return deleteResults;
}

public override List<Database.EmptyRecycleBinResult> emptyRecycleBin(List<SObject> records) {
this.emptyRecycleBinCallCount++;
this.hardDeletedRecords = records;
List<Database.EmptyRecycleBinResult> emptyRecycleBinResults = new List<Database.EmptyRecycleBinResult>();
for (SObject record : records) {
emptyRecycleBinResults.add(LoggerMockDataCreator.createDatabaseEmptyRecycleBinResult(true, record.Id));
}
return emptyRecycleBinResults;
}
}
}