[FA-misc] Mass transit overhaul, needs testing and review

This commit is contained in:
gamer147
2026-01-21 23:16:31 -05:00
parent 055ef33666
commit f88f340d0a
97 changed files with 1150 additions and 858 deletions

View File

@@ -0,0 +1,9 @@
namespace FictionArchive.Service.Shared.MassTransit.Configuration;
public class MassTransitOptions
{
public string Host { get; set; } = "localhost";
public string VirtualHost { get; set; } = "/";
public string Username { get; set; } = "guest";
public string Password { get; set; } = "guest";
}

View File

@@ -0,0 +1,6 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands;
public record ImportNovelCommand : ICommand
{
public required string NovelUrl { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands;
public record PullChapterContentCommand : ICommand
{
public required uint NovelId { get; init; }
public required uint VolumeId { get; init; }
public required uint ChapterOrder { get; init; }
}

View File

@@ -0,0 +1,12 @@
using FictionArchive.Common.Enums;
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands;
public record TranslateTextCommand : ICommand
{
public Guid TranslationRequestId { get; init; }
public Language From { get; init; }
public Language To { get; init; }
public required string Body { get; init; }
public required string TranslationEngineKey { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Commands;
public record UploadFileCommand : ICommand
{
public Guid RequestId { get; init; }
public required string FilePath { get; init; }
public required byte[] FileData { get; init; }
}

View File

@@ -0,0 +1,9 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record AuthUserAddedEvent : IEvent
{
public required string OAuthProviderId { get; init; }
public required string InviterOAuthProviderId { get; init; }
public required string EventUserEmail { get; init; }
public required string EventUserUsername { get; init; }
}

View File

@@ -0,0 +1,11 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record ChapterCreatedEvent : IEvent
{
public required uint ChapterId { get; init; }
public required uint NovelId { get; init; }
public required uint VolumeId { get; init; }
public required int VolumeOrder { get; init; }
public required uint ChapterOrder { get; init; }
public required string ChapterTitle { get; init; }
}

View File

@@ -0,0 +1,11 @@
using FictionArchive.Common.Enums;
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record FileUploadCompletedEvent : IEvent
{
public Guid RequestId { get; init; }
public RequestStatus Status { get; init; }
public string? FileAccessUrl { get; init; }
public string? ErrorMessage { get; init; }
}

View File

@@ -0,0 +1,12 @@
using FictionArchive.Common.Enums;
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record NovelCreatedEvent : IEvent
{
public required uint NovelId { get; init; }
public required string Title { get; init; }
public required Language OriginalLanguage { get; init; }
public required string Source { get; init; }
public required string AuthorName { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record TranslationCompletedEvent : IEvent
{
public Guid TranslationRequestId { get; init; }
public required string TranslatedText { get; init; }
}

View File

@@ -0,0 +1,13 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts.Events;
public record UserInvitedEvent : IEvent
{
public Guid InvitedUserId { get; init; }
public required string InvitedUsername { get; init; }
public required string InvitedEmail { get; init; }
public required string InvitedOAuthProviderId { get; init; }
public Guid InviterId { get; init; }
public required string InviterUsername { get; init; }
public required string InviterOAuthProviderId { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts;
/// <summary>
/// Marker interface for commands (do something, single consumer)
/// </summary>
public interface ICommand { }

View File

@@ -0,0 +1,6 @@
namespace FictionArchive.Service.Shared.MassTransit.Contracts;
/// <summary>
/// Marker interface for events (something happened, multiple subscribers)
/// </summary>
public interface IEvent { }

View File

@@ -0,0 +1,18 @@
using NodaTime;
namespace FictionArchive.Service.Shared.MassTransit.Contracts;
/// <summary>
/// Published by sagas on state transitions for centralized job tracking
/// </summary>
public record JobStateChangedEvent : IEvent
{
public Guid JobId { get; init; }
public required string JobType { get; init; }
public required string FromState { get; init; }
public required string ToState { get; init; }
public string? Message { get; init; }
public string? Error { get; init; }
public Instant Timestamp { get; init; }
public Dictionary<string, object>? Metadata { get; init; }
}

View File

@@ -0,0 +1,103 @@
using FictionArchive.Service.Shared.MassTransit.Configuration;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace FictionArchive.Service.Shared.MassTransit;
public static class MassTransitExtensions
{
/// <summary>
/// Adds MassTransit with RabbitMQ and Entity Framework outbox
/// </summary>
public static IServiceCollection AddFictionArchiveMassTransit<TDbContext>(
this IServiceCollection services,
IConfiguration configuration,
Action<IBusRegistrationConfigurator>? configureConsumers = null)
where TDbContext : DbContext
{
services.AddMassTransit(x =>
{
configureConsumers?.Invoke(x);
x.AddEntityFrameworkOutbox<TDbContext>(o =>
{
o.UsePostgres();
o.UseBusOutbox();
});
x.UsingRabbitMq((context, cfg) =>
{
var options = configuration.GetSection("RabbitMQ").Get<MassTransitOptions>()
?? new MassTransitOptions();
cfg.Host(options.Host, options.VirtualHost, h =>
{
h.Username(options.Username);
h.Password(options.Password);
});
// Immediate retries for transient failures
cfg.UseMessageRetry(r => r.Intervals(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)));
// Delayed redelivery for longer outages
cfg.UseDelayedRedelivery(r => r.Intervals(
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(30)));
cfg.ConfigureEndpoints(context);
});
});
return services;
}
/// <summary>
/// Adds MassTransit with RabbitMQ without outbox (for services without EF)
/// </summary>
public static IServiceCollection AddFictionArchiveMassTransit(
this IServiceCollection services,
IConfiguration configuration,
Action<IBusRegistrationConfigurator>? configureConsumers = null)
{
services.AddMassTransit(x =>
{
configureConsumers?.Invoke(x);
x.UsingRabbitMq((context, cfg) =>
{
var options = configuration.GetSection("RabbitMQ").Get<MassTransitOptions>()
?? new MassTransitOptions();
cfg.Host(options.Host, options.VirtualHost, h =>
{
h.Username(options.Username);
h.Password(options.Password);
});
cfg.UseMessageRetry(r => r.Intervals(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)));
cfg.UseDelayedRedelivery(r => r.Intervals(
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(30)));
cfg.ConfigureEndpoints(context);
});
});
return services;
}
}