mirror of
https://github.com/bitwarden/server.git
synced 2025-04-12 00:28:11 -05:00
[PM-17562] Refactor existing RabbitMq implementation (#5357)
* [PM-17562] Refactor existing RabbitMq implementation * Fixed issues noted in PR review
This commit is contained in:
parent
f1b9bd9a09
commit
3f3da558b6
8
src/Core/AdminConsole/Services/IEventMessageHandler.cs
Normal file
8
src/Core/AdminConsole/Services/IEventMessageHandler.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public interface IEventMessageHandler
|
||||||
|
{
|
||||||
|
Task HandleEventAsync(EventMessage eventMessage);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class EventRepositoryHandler(
|
||||||
|
[FromKeyedServices("persistent")] IEventWriteService eventWriteService)
|
||||||
|
: IEventMessageHandler
|
||||||
|
{
|
||||||
|
public Task HandleEventAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
return eventWriteService.CreateAsync(eventMessage);
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +1,25 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class RabbitMqEventHttpPostListener : RabbitMqEventListenerBase
|
public class HttpPostEventHandler : IEventMessageHandler
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly string _httpPostUrl;
|
private readonly string _httpPostUrl;
|
||||||
private readonly string _queueName;
|
|
||||||
|
|
||||||
protected override string QueueName => _queueName;
|
public const string HttpClientName = "HttpPostEventHandlerHttpClient";
|
||||||
|
|
||||||
public const string HttpClientName = "EventHttpPostListenerHttpClient";
|
public HttpPostEventHandler(
|
||||||
|
|
||||||
public RabbitMqEventHttpPostListener(
|
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
ILogger<RabbitMqEventListenerBase> logger,
|
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
: base(logger, globalSettings)
|
|
||||||
{
|
{
|
||||||
_httpClient = httpClientFactory.CreateClient(HttpClientName);
|
_httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||||
_httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl;
|
_httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl;
|
||||||
_queueName = globalSettings.EventLogging.RabbitMq.HttpPostQueueName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleMessageAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var content = JsonContent.Create(eventMessage);
|
var content = JsonContent.Create(eventMessage);
|
||||||
var response = await _httpClient.PostAsync(_httpPostUrl, content);
|
var response = await _httpClient.PostAsync(_httpPostUrl, content);
|
@ -1,29 +0,0 @@
|
|||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
|
||||||
|
|
||||||
public class RabbitMqEventRepositoryListener : RabbitMqEventListenerBase
|
|
||||||
{
|
|
||||||
private readonly IEventWriteService _eventWriteService;
|
|
||||||
private readonly string _queueName;
|
|
||||||
|
|
||||||
protected override string QueueName => _queueName;
|
|
||||||
|
|
||||||
public RabbitMqEventRepositoryListener(
|
|
||||||
[FromKeyedServices("persistent")] IEventWriteService eventWriteService,
|
|
||||||
ILogger<RabbitMqEventListenerBase> logger,
|
|
||||||
GlobalSettings globalSettings)
|
|
||||||
: base(logger, globalSettings)
|
|
||||||
{
|
|
||||||
_eventWriteService = eventWriteService;
|
|
||||||
_queueName = globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task HandleMessageAsync(EventMessage eventMessage)
|
|
||||||
{
|
|
||||||
return _eventWriteService.CreateAsync(eventMessage);
|
|
||||||
}
|
|
||||||
}
|
|
13
src/Core/Services/EventLoggingListenerService.cs
Normal file
13
src/Core/Services/EventLoggingListenerService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public abstract class EventLoggingListenerService : BackgroundService
|
||||||
|
{
|
||||||
|
protected readonly IEventMessageHandler _handler;
|
||||||
|
|
||||||
|
protected EventLoggingListenerService(IEventMessageHandler handler)
|
||||||
|
{
|
||||||
|
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,26 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public abstract class RabbitMqEventListenerBase : BackgroundService
|
public class RabbitMqEventListenerService : EventLoggingListenerService
|
||||||
{
|
{
|
||||||
private IChannel _channel;
|
private IChannel _channel;
|
||||||
private IConnection _connection;
|
private IConnection _connection;
|
||||||
private readonly string _exchangeName;
|
private readonly string _exchangeName;
|
||||||
private readonly ConnectionFactory _factory;
|
private readonly ConnectionFactory _factory;
|
||||||
private readonly ILogger<RabbitMqEventListenerBase> _logger;
|
private readonly ILogger<RabbitMqEventListenerService> _logger;
|
||||||
|
private readonly string _queueName;
|
||||||
|
|
||||||
protected abstract string QueueName { get; }
|
public RabbitMqEventListenerService(
|
||||||
|
IEventMessageHandler handler,
|
||||||
protected RabbitMqEventListenerBase(
|
ILogger<RabbitMqEventListenerService> logger,
|
||||||
ILogger<RabbitMqEventListenerBase> logger,
|
GlobalSettings globalSettings,
|
||||||
GlobalSettings globalSettings)
|
string queueName) : base(handler)
|
||||||
{
|
{
|
||||||
_factory = new ConnectionFactory
|
_factory = new ConnectionFactory
|
||||||
{
|
{
|
||||||
@ -30,6 +30,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService
|
|||||||
};
|
};
|
||||||
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_queueName = queueName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
@ -38,13 +39,13 @@ public abstract class RabbitMqEventListenerBase : BackgroundService
|
|||||||
_channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken);
|
_channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken);
|
||||||
|
|
||||||
await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true);
|
await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true);
|
||||||
await _channel.QueueDeclareAsync(queue: QueueName,
|
await _channel.QueueDeclareAsync(queue: _queueName,
|
||||||
durable: true,
|
durable: true,
|
||||||
exclusive: false,
|
exclusive: false,
|
||||||
autoDelete: false,
|
autoDelete: false,
|
||||||
arguments: null,
|
arguments: null,
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
await _channel.QueueBindAsync(queue: QueueName,
|
await _channel.QueueBindAsync(queue: _queueName,
|
||||||
exchange: _exchangeName,
|
exchange: _exchangeName,
|
||||||
routingKey: string.Empty,
|
routingKey: string.Empty,
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
@ -59,7 +60,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var eventMessage = JsonSerializer.Deserialize<EventMessage>(eventArgs.Body.Span);
|
var eventMessage = JsonSerializer.Deserialize<EventMessage>(eventArgs.Body.Span);
|
||||||
await HandleMessageAsync(eventMessage);
|
await _handler.HandleEventAsync(eventMessage);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -67,7 +68,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await _channel.BasicConsumeAsync(QueueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken);
|
await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken);
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@ -88,6 +89,4 @@ public abstract class RabbitMqEventListenerBase : BackgroundService
|
|||||||
_connection.Dispose();
|
_connection.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Task HandleMessageAsync(EventMessage eventMessage);
|
|
||||||
}
|
}
|
@ -27,4 +27,5 @@ public interface IGlobalSettings
|
|||||||
string DatabaseProvider { get; set; }
|
string DatabaseProvider { get; set; }
|
||||||
GlobalSettings.SqlSettings SqlServer { get; set; }
|
GlobalSettings.SqlSettings SqlServer { get; set; }
|
||||||
string DevelopmentDirectory { get; set; }
|
string DevelopmentDirectory { get; set; }
|
||||||
|
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -89,13 +89,26 @@ public class Startup
|
|||||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<EventRepositoryHandler>();
|
||||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||||
services.AddHostedService<RabbitMqEventRepositoryListener>();
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
|
new RabbitMqEventListenerService(
|
||||||
|
provider.GetRequiredService<EventRepositoryHandler>(),
|
||||||
|
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||||
|
provider.GetRequiredService<GlobalSettings>(),
|
||||||
|
globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl))
|
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl))
|
||||||
{
|
{
|
||||||
services.AddHttpClient(RabbitMqEventHttpPostListener.HttpClientName);
|
services.AddSingleton<HttpPostEventHandler>();
|
||||||
services.AddHostedService<RabbitMqEventHttpPostListener>();
|
services.AddHttpClient(HttpPostEventHandler.HttpClientName);
|
||||||
|
|
||||||
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
|
new RabbitMqEventListenerService(
|
||||||
|
provider.GetRequiredService<HttpPostEventHandler>(),
|
||||||
|
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||||
|
provider.GetRequiredService<GlobalSettings>(),
|
||||||
|
globalSettings.EventLogging.RabbitMq.HttpPostQueueName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ public class MockedHttpMessageHandler : HttpMessageHandler
|
|||||||
{
|
{
|
||||||
private readonly List<IHttpRequestMatcher> _matchers = new();
|
private readonly List<IHttpRequestMatcher> _matchers = new();
|
||||||
|
|
||||||
|
public List<HttpRequestMessage> CapturedRequests { get; } = new List<HttpRequestMessage>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The fallback handler to use when the request does not match any of the provided matchers.
|
/// The fallback handler to use when the request does not match any of the provided matchers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -16,6 +18,7 @@ public class MockedHttpMessageHandler : HttpMessageHandler
|
|||||||
|
|
||||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
CapturedRequests.Add(request);
|
||||||
var matcher = _matchers.FirstOrDefault(x => x.Matches(request));
|
var matcher = _matchers.FirstOrDefault(x => x.Matches(request));
|
||||||
if (matcher == null)
|
if (matcher == null)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class EventRepositoryHandlerTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_WritesEventToIEventWriteService(
|
||||||
|
EventMessage eventMessage,
|
||||||
|
SutProvider<EventRepositoryHandler> sutProvider)
|
||||||
|
{
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(
|
||||||
|
Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessage))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Bit.Test.Common.MockedHttpClient;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class HttpPostEventHandlerTests
|
||||||
|
{
|
||||||
|
private readonly MockedHttpMessageHandler _handler;
|
||||||
|
private HttpClient _httpClient;
|
||||||
|
|
||||||
|
private const string _httpPostUrl = "http://localhost/test/event";
|
||||||
|
|
||||||
|
public HttpPostEventHandlerTests()
|
||||||
|
{
|
||||||
|
_handler = new MockedHttpMessageHandler();
|
||||||
|
_handler.Fallback
|
||||||
|
.WithStatusCode(HttpStatusCode.OK)
|
||||||
|
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||||
|
_httpClient = _handler.ToHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SutProvider<HttpPostEventHandler> GetSutProvider()
|
||||||
|
{
|
||||||
|
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||||
|
clientFactory.CreateClient(HttpPostEventHandler.HttpClientName).Returns(_httpClient);
|
||||||
|
|
||||||
|
var globalSettings = new GlobalSettings();
|
||||||
|
globalSettings.EventLogging.RabbitMq.HttpPostUrl = _httpPostUrl;
|
||||||
|
|
||||||
|
return new SutProvider<HttpPostEventHandler>()
|
||||||
|
.SetDependency(globalSettings)
|
||||||
|
.SetDependency(clientFactory)
|
||||||
|
.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_PostsEventsToUrl(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider();
|
||||||
|
var content = JsonContent.Create(eventMessage);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||||
|
Arg.Is(AssertHelper.AssertPropertyEqual<string>(HttpPostEventHandler.HttpClientName))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Single(_handler.CapturedRequests);
|
||||||
|
var request = _handler.CapturedRequests[0];
|
||||||
|
Assert.NotNull(request);
|
||||||
|
var returned = await request.Content.ReadFromJsonAsync<EventMessage>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpMethod.Post, request.Method);
|
||||||
|
Assert.Equal(_httpPostUrl, request.RequestUri.ToString());
|
||||||
|
AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" });
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user