summaryrefslogtreecommitdiff
path: root/Util
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-09-27 03:38:58 +1000
committerJake Mannens <jake@asger.xyz>2023-09-27 03:38:58 +1000
commitbc82b2dc2f7405c0fd4d179830412ea8209137b1 (patch)
tree26fdaa8635a1487e3ae9fd112336be8df0f723ec /Util
parent39eead0052215d7be4f49906e987fef7fb0c700b (diff)
Added MemoryCache class and implemented principal/ACL cache in the security service
Diffstat (limited to 'Util')
-rw-r--r--Util/Extensions.cs33
-rw-r--r--Util/LimitedConcurrencyTaskScheduler.cs88
-rw-r--r--Util/MemoryCache.cs98
3 files changed, 219 insertions, 0 deletions
diff --git a/Util/Extensions.cs b/Util/Extensions.cs
new file mode 100644
index 0000000..bbf9a12
--- /dev/null
+++ b/Util/Extensions.cs
@@ -0,0 +1,33 @@
+namespace HyperBooru.Util;
+
+public static class Extensions {
+ public static readonly string[] MagnitudeOrders = new[] {
+ "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"
+ };
+
+ public static DateTime? TryParseDateTimeUtc(this string s) {
+ bool success = DateTime.TryParse(s, out var dateTime);
+ return success ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) : null;
+ }
+
+ public static string ToBytesSI(this long x) {
+ var exp = (int) Math.Log10(x);
+ var suffix = MagnitudeOrders.ElementAtOrDefault(exp / 3 - 1);
+ if(suffix is null)
+ return x.ToString();
+ double n = x / Math.Pow(10, exp / 3 * 3);
+ return $"{Math.Round(n, 2 - (exp % 3))} {suffix}B";
+ }
+
+ public static string ToStringHumanReadable(this TimeSpan t) {
+ if(t.TotalMilliseconds < 1000)
+ return string.Format("{0:0}ms", t.TotalMilliseconds);
+ if(t.TotalSeconds < 60)
+ return string.Format("{0:0.00}s", t.TotalSeconds);
+ if(t.TotalMinutes < 60)
+ return string.Format("{0:0}m{0:0}s", t.TotalMinutes, t.Seconds);
+ if(t.TotalHours < 24)
+ return string.Format("{0:0}h{0:0}m", t.TotalHours, t.Minutes);
+ return string.Format("{0:0.00}d", t.TotalDays);
+ }
+}
diff --git a/Util/LimitedConcurrencyTaskScheduler.cs b/Util/LimitedConcurrencyTaskScheduler.cs
new file mode 100644
index 0000000..8d5adf2
--- /dev/null
+++ b/Util/LimitedConcurrencyTaskScheduler.cs
@@ -0,0 +1,88 @@
+namespace HyperBooru.Util;
+
+public class LimitedConcurrencyTaskScheduler : TaskScheduler {
+ public sealed override int MaximumConcurrencyLevel =>
+ maxConcurrency;
+
+ private int maxConcurrency;
+
+ [ThreadStatic]
+ private static bool threadIsProcessingItems;
+
+ private readonly LinkedList<Task> tasks = new();
+
+ private int delegatesQueuedOrRunning = 0;
+
+ public LimitedConcurrencyTaskScheduler() {
+ maxConcurrency = Environment.ProcessorCount;
+ }
+
+ public LimitedConcurrencyTaskScheduler(int maxConcurrency) {
+ if(maxConcurrency < 1)
+ throw new ArgumentOutOfRangeException("maxConcurrency must be greater than 0");
+ this.maxConcurrency = (int) maxConcurrency;
+ }
+
+ protected sealed override void QueueTask(Task task) {
+ lock(tasks) {
+ tasks.AddLast(task);
+ if(delegatesQueuedOrRunning < maxConcurrency) {
+ delegatesQueuedOrRunning++;
+ NotifyThreadPoolOfPendingWork();
+ }
+ }
+ }
+
+ private void NotifyThreadPoolOfPendingWork() {
+ ThreadPool.UnsafeQueueUserWorkItem(_ => {
+ threadIsProcessingItems = true;
+ try {
+ while(true) {
+ Task item;
+ lock(tasks) {
+ if(tasks.Count == 0) {
+ delegatesQueuedOrRunning--;
+ break;
+ } else {
+ item = tasks.First.Value;
+ tasks.RemoveFirst();
+ }
+ }
+ TryExecuteTask(item);
+ }
+ } finally {
+ threadIsProcessingItems = false;
+ }
+ }, null);
+ }
+
+ protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) {
+ if(!threadIsProcessingItems)
+ return false;
+
+ if(taskWasPreviouslyQueued)
+ return TryDequeue(task) ? TryExecuteTask(task) : false;
+ else
+ return TryExecuteTask(task);
+ }
+
+ protected sealed override bool TryDequeue(Task task) {
+ lock(tasks) {
+ return tasks.Remove(task);
+ }
+ }
+
+ protected sealed override IEnumerable<Task> GetScheduledTasks() {
+ bool lockTaken = false;
+ try {
+ Monitor.TryEnter(tasks, ref lockTaken);
+ if(lockTaken)
+ return tasks;
+ else
+ throw new NotSupportedException();
+ } finally {
+ if(lockTaken)
+ Monitor.Exit(tasks);
+ }
+ }
+}
diff --git a/Util/MemoryCache.cs b/Util/MemoryCache.cs
new file mode 100644
index 0000000..ba314cf
--- /dev/null
+++ b/Util/MemoryCache.cs
@@ -0,0 +1,98 @@
+using System.Collections;
+
+namespace HyperBooru.Util;
+
+public class MemoryCache<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : struct {
+ /// <summary>Maximum number of items this cache may hold (unlimited if null)</summary>
+ public int? MaxItems { get; init; }
+ /// <summary>Maximum amount of time an item may stay in cache before it is removed (unlimited if null)</summary>
+ public TimeSpan? MaxAge { get; init; }
+ /// <summary>Function that will be called to populate the cache in the event of a cache-miss</summary>
+ public Func<TKey, TValue?>? DataSource { get; init; }
+
+ private Dictionary<TKey, (DateTime createTime, TValue value)> cache = new();
+
+ public TValue this[TKey key] {
+ get => GetValue(key);
+ set {
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ }
+ }
+
+ public TValue GetValue(TKey key) {
+ bool success = cache.TryGetValue(key, out var result);
+ if(success) {
+ if(MaxAge is null)
+ return result.value;
+ if(result.createTime > DateTime.Now - MaxAge)
+ return result.value;
+ }
+
+ if(DataSource is null)
+ throw new KeyNotFoundException();
+
+ TValue? value = DataSource(key);
+
+ if(value is null)
+ throw new KeyNotFoundException();
+
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ return value;
+ }
+
+ public bool TryGetValue(TKey key, out TValue? value) {
+ try {
+ value = GetValue(key);
+ return true;
+ } catch(KeyNotFoundException) {
+ value = default;
+ return false;
+ }
+ }
+
+ public bool Remove(TKey key) => cache.Remove(key);
+
+ public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ foreach(var kv in cache) {
+ // Don't return expired cache items
+ if(expiry is not null)
+ if(kv.Value.createTime < expiry)
+ continue;
+
+ yield return new KeyValuePair<TKey, TValue>(kv.Key, kv.Value.value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ private void Prune() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ // If an expiry time for cache items was
+ // specified, remove expired cache items.
+ if(expiry is not null) {
+ foreach(var kv in cache) {
+ if(kv.Value.createTime < expiry)
+ cache.Remove(kv.Key);
+ }
+ }
+
+ // If this cache was created with a maximum size,
+ // remove elements until that size is reached.
+ if(MaxItems is null || cache.Count() < MaxItems)
+ return;
+
+ var toRemove = cache
+ .OrderBy(kv => kv.Value.createTime)
+ .Take(cache.Count() - (int) MaxItems + 1)
+ .Select(kv => kv.Key)
+ .ToArray();
+
+ foreach(var key in toRemove)
+ cache.Remove(key);
+ }
+}