From 4930a52e9debf81d7ca12e8e5a7ae6bdbcf79568 Mon Sep 17 00:00:00 2001 From: ilitirit Date: Wed, 12 Apr 2023 19:32:05 +0200 Subject: [PATCH] Refactor user name generation --- .../Controllers/ExpedienceController.cs | 78 +++++++++++++++++-- Expedience.Api/Program.cs | 7 +- .../Concurrency/IDistributedLock.cs | 47 +++++++++++ .../Expedience.Infrastructure.csproj | 3 +- 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 Expedience.Infrastructure/Concurrency/IDistributedLock.cs diff --git a/Expedience.Api/Controllers/ExpedienceController.cs b/Expedience.Api/Controllers/ExpedienceController.cs index eccfab2..633caca 100644 --- a/Expedience.Api/Controllers/ExpedienceController.cs +++ b/Expedience.Api/Controllers/ExpedienceController.cs @@ -1,8 +1,12 @@ -using Expedience.Api.Encryption; -using Expedience.Models; +using System; +using System.Net; +using System.Text.Json; +using Expedience.Api.Encryption; +using Expedience.Infrastructure; +using Expedience.Infrastructure.Concurrency; +using Expedience.Infrastructure.Models; using MassTransit; using Microsoft.AspNetCore.Mvc; -using System.Text.Json; namespace Expedience.Api.Controllers { @@ -13,12 +17,20 @@ namespace Expedience.Api.Controllers private readonly ILogger _logger; private readonly IPublishEndpoint _publisher; private readonly IDecryptor _decrypytor; + private readonly IDistributedLock _distributedLock; + private readonly IServiceScopeFactory _serviceScopeFactory; - public ExpedienceController(ILogger logger, IPublishEndpoint publisher, IDecryptor decrypytor) + public ExpedienceController(ILogger logger, + IPublishEndpoint publisher, + IDecryptor decrypytor, + IDistributedLock distributedLock, + IServiceScopeFactory serviceScopeFactory) { _logger = logger; _publisher = publisher; _decrypytor = decrypytor; + _distributedLock = distributedLock; + _serviceScopeFactory = serviceScopeFactory; } [HttpPost("DutyCompletion")] @@ -30,7 +42,7 @@ namespace Expedience.Api.Controllers foreach (var payload in encryptedPayloads) { var decryptedData = _decrypytor.Decrypt(payload); - var completionResult = JsonSerializer.Deserialize(decryptedData)!; + var completionResult = JsonSerializer.Deserialize(decryptedData)!; completionResult.UploadDateUtc = utcNow; await _publisher.Publish(completionResult); } @@ -42,5 +54,61 @@ namespace Expedience.Api.Controllers return Ok(); } + + [HttpGet("UserName/{worldId}/{userHash}")] + public async Task GetUserName(int worldId, string userHash, CancellationToken cancellationToken) + { + using var scope = _serviceScopeFactory.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + var lockKey = $"{worldId}-{userHash}"; + if (_distributedLock.AcquireLock(lockKey, TimeSpan.FromSeconds(10))) + { + try + { + var user = dbContext.Users.FirstOrDefault(x => x.UserHash == userHash && x.WorldId == worldId); + if (user == null) + { + string userName; + var isDuplicate = false; + do + { + userName = UserNameGenerator.Generate(); + isDuplicate = dbContext.Users.Any(x => x.WorldId == worldId && x.UserName == userName); + await Task.Delay(20, cancellationToken); // Don't hog the CPU + } + while (isDuplicate == false); + + user = new User + { + UserHash = userHash, + WorldId = worldId, + UserName = userName, + CreatedAt = DateTime.UtcNow, + }; + + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(cancellationToken); + } + + return Ok(user.UserName); + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error obtaining user name for World Id {worldId} and hash {userHash}: {errorMessage}", ex.Message); + } + finally + { + _distributedLock.ReleaseLock(lockKey); + } + } + else + { + _logger.LogError("Could not acquire lock for {lockKey}", lockKey); + } + + return StatusCode(500); + } } } diff --git a/Expedience.Api/Program.cs b/Expedience.Api/Program.cs index f6fb5f5..246104e 100644 --- a/Expedience.Api/Program.cs +++ b/Expedience.Api/Program.cs @@ -3,16 +3,16 @@ using Expedience.Infrastructure; using Expedience.Api.Encryption; using MassTransit; using Microsoft.EntityFrameworkCore; +using Expedience.Infrastructure.Concurrency; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. var connectionString = builder.Configuration.GetConnectionString("Expedience"); builder.Services.AddDbContext(options => options.UseNpgsql(connectionString).UseLowerCaseNamingConvention()); builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -34,6 +34,9 @@ builder.Services.AddMassTransit(opt => }); }); +builder.Services.AddEnyimMemcached(); + +builder.Services.AddSingleton(); builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/Expedience.Infrastructure/Concurrency/IDistributedLock.cs b/Expedience.Infrastructure/Concurrency/IDistributedLock.cs new file mode 100644 index 0000000..b2cfe20 --- /dev/null +++ b/Expedience.Infrastructure/Concurrency/IDistributedLock.cs @@ -0,0 +1,47 @@ +using System; +using Enyim.Caching; +using Enyim.Caching.Memcached; +using Microsoft.Extensions.Logging; + +namespace Expedience.Infrastructure.Concurrency +{ + public interface IDistributedLock + { + bool AcquireLock(string lockKey, TimeSpan timeout); + void ReleaseLock(string lockKey); + } + + public class DistributedLock + { + private readonly IMemcachedClient _memcachedClient; + + public DistributedLock(ILogger logger, IMemcachedClient memcachedClient) + { + _memcachedClient = memcachedClient; + } + + public bool AcquireLock(string lockKey, TimeSpan timeout, TimeSpan lockExpiration) + { + var startTime = DateTime.UtcNow; + while (true) + { + if (_memcachedClient.Store(StoreMode.Add, lockKey, true, lockExpiration)) + { + return true; // Lock acquired + } + + if (DateTime.UtcNow - startTime > timeout) + { + return false; // Timeout + } + + Thread.Sleep(100); // Wait and try again + } + } + + public void ReleaseLock(string lockKey) + { + _memcachedClient.Remove(lockKey); + } + } +} diff --git a/Expedience.Infrastructure/Expedience.Infrastructure.csproj b/Expedience.Infrastructure/Expedience.Infrastructure.csproj index cddfd0f..ec8a6bf 100644 --- a/Expedience.Infrastructure/Expedience.Infrastructure.csproj +++ b/Expedience.Infrastructure/Expedience.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -7,6 +7,7 @@ +