forked from RPCS3/discord-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathStatsStorage.cs
167 lines (154 loc) · 6.63 KB
/
StatsStorage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace CompatBot.Database.Providers;
internal static class StatsStorage
{
private static readonly TimeSpan CacheTime = TimeSpan.FromDays(1);
private static readonly MemoryCache CmdStatCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromDays(1) });
private static readonly MemoryCache ExplainStatCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromDays(1) });
private static readonly MemoryCache GameStatCache = new(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromDays(1) });
private const char PrefixSeparator = '\0';
private static readonly SemaphoreSlim Barrier = new(1, 1);
private static readonly SemaphoreSlim BucketLock = new(1, 1);
private static readonly (string name, MemoryCache cache)[] AllCaches =
[
(nameof(CmdStatCache), CmdStatCache),
(nameof(ExplainStatCache), ExplainStatCache),
(nameof(GameStatCache), GameStatCache),
];
private static ((int y, int m, int d, int h) Key, string Value) bucketPrefix = ((0, 0, 0, 0), "");
private static string Prefix
{
get
{
var ts = DateTime.UtcNow;
var key = (ts.Year, ts.Month, ts.Day, ts.Hour);
if (bucketPrefix.Key == key)
return bucketPrefix.Value;
if (!BucketLock.Wait(0))
return bucketPrefix.Value;
bucketPrefix = (key, ts.ToString("yyyyMMddHH") + PrefixSeparator);
BucketLock.Release();
return bucketPrefix.Value;
}
}
public static void IncCmdStat(string qualifiedName) => IncStat(qualifiedName, CmdStatCache);
public static void IncExplainStat(string term) => IncStat(term, ExplainStatCache);
public static void IncGameStat(string title) => IncStat(title, GameStatCache);
private static void IncStat(string key, MemoryCache cache)
{
var bucketKey = Prefix + key;
cache.TryGetValue(bucketKey, out int stat);
cache.Set(bucketKey, ++stat, CacheTime);
}
public static List<(string name, int stat)> GetCmdStats() => GetStats(CmdStatCache);
public static List<(string name, int stat)> GetExplainStats() => GetStats(ExplainStatCache);
public static List<(string name, int stat)> GetGameStats() => GetStats(GameStatCache);
private static List<(string name, int stat)> GetStats(MemoryCache cache)
{
return cache.GetCacheKeys<string>()
.Select(c => (name: c.Split(PrefixSeparator, 2)[^1], stat: cache.Get(c) as int?))
.Where(s => s.stat.HasValue)
.GroupBy(s => s.name)
.Select(g => (name: g.Key, stat: (int)g.Sum(s => s.stat)!))
.OrderByDescending(s => s.stat)
.ToList();
}
public static async Task SaveAsync(bool wait = false)
{
if (await Barrier.WaitAsync(0).ConfigureAwait(false))
{
try
{
Config.Log.Debug("Got stats saving lock");
await using var db = new BotDb();
foreach (var (category, cache) in AllCaches)
{
var entries = cache.GetCacheEntries<string>();
var savedKeys = new HashSet<string>();
foreach (var (key, value) in entries)
if (savedKeys.Add(key))
{
var keyParts = key.Split(PrefixSeparator, 2);
var bucket = keyParts.Length == 2 ? keyParts[0] : null;
var statKey = keyParts[^1];
var statValue = (int?)value?.Value ?? 0;
var ts = value?.AbsoluteExpiration?.ToUniversalTime().Ticks ?? 0;
var currentEntry = db.Stats.FirstOrDefault(e => e.Category == category && e.Bucket == bucket && e.Key == statKey);
if (currentEntry is null)
await db.Stats.AddAsync(new()
{
Category = category,
Bucket = bucket,
Key = statKey,
Value = statValue,
ExpirationTimestamp = ts
}).ConfigureAwait(false);
else
{
currentEntry.Value = statValue;
currentEntry.ExpirationTimestamp = ts;
}
}
else
Config.Log.Warn($"Somehow there's another '{key}' in the {category} cache");
}
await db.SaveChangesAsync().ConfigureAwait(false);
}
catch(Exception e)
{
Config.Log.Error(e, "Failed to save user stats");
}
finally
{
Barrier.Release();
Config.Log.Debug("Released stats saving lock");
}
}
else if (wait)
{
await Barrier.WaitAsync().ConfigureAwait(false);
Barrier.Release();
}
}
public static async Task RestoreAsync()
{
var now = DateTime.UtcNow;
await using var db = new BotDb();
foreach (var (category, cache) in AllCaches)
{
var entries = await db.Stats.Where(e => e.Category == category).ToListAsync().ConfigureAwait(false);
foreach (var entry in entries)
{
var time = entry.ExpirationTimestamp.AsUtc();
if (time > now)
{
var key = entry.Key;
if (entry.Bucket is { Length: > 0 } bucket)
key = bucket + PrefixSeparator + key;
cache.Set(key, entry.Value, time);
}
else
{
db.Stats.Remove(entry);
}
}
}
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
}
public static async Task BackgroundSaveAsync()
{
while (!Config.Cts.IsCancellationRequested)
{
await Task.Delay(60 * 60 * 1000, Config.Cts.Token).ConfigureAwait(false);
if (!Config.Cts.IsCancellationRequested)
await SaveAsync().ConfigureAwait(false);
}
}
}