-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from bertusviljoen/feature/bertus_elevator_pool
feature/bertus_elevator_pool
- Loading branch information
Showing
17 changed files
with
1,048 additions
and
143 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
src/Application/Abstractions/Services/IInMemoryElevatorPoolService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using Domain.Common; | ||
using Domain.Elevators; | ||
|
||
namespace Application.Abstractions.Services; | ||
|
||
/// <summary> Interface for managing the in-memory pool of elevators. </summary> | ||
public interface IInMemoryElevatorPoolService | ||
{ | ||
/// <summary> Gets an elevator by its ID. </summary> | ||
/// <param name="elevatorId">The ID of the elevator.</param> | ||
/// <param name="cancellationToken">The cancellation token for cancelling the operation.</param> | ||
/// <returns>A Result containing the elevator if found.</returns> | ||
Task<Result<ElevatorItem>> GetElevatorByIdAsync(Guid elevatorId, CancellationToken cancellationToken); | ||
|
||
/// <summary> Updates an elevator's information in the pool. </summary> | ||
/// <param name="elevator">The elevator with updated information.</param> | ||
/// <param name="cancellationToken">The cancellation token for cancelling the operation.</param> | ||
/// <returns>A Result indicating success or failure.</returns> | ||
Task<Result> UpdateElevatorAsync(ElevatorItem elevator, CancellationToken cancellationToken); | ||
|
||
/// <summary> Gets all elevators in a building. </summary> | ||
/// <param name="buildingId">The ID of the building.</param> | ||
/// <param name="cancellationToken">The cancellation token for cancelling the operation.</param> | ||
/// <returns>A Result containing a list of all elevators in the building.</returns> | ||
Task<Result<IEnumerable<ElevatorItem>>> GetAllElevatorsAsync(Guid buildingId, CancellationToken cancellationToken); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
211 changes: 211 additions & 0 deletions
211
src/Application/Services/InMemoryElevatorPoolService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
using System.Collections.Concurrent; | ||
using Application.Abstractions.Data; | ||
using Application.Abstractions.Services; | ||
using Domain.Common; | ||
using Domain.Elevators; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Application.Services; | ||
|
||
///<inheritdoc cref="IInMemoryElevatorPoolService"/> | ||
public sealed class InMemoryElevatorPoolService( | ||
ILogger<InMemoryElevatorPoolService> logger, | ||
IServiceProvider serviceProvider) | ||
: IInMemoryElevatorPoolService, IDisposable | ||
{ | ||
private readonly ILogger<InMemoryElevatorPoolService> _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); | ||
private readonly ConcurrentDictionary<Guid, ElevatorItem> _elevators = new(); | ||
private readonly SemaphoreSlim _semaphore = new(1, 1); | ||
private DateTime _lastUpdate = DateTime.MinValue; // Initialize to MinValue to force first update | ||
private readonly TimeSpan _updateInterval = TimeSpan.FromSeconds(30); | ||
private bool _disposed; | ||
|
||
///<inheritdoc cref="IInMemoryElevatorPoolService"/> | ||
public async Task<Result<ElevatorItem>> GetElevatorByIdAsync(Guid elevatorId, CancellationToken cancellationToken) | ||
{ | ||
_logger.LogInformation("Getting elevator by ID {ElevatorId}", elevatorId); | ||
try | ||
{ | ||
await Task.Yield(); // Ensure async context | ||
|
||
// TryGetValue is already thread-safe in ConcurrentDictionary | ||
if (_elevators.TryGetValue(elevatorId, out var elevator)) | ||
{ | ||
_logger.LogInformation("Elevator found by ID {ElevatorId}", elevatorId); | ||
// Create a deep copy to ensure thread safety | ||
return Result.Success(elevator.Clone()); | ||
} | ||
|
||
// If not in cache, try to fetch from database | ||
using var scope = _serviceProvider.CreateScope(); | ||
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>(); | ||
|
||
var dbElevator = await context.Elevators | ||
.FirstOrDefaultAsync(e => e.Id == elevatorId, cancellationToken); | ||
|
||
if (dbElevator != null) | ||
{ | ||
var elevatorItem = ElevatorItem.FromElevator(dbElevator); | ||
_elevators.TryAdd(elevatorId, elevatorItem); | ||
return Result.Success(elevatorItem.Clone()); | ||
} | ||
|
||
_logger.LogWarning("Elevator not found by ID {ElevatorId}", elevatorId); | ||
return Result.Failure<ElevatorItem>( | ||
new Error("GetElevatorById.NotFound", $"Elevator with ID {elevatorId} not found", ErrorType.NotFound)); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Error getting elevator by ID {ElevatorId}", elevatorId); | ||
return Result.Failure<ElevatorItem>( | ||
new Error("GetElevatorById.Error", ex.Message, ErrorType.Failure)); | ||
} | ||
} | ||
|
||
///<inheritdoc cref="IInMemoryElevatorPoolService"/> | ||
public async Task<Result> UpdateElevatorAsync(ElevatorItem elevator, CancellationToken cancellationToken) | ||
{ | ||
_logger.LogInformation("Updating elevator {ElevatorId}", elevator.Id); | ||
try | ||
{ | ||
await _semaphore.WaitAsync(cancellationToken); | ||
try | ||
{ | ||
// Create a deep copy of the elevator to ensure thread safety | ||
var elevatorCopy = elevator.Clone(); | ||
|
||
if (_elevators.TryGetValue(elevatorCopy.Id, out var existingElevator)) | ||
{ | ||
if (_elevators.TryUpdate(elevatorCopy.Id, elevatorCopy, existingElevator)) | ||
{ | ||
_logger.LogInformation("Elevator updated in memory {ElevatorId}", elevator.Id); | ||
|
||
// Update in database | ||
using var scope = _serviceProvider.CreateScope(); | ||
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>(); | ||
|
||
var dbElevator = await context.Elevators | ||
.FirstOrDefaultAsync(e => e.Id == elevator.Id, cancellationToken); | ||
|
||
if (dbElevator != null) | ||
{ | ||
// Update database entity with new values | ||
dbElevator.CurrentFloor = elevator.CurrentFloor; | ||
dbElevator.ElevatorStatus = elevator.ElevatorStatus; | ||
dbElevator.ElevatorDirection = elevator.ElevatorDirection; | ||
await context.SaveChangesAsync(cancellationToken); | ||
} | ||
|
||
return Result.Success(); | ||
} | ||
|
||
_logger.LogWarning("Failed to update elevator {ElevatorId}", elevator.Id); | ||
return Result.Failure( | ||
new Error("UpdateElevator.ConcurrencyError", "Failed to update elevator due to concurrent modification", ErrorType.Conflict)); | ||
} | ||
|
||
_logger.LogInformation("Adding elevator {ElevatorId}", elevator.Id); | ||
if (_elevators.TryAdd(elevatorCopy.Id, elevatorCopy)) | ||
{ | ||
return Result.Success(); | ||
} | ||
|
||
_logger.LogWarning("Failed to add elevator {ElevatorId}", elevator.Id); | ||
return Result.Failure( | ||
new Error("UpdateElevator.AddError", "Failed to add elevator to the pool", ErrorType.Failure)); | ||
} | ||
finally | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Error updating elevator {ElevatorId}", elevator.Id); | ||
return Result.Failure( | ||
new Error("UpdateElevator.Error", ex.Message, ErrorType.Failure)); | ||
} | ||
} | ||
|
||
///<inheritdoc cref="IInMemoryElevatorPoolService"/> | ||
public async Task<Result<IEnumerable<ElevatorItem>>> GetAllElevatorsAsync(Guid buildingId, CancellationToken cancellationToken) | ||
{ | ||
_logger.LogInformation("Getting all elevators in building {BuildingId}", buildingId); | ||
try | ||
{ | ||
await _semaphore.WaitAsync(cancellationToken); | ||
try | ||
{ | ||
// Always update from database if there are no elevators for this building | ||
var hasElevatorsForBuilding = _elevators.Values.Any(e => e.BuildingId == buildingId); | ||
var needsUpdate = DateTime.UtcNow - _lastUpdate > _updateInterval || !hasElevatorsForBuilding; | ||
|
||
if (needsUpdate) | ||
{ | ||
_logger.LogInformation("Updating elevators from database for building {BuildingId}. LastUpdate: {LastUpdate}", buildingId, _lastUpdate); | ||
|
||
using var scope = _serviceProvider.CreateScope(); | ||
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>(); | ||
|
||
var elevators = await context.Elevators | ||
.Where(e => e.BuildingId == buildingId) | ||
.ToListAsync(cancellationToken); | ||
|
||
// Update only elevators for this building | ||
foreach (var elevator in _elevators.Values.Where(e => e.BuildingId == buildingId).ToList()) | ||
{ | ||
_elevators.TryRemove(elevator.Id, out _); | ||
} | ||
|
||
foreach (var elevator in elevators) | ||
{ | ||
_elevators.TryAdd(elevator.Id, ElevatorItem.FromElevator(elevator)); | ||
} | ||
|
||
_lastUpdate = DateTime.UtcNow; | ||
} | ||
|
||
// Create a deep copy of each elevator to ensure thread safety | ||
var allElevators = _elevators.Values | ||
.Where(e => e.BuildingId == buildingId) | ||
.Select(e => e.Clone()) | ||
.ToList(); | ||
|
||
_logger.LogInformation("Returning {ElevatorCount} elevators in building {BuildingId}", | ||
allElevators.Count, buildingId); | ||
return Result.Success<IEnumerable<ElevatorItem>>(allElevators); | ||
} | ||
finally | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "Error getting all elevators for building {BuildingId}", buildingId); | ||
return Result.Failure<IEnumerable<ElevatorItem>>( | ||
new Error("GetAllElevators.Error", ex.Message, ErrorType.Failure)); | ||
} | ||
} | ||
|
||
private void Dispose(bool disposing) | ||
{ | ||
if (!_disposed) | ||
{ | ||
if (disposing) | ||
{ | ||
_semaphore.Dispose(); | ||
} | ||
_disposed = true; | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
Dispose(true); | ||
GC.SuppressFinalize(this); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace Domain.Elevators; | ||
|
||
/// <summary> Data transfer object representing an elevator in memory. </summary> | ||
public class ElevatorItem | ||
{ | ||
/// <summary> Get or set the unique identifier for the elevator. </summary> | ||
public Guid Id { get; set; } | ||
|
||
/// <summary> Get or set the number of the elevator. </summary> | ||
public int Number { get; set; } | ||
|
||
/// <summary> Get or set the current floor the elevator is on. </summary> | ||
public int CurrentFloor { get; set; } | ||
|
||
/// <summary> Get or set the direction the elevator is moving. </summary> | ||
[JsonConverter(typeof(JsonStringEnumConverter))] | ||
public ElevatorDirection ElevatorDirection { get; set; } | ||
|
||
/// <summary> Get or set the status of the elevator. </summary> | ||
[JsonConverter(typeof(JsonStringEnumConverter))] | ||
public ElevatorStatus ElevatorStatus { get; set; } | ||
|
||
/// <summary> Get or set the type of elevator. </summary> | ||
[JsonConverter(typeof(JsonStringEnumConverter))] | ||
public ElevatorType ElevatorType { get; set; } | ||
|
||
/// <summary> Get or set the speed of the elevator. </summary> | ||
public double Speed { get; set; } = 0.5; | ||
|
||
/// <summary> Get or set the capacity of the elevator. </summary> | ||
public int Capacity { get; set; } = 10; | ||
|
||
/// <summary> Get or set the unique identifier of the building the elevator is in. </summary> | ||
public Guid BuildingId { get; set; } | ||
|
||
public static ElevatorItem FromElevator(Elevator elevator) => new() | ||
{ | ||
Id = elevator.Id, | ||
Number = elevator.Number, | ||
CurrentFloor = elevator.CurrentFloor, | ||
ElevatorDirection = elevator.ElevatorDirection, | ||
ElevatorStatus = elevator.ElevatorStatus, | ||
ElevatorType = elevator.ElevatorType, | ||
Speed = elevator.Speed, | ||
Capacity = elevator.Capacity, | ||
BuildingId = elevator.BuildingId | ||
}; | ||
|
||
public Elevator ToElevator() => new() | ||
{ | ||
Id = Id, | ||
Number = Number, | ||
CurrentFloor = CurrentFloor, | ||
ElevatorDirection = ElevatorDirection, | ||
ElevatorStatus = ElevatorStatus, | ||
ElevatorType = ElevatorType, | ||
Speed = Speed, | ||
Capacity = Capacity, | ||
BuildingId = BuildingId | ||
}; | ||
|
||
/// <summary> Creates a deep copy of the elevator item. </summary> | ||
public ElevatorItem Clone() => new() | ||
{ | ||
Id = Id, | ||
Number = Number, | ||
CurrentFloor = CurrentFloor, | ||
ElevatorDirection = ElevatorDirection, | ||
ElevatorStatus = ElevatorStatus, | ||
ElevatorType = ElevatorType, | ||
Speed = Speed, | ||
Capacity = Capacity, | ||
BuildingId = BuildingId | ||
}; | ||
} |
Oops, something went wrong.