6 changed files with 460 additions and 1 deletions
@ -0,0 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
public interface IStaticWebhookSaver |
||||
|
{ |
||||
|
Task SaveAsync(); |
||||
|
} |
||||
@ -0,0 +1,296 @@ |
|||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.Threading; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
public class StaticWebhookSaver : IStaticWebhookSaver, ITransientDependency |
||||
|
{ |
||||
|
protected IStaticWebhookDefinitionStore StaticStore { get; } |
||||
|
protected IWebhookGroupDefinitionRecordRepository WebhookGroupRepository { get; } |
||||
|
protected IWebhookDefinitionRecordRepository WebhookRepository { get; } |
||||
|
protected IWebhookDefinitionSerializer WebhookSerializer { get; } |
||||
|
protected IDistributedCache Cache { get; } |
||||
|
protected IApplicationInfoAccessor ApplicationInfoAccessor { get; } |
||||
|
protected IAbpDistributedLock DistributedLock { get; } |
||||
|
protected AbpWebhooksOptions WebhookOptions { get; } |
||||
|
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
||||
|
protected AbpDistributedCacheOptions CacheOptions { get; } |
||||
|
|
||||
|
public StaticWebhookSaver( |
||||
|
IStaticWebhookDefinitionStore staticStore, |
||||
|
IWebhookGroupDefinitionRecordRepository webhookGroupRepository, |
||||
|
IWebhookDefinitionRecordRepository webhookRepository, |
||||
|
IWebhookDefinitionSerializer webhookSerializer, |
||||
|
IDistributedCache cache, |
||||
|
IOptions<AbpDistributedCacheOptions> cacheOptions, |
||||
|
IApplicationInfoAccessor applicationInfoAccessor, |
||||
|
IAbpDistributedLock distributedLock, |
||||
|
IOptions<AbpWebhooksOptions> webhookOptions, |
||||
|
ICancellationTokenProvider cancellationTokenProvider) |
||||
|
{ |
||||
|
StaticStore = staticStore; |
||||
|
WebhookGroupRepository = webhookGroupRepository; |
||||
|
WebhookRepository = webhookRepository; |
||||
|
WebhookSerializer = webhookSerializer; |
||||
|
Cache = cache; |
||||
|
ApplicationInfoAccessor = applicationInfoAccessor; |
||||
|
DistributedLock = distributedLock; |
||||
|
CancellationTokenProvider = cancellationTokenProvider; |
||||
|
WebhookOptions = webhookOptions.Value; |
||||
|
CacheOptions = cacheOptions.Value; |
||||
|
} |
||||
|
|
||||
|
[UnitOfWork] |
||||
|
public virtual async Task SaveAsync() |
||||
|
{ |
||||
|
await using var applicationLockHandle = await DistributedLock.TryAcquireAsync( |
||||
|
GetApplicationDistributedLockKey() |
||||
|
); |
||||
|
|
||||
|
if (applicationLockHandle == null) |
||||
|
{ |
||||
|
/* Another application instance is already doing it */ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
/* NOTE: This can be further optimized by using 4 cache values for: |
||||
|
* Groups, webhooks, deleted groups and deleted webhooks. |
||||
|
* But the code would be more complex. This is enough for now. |
||||
|
*/ |
||||
|
|
||||
|
var cacheKey = GetApplicationHashCacheKey(); |
||||
|
var cachedHash = await Cache.GetStringAsync(cacheKey, CancellationTokenProvider.Token); |
||||
|
|
||||
|
var (webhookGroupRecords, webhookRecords) = await WebhookSerializer.SerializeAsync( |
||||
|
await StaticStore.GetGroupsAsync() |
||||
|
); |
||||
|
|
||||
|
var currentHash = CalculateHash( |
||||
|
webhookGroupRecords, |
||||
|
webhookRecords, |
||||
|
WebhookOptions.DeletedWebhookGroups, |
||||
|
WebhookOptions.DeletedWebhooks |
||||
|
); |
||||
|
|
||||
|
if (cachedHash == currentHash) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await using (var commonLockHandle = await DistributedLock.TryAcquireAsync( |
||||
|
GetCommonDistributedLockKey(), |
||||
|
TimeSpan.FromMinutes(5))) |
||||
|
{ |
||||
|
if (commonLockHandle == null) |
||||
|
{ |
||||
|
/* It will re-try */ |
||||
|
throw new AbpException("Could not acquire distributed lock for saving static webhooks!"); |
||||
|
} |
||||
|
|
||||
|
var hasChangesInGroups = await UpdateChangedWebhookGroupsAsync(webhookGroupRecords); |
||||
|
var hasChangesInWebhooks = await UpdateChangedWebhooksAsync(webhookRecords); |
||||
|
|
||||
|
if (hasChangesInGroups || hasChangesInWebhooks) |
||||
|
{ |
||||
|
await Cache.SetStringAsync( |
||||
|
GetCommonStampCacheKey(), |
||||
|
Guid.NewGuid().ToString(), |
||||
|
new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable?
|
||||
|
}, |
||||
|
CancellationTokenProvider.Token |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await Cache.SetStringAsync( |
||||
|
cacheKey, |
||||
|
currentHash, |
||||
|
new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable?
|
||||
|
}, |
||||
|
CancellationTokenProvider.Token |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private async Task<bool> UpdateChangedWebhookGroupsAsync( |
||||
|
IEnumerable<WebhookGroupDefinitionRecord> webhookGroupRecords) |
||||
|
{ |
||||
|
var newRecords = new List<WebhookGroupDefinitionRecord>(); |
||||
|
var changedRecords = new List<WebhookGroupDefinitionRecord>(); |
||||
|
|
||||
|
var webhookGroupRecordsInDatabase = (await WebhookGroupRepository.GetListAsync()) |
||||
|
.ToDictionary(x => x.Name); |
||||
|
|
||||
|
foreach (var webhookGroupRecord in webhookGroupRecords) |
||||
|
{ |
||||
|
var webhookGroupRecordInDatabase = webhookGroupRecordsInDatabase.GetOrDefault(webhookGroupRecord.Name); |
||||
|
if (webhookGroupRecordInDatabase == null) |
||||
|
{ |
||||
|
/* New group */ |
||||
|
newRecords.Add(webhookGroupRecord); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (webhookGroupRecord.HasSameData(webhookGroupRecordInDatabase)) |
||||
|
{ |
||||
|
/* Not changed */ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
/* Changed */ |
||||
|
webhookGroupRecordInDatabase.Patch(webhookGroupRecord); |
||||
|
changedRecords.Add(webhookGroupRecordInDatabase); |
||||
|
} |
||||
|
|
||||
|
/* Deleted */ |
||||
|
var deletedRecords = WebhookOptions.DeletedWebhookGroups.Any() |
||||
|
? webhookGroupRecordsInDatabase.Values |
||||
|
.Where(x => WebhookOptions.DeletedWebhookGroups.Contains(x.Name)) |
||||
|
.ToArray() |
||||
|
: Array.Empty<WebhookGroupDefinitionRecord>(); |
||||
|
|
||||
|
if (newRecords.Any()) |
||||
|
{ |
||||
|
await WebhookGroupRepository.InsertManyAsync(newRecords); |
||||
|
} |
||||
|
|
||||
|
if (changedRecords.Any()) |
||||
|
{ |
||||
|
await WebhookGroupRepository.UpdateManyAsync(changedRecords); |
||||
|
} |
||||
|
|
||||
|
if (deletedRecords.Any()) |
||||
|
{ |
||||
|
await WebhookGroupRepository.DeleteManyAsync(deletedRecords); |
||||
|
} |
||||
|
|
||||
|
return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); |
||||
|
} |
||||
|
|
||||
|
private async Task<bool> UpdateChangedWebhooksAsync( |
||||
|
IEnumerable<WebhookDefinitionRecord> webhookRecords) |
||||
|
{ |
||||
|
var newRecords = new List<WebhookDefinitionRecord>(); |
||||
|
var changedRecords = new List<WebhookDefinitionRecord>(); |
||||
|
|
||||
|
var webhookRecordsInDatabase = (await WebhookRepository.GetListAsync()) |
||||
|
.ToDictionary(x => x.Name); |
||||
|
|
||||
|
foreach (var webhookRecord in webhookRecords) |
||||
|
{ |
||||
|
var webhookRecordInDatabase = webhookRecordsInDatabase.GetOrDefault(webhookRecord.Name); |
||||
|
if (webhookRecordInDatabase == null) |
||||
|
{ |
||||
|
/* New group */ |
||||
|
newRecords.Add(webhookRecord); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (webhookRecord.HasSameData(webhookRecordInDatabase)) |
||||
|
{ |
||||
|
/* Not changed */ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
/* Changed */ |
||||
|
webhookRecordInDatabase.Patch(webhookRecord); |
||||
|
changedRecords.Add(webhookRecordInDatabase); |
||||
|
} |
||||
|
|
||||
|
/* Deleted */ |
||||
|
var deletedRecords = new List<WebhookDefinitionRecord>(); |
||||
|
|
||||
|
if (WebhookOptions.DeletedWebhooks.Any()) |
||||
|
{ |
||||
|
deletedRecords.AddRange( |
||||
|
webhookRecordsInDatabase.Values |
||||
|
.Where(x => WebhookOptions.DeletedWebhooks.Contains(x.Name)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (WebhookOptions.DeletedWebhookGroups.Any()) |
||||
|
{ |
||||
|
deletedRecords.AddIfNotContains( |
||||
|
webhookRecordsInDatabase.Values |
||||
|
.Where(x => WebhookOptions.DeletedWebhookGroups.Contains(x.GroupName)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (newRecords.Any()) |
||||
|
{ |
||||
|
await WebhookRepository.InsertManyAsync(newRecords); |
||||
|
} |
||||
|
|
||||
|
if (changedRecords.Any()) |
||||
|
{ |
||||
|
await WebhookRepository.UpdateManyAsync(changedRecords); |
||||
|
} |
||||
|
|
||||
|
if (deletedRecords.Any()) |
||||
|
{ |
||||
|
await WebhookRepository.DeleteManyAsync(deletedRecords); |
||||
|
} |
||||
|
|
||||
|
return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); |
||||
|
} |
||||
|
|
||||
|
private string GetApplicationDistributedLockKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWebhookUpdateLock"; |
||||
|
} |
||||
|
|
||||
|
private string GetCommonDistributedLockKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_Common_AbpWebhookUpdateLock"; |
||||
|
} |
||||
|
|
||||
|
private string GetApplicationHashCacheKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_{ApplicationInfoAccessor.ApplicationName}_AbpWebhooksHash"; |
||||
|
} |
||||
|
|
||||
|
private string GetCommonStampCacheKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_AbpInMemoryWebhookCacheStamp"; |
||||
|
} |
||||
|
|
||||
|
private static string CalculateHash( |
||||
|
WebhookGroupDefinitionRecord[] webhookGroupRecords, |
||||
|
WebhookDefinitionRecord[] webhookRecords, |
||||
|
IEnumerable<string> deletedWebhookGroups, |
||||
|
IEnumerable<string> deletedWebhooks) |
||||
|
{ |
||||
|
var stringBuilder = new StringBuilder(); |
||||
|
|
||||
|
stringBuilder.Append("WebhookGroupRecords:"); |
||||
|
stringBuilder.AppendLine(JsonSerializer.Serialize(webhookGroupRecords)); |
||||
|
|
||||
|
stringBuilder.Append("WebhookRecords:"); |
||||
|
stringBuilder.AppendLine(JsonSerializer.Serialize(webhookRecords)); |
||||
|
|
||||
|
stringBuilder.Append("DeletedWebhookGroups:"); |
||||
|
stringBuilder.AppendLine(deletedWebhookGroups.JoinAsString(",")); |
||||
|
|
||||
|
stringBuilder.Append("DeletedWebhook:"); |
||||
|
stringBuilder.Append(deletedWebhooks.JoinAsString(",")); |
||||
|
|
||||
|
return stringBuilder |
||||
|
.ToString() |
||||
|
.ToMd5(); |
||||
|
} |
||||
|
} |
||||
@ -1,10 +1,12 @@ |
|||||
namespace LINGYUN.Abp.WebhooksManagement; |
namespace LINGYUN.Abp.WebhooksManagement; |
||||
public class WebhookManagementOptions |
public class WebhookManagementOptions |
||||
{ |
{ |
||||
|
public bool SaveStaticWebhooksToDatabase { get; set; } |
||||
public bool IsDynamicWebhookStoreEnabled { get; set; } |
public bool IsDynamicWebhookStoreEnabled { get; set; } |
||||
|
|
||||
public WebhookManagementOptions() |
public WebhookManagementOptions() |
||||
{ |
{ |
||||
IsDynamicWebhookStoreEnabled = true; |
IsDynamicWebhookStoreEnabled = true; |
||||
|
SaveStaticWebhooksToDatabase = true; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue