Skip to content

Commit

Permalink
#292: Do not log thumbnail exceptions
Browse files Browse the repository at this point in the history
Implement a system to also persist blacklisted user IDs in production
  • Loading branch information
jf-06 committed Mar 3, 2024
1 parent f40e630 commit cf7cf81
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 29 deletions.
59 changes: 31 additions & 28 deletions shared/commands/Modules/Render.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,46 +103,49 @@ long id
_avatarSettings.RenderYDimension
);

try {
try
{

var (stream, fileName) = _avatarUtility.RenderUser(
id,
_avatarSettings.PlaceIdForRenders,
_avatarSettings.RenderXDimension,
_avatarSettings.RenderYDimension
);
var (stream, fileName) = _avatarUtility.RenderUser(
id,
_avatarSettings.PlaceIdForRenders,
_avatarSettings.RenderXDimension,
_avatarSettings.RenderYDimension
);

if (stream == null)
{
await FollowupAsync("An error occurred while rendering the character.");
if (stream == null)
{
await FollowupAsync("An error occurred while rendering the character.");

return;
}
return;
}

using (stream)
await FollowupWithFileAsync(
stream,
fileName
);
using (stream)
await FollowupWithFileAsync(
stream,
fileName
);

}
catch (Exception e)
catch (ThumbnailResponseException e)
{
_logger.Error("An error occurred while rendering the character for the user '{0}': {1}", id, e);
_logger.Warning("The thumbnail service responded with the following state: {0}, message: {1}", e.State, e.Message);

if (e is ThumbnailResponseException thumbnailResponseException)
if (e.State == ThumbnailResponseState.InReview)
{
if (thumbnailResponseException.State == ThumbnailResponseState.InReview)
{
// Bogus error here for the sake of the user. Like flood checker error.
await FollowupAsync("You are sending render commands too quickly, please wait a few moments and try again.");

return;
}
// Bogus error here for the sake of the user. Like flood checker error.
await FollowupAsync("The thumbnail service placed the request in review, please try again later.");

// Bogus error for anything else, we don't need this to be noted that we are using rbx-thumbnails.
return;
}

// Bogus error for anything else, we don't need this to be noted that we are using rbx-thumbnails.
await FollowupAsync($"The thumbnail service responded with the following state: {e.State}");
}
catch (Exception e)
{
_logger.Error("An error occurred while rendering the character for the user '{0}': {1}", id, e);

await FollowupAsync("An error occurred while rendering the character.");
}
}
Expand Down
21 changes: 21 additions & 0 deletions shared/settings/Providers/AvatarSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,25 @@ public class AvatarSettings : BaseSettingsProvider
nameof(LocalCacheTtl),
TimeSpan.FromMinutes(5)
);

#if USE_VAULT_SETTINGS_PROVIDER
/// <summary>
/// A list of user IDs that should be automatically blacklisted.
/// </summary>
public long[] BlacklistUserIds {
get => GetOrDefault(
nameof(BlacklistUserIds),
Array.Empty<long>()
);
set => Set(nameof(BlacklistUserIds), value);
}

/// <summary>
/// Gets the period to wait before persisting the blacklist.
/// </summary>
public TimeSpan BlacklistPersistPeriod => GetOrDefault(
nameof(BlacklistPersistPeriod),
TimeSpan.FromMinutes(5)
);
#endif
}
57 changes: 56 additions & 1 deletion shared/utility/Implementation/AvatarUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Grid.Commands;

using GridJob = Grid.Client.Job;
using System.Collections.Concurrent;

/// <summary>
/// Exception thrown when rbx-thumbnails returns a state that is not pending or completed.
Expand Down Expand Up @@ -54,6 +55,7 @@ public class AvatarUtility : IAvatarUtility
private readonly IPercentageInvoker _percentageInvoker;

private readonly ExpirableDictionary<(long, ThumbnailCommandType), string> _localCachedPaths;
private readonly ConcurrentBag<long> _idsNotToUse = new(); // These IDs error out when trying to render (they are blocked? or they are moderated?)

/// <summary>
/// Construct a new instance of <see cref="AvatarUtility"/>.
Expand Down Expand Up @@ -82,7 +84,42 @@ IPercentageInvoker percentageInvoker

_localCachedPaths = new(avatarSettings.LocalCacheTtl);
_localCachedPaths.EntryRemoved += OnLocalCacheEntryRemoved;

#if USE_VAULT_SETTINGS_PROVIDER
foreach (var id in avatarSettings.BlacklistUserIds)
_idsNotToUse.Add(id);

if (!_idsNotToUse.IsEmpty)
_logger.Warning("Blacklisted user IDs: {0}", string.Join(", ", avatarSettings.BlacklistUserIds));

Task.Factory.StartNew(PersistBlacklistedIds, TaskCreationOptions.LongRunning);
#endif
}

#if USE_VAULT_SETTINGS_PROVIDER
private void PersistBlacklistedIds()
{
while (true)
{
Task.Delay(_avatarSettings.BlacklistPersistPeriod).Wait();

if (_avatarSettings.BlacklistUserIds.SequenceEqual(_idsNotToUse))
continue;

// Logging purposes: grab any new ones that were added.
var newIds = _idsNotToUse.Except(_avatarSettings.BlacklistUserIds).ToArray();
var removedIds = _avatarSettings.BlacklistUserIds.Except(_idsNotToUse).ToArray();

_logger.Warning(
"Blacklisted user IDs were updated. New IDs: {0}, Removed IDs: {1}",
string.Join(", ", newIds),
string.Join(", ", removedIds)
);

_avatarSettings.BlacklistUserIds = [.. _idsNotToUse];
}
}
#endif

private void OnLocalCacheEntryRemoved(string path, RemovalReason reason)
{
Expand Down Expand Up @@ -117,15 +154,20 @@ private IEnumerable<object> GetThumbnailArgs(string url, int x, int y)
yield return 0; // cameraOffsetY
}

private static string PollUntilCompleted(Func<ThumbnailResponse> func)
private string PollUntilCompleted(long userId, Func<ThumbnailResponse> func)
{
var response = func() ?? throw new ThumbnailResponseException(ThumbnailResponseState.Error, "The thumbnail response was null.");

if (response.State == ThumbnailResponseState.Completed)
return response.ImageUrl;

if (response.State != ThumbnailResponseState.Pending)
{
if (response.State != ThumbnailResponseState.InReview)
_idsNotToUse.Add(userId);

throw new ThumbnailResponseException(response.State.GetValueOrDefault(), "The thumbnail response was not pending.");
}

while (response.State == ThumbnailResponseState.Pending)
{
Expand All @@ -135,7 +177,12 @@ private static string PollUntilCompleted(Func<ThumbnailResponse> func)
}

if (response.State != ThumbnailResponseState.Completed)
{
if (response.State != ThumbnailResponseState.InReview)
_idsNotToUse.Add(userId);

throw new ThumbnailResponseException(response.State.GetValueOrDefault(), "The thumbnail response was not completed.");
}

return response.ImageUrl;
}
Expand All @@ -158,6 +205,12 @@ private static string DownloadFile(string url)

private (Stream, string) GetThumbnail(long userId, ThumbnailCommandType thumbnailCommandType)
{
if (userId == 0)
throw new ArgumentException("The user ID cannot be 0.", nameof(userId));

if (_idsNotToUse.Contains(userId))
throw new ThumbnailResponseException(ThumbnailResponseState.Blocked, "The user ID is blacklisted.");

if (thumbnailCommandType != ThumbnailCommandType.Closeup && thumbnailCommandType != ThumbnailCommandType.Avatar_R15_Action)
throw new ArgumentException("The thumbnail command type must be either closeup or avatar_r15_action.", nameof(thumbnailCommandType));

Expand All @@ -180,6 +233,7 @@ private static string DownloadFile(string url)
{
case ThumbnailCommandType.Closeup:
url = PollUntilCompleted(
userId,
() => _thumbnailsClient.GetAvatarHeadshotThumbnailAsync(
userIds,
_avatarSettings.RenderDimensions,
Expand All @@ -191,6 +245,7 @@ private static string DownloadFile(string url)
return DownloadFile(url);
case ThumbnailCommandType.Avatar_R15_Action:
url = PollUntilCompleted(
userId,
() => _thumbnailsClient.GetAvatarThumbnailAsync(
userIds,
_avatarSettings.RenderDimensions,
Expand Down
4 changes: 4 additions & 0 deletions shared/utility/Shared.Utility.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Condition="'$(USE_VAULT_SETTINGS_PROVIDER)' == 'true'">
<DefineConstants>$(DefineConstants);USE_VAULT_SETTINGS_PROVIDER</DefineConstants>
</PropertyGroup>

<PropertyGroup>
<Description>Shared utility classes used by the grid-bot</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down

0 comments on commit cf7cf81

Please sign in to comment.