Skip to content

Commit

Permalink
Merge pull request #12 from bertusviljoen/feature/bertus_elevator_pool
Browse files Browse the repository at this point in the history
feature/bertus_elevator_pool
  • Loading branch information
bertusviljoen authored Dec 9, 2024
2 parents e94d047 + e74e4cd commit 078bf41
Show file tree
Hide file tree
Showing 17 changed files with 1,048 additions and 143 deletions.
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);
}
5 changes: 5 additions & 0 deletions src/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Application.Abstractions.Behaviors;
using Application.Abstractions.Services;
using Application.Services;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -18,6 +20,9 @@ public static IServiceCollection AddApplication(this IServiceCollection services

services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly, includeInternalTypes: true);

// Register elevator services
services.AddSingleton<IInMemoryElevatorPoolService, InMemoryElevatorPoolService>();

return services;
}
}
211 changes: 211 additions & 0 deletions src/Application/Services/InMemoryElevatorPoolService.cs
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);
}
}
2 changes: 2 additions & 0 deletions src/Domain/Elevators/Elevator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class Elevator : AuditableEntity
{
/// <summary> Get or set the unique identifier for the elevator. </summary>
public required Guid Id { get; set; }
/// <summary> Get or set the number of the elevator. </summary>
public required int Number { get; set; }
/// <summary> Get or set the current floor the elevator is on. </summary>
public required int CurrentFloor { get; set; }
/// <summary> Get or set the direction the elevator is moving. </summary>
Expand Down
77 changes: 77 additions & 0 deletions src/Domain/Elevators/ElevatorItem.cs
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
};
}
Loading

0 comments on commit 078bf41

Please sign in to comment.