Skip to content
Open
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
Expand Up @@ -338,13 +338,13 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria
public void WriteYaml(IEmitter emitter, object value, Type type)
{
var timeSpan = (TimeSpan)value;
emitter.Emit(new Scalar(timeSpan.ToString(@"hh\:mm")));
emitter.Emit(new Scalar(timeSpan.ToString(@"HH\:mm")));
}

public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
{
var timeSpan = (TimeSpan)value;
emitter.Emit(new Scalar(timeSpan.ToString(@"hh\:mm")));
emitter.Emit(new Scalar(timeSpan.ToString(@"HH\:mm")));
}
}

Expand Down
145 changes: 140 additions & 5 deletions src/server/HSMServer.Core/AlertSchedule/AlertScheduleProvider.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NLog;
using HSMServer.Core.DataLayer;
using HSMServer.Core.Model.Policies;



namespace HSMServer.Core.Schedule
{
public class AlertScheduleProvider : IAlertScheduleProvider
private readonly struct CacheEntryKey : IEquatable<CacheEntryKey>
{
public DateTime StartTime { get; }
public DateTime EndTime { get; }

public CacheEntryKey(DateTime startTime, DateTime endTime)
{
StartTime = startTime;
EndTime = endTime;
}

public bool Equals(CacheEntryKey other) =>
StartTime == other.StartTime && EndTime == other.EndTime;

public override bool Equals(object obj) => obj is CacheEntryKey other && Equals(other);

public override int GetHashCode() => HashCode.Combine(StartTime, EndTime);

public static bool operator ==(CacheEntryKey left, CacheEntryKey right) => left.Equals(right);
public static bool operator !=(CacheEntryKey left, CacheEntryKey right) => !left.Equals(right);
}

public class AlertScheduleProvider : IAlertScheduleProvider, IDisposable
{
private readonly TimeSpan CLEANUP_PERIOD = TimeSpan.FromMinutes(5);

private readonly IDatabaseCore _database;
private readonly AlertScheduleParser _parser = new();
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
Expand All @@ -19,12 +43,18 @@ public class AlertScheduleProvider : IAlertScheduleProvider

private readonly object _lock = new object();

private readonly Timer _cleanupTimer;
private bool _disposed = false;

private class CacheEntry
{
public AlertSchedule Schedule { get; set; }
public DateTime? CachedTime { get; set; }
public bool? WorkingTimeResult { get; set; }

public Dictionary<CacheEntryKey, bool> IntervalCache { get; set; } = new();


public bool IsCacheValid(DateTime time)
{
if (!CachedTime.HasValue || !WorkingTimeResult.HasValue)
Expand All @@ -33,7 +63,28 @@ public bool IsCacheValid(DateTime time)
return RoundToMinute(CachedTime.Value) == RoundToMinute(time);
}

private DateTime RoundToMinute(DateTime time)
public void AddIntervalToCache(DateTime startTime, DateTime endTime, bool result)
{
var key = new CacheEntryKey(startTime, endTime);
IntervalCache[key] = result;
}

public bool? GetCachedIntervalResult(DateTime startTime, DateTime endTime)
{
var key = new CacheEntryKey(startTime, endTime);
if (IntervalCache.TryGetValue(key, out var result))
return result;
return null;
}

public void InvalidateCache()
{
CachedTime = null;
WorkingTimeResult = null;
IntervalCache.Clear();
}

private static DateTime RoundToMinute(DateTime time)
{
return new DateTime(time.Year, time.Month, time.Day,
time.Hour, time.Minute, 0, time.Kind);
Expand All @@ -45,6 +96,11 @@ public AlertScheduleProvider(IDatabaseCore database)
{
_database = database;
LoadSchedulesFromDatabase();

_cleanupTimer = new Timer(_ => CleanupIntervalCache(),
null,
CLEANUP_PERIOD,
CLEANUP_PERIOD);
}

public bool IsWorkingTime(Guid id, DateTime time)
Expand Down Expand Up @@ -73,6 +129,35 @@ public bool IsWorkingTime(Guid id, DateTime time)
}
}

public bool IsWorkingTime(Guid id, DateTime startTime, DateTime endTime)
{
if (startTime >= endTime)
throw new ArgumentException("Start time must be less than end time", nameof(startTime));

lock (_lock)
{
if (_cache.TryGetValue(id, out var cacheEntry))
{
var cachedResult = cacheEntry.GetCachedIntervalResult(startTime, endTime);
if (cachedResult.HasValue)
{
return cachedResult.Value;
}

var result = cacheEntry.Schedule.IsWorkingTime(startTime, endTime);

cacheEntry.AddIntervalToCache(startTime, endTime, result);

return result;
}
else
{
_logger.Error($"Alert Schedule with id = {id} was not found.");
return true;
}
}
}

public void DeleteSchedule(Guid id)
{
lock (_lock)
Expand Down Expand Up @@ -106,8 +191,7 @@ public void SaveSchedule(AlertSchedule schedule)
if (_cache.TryGetValue(schedule.Id, out var cacheEntry))
{
cacheEntry.Schedule = schedule;
cacheEntry.CachedTime = null;
cacheEntry.WorkingTimeResult = null;
cacheEntry.InvalidateCache();
}
else
{
Expand All @@ -121,6 +205,47 @@ public void SaveSchedule(AlertSchedule schedule)
}
}

public void CleanupIntervalCache()
{
try
{
lock (_lock)
{
var cleanupThreshold = DateTime.UtcNow.AddMinutes(-1);
int totalRemoved = 0;

foreach (var cacheEntry in _cache.Values)
{
var keysToRemove = new HashSet<CacheEntryKey>();

foreach (var kvp in cacheEntry.IntervalCache)
{
if (kvp.Key.EndTime < cleanupThreshold)
{
keysToRemove.Add(kvp.Key);
}
}

foreach (var key in keysToRemove)
{
cacheEntry.IntervalCache.Remove(key);
}

totalRemoved += keysToRemove.Count;
}

if (totalRemoved > 0)
{
_logger.Debug($"Cleaned up {totalRemoved} expired interval cache entries");
}
}
}
catch (Exception ex)
{
_logger.Error(ex, "Error during interval cache cleanup");
}
}

private void LoadSchedulesFromDatabase()
{
var scheduleEntities = _database.GetAllAlertSchedules();
Expand Down Expand Up @@ -152,5 +277,15 @@ private void LoadSchedulesFromDatabase()
}
}
}

public void Dispose()
{
if (!_disposed)
{
_cleanupTimer?.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ public interface IAlertScheduleProvider
void DeleteSchedule(Guid Id);

bool IsWorkingTime(Guid id, DateTime time);

bool IsWorkingTime(Guid id, DateTime startTime, DateTime endTime);
}
}
1 change: 1 addition & 0 deletions src/server/HSMServer.Core/Cache/ITreeValuesCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,6 @@ public interface ITreeValuesCache

Task RunProductsSelfDestroyAsync(CancellationToken token = default);

List<BaseSensorModel> GetSensorsByAlertSchedule(Guid id);
}
}
18 changes: 18 additions & 0 deletions src/server/HSMServer.Core/Cache/TreeValuesCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2292,5 +2292,23 @@ private void RemoveProductFromCache(ProductModel product)
_cache.TryRemove(product.Id, out var value);
}

public List<BaseSensorModel> GetSensorsByAlertSchedule(Guid id)
{
var result = new List<BaseSensorModel>();

foreach (var sensor in _sensorsById.Values)
{
if (sensor.Policies.TimeToLive?.ScheduleId == id)
{
result.Add(sensor);
continue;
}

if (sensor.Policies.Any(policy => policy.ScheduleId == id))
result.Add(sensor);
}

return result;
}
}
}
52 changes: 51 additions & 1 deletion src/server/HSMServer.Core/Model/Policies/AlertSchedule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,66 @@ public bool IsWorkingTime(DateTime dateTime)
timeOfDay >= w.Start && timeOfDay < w.End) ?? false;
}

public bool IsWorkingTime(DateTime startTime, DateTime endTime)
{
if (startTime >= endTime)
throw new ArgumentException("Start time must be less than end time", nameof(startTime));

var currentTime = ConvertUtcToLocalTime(startTime);
var end = ConvertUtcToLocalTime(endTime);

while (currentTime.Date <= end.Date)
{
var date = currentTime.Date;

var workingWindows = GetWorkingWindowsForDate(date);

if (workingWindows.Count != 0)
{
var dayStart = currentTime;
var dayEnd = currentTime.Date.AddDays(1);

if (date == end.Date)
dayEnd = end;

foreach (var window in workingWindows)
{
var windowStart = date.Add(window.Start);
var windowEnd = date.Add(window.End);

if (dayStart < windowEnd && dayEnd > windowStart)
return true;
}
}

currentTime = currentTime.Date.AddDays(1);
}

return false;
}

private DateTime ConvertUtcToLocalTime(DateTime utcDateTime)
{
var timezone = TimeZoneInfo.FindSystemTimeZoneById(Timezone);
var timezone = TryGetTimeZone() ?? TimeZoneInfo.Utc;
var utc = utcDateTime.Kind == DateTimeKind.Local
? utcDateTime.ToUniversalTime()
: DateTime.SpecifyKind(utcDateTime, DateTimeKind.Utc);

return TimeZoneInfo.ConvertTimeFromUtc(utc, timezone);
}

private TimeZoneInfo TryGetTimeZone()
{
try
{
return TimeZoneInfo.FindSystemTimeZoneById(Timezone);
}
catch (TimeZoneNotFoundException)
{
return null;
}
}

private DaySchedule GetDaySchedule(DayOfWeek dayOfWeek)
{
return DaySchedules?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ protected override bool CalculateStorageResult(ValueType value, bool isLastValue
bool schedulePassed = true;
if (policy.ScheduleId.HasValue)
{
schedulePassed = _scheduleProvider.IsWorkingTime(policy.ScheduleId.Value, value.LastUpdateTime);
if (value is BarBaseValue barValue)
schedulePassed = _scheduleProvider.IsWorkingTime(policy.ScheduleId.Value, barValue.OpenTime, barValue.CloseTime);
else
schedulePassed = _scheduleProvider.IsWorkingTime(policy.ScheduleId.Value, value.LastUpdateTime);
}

if (!schedulePassed)
Expand Down
Loading
Loading