Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/bertus_elevator_pool #12

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading