Skip to content

Commit

Permalink
Update versions.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nihlus committed Sep 21, 2024
1 parent 5ada687 commit d81f325
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 120 deletions.
2 changes: 1 addition & 1 deletion Remora.Discord.Extensions/Remora.Discord.Extensions.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Remora.Sdk">

<PropertyGroup>
<VersionPrefix>5.3.5</VersionPrefix>
<VersionPrefix>5.3.6</VersionPrefix>
<Description>Utilities and components which extend upon Remora.Discord's base resources</Description>
<PackageReleaseNotes>
Update dependencies.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Remora.Sdk">

<PropertyGroup>
<VersionPrefix>4.5.4</VersionPrefix>
<VersionPrefix>5.0.0</VersionPrefix>
<Description>Framework for using Discord's interaction-driven message components</Description>
<PackageReleaseNotes>
Update dependencies.
BREAKING: Rework deletion logic for data leases to prevent deadlocks.
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
39 changes: 16 additions & 23 deletions Remora.Discord.Interactivity/Services/DataLease.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ namespace Remora.Discord.Interactivity.Services;
public class DataLease<TKey, TData> : IAsyncDisposable where TKey : notnull
{
private readonly InMemoryDataService<TKey, TData> _dataService;
private readonly TKey _key;
private readonly SemaphoreSlim _semaphore;

private bool _shouldDelete;
private bool _isDisposed;
private TData _data;

/// <summary>
/// Gets the key associated with the lease.
/// </summary>
public TKey Key { get; }

/// <summary>
/// Gets or sets the data associated with the lease.
/// </summary>
Expand Down Expand Up @@ -73,9 +77,10 @@ public TData Data
internal DataLease(InMemoryDataService<TKey, TData> dataService, TKey key, SemaphoreSlim semaphore, TData data)
{
_dataService = dataService;
_key = key;
_semaphore = semaphore;
_data = data;

this.Key = key;
}

/// <summary>
Expand All @@ -94,34 +99,22 @@ public async ValueTask DisposeAsync()
return;
}

_isDisposed = true;

GC.SuppressFinalize(this);

if (_shouldDelete)
try
{
var couldDelete = await _dataService.DeleteDataAsync(_key);
if (couldDelete)
if (_shouldDelete)
{
_ = await _dataService.TryDeleteDataAsync(this);
return;
}

// manual cleanup, someone must have deleted the data while we were using it
if (_data is IAsyncDisposable asyncDisposableData)
{
await asyncDisposableData.DisposeAsync();
}

if (_data is IDisposable disposableData)
{
disposableData.Dispose();
}

_semaphore.Dispose();
return;
_dataService.UpdateData(this.Key, _data);
}
finally
{
_isDisposed = true;
_semaphore.Release();
}

_dataService.UpdateData(_key, _data);
_semaphore.Release();
}
}
122 changes: 46 additions & 76 deletions Remora.Discord.Interactivity/Services/InMemoryDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,126 +100,92 @@ public async Task<Result<DataLease<TKey, TData>>> LeaseDataAsync(TKey key, Cance
var (semaphore, data) = tuple;
await semaphore.WaitAsync(ct);

return new DataLease<TKey, TData>(this, key, semaphore, data);
}

/// <summary>
/// Rents the data associated with the given key, blocking until a lock can be taken on the data object.
/// </summary>
/// <remarks>
/// The semaphore returned by this method has the lock held on it and must be released once the caller is done with
/// the object.
/// </remarks>
/// <param name="key">The key the data object is associated with.</param>
/// <param name="ct">The cancellation token for this operation.</param>
/// <returns>The data and semaphore associated with the data or a <see cref="NotFoundError"/>.</returns>
[Obsolete("Use LeaseDataAsync instead.", true)]
public async Task<Result<(SemaphoreSlim Semaphore, TData Data)>> RentDataAsync
(
TKey key,
CancellationToken ct = default
)
{
if (_isDisposed)
{
throw new ObjectDisposedException("The data service has been disposed of and cannot be used.");
}

if (!_data.TryGetValue(key, out var tuple))
// may have been deleted while we were waiting - check first
if (_data.ContainsKey(key))
{
return new NotFoundError();
return new DataLease<TKey, TData>(this, key, semaphore, data);
}

var (semaphore, _) = tuple;
await semaphore.WaitAsync(ct);

return tuple;
semaphore.Release();
return new NotFoundError();
}

/// <summary>
/// Removes the data associated with the given key. This method does nothing if no data is associated with
/// the given key.
/// Deletes the data associated with the given lease.
/// </summary>
/// <param name="key">The key the data object is associated with.</param>
/// <param name="lease">The lease you have on the data object.</param>
/// <returns>true if the data was successfully removed; otherwise, false.</returns>
public bool TryRemoveData(TKey key)
public bool TryDeleteData(DataLease<TKey, TData> lease)
{
if (_isDisposed)
{
throw new ObjectDisposedException("The data service has been disposed of and cannot be used.");
}

if (!_data.TryRemove(key, out var tuple))
if (!_data.TryRemove(lease.Key, out _))
{
// already removed
return false;
}

var (semaphore, data) = tuple;

if (data is IAsyncDisposable and not IDisposable)
if (lease.Data is IAsyncDisposable and not IDisposable)
{
throw new InvalidOperationException
(
$"Unable to synchronously dispose of the held data belonging to key {key}."
$"Unable to synchronously dispose of the held data belonging to key {lease.Key}."
);
}

if (data is IDisposable disposableData)
{
disposableData.Dispose();
}

if (data is IAsyncDisposable and not IDisposable)
if (lease.Data is IDisposable disposable)
{
throw new InvalidOperationException
(
$"Unable to synchronously dispose of the held data belonging to key {key}."
);
disposable.Dispose();
}

semaphore.Dispose();
return true;
}

/// <summary>
/// Removes the data associated with the given key. This method does nothing if no data is associated with
/// the given key.
/// Deletes the data associated with the given lease.
/// </summary>
/// <param name="key">The key the data object is associated with.</param>
/// <param name="lease">The lease you have on the data object.</param>
/// <returns>true if the data was successfully removed; otherwise, false.</returns>
public async ValueTask<bool> TryRemoveDataAsync(TKey key)
public async Task<bool> TryDeleteDataAsync(DataLease<TKey, TData> lease)
{
if (_isDisposed)
{
throw new ObjectDisposedException("The data service has been disposed of and cannot be used.");
}

if (!_data.TryRemove(key, out var tuple))
if (!_data.TryRemove(lease.Key, out _))
{
// already removed
return false;
}

var (semaphore, data) = tuple;

if (data is IAsyncDisposable asyncDisposableData)
{
await asyncDisposableData.DisposeAsync();
}

if (data is IDisposable disposableData)
switch (lease.Data)
{
disposableData.Dispose();
// preferentially use the asynchronous disposal logic
case IAsyncDisposable asyncDisposable:
{
await asyncDisposable.DisposeAsync();
break;
}
case IDisposable disposable:
{
disposable.Dispose();
break;
}
}

semaphore.Dispose();
return true;
}

/// <summary>
/// Deletes the data, disposing of the semaphore and, if necessary, the data.
/// Deletes the data associated with the key, disposing of the data if necessary. A lock is acquired before the data
/// is disposed.
/// </summary>
/// <param name="key">The key the data object is associated with.</param>
/// <returns>true if the data was successfully removed; otherwise, false..</returns>
/// <returns>true if the data was successfully removed; otherwise, false.</returns>
internal async ValueTask<bool> DeleteDataAsync(TKey key)
{
if (_isDisposed)
Expand All @@ -229,22 +195,26 @@ internal async ValueTask<bool> DeleteDataAsync(TKey key)

if (!_data.TryRemove(key, out var tuple))
{
// already gone
return false;
}

var (semaphore, data) = tuple;
if (data is IAsyncDisposable asyncDisposableData)
{
await asyncDisposableData.DisposeAsync();
}
await semaphore.WaitAsync();

if (data is IDisposable disposableData)
switch (data)
{
disposableData.Dispose();
case IAsyncDisposable asyncDisposableData:
{
await asyncDisposableData.DisposeAsync();
break;
}
case IDisposable disposableData:
{
disposableData.Dispose();
break;
}
}

semaphore.Dispose();
return true;
}

Expand Down
5 changes: 1 addition & 4 deletions Remora.Discord.Pagination/Remora.Discord.Pagination.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
<Project Sdk="Remora.Sdk">

<PropertyGroup>
<VersionPrefix>4.0.0</VersionPrefix>
<VersionPrefix>4.0.1</VersionPrefix>
<Description>Button-based pagination of messages for Remora.Discord</Description>
<PackageReleaseNotes>
Update dependencies.
BREAKING: Change help text in pagination to a full embed.
Make help text embeds ephemeral by default.
Fix that the initial message in the pagination doesn't use the footer format.
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
19 changes: 17 additions & 2 deletions Remora.Discord.Pagination/Responders/MessageDeletedResponder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ public MessageDeletedResponder(InMemoryDataService<Snowflake, PaginatedMessageDa
/// <inheritdoc />
public async Task<Result> RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default)
{
_ = await _paginationData.TryRemoveDataAsync(gatewayEvent.ID);
var getLease = await _paginationData.LeaseDataAsync(gatewayEvent.ID, ct);
if (!getLease.IsSuccess)
{
return Result.FromSuccess();
}

await using var lease = getLease.Entity;
lease.Delete();

return Result.FromSuccess();
}

Expand All @@ -58,7 +66,14 @@ public async Task<Result> RespondAsync(IMessageDeleteBulk gatewayEvent, Cancella
{
foreach (var id in gatewayEvent.IDs)
{
_ = await _paginationData.TryRemoveDataAsync(id);
var getLease = await _paginationData.LeaseDataAsync(id, ct);
if (!getLease.IsSuccess)
{
continue;
}

await using var lease = getLease.Entity;
lease.Delete();
}

return Result.FromSuccess();
Expand Down
14 changes: 2 additions & 12 deletions Remora.Discord/Remora.Discord.csproj
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
<Project Sdk="Remora.Sdk">

<PropertyGroup>
<VersionPrefix>2024.2</VersionPrefix>
<VersionPrefix>2024.3</VersionPrefix>
<Description>Metapackage for Remora.Discord's various components</Description>
<PackageReleaseNotes>
Update dependencies.
BREAKING: Change help text in pagination to a full embed.
BREAKING: Implement bulk banning endpoint.
BREAKING: Implement support for nonce enforcement.
BREAKING: Implement support for one-time purchases.
BREAKING: Implement support for polls.
BREAKING: Implement support for user applications.
Fix that the initial message in the pagination doesn't use the footer format.
Handle reconnection requests on connect.
Implement support for parsing partial messages.
Make help text embeds ephemeral by default.
Update maximum command length.
BREAKING: Rework deletion logic for data leases to prevent deadlocks.
</PackageReleaseNotes>

<!-- No need for build output, since this is a metapackage -->
Expand Down

0 comments on commit d81f325

Please sign in to comment.