Skip to content
Merged
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
60 changes: 60 additions & 0 deletions SemanticBackup.Core/Helpers/WithRetry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace SemanticBackup.Core.Helpers
{
public static class WithRetry
{
/// <summary>
/// Retries an asynchronous function that returns a result (Task&lt;T&gt;).
/// </summary>
public static async Task<T> TaskAsync<T>(Func<Task<T>> operation, int maxRetries = 2, TimeSpan? delay = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(operation);
ArgumentOutOfRangeException.ThrowIfNegative(maxRetries);

delay ??= TimeSpan.FromSeconds(1);

for (int attempt = 1; ; attempt++)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
return await operation().ConfigureAwait(false);
}
catch (Exception ex) when (attempt < maxRetries)
{
Console.WriteLine($"[Attempt {attempt}] Failed, Error: {ex.Message}");
await Task.Delay(delay.Value, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// Retries an asynchronous function that does not return a result (Task).
/// </summary>
public static async Task TaskAsync(Func<Task> operation, int maxRetries = 2, TimeSpan? delay = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(operation);
ArgumentOutOfRangeException.ThrowIfNegative(maxRetries);

delay ??= TimeSpan.FromSeconds(1);

for (int attempt = 1; ; attempt++)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
await operation().ConfigureAwait(false);
return;
}
catch (Exception ex) when (attempt < maxRetries)
{
Console.WriteLine($"[Attempt {attempt}] Failed, Error: {ex.Message}");
await Task.Delay(delay.Value, cancellationToken).ConfigureAwait(false);
}
}
}
}
}
2 changes: 1 addition & 1 deletion SemanticBackup.Core/Interfaces/IBackupRecordRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public interface IBackupRecordRepository
{
Task<List<BackupRecord>> GetAllAsync(string resourceGroupId);
Task<BackupRecord> GetByIdAsync(long id);
Task<bool> RemoveAsync(long id);
Task<bool> AddOrUpdateAsync(BackupRecord record);
Task<bool> UpdateAsync(BackupRecord record);
Task RemoveWithFileAsync(long id);
Task<bool> UpdateStatusFeedAsync(long id, string status, string message = null, long executionInMilliseconds = 0, string newFilePath = null);
Task<bool> UpdateRestoreStatusFeedAsync(long id, string status, string message = null, string confirmationToken = null);
Task<List<BackupRecord>> GetAllByStatusAsync(string status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace SemanticBackup.Core.Interfaces
{
public interface IContentDeliveryRecordRepository
{
Task<bool> RemoveAsync(string id);
Task<bool> AddOrUpdateAsync(BackupRecordDelivery record);
Task<bool> UpdateStatusFeedAsync(string id, string status, string message = null, long executionInMilliseconds = 0);
Task RemoveAsync(string id);
Task<List<BackupRecordDelivery>> GetAllByStatusAsync(string status);
Task<List<BackupRecordDelivery>> GetAllByBackupRecordIdAsync(long id);
Task<List<string>> GetAllNoneResponsiveAsync(List<string> statusChecks, int minuteDifference);
Expand Down
112 changes: 58 additions & 54 deletions SemanticBackup.Infrastructure/BackgroundJobs/BackupBackgroundJob.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SemanticBackup.Core;
using SemanticBackup.Core.Interfaces;
Expand All @@ -17,15 +16,38 @@ public class BackupBackgroundJob : IHostedService
{
private readonly ILogger<BackupBackgroundJob> _logger;
private readonly SystemConfigOptions _persistanceOptions;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly BotsManagerBackgroundJob _botsManagerBackgroundJob;

public BackupBackgroundJob(ILogger<BackupBackgroundJob> logger, SystemConfigOptions persistanceOptions, IServiceScopeFactory serviceScopeFactory, BotsManagerBackgroundJob botsManagerBackgroundJob)
private readonly IBackupProviderForSQLServer _providerForSQLServer;
private readonly IBackupProviderForMySQLServer _providerForMySqlServer;

private readonly IResourceGroupRepository _resourceGroupRepository;
private readonly IBackupRecordRepository _backupRecordRepository;
private readonly IContentDeliveryRecordRepository _deliveryRecordRepository;
private readonly IDatabaseInfoRepository _databaseInfoRepository;

public BackupBackgroundJob(
ILogger<BackupBackgroundJob> logger,
SystemConfigOptions persistanceOptions,
BotsManagerBackgroundJob botsManagerBackgroundJob,
IBackupProviderForSQLServer providerForSQLServer,
IBackupProviderForMySQLServer providerForMySqlServer,

IResourceGroupRepository resourceGroupRepository,
IBackupRecordRepository backupRecordRepository,
IContentDeliveryRecordRepository contentDeliveryRecordRepository,
IDatabaseInfoRepository databaseInfoRepository
)
{
_logger = logger;
_persistanceOptions = persistanceOptions;
_serviceScopeFactory = serviceScopeFactory;
_botsManagerBackgroundJob = botsManagerBackgroundJob;
_providerForSQLServer = providerForSQLServer;
_providerForMySqlServer = providerForMySqlServer;
_resourceGroupRepository = resourceGroupRepository;
_backupRecordRepository = backupRecordRepository;
_deliveryRecordRepository = contentDeliveryRecordRepository;
_databaseInfoRepository = databaseInfoRepository;
}

public Task StartAsync(CancellationToken cancellationToken)
Expand All @@ -48,23 +70,18 @@ private void SetupBackgroundService(CancellationToken cancellationToken)
while (!cancellationToken.IsCancellationRequested)
{
//Await
await Task.Delay(5000, cancellationToken);
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
try
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
//DI Injections
IBackupRecordRepository backupRecordPersistanceService = scope.ServiceProvider.GetRequiredService<IBackupRecordRepository>();
IDatabaseInfoRepository databaseInfoPersistanceService = scope.ServiceProvider.GetRequiredService<IDatabaseInfoRepository>();
IResourceGroupRepository resourceGroupPersistanceService = scope.ServiceProvider.GetRequiredService<IResourceGroupRepository>();
//Proceed
List<BackupRecord> queuedBackups = await backupRecordPersistanceService.GetAllByStatusAsync(BackupRecordStatus.QUEUED.ToString());
List<BackupRecord> queuedBackups = await _backupRecordRepository.GetAllByStatusAsync(BackupRecordStatus.QUEUED.ToString());
if (queuedBackups != null && queuedBackups.Count > 0)
{
List<long> scheduleToDelete = [];
foreach (BackupRecord backupRecord in queuedBackups.OrderBy(x => x.RegisteredDateUTC).ToList())
{
_logger.LogInformation($"Processing Queued Backup Record Key: #{backupRecord.Id}...");
BackupDatabaseInfo backupDatabaseInfo = await databaseInfoPersistanceService.GetByIdAsync(backupRecord.BackupDatabaseInfoId);
BackupDatabaseInfo backupDatabaseInfo = await _databaseInfoRepository.GetByIdAsync(backupRecord.BackupDatabaseInfoId);
if (backupDatabaseInfo == null)
{
_logger.LogWarning($"No Database Info matches with Id: {backupRecord.BackupDatabaseInfoId}, Backup Database Record will be Deleted: {backupRecord.Id}");
Expand All @@ -73,7 +90,7 @@ private void SetupBackgroundService(CancellationToken cancellationToken)
else
{
//Check if valid Resource Group
ResourceGroup resourceGroup = await resourceGroupPersistanceService.GetByIdOrKeyAsync(backupDatabaseInfo.ResourceGroupId);
ResourceGroup resourceGroup = await _resourceGroupRepository.GetByIdOrKeyAsync(backupDatabaseInfo.ResourceGroupId);
if (resourceGroup == null)
{
_logger.LogWarning($"The Database Id: {backupRecord.BackupDatabaseInfoId}, doesn't seem to have been assigned to a valid Resource Group Id: {backupDatabaseInfo.ResourceGroupId}, Record will be Deleted");
Expand All @@ -84,13 +101,13 @@ private void SetupBackgroundService(CancellationToken cancellationToken)
if (_botsManagerBackgroundJob.HasAvailableResourceGroupBotsCount(resourceGroup.Id, resourceGroup.MaximumRunningBots))
{
if (resourceGroup.DbType.Contains("SQLSERVER"))
_botsManagerBackgroundJob.AddBot(new SQLBackupBot(backupDatabaseInfo.DatabaseName, resourceGroup, backupRecord, _serviceScopeFactory));
_botsManagerBackgroundJob.AddBot(new SQLBackupBot(backupDatabaseInfo.DatabaseName, resourceGroup, backupRecord, _providerForSQLServer));
else if (resourceGroup.DbType.Contains("MYSQL") || resourceGroup.DbType.Contains("MARIADB"))
_botsManagerBackgroundJob.AddBot(new MySQLBackupBot(backupDatabaseInfo.DatabaseName, resourceGroup, backupRecord, _serviceScopeFactory));
_botsManagerBackgroundJob.AddBot(new MySQLBackupBot(backupDatabaseInfo.DatabaseName, resourceGroup, backupRecord, _providerForMySqlServer));
else
throw new Exception($"No Bot is registered to Handle Database Backups of Type: {resourceGroup.DbType}");
//Finally Update Status
bool updated = await backupRecordPersistanceService.UpdateStatusFeedAsync(backupRecord.Id, BackupRecordStatus.EXECUTING.ToString());
bool updated = await _backupRecordRepository.UpdateStatusFeedAsync(backupRecord.Id, BackupRecordStatus.EXECUTING.ToString());
if (updated)
_logger.LogInformation($"Processing Queued Backup Record Key: #{backupRecord.Id}...SUCCESS");
else
Expand All @@ -103,9 +120,8 @@ private void SetupBackgroundService(CancellationToken cancellationToken)
}
}
//Check if Any Delete
if (scheduleToDelete.Count > 0)
foreach (var rm in scheduleToDelete)
await backupRecordPersistanceService.RemoveAsync(rm);
foreach (var rm in scheduleToDelete)
await _backupRecordRepository.RemoveWithFileAsync(rm);
}

}
Expand All @@ -124,26 +140,21 @@ private void SetupBackgroundRemovedExpiredBackupsService(CancellationToken cance
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(3000); //Runs After 3sec
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
try
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
//DI Injections
IBackupRecordRepository backupRecordPersistanceService = scope.ServiceProvider.GetRequiredService<IBackupRecordRepository>();
//Proceed
List<BackupRecord> expiredBackups = await backupRecordPersistanceService.GetAllExpiredAsync();
if (expiredBackups != null && expiredBackups.Count > 0)
List<BackupRecord> expiredBackups = (await _backupRecordRepository.GetAllExpiredAsync()) ?? [];
//proceed
foreach (BackupRecord rm in expiredBackups.Take(50).ToList())
{
foreach (BackupRecord rm in expiredBackups.Take(50).ToList())
if (!await backupRecordPersistanceService.RemoveAsync(rm.Id))
_logger.LogWarning($"Unable to delete Expired Backup Record: {rm.Id}");
else
{
_logger.LogInformation($"Removed Expired Backup Record, Id: {rm.Id}");
//Options InDepth Delete
if (_persistanceOptions.InDepthBackupRecordDeleteEnabled)
await StartInDepthDeleteForAsync(rm);
}
//get relation
List<BackupRecordDelivery> rmBackupRecords = (await _deliveryRecordRepository.GetAllByBackupRecordIdAsync(rm.Id)) ?? [];
//remove with file
await _backupRecordRepository.RemoveWithFileAsync(rm.Id);
//Options InDepth Delete
if (_persistanceOptions.InDepthBackupRecordDeleteEnabled)
await StartInDepthDeleteForAsync(rm, rmBackupRecords);
}
}
catch (Exception ex)
Expand All @@ -155,42 +166,35 @@ private void SetupBackgroundRemovedExpiredBackupsService(CancellationToken cance
t.Start();
}

private async Task StartInDepthDeleteForAsync(BackupRecord rm)
private async Task StartInDepthDeleteForAsync(BackupRecord backupRecord, List<BackupRecordDelivery> rmBackupRecords)
{
try
{
if (rm == null) return;
//scope
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IResourceGroupRepository resourceGroupPersistanceService = scope.ServiceProvider.GetRequiredService<IResourceGroupRepository>();
IContentDeliveryRecordRepository contentDeliveryRecordsService = scope.ServiceProvider.GetRequiredService<IContentDeliveryRecordRepository>();
BotsManagerBackgroundJob botsManagerBackgroundJob = scope.ServiceProvider.GetRequiredService<BotsManagerBackgroundJob>();
IDatabaseInfoRepository databaseInfoRepository = scope.ServiceProvider.GetRequiredService<IDatabaseInfoRepository>();
if (backupRecord == null) return;
//get db information
BackupDatabaseInfo backupRecordDbInfo = await databaseInfoRepository.GetByIdAsync(rm.BackupDatabaseInfoId);
BackupDatabaseInfo backupRecordDbInfo = await _databaseInfoRepository.GetByIdAsync(backupRecord.BackupDatabaseInfoId);
//Check if valid Resource Group
ResourceGroup resourceGroup = await resourceGroupPersistanceService.GetByIdOrKeyAsync(backupRecordDbInfo?.ResourceGroupId ?? string.Empty);
ResourceGroup resourceGroup = await _resourceGroupRepository.GetByIdOrKeyAsync(backupRecordDbInfo?.ResourceGroupId ?? string.Empty);
if (resourceGroup == null)
return;
//Proceed
List<BackupRecordDelivery> dbRecords = await contentDeliveryRecordsService.GetAllByBackupRecordIdAsync(rm.Id); //database record content delivery
if (dbRecords == null)
if (rmBackupRecords == null)
return;
List<string> supportedInDepthDelete = [BackupDeliveryConfigTypes.Dropbox.ToString(), BackupDeliveryConfigTypes.AzureBlobStorage.ToString()];
List<BackupRecordDelivery> supportedDeliveryRecords = [.. dbRecords.Where(x => supportedInDepthDelete.Contains(x.DeliveryType))];
List<BackupRecordDelivery> supportedDeliveryRecords = [.. rmBackupRecords.Where(x => supportedInDepthDelete.Contains(x.DeliveryType))];
if (supportedDeliveryRecords == null || supportedDeliveryRecords.Count == 0)
return;
foreach (BackupRecordDelivery rec in supportedDeliveryRecords)
foreach (BackupRecordDelivery deliveryRecord in supportedDeliveryRecords)
{
if (rec.DeliveryType == BackupDeliveryConfigTypes.Dropbox.ToString())
if (deliveryRecord.DeliveryType == BackupDeliveryConfigTypes.Dropbox.ToString())
{
//In Depth Remove From DropBox
botsManagerBackgroundJob.AddBot(new InDepthDeleteDropboxBot(resourceGroup, rm, rec, _serviceScopeFactory));
_botsManagerBackgroundJob.AddBot(new InDepthDeleteDropboxBot(resourceGroup, backupRecord, deliveryRecord));
}
else if (rec.DeliveryType == BackupDeliveryConfigTypes.ObjectStorage.ToString())
else if (deliveryRecord.DeliveryType == BackupDeliveryConfigTypes.ObjectStorage.ToString())
{
//In Depth Remove From Object Storage
botsManagerBackgroundJob.AddBot(new InDepthDeleteObjectStorageBot(resourceGroup, rm, rec, _serviceScopeFactory));
_botsManagerBackgroundJob.AddBot(new InDepthDeleteObjectStorageBot(resourceGroup, backupRecord, deliveryRecord));
}
}
}
Expand Down
Loading
Loading