mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 15:53:30 -07:00
Improve concurrency handling in ExportAssetDownloader (#1002)
Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com>
This commit is contained in:
parent
2c67e502e4
commit
ea4cb43479
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using AngleSharp.Html.Dom;
|
using AngleSharp.Html.Dom;
|
||||||
|
using AsyncKeyedLock;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using DiscordChatExporter.Cli.Commands;
|
using DiscordChatExporter.Cli.Commands;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
using DiscordChatExporter.Cli.Tests.Utils;
|
||||||
|
|
@ -20,7 +21,11 @@ namespace DiscordChatExporter.Cli.Tests.Infra;
|
||||||
|
|
||||||
public static class ExportWrapper
|
public static class ExportWrapper
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new(StringComparer.Ordinal);
|
private static readonly AsyncKeyedLocker<string> Locker = new(o =>
|
||||||
|
{
|
||||||
|
o.PoolSize = 20;
|
||||||
|
o.PoolInitialFill = 1;
|
||||||
|
});
|
||||||
|
|
||||||
private static readonly string DirPath = Path.Combine(
|
private static readonly string DirPath = Path.Combine(
|
||||||
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(),
|
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Directory.GetCurrentDirectory(),
|
||||||
|
|
@ -46,10 +51,7 @@ public static class ExportWrapper
|
||||||
var filePath = Path.Combine(DirPath, fileName);
|
var filePath = Path.Combine(DirPath, fileName);
|
||||||
|
|
||||||
// Lock separately for each channel and format
|
// Lock separately for each channel and format
|
||||||
var channelLock = Locks.GetOrAdd(filePath, _ => new SemaphoreSlim(1, 1));
|
using (await Locker.LockAsync(filePath))
|
||||||
await channelLock.WaitAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
// Perform export only if it hasn't been done before
|
// Perform export only if it hasn't been done before
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
|
|
@ -65,10 +67,6 @@ public static class ExportWrapper
|
||||||
|
|
||||||
return await File.ReadAllTextAsync(filePath);
|
return await File.ReadAllTextAsync(filePath);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
channelLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async ValueTask<IHtmlDocument> ExportAsHtmlAsync(Snowflake channelId) => Html.Parse(
|
public static async ValueTask<IHtmlDocument> ExportAsHtmlAsync(Snowflake channelId) => Html.Parse(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AsyncKeyedLock" Version="6.1.1" />
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||||
<PackageReference Include="Polly" Version="7.2.3" />
|
<PackageReference Include="Polly" Version="7.2.3" />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AsyncKeyedLock;
|
||||||
using DiscordChatExporter.Core.Utils;
|
using DiscordChatExporter.Core.Utils;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
|
|
@ -14,6 +15,11 @@ namespace DiscordChatExporter.Core.Exporting;
|
||||||
|
|
||||||
internal partial class ExportAssetDownloader
|
internal partial class ExportAssetDownloader
|
||||||
{
|
{
|
||||||
|
private static readonly AsyncKeyedLocker<string> _locker = new(o =>
|
||||||
|
{
|
||||||
|
o.PoolSize = 20;
|
||||||
|
o.PoolInitialFill = 1;
|
||||||
|
});
|
||||||
private readonly string _workingDirPath;
|
private readonly string _workingDirPath;
|
||||||
private readonly bool _reuse;
|
private readonly bool _reuse;
|
||||||
|
|
||||||
|
|
@ -28,12 +34,14 @@ internal partial class ExportAssetDownloader
|
||||||
|
|
||||||
public async ValueTask<string> DownloadAsync(string url, CancellationToken cancellationToken = default)
|
public async ValueTask<string> DownloadAsync(string url, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_pathCache.TryGetValue(url, out var cachedFilePath))
|
|
||||||
return cachedFilePath;
|
|
||||||
|
|
||||||
var fileName = GetFileNameFromUrl(url);
|
var fileName = GetFileNameFromUrl(url);
|
||||||
var filePath = Path.Combine(_workingDirPath, fileName);
|
var filePath = Path.Combine(_workingDirPath, fileName);
|
||||||
|
|
||||||
|
using (await _locker.LockAsync(filePath, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (_pathCache.TryGetValue(url, out var cachedFilePath))
|
||||||
|
return cachedFilePath;
|
||||||
|
|
||||||
// Reuse existing files if we're allowed to
|
// Reuse existing files if we're allowed to
|
||||||
if (!_reuse || !File.Exists(filePath))
|
if (!_reuse || !File.Exists(filePath))
|
||||||
{
|
{
|
||||||
|
|
@ -75,6 +83,7 @@ internal partial class ExportAssetDownloader
|
||||||
return _pathCache[url] = filePath;
|
return _pathCache[url] = filePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal partial class ExportAssetDownloader
|
internal partial class ExportAssetDownloader
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue