mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:16:30 +00:00
throttle reimplemented
This commit is contained in:
@@ -261,11 +261,11 @@ public partial class Annotator
|
|||||||
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||||
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||||
|
|
||||||
await ThrottleExt.Throttle(() =>
|
ThrottleExt.Throttle(() =>
|
||||||
{
|
{
|
||||||
_configUpdater.Save(_appConfig);
|
_configUpdater.Save(_appConfig);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}, TimeSpan.FromSeconds(5));
|
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowTimeAnnotations(TimeSpan time)
|
private void ShowTimeAnnotations(TimeSpan time)
|
||||||
|
|||||||
@@ -1,22 +1,70 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
public static class ThrottleExt
|
public static class ThrottleExt
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<Delegate, DateTime> LastExecution = new();
|
private class ThrottleState(Func<Task> action)
|
||||||
private static readonly object Lock = new();
|
|
||||||
|
|
||||||
public static async Task Throttle(this Func<Task> func, TimeSpan interval, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(func);
|
public Func<Task> Action { get; } = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
|
public bool IsCoolingDown = false;
|
||||||
|
public bool CallScheduledDuringCooldown = false;
|
||||||
|
public Task CooldownTask = Task.CompletedTask;
|
||||||
|
public readonly object StateLock = new();
|
||||||
|
}
|
||||||
|
|
||||||
lock (Lock)
|
private static readonly ConcurrentDictionary<Guid, ThrottleState> ThrottlerStates = new();
|
||||||
|
|
||||||
|
public static void Throttle(Func<Task> action, Guid actionId, TimeSpan interval)
|
||||||
{
|
{
|
||||||
if (LastExecution.ContainsKey(func) && DateTime.UtcNow - LastExecution[func] < interval)
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
return;
|
if (actionId == Guid.Empty)
|
||||||
|
throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId));
|
||||||
|
if (interval <= TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive.");
|
||||||
|
|
||||||
func();
|
var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action));
|
||||||
LastExecution[func] = DateTime.UtcNow;
|
|
||||||
|
lock (state.StateLock)
|
||||||
|
{
|
||||||
|
if (!state.IsCoolingDown)
|
||||||
|
{
|
||||||
|
state.IsCoolingDown = true;
|
||||||
|
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.CallScheduledDuringCooldown = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await state.Action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Task.Delay(interval);
|
||||||
|
|
||||||
|
lock (state.StateLock)
|
||||||
|
{
|
||||||
|
if (state.CallScheduledDuringCooldown)
|
||||||
|
{
|
||||||
|
state.CallScheduledDuringCooldown = false;
|
||||||
|
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.IsCoolingDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -65,25 +65,22 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
Password = _queueConfig.ConsumerPassword
|
Password = _queueConfig.ConsumerPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
var offset = (ulong)(_api.CurrentUser.UserConfig?.QueueConfig?.AnnotationsOffset ?? 0);
|
var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets();
|
||||||
|
|
||||||
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
||||||
{
|
{
|
||||||
Reference = _api.CurrentUser.Email,
|
Reference = _api.CurrentUser.Email,
|
||||||
OffsetSpec = new OffsetTypeOffset(offset + 1),
|
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1),
|
||||||
MessageHandler = async (_, _, context, message) =>
|
MessageHandler = async (_, _, context, message) =>
|
||||||
{
|
{
|
||||||
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
|
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
|
||||||
await _dbFactory.Run(async db => await db.QueueOffsets
|
|
||||||
.Where(x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE)
|
|
||||||
.Set(x => x.Offset, context.Offset)
|
|
||||||
.UpdateAsync(token: cancellationToken));
|
|
||||||
|
|
||||||
await ThrottleExt.Throttle(() =>
|
offsets.AnnotationsOffset = context.Offset;
|
||||||
|
ThrottleExt.Throttle(() =>
|
||||||
{
|
{
|
||||||
_dbFactory.SaveToDisk();
|
_api.UpdateOffsets(offsets);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}, TimeSpan.FromSeconds(10), cancellationToken);
|
}, SaveTaskId, TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
|
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
|
||||||
return;
|
return;
|
||||||
@@ -194,11 +191,11 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
await _producer.SendToInnerQueue(annotation, token);
|
await _producer.SendToInnerQueue(annotation, token);
|
||||||
|
|
||||||
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
||||||
await ThrottleExt.Throttle(() =>
|
ThrottleExt.Throttle(async () =>
|
||||||
{
|
{
|
||||||
_dbFactory.SaveToDisk();
|
_dbFactory.SaveToDisk();
|
||||||
return Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}, TimeSpan.FromSeconds(5), token);
|
}, SaveTaskId, TimeSpan.FromSeconds(5));
|
||||||
return annotation;
|
return annotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ public class User
|
|||||||
|
|
||||||
public class UserConfig
|
public class UserConfig
|
||||||
{
|
{
|
||||||
public UserQueueOffsets? QueueConfig { get; set; } = new();
|
public UserQueueOffsets? QueueOffsets { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserQueueOffsets
|
public class UserQueueOffsets
|
||||||
{
|
{
|
||||||
public int AnnotationsOffset { get; set; }
|
public ulong AnnotationsOffset { get; set; }
|
||||||
public int AnnotationsConfirmOffset { get; set; }
|
public ulong AnnotationsConfirmOffset { get; set; }
|
||||||
public int AnnotationsCommandsOffset { get; set; }
|
public ulong AnnotationsCommandsOffset { get; set; }
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ public interface IAzaionApi
|
|||||||
{
|
{
|
||||||
ApiCredentials Credentials { get; }
|
ApiCredentials Credentials { get; }
|
||||||
User CurrentUser { get; }
|
User CurrentUser { get; }
|
||||||
T? Get<T>(string url);
|
void UpdateOffsets(UserQueueOffsets offsets);
|
||||||
Stream GetResource(string filename);
|
Stream GetResource(string filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,13 +28,27 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
|
|||||||
() => Get<User>("currentUser"));
|
() => Get<User>("currentUser"));
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new Exception("Can't get current user");
|
throw new Exception("Can't get current user");
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct = default)
|
public Stream GetResource(string filename)
|
||||||
|
{
|
||||||
|
var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware);
|
||||||
|
|
||||||
|
var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}")
|
||||||
|
{
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON)
|
||||||
|
});
|
||||||
|
return response.Content.ReadAsStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateOffsets(UserQueueOffsets offsets)
|
||||||
|
{
|
||||||
|
Put($"/users/queue-offsets/{CurrentUser.Email}", offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage Send(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_jwtToken))
|
if (string.IsNullOrEmpty(_jwtToken))
|
||||||
Authorize();
|
Authorize();
|
||||||
@@ -61,15 +75,20 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
|
|||||||
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
|
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream GetResource(string filename)
|
private T? Get<T>(string url)
|
||||||
{
|
{
|
||||||
var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware);
|
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var json = new StreamReader(stream).ReadToEnd();
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}")
|
private void Put<T>(string url, T obj)
|
||||||
{
|
{
|
||||||
Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON)
|
Send(new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
|
{
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
|
||||||
});
|
});
|
||||||
return response.Content.ReadAsStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Authorize()
|
private void Authorize()
|
||||||
@@ -107,11 +126,5 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public T? Get<T>(string url)
|
|
||||||
{
|
|
||||||
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
|
|
||||||
var stream = response.Content.ReadAsStream();
|
|
||||||
var json = new StreamReader(stream).ReadToEnd();
|
|
||||||
return JsonConvert.DeserializeObject<T>(json);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ public partial class App
|
|||||||
|
|
||||||
services.ConfigureSection<InferenceClientConfig>(context.Configuration);
|
services.ConfigureSection<InferenceClientConfig>(context.Configuration);
|
||||||
services.ConfigureSection<GpsDeniedClientConfig>(context.Configuration);
|
services.ConfigureSection<GpsDeniedClientConfig>(context.Configuration);
|
||||||
services.AddSingleton<IInferenceClient, InferenceClient>();
|
services.AddSingleton<IInferenceClient>(_inferenceClient);
|
||||||
services.AddSingleton<IGpsMatcherClient, GpsMatcherClient>();
|
services.AddSingleton<IGpsMatcherClient, GpsMatcherClient>();
|
||||||
services.AddSingleton<IInferenceService, InferenceService>();
|
services.AddSingleton<IInferenceService, InferenceService>();
|
||||||
services.AddSingleton<IGpsMatcherService, GpsMatcherService>();
|
services.AddSingleton<IGpsMatcherService, GpsMatcherService>();
|
||||||
@@ -238,7 +238,7 @@ public partial class App
|
|||||||
{
|
{
|
||||||
var args = (KeyEventArgs)e;
|
var args = (KeyEventArgs)e;
|
||||||
var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow);
|
var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow);
|
||||||
_ = ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50));
|
ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnExit(ExitEventArgs e)
|
protected override async void OnExit(ExitEventArgs e)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using Azaion.Common.DTO;
|
|||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using Azaion.CommonSecurity;
|
|
||||||
using Azaion.CommonSecurity.Services;
|
using Azaion.CommonSecurity.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -130,13 +129,14 @@ public partial class MainSuite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task SaveUserSettings()
|
private async Task SaveUserSettings()
|
||||||
{
|
{
|
||||||
await ThrottleExt.Throttle(() =>
|
ThrottleExt.Throttle(() =>
|
||||||
{
|
{
|
||||||
_configUpdater.Save(_appConfig);
|
_configUpdater.Save(_appConfig);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}, TimeSpan.FromSeconds(2));
|
}, SaveConfigTaskId, TimeSpan.FromSeconds(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFormClosed(object? sender, EventArgs e)
|
private void OnFormClosed(object? sender, EventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user