refactor(backend): replace coravel with hangfire for background jobs
for *some reason*, coravel locks a persistent job queue behind a paywall. this means that if the server ever crashes, all pending jobs are lost. this is... not good, so we're switching to hangfire for that instead. coravel is still used for emails, though. BREAKING CHANGE: Foxnouns.NET now requires Redis to work. the EFCore storage for hangfire doesn't work well enough, unfortunately.
This commit is contained in:
parent
cd24196cd1
commit
7759225428
24 changed files with 272 additions and 269 deletions
|
@ -14,11 +14,11 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
@ -26,7 +26,7 @@ using NodaTime.Text;
|
|||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateDataExportInvocable(
|
||||
public class CreateDataExportJob(
|
||||
DatabaseContext db,
|
||||
IClock clock,
|
||||
UserRendererService userRenderer,
|
||||
|
@ -34,37 +34,41 @@ public class CreateDataExportInvocable(
|
|||
ObjectStorageService objectStorageService,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<CreateDataExportPayload>
|
||||
)
|
||||
{
|
||||
private static readonly HttpClient Client = new();
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportInvocable>();
|
||||
public required CreateDataExportPayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<CreateDataExportJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(Snowflake userId)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateDataExportJob>(j => j.InvokeAsync(userId));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(Snowflake userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await InvokeAsync();
|
||||
await InvokeAsyncInner(userId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", Payload.UserId);
|
||||
_logger.Error(e, "Error generating data export for user {UserId}", userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvokeAsync()
|
||||
private async Task InvokeAsyncInner(Snowflake userId)
|
||||
{
|
||||
User? user = await db
|
||||
.Users.Include(u => u.AuthMethods)
|
||||
.Include(u => u.Flags)
|
||||
.Include(u => u.ProfileFlags)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync(u => u.Id == Payload.UserId);
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Received create data export request for user {UserId} but no such user exists, deleted or otherwise. Ignoring request",
|
||||
Payload.UserId
|
||||
userId
|
||||
);
|
||||
return;
|
||||
}
|
|
@ -12,49 +12,53 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class CreateFlagInvocable(
|
||||
public class CreateFlagJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<CreateFlagPayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagInvocable>();
|
||||
public required CreateFlagPayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<CreateFlagJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(CreateFlagPayload payload)
|
||||
{
|
||||
BackgroundJob.Enqueue<CreateFlagJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(CreateFlagPayload payload)
|
||||
{
|
||||
_logger.Information(
|
||||
"Creating flag {FlagId} for user {UserId} with image data length {DataLength}",
|
||||
Payload.Id,
|
||||
Payload.UserId,
|
||||
Payload.ImageData.Length
|
||||
payload.Id,
|
||||
payload.UserId,
|
||||
payload.ImageData.Length
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f =>
|
||||
f.Id == Payload.Id && f.UserId == Payload.UserId
|
||||
f.Id == payload.Id && f.UserId == payload.UserId
|
||||
);
|
||||
if (flag == null)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Got a flag create job for {FlagId} but it doesn't exist, aborting",
|
||||
Payload.Id
|
||||
payload.Id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
(string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||
Payload.ImageData,
|
||||
payload.ImageData,
|
||||
256,
|
||||
false
|
||||
);
|
||||
|
@ -68,7 +72,7 @@ public class CreateFlagInvocable(
|
|||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
_logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", Payload.Id, ae.Message);
|
||||
_logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", payload.Id, ae.Message);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
|
|
|
@ -12,29 +12,33 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class MemberAvatarUpdateInvocable(
|
||||
public class MemberAvatarUpdateJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<MemberAvatarUpdateJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateMemberAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
BackgroundJob.Enqueue<MemberAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (payload.NewAvatar != null)
|
||||
await UpdateMemberAvatarAsync(payload.Id, payload.NewAvatar);
|
||||
else
|
||||
await ClearMemberAvatarAsync(Payload.Id);
|
||||
await ClearMemberAvatarAsync(payload.Id);
|
||||
}
|
||||
|
||||
private async Task UpdateMemberAvatarAsync(Snowflake id, string newAvatar)
|
|
@ -19,5 +19,3 @@ namespace Foxnouns.Backend.Jobs;
|
|||
public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar);
|
||||
|
||||
public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string ImageData);
|
||||
|
||||
public record CreateDataExportPayload(Snowflake UserId);
|
||||
|
|
|
@ -12,29 +12,33 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Coravel.Invocable;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Hangfire;
|
||||
|
||||
namespace Foxnouns.Backend.Jobs;
|
||||
|
||||
public class UserAvatarUpdateInvocable(
|
||||
public class UserAvatarUpdateJob(
|
||||
DatabaseContext db,
|
||||
ObjectStorageService objectStorageService,
|
||||
ILogger logger
|
||||
) : IInvocable, IInvocableWithPayload<AvatarUpdatePayload>
|
||||
)
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateInvocable>();
|
||||
public required AvatarUpdatePayload Payload { get; set; }
|
||||
private readonly ILogger _logger = logger.ForContext<UserAvatarUpdateJob>();
|
||||
|
||||
public async Task Invoke()
|
||||
public static void Enqueue(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (Payload.NewAvatar != null)
|
||||
await UpdateUserAvatarAsync(Payload.Id, Payload.NewAvatar);
|
||||
BackgroundJob.Enqueue<UserAvatarUpdateJob>(j => j.InvokeAsync(payload));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(AvatarUpdatePayload payload)
|
||||
{
|
||||
if (payload.NewAvatar != null)
|
||||
await UpdateUserAvatarAsync(payload.Id, payload.NewAvatar);
|
||||
else
|
||||
await ClearUserAvatarAsync(Payload.Id);
|
||||
await ClearUserAvatarAsync(payload.Id);
|
||||
}
|
||||
|
||||
private async Task UpdateUserAvatarAsync(Snowflake id, string newAvatar)
|
Loading…
Add table
Add a link
Reference in a new issue