I have SignalR app that publishes sent messages to Redis. My console app subscribe to channel where these messages are sent, deserializes it and saves in database.
Problem is with handling these events, as I want to make it in parallel. If many events are published, then the code crashes on opening database connection, because every thread want its own connection, so limit of physical connections is exceeded.
So I use SemaphoreSlim
to use threads that are available on my machine (8 logical threads).
The problem is that the performance is very slow: about 300 inserts to DB per second. I think it's not due to my hardware, as it's pretty good (i7-9700F, 16GB RAM).
The application runs at WSL2 (Ubuntu-22.04).
That's code:
using Dapper;
using Messages.Save.Dtos;
using Microsoft.Extensions.Configuration;
using Npgsql;
using StackExchange.Redis;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
const int MESSAGE_MAX_SENT_AT_OFFSET = 60;
RedisChannel UserChannel = new RedisChannel("*SignalRHub.MessagingHub:user:*", RedisChannel.PatternMode.Pattern);
RedisChannel GroupChannel = new RedisChannel("*SignalRHub.MessagingHub:group:*", RedisChannel.PatternMode.Pattern);
var connectionMultiplexer = ConnectionMultiplexer.Connect(configuration.GetConnectionString("Redis"));
var yugabyteConnectionString = configuration.GetConnectionString("YugabyteDB");
var semaphore = new SemaphoreSlim(Environment.ProcessorCount);
async void Handler(RedisChannel channel, RedisValue value)
{
await semaphore.WaitAsync();
await HandleMessageAsync(channel, value);
semaphore.Release();
}
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.SubscribeAsync(UserChannel, Handler);
await subscriber.SubscribeAsync(GroupChannel, Handler);
async Task HandleMessageAsync(RedisChannel channel, RedisValue value)
{
if (channel.IsNullOrEmpty || value.IsNullOrEmpty)
return;
var saveMessageDto = ReadMessage(value);
if (!ValidateMessage(saveMessageDto))
return;
using var connection = new NpgsqlConnection(yugabyteConnectionString);
await connection.OpenAsync();
var isUserGroupMember = await connection.QuerySingleAsync<bool>(@"SELECT EXISTS(
SELECT 1 FROM ConversationMembers
WHERE ConversationId=@ConversationId AND UserId=@UserId)::boolean",
new
{
saveMessageDto.ConversationId,
UserId = saveMessageDto.SenderId
});
if (!isUserGroupMember)
return;
await connection.ExecuteAsync("INSERT INTO Messages(ConversationId, Message, SentAt) VALUES(@ConversationId, @Message, @SentAt);",
new
{
saveMessageDto.ConversationId,
saveMessageDto.Message,
saveMessageDto.SentAt
});
}
static SaveMessageDto? ReadMessage(RedisValue value)
{
try
{
var str = value.ToString();
var startIndex = str.IndexOf('{');
var endIndex = str.LastIndexOf('}');
var json = JsonNode.Parse(str.AsSpan(startIndex, endIndex - startIndex + 1).ToString())?.AsObject();
if (json is null || !json.TryGetPropertyValue("arguments", out JsonNode? argsNode)
|| argsNode is not JsonArray arguments || !arguments.Any())
return null;
return JsonSerializer.Deserialize<SaveMessageDto>(arguments[0], new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch
{
return null;
}
}
static bool ValidateMessage([NotNullWhen(true)] SaveMessageDto? saveMessageDto)
{
// some ultra lighweight validation (no impact on performance)
return true;
}
await Task.Delay(Timeout.Infinite);