use read/write locking for sqlite to fix Error 5: 'database is locked'

This commit is contained in:
13xforever
2025-03-27 12:20:33 +05:00
parent 7da98158f6
commit 55f45457a2
56 changed files with 255 additions and 145 deletions

View File

@@ -18,7 +18,7 @@ public class BotConfigurationAutoCompleteProvider: IAutoCompleteProvider
if (!ModProvider.IsSudoer(context.User.Id))
return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IEnumerable<string> result;
var input = context.UserInput;
if (input is not { Length: > 0 })

View File

@@ -11,7 +11,7 @@ public class ContentFilterAutoCompleteProvider: IAutoCompleteProvider
if (!ModProvider.IsMod(context.User.Id))
return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IEnumerable<(int id, string trigger)> result;
if (context.UserInput is not {Length: >0} prefix)
result = db.Piracystring

View File

@@ -7,7 +7,7 @@ public class EventIdAutoCompleteProvider: IAutoCompleteProvider
{
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IEnumerable<EventSchedule> query;
if (context.UserInput is not { Length: > 0 } input)
{

View File

@@ -7,7 +7,7 @@ public class EventNameAutoCompleteProvider: IAutoCompleteProvider
{
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
#if DEBUG
var currentTime = DateTime.UtcNow.AddYears(-10);
#else

View File

@@ -9,7 +9,7 @@ public class ExplainAutoCompleteProvider: IAutoCompleteProvider
{
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IEnumerable<string> result;
if (context.UserInput is not { Length: > 0 } prefix)
{

View File

@@ -11,7 +11,7 @@ public class InviteAutoCompleteProvider: IAutoCompleteProvider
if (!ModProvider.IsMod(context.User.Id))
return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)];
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IQueryable<WhitelistedInvite> result;
if (context.UserInput is not { Length: > 0 } input)
result = db.WhitelistedInvites

View File

@@ -8,7 +8,7 @@ public class ProductCodeAutoCompleteProvider: IAutoCompleteProvider
{
public async ValueTask<IEnumerable<DiscordAutoCompleteChoice>> AutoCompleteAsync(AutoCompleteContext context)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
IEnumerable<(string code, string title)> result;
if (context.UserInput is not { Length: > 0 } prefix)
{

View File

@@ -20,7 +20,7 @@ public class WarningAutoCompleteProvider: IAutoCompleteProvider
? w => w.Retracted && w.DiscordId == userId
: w => !w.Retracted && w.DiscordId == userId;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
List<Warning> result;
if (context.UserInput is not { Length: > 0 } prefix)
result = await db.Warning

View File

@@ -15,7 +15,7 @@ internal static partial class Bot
[Description("List set variable names")]
public static async ValueTask List(SlashCommandContext ctx)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var setVars = await db.BotState
.AsNoTracking()
.Where(v => v.Key.StartsWith(SqlConfiguration.ConfigVarPrefix))
@@ -49,7 +49,7 @@ internal static partial class Bot
Config.InMemorySettings[key] = value;
Config.RebuildConfiguration();
key = SqlConfiguration.ConfigVarPrefix + key;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var stateValue = await db.BotState.FirstOrDefaultAsync(v => v.Key == key).ConfigureAwait(false);
if (stateValue == null)
{
@@ -72,7 +72,7 @@ internal static partial class Bot
Config.InMemorySettings.TryRemove(key, out _);
Config.RebuildConfiguration();
key = SqlConfiguration.ConfigVarPrefix + key;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var stateValue = await db.BotState.Where(v => v.Key == key).FirstOrDefaultAsync().ConfigureAwait(false);
if (stateValue is not null)
{

View File

@@ -17,7 +17,7 @@ internal static partial class Bot
try
{
await CompatList.ImportMetacriticScoresAsync().ConfigureAwait(false);
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var linkedItems = await db.Thumbnail.CountAsync(i => i.MetacriticId != null).ConfigureAwait(false);
await ctx.Channel.SendMessageAsync($"Importing Metacritic info was successful, linked {linkedItems} items").ConfigureAwait(false);
}

View File

@@ -89,7 +89,7 @@ internal static partial class Bot
await using var thumbStream = Config.MemoryStreamManager.GetStream();
if (name != BackupDbType.Hardware)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
if (await BackupDbAsync(db, thumbStream, maxSize).ConfigureAwait(false) is { Length: > 0 } error)
msg += error + '\n';
else
@@ -98,7 +98,7 @@ internal static partial class Bot
await using var hwStream = Config.MemoryStreamManager.GetStream();
if (name != BackupDbType.Thumbs)
{
await using var db = new HardwareDb();
await using var db = HardwareDb.OpenRead();
if (await BackupDbAsync(db, hwStream, maxSize).ConfigureAwait(false) is { Length: > 0 } error)
msg += error + '\n';
else
@@ -146,7 +146,7 @@ internal static partial class Bot
{
try
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var status = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-activity").ConfigureAwait(false);
var txt = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-text").ConfigureAwait(false);
if (message is {Length: >0})
@@ -187,7 +187,7 @@ internal static partial class Bot
string? dbName = null;
try
{
await using var botDb = new BotDb();
await using var botDb = BotDb.OpenRead();
string dbPath, dbDir;
await using (var connection = db.Database.GetDbConnection())
{
@@ -305,7 +305,7 @@ internal static partial class Bot
internal static void Restart(ulong channelId, string? restartMsg)
{
Config.Log.Info($"Saving channelId {channelId} into settings…");
using var db = new BotDb();
using var db = BotDb.OpenRead();
var ch = db.BotState.FirstOrDefault(k => k.Key == "bot-restart-channel");
if (ch is null)
{

View File

@@ -82,7 +82,7 @@ internal static class BotStatus
{
try
{
using var db = new BotDb();
using var db = BotDb.OpenRead();
var timestamps = db.Warning
.Where(w => w.Timestamp.HasValue && !w.Retracted)
.OrderBy(w => w.Timestamp)
@@ -220,7 +220,7 @@ internal static class BotStatus
{
try
{
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var syscallCount = db.SyscallInfo.AsNoTracking().Where(sci => sci.Function.StartsWith("sys_") || sci.Function.StartsWith("_sys_")).Distinct().Count();
var totalFuncCount = db.SyscallInfo.AsNoTracking().Select(sci => sci.Function).Distinct().Count();
var fwCallCount = totalFuncCount - syscallCount;
@@ -241,7 +241,7 @@ internal static class BotStatus
{
try
{
using var db = new HardwareDb();
using var db = HardwareDb.OpenRead();
var monthAgo = DateTime.UtcNow.AddDays(-30).Ticks;
var monthCount = db.HwInfo.Count(i => i.Timestamp > monthAgo);
if (monthCount == 0)
@@ -272,7 +272,7 @@ internal static class BotStatus
{
try
{
using var db = new BotDb();
using var db = BotDb.OpenRead();
var kots = db.Kot.Count();
var doggos = db.Doggo.Count();
if (kots == 0 && doggos == 0)

View File

@@ -155,7 +155,7 @@ internal static partial class CompatList
await compatChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
lastUpdateInfo = latestUpdatePr;
lastFullBuildNumber = latestUpdateBuild;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var currentState = await db.BotState.FirstOrDefaultAsync(k => k.Key == Rpcs3UpdateStateKey).ConfigureAwait(false);
if (currentState == null)
await db.BotState.AddAsync(new() {Key = Rpcs3UpdateStateKey, Value = latestUpdatePr}).ConfigureAwait(false);

View File

@@ -28,7 +28,7 @@ internal static partial class CompatList
if (!Enum.TryParse(status, true, out CompatStatus s))
s = CompatStatus.Playable;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var queryBase = db.Thumbnail.AsNoTracking();
if (exactStatus)
queryBase = queryBase.Where(g => g.CompatibilityStatus == s);

View File

@@ -33,7 +33,7 @@ internal static partial class CompatList
static CompatList()
{
using var db = new BotDb();
using var db = BotDb.OpenRead();
lastUpdateInfo = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateStateKey)?.Value;
lastFullBuildNumber = db.BotState.FirstOrDefault(k => k.Key == Rpcs3UpdateBuildKey)?.Value;
//lastUpdateInfo = "8022";
@@ -133,7 +133,7 @@ internal static partial class CompatList
{
var timer = Stopwatch.StartNew();
var title = requestBuilder.Search;
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var matches = db.Thumbnail
.AsNoTracking()
.AsEnumerable()
@@ -227,7 +227,7 @@ internal static partial class CompatList
if (list is null)
return;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
foreach (var kvp in list.Results)
{
var (productCode, info) = kvp;
@@ -306,7 +306,7 @@ internal static partial class CompatList
.Select(i => i.WithTitle(i.Title.Replace("HAWX", "H.A.W.X")))
);
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
foreach (var mcScore in scoreList.Where(s => s.CriticScore > 0 || s.UserScore > 0))
{
if (Config.Cts.IsCancellationRequested)

View File

@@ -37,7 +37,7 @@ internal sealed partial class ContentFilters
new AsciiColumn("Actions"),
new AsciiColumn("Custom message", maxWidth: 2048)
);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var duplicates = new Dictionary<string, FilterContext>(StringComparer.InvariantCultureIgnoreCase);
var filters = db.Piracystring.Where(ps => !ps.Disabled).AsNoTracking().AsEnumerable().OrderBy(ps => ps.String.ToUpperInvariant()).ToList();
var nonUniqueTriggers = (
@@ -130,7 +130,7 @@ internal sealed partial class ContentFilters
}
explanation = explanation?.ToLowerInvariant();
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
if (explanation is {Length: >0} && !await db.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false))
{
await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral).ConfigureAwait(false);
@@ -235,7 +235,7 @@ internal sealed partial class ContentFilters
return;
}
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
foreach (var element in xml.Root.Elements("game"))
{
var name = element.Element("rom")?.Attribute("name")?.Value;
@@ -312,7 +312,7 @@ internal sealed partial class ContentFilters
}
}
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
explanation = explanation?.ToLowerInvariant();
if (explanation is {Length: >0} && !await db.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false))
{
@@ -367,7 +367,7 @@ internal sealed partial class ContentFilters
)
{
var ephemeral = !ctx.Channel.IsPrivate;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id && !ps.Disabled).ConfigureAwait(false);
if (filter is null)
{
@@ -391,7 +391,7 @@ internal sealed partial class ContentFilters
var ephemeral = !ctx.Channel.IsPrivate;
int removedFilters;
var removedTriggers = new StringBuilder();
await using (var db = new BotDb())
await using (var db = BotDb.OpenRead())
{
foreach (var f in db.Piracystring.Where(ps => ps.Id == id && !ps.Disabled))
{

View File

@@ -30,7 +30,7 @@ internal static partial class Events
var current = DateTime.UtcNow;
#endif
var currentTicks = current.Ticks;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var currentEvents = await db.EventSchedule.OrderBy(e => e.End).Where(e => e.Start <= currentTicks && e.End >= currentTicks).ToListAsync().ConfigureAwait(false);
var nextEvent = await db.EventSchedule.OrderBy(e => e.Start).FirstOrDefaultAsync(e => e.Start > currentTicks).ConfigureAwait(false);
if (string.IsNullOrEmpty(name))
@@ -203,7 +203,7 @@ internal static partial class Events
evt.Name = name;
evt.EventName = string.IsNullOrWhiteSpace(@event) || @event == "-" ? null : @event;
evt.Year = newTime.Year;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
await db.EventSchedule.AddAsync(evt).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
await ctx.RespondAsync(embed: FormatEvent(evt).WithTitle("Created new event schedule entry #" + evt.Id), ephemeral: ephemeral).ConfigureAwait(false);
@@ -218,7 +218,7 @@ internal static partial class Events
)
{
var ephemeral = !ctx.Channel.IsSpamChannel();
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var eventsToRemove = await db.EventSchedule.Where(evt => evt.Id == id).ToListAsync().ConfigureAwait(false);
db.EventSchedule.RemoveRange(eventsToRemove);
var removedCount = await db.SaveChangesAsync().ConfigureAwait(false);
@@ -234,7 +234,7 @@ internal static partial class Events
public Task ClearGeneric(CommandContext ctx, [Description("Optional year to remove, by default everything before current year")] int? year = null)
{
var currentYear = DateTime.UtcNow.Year;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var itemsToRemove = await db.EventSchedule.Where(e =>
year.HasValue
? e.Year == year
@@ -262,7 +262,7 @@ internal static partial class Events
)
{
var ephemeral = !ctx.Channel.IsSpamChannel();
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var evt = db.EventSchedule.FirstOrDefault(e => e.Id == id);
if (evt is null)
{
@@ -312,7 +312,7 @@ internal static partial class Events
var ephemeral = !ctx.Channel.IsSpamChannel() || ModProvider.IsMod(ctx.User.Id);
var showAll = "all".Equals(@event, StringComparison.InvariantCultureIgnoreCase);
var currentTicks = DateTime.UtcNow.Ticks;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IQueryable<EventSchedule> query = db.EventSchedule;
if (year.HasValue)
query = query.Where(e => e.Year == year);

View File

@@ -149,7 +149,7 @@ internal static class Explain
var response = new DiscordInteractionResponseBuilder().AsEphemeral();
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, response).ConfigureAwait(false);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
if (await db.Explanation.AnyAsync(e => e.Keyword == term).ConfigureAwait(false))
{
await interaction.EditOriginalResponseAsync(
@@ -224,7 +224,7 @@ internal static class Explain
}
}
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item is null)
{
@@ -298,7 +298,7 @@ internal static class Explain
)
{
term = term.ToLowerInvariant().StripQuotes();
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item is null)
{
@@ -335,7 +335,7 @@ internal static class Explain
{
var ephemeral = !ctx.Channel.IsSpamChannel();
await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var allTerms = await db.Explanation.AsNoTracking().Select(e => e.Keyword).ToListAsync();
await using var stream = Config.MemoryStreamManager.GetStream();
await using var writer = new StreamWriter(stream);
@@ -357,7 +357,7 @@ internal static class Explain
string term
)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
if (item is null)
{
@@ -435,7 +435,7 @@ internal static class Explain
internal static async ValueTask<(Explanation? explanation, string? fuzzyMatch, double score)> LookupTerm(string term)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
string? fuzzyMatch = null;
double coefficient;
var explanation = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);

View File

@@ -72,7 +72,7 @@ internal static class ForcedNicknames
guilds = [ctx.Guild];
int changed = 0, noPermissions = 0, failed = 0;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
foreach (var guild in guilds)
{
if (!discordUser.IsBotSafeCheck())
@@ -157,7 +157,7 @@ internal static class ForcedNicknames
return;
}
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var enforcedRules = ctx.Guild is null
? await db.ForcedNicknames.Where(mem => mem.UserId == discordUser.Id).ToListAsync().ConfigureAwait(false)
: await db.ForcedNicknames.Where(mem => mem.UserId == discordUser.Id && mem.GuildId == ctx.Guild.Id).ToListAsync().ConfigureAwait(false);
@@ -256,7 +256,7 @@ internal static class ForcedNicknames
[Description("Lists all users who has restricted nickname.")]
public async Task List(CommandContext ctx)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var selectExpr = db.ForcedNicknames.AsNoTracking();
if (ctx.Guild != null)
selectExpr = selectExpr.Where(mem => mem.GuildId == ctx.Guild.Id);

View File

@@ -50,7 +50,7 @@ internal static class Fortune
await ctx.RespondAsync("Importing…", ephemeral: true).ConfigureAwait(false);
var stopwatch = Stopwatch.StartNew();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
using var httpClient = HttpClientFactory.Create(new CompressionMessageHandler());
using var request = new HttpRequestMessage(HttpMethod.Get, url);
var response = await httpClient.SendAsync(request, cts.Token).ConfigureAwait(false);
@@ -149,7 +149,7 @@ internal static class Fortune
var count = 0;
await using var outputStream = Config.MemoryStreamManager.GetStream();
await using var writer = new StreamWriter(outputStream);
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
foreach (var fortune in db.Fortune.AsNoTracking())
{
if (Config.Cts.Token.IsCancellationRequested)
@@ -184,7 +184,7 @@ internal static class Fortune
}
await ctx.DeferResponseAsync(true).ConfigureAwait(false);
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
db.Fortune.RemoveRange(db.Fortune);
var count = await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
await ctx.RespondAsync($"{Config.Reactions.Success} Removed {count} fortune{(count == 1 ? "" : "s")}", ephemeral: true).ConfigureAwait(false);
@@ -194,7 +194,7 @@ internal static class Fortune
{
var prefix = DateTime.UtcNow.ToString("yyyyMMdd")+ user.Id.ToString("x16");
var rng = new Random(prefix.GetStableHash());
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
Database.Fortune? fortune;
do
{

View File

@@ -17,7 +17,7 @@ internal static class Hardware
var maxDays = DateTime.UtcNow - new DateTime(2011, 5, 23, 0, 0, 0, DateTimeKind.Utc);
period = Math.Clamp(Math.Abs(period), 1, (int)maxDays.TotalDays);
var ts = DateTime.UtcNow.AddDays(-period).Ticks;
await using var db = new HardwareDb();
await using var db = HardwareDb.OpenRead();
var count = await db.HwInfo.AsNoTracking().CountAsync(i => i.Timestamp > ts).ConfigureAwait(false);
if (count is 0)
{

View File

@@ -16,7 +16,7 @@ internal static class Invites
public static async ValueTask List(SlashCommandContext ctx)
{
const string linkPrefix = "discord.gg/";
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var whitelistedInvites = await db.WhitelistedInvites.ToListAsync().ConfigureAwait(false);
if (whitelistedInvites.Count is 0)
{
@@ -144,7 +144,7 @@ internal static class Invites
[Description("Custom server name"), MinMaxLength(3)] string name
)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var invite = await db.WhitelistedInvites.FirstOrDefaultAsync(i => i.Id == id).ConfigureAwait(false);
if (invite is null)
{

View File

@@ -242,7 +242,7 @@ internal static partial class Misc
public static async ValueTask Game(SlashCommandContext ctx)
{
var ephemeral = !ctx.Channel.IsSpamChannel();
var db = new ThumbnailDb();
var db = ThumbnailDb.OpenRead();
await using var _ = db.ConfigureAwait(false);
var count = await db.Thumbnail.CountAsync().ConfigureAwait(false);
if (count is 0)

View File

@@ -80,7 +80,7 @@ internal static partial class Psn
return;
var newVersion = fwList[0].Version;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var fwVersionState = db.BotState.FirstOrDefault(s => s.Key == "Latest-Firmware-Version");
latestFwVersion ??= fwVersionState?.Value;
if (latestFwVersion is null

View File

@@ -25,7 +25,7 @@ internal static partial class Psn
{
var ephemeral = !ctx.Channel.IsSpamChannel();
productCode = productCode.ToUpperInvariant();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var item = db.Thumbnail.AsNoTracking().FirstOrDefault(t => t.ProductCode == productCode);
if (item is null)
await ctx.RespondAsync($"{Config.Reactions.Failure} Unknown product code {productCode}", ephemeral: true).ConfigureAwait(false);
@@ -67,7 +67,7 @@ internal static partial class Psn
return;
}
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var item = db.Thumbnail.AsNoTracking().FirstOrDefault(t => t.ProductCode == productCode);
if (item is null)
{

View File

@@ -25,7 +25,7 @@ internal static partial class Sudo
try
{
var @fixed = 0;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
foreach (var warning in db.Warning)
if (!string.IsNullOrEmpty(warning.FullReason))
{
@@ -54,7 +54,7 @@ internal static partial class Sudo
try
{
var @fixed = 0;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
foreach (var warning in db.Warning)
{
var newReason = await FixChannelMentionAsync(ctx, warning.Reason).ConfigureAwait(false);
@@ -106,7 +106,7 @@ internal static partial class Sudo
public static async ValueTask TitleMarks(TextCommandContext ctx)
{
var changed = 0;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
foreach (var thumb in db.Thumbnail)
{
if (string.IsNullOrEmpty(thumb.Name))
@@ -135,7 +135,7 @@ internal static partial class Sudo
public static async ValueTask MetacriticLinks(TextCommandContext ctx, [Description("Remove links for trial and demo versions only")] bool demosOnly = true)
{
var changed = 0;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
foreach (var thumb in db.Thumbnail.Where(t => t.MetacriticId != null))
{
if (demosOnly

View File

@@ -26,7 +26,7 @@ internal static class Syscall
return;
}
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
if (db.SyscallInfo.Any(sci => sci.Function == search))
{
var productInfoList = db.SyscallToProductMap
@@ -119,7 +119,7 @@ internal static class Syscall
string newFunctionName
)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var oldMatches = await db.SyscallInfo.Where(sci => sci.Function == oldFunctionName).ToListAsync().ConfigureAwait(false);
if (oldMatches.Count is 0)
{
@@ -148,7 +148,7 @@ internal static class Syscall
private static async ValueTask ReturnSyscallsByGameAsync(SlashCommandContext ctx, string productId, bool ephemeral)
{
productId = productId.ToUpperInvariant();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var title = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productId)?.Name;
title = string.IsNullOrEmpty(title) ? productId : $"[{productId}] {title.Trim(40)}";
var sysInfoList = db.SyscallToProductMap.AsNoTracking()

View File

@@ -40,7 +40,7 @@ internal static partial class Warnings
new AsciiColumn("Count", alignToRight: true),
new AsciiColumn("All time", alignToRight: true)
);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var query = from warn in db.Warning.AsEnumerable()
group warn by warn.DiscordId
into userGroup
@@ -84,7 +84,7 @@ internal static partial class Warnings
new AsciiColumn("Warnings given", alignToRight: true),
new AsciiColumn("Including retracted", alignToRight: true)
);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var query = from warn in db.Warning.AsEnumerable()
group warn by warn.IssuerId
into modGroup
@@ -129,7 +129,7 @@ internal static partial class Warnings
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !ctx.Channel.IsPrivate)
);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var query = from warn in db.Warning
where warn.IssuerId == moderator.Id && !warn.Retracted
orderby warn.Id descending
@@ -169,7 +169,7 @@ internal static partial class Warnings
new AsciiColumn("Reason"),
new AsciiColumn("Context", disabled: !isMod)
);
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
IOrderedQueryable<Warning> query;
if (isMod)
query = from warn in db.Warning

View File

@@ -53,7 +53,7 @@ internal static partial class Warnings
DiscordUser? user = null
)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var warnings = await db.Warning.Where(w => id.Equals(w.Id)).ToListAsync().ConfigureAwait(false);
if (warnings.Count is 0)
{
@@ -85,7 +85,7 @@ internal static partial class Warnings
DiscordUser? user = null
)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var warningsToRemove = await db.Warning.Where(w => w.Id == id).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
@@ -117,7 +117,7 @@ internal static partial class Warnings
{
try
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var warningsToRemove = await db.Warning.Where(w => w.DiscordId == user.Id && !w.Retracted).ToListAsync().ConfigureAwait(false);
foreach (var w in warningsToRemove)
{
@@ -146,7 +146,7 @@ internal static partial class Warnings
DiscordUser? user = null
)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var warn = await db.Warning.FirstOrDefaultAsync(w => w.Id == id).ConfigureAwait(false);
if (warn is { Retracted: true })
{
@@ -166,7 +166,7 @@ internal static partial class Warnings
{
try
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
await db.Warning.AddAsync(
new()
{
@@ -211,7 +211,7 @@ internal static partial class Warnings
const bool ephemeral = true;
int count, removed;
bool isKot, isDoggo;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
count = await db.Warning.CountAsync(w => w.DiscordId == userId && !w.Retracted).ConfigureAwait(false);
removed = await db.Warning.CountAsync(w => w.DiscordId == userId && w.Retracted).ConfigureAwait(false);
isKot = db.Kot.Any(k => k.UserId == userId);

View File

@@ -7,6 +7,9 @@ namespace CompatBot.Database;
internal class BotDb: DbContext
{
private static ReaderWriterLockSlim dbLock = new();
private bool isWriteMode;
public DbSet<BotState> BotState { get; set; } = null!;
public DbSet<Moderator> Moderator { get; set; } = null!;
public DbSet<Piracystring> Piracystring { get; set; } = null!;
@@ -20,6 +23,20 @@ internal class BotDb: DbContext
public DbSet<Kot> Kot { get; set; } = null!;
public DbSet<Doggo> Doggo { get; set; } = null!;
public DbSet<ForcedNickname> ForcedNicknames { get; set; } = null!;
private BotDb(bool writeMode = false) => isWriteMode = writeMode;
public static BotDb OpenRead()
{
dbLock.EnterReadLock();
return new();
}
public static BotDb OpenWrite()
{
dbLock.EnterWriteLock();
return new();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@@ -54,6 +71,24 @@ internal class BotDb: DbContext
//configure name conversion for all configured entities from CamelCase to snake_case
modelBuilder.ConfigureMapping(NamingStyles.Underscore);
}
public override void Dispose()
{
base.Dispose();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
}
internal class BotState

View File

@@ -10,11 +10,11 @@ public static class DbImporter
{
public static async Task<bool> UpgradeAsync(CancellationToken cancellationToken)
{
await using (var db = new BotDb())
await using (var db = BotDb.OpenWrite())
if (!await UpgradeAsync(db, cancellationToken).ConfigureAwait(false))
return false;
await using (var db = new ThumbnailDb())
await using (var db = ThumbnailDb.OpenWrite())
{
if (!await UpgradeAsync(db,cancellationToken).ConfigureAwait(false))
return false;
@@ -23,7 +23,7 @@ public static class DbImporter
return false;
}
await using (var db = new HardwareDb())
await using (var db = HardwareDb.OpenWrite())
if (!await UpgradeAsync(db, cancellationToken).ConfigureAwait(false))
return false;

View File

@@ -6,8 +6,25 @@ namespace CompatBot.Database;
internal class HardwareDb : DbContext
{
private static ReaderWriterLockSlim dbLock = new();
private bool isWriteMode;
public DbSet<HwInfo> HwInfo { get; set; } = null!;
private HardwareDb(bool writeMode = false) => isWriteMode = writeMode;
public static HardwareDb OpenRead()
{
dbLock.EnterReadLock();
return new();
}
public static HardwareDb OpenWrite()
{
dbLock.EnterWriteLock();
return new();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("hw.db", Environment.SpecialFolder.LocalApplicationData);
@@ -24,6 +41,24 @@ internal class HardwareDb : DbContext
//configure name conversion for all configured entities from CamelCase to snake_case
modelBuilder.ConfigureMapping(NamingStyles.Underscore);
}
public override void Dispose()
{
base.Dispose();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
}
[Flags]

View File

@@ -58,7 +58,7 @@ internal static class AmdDriverVersionProvider
}
}
public static async Task<string> GetFromOpenglAsync(string openglVersion, bool autoRefresh = true)
public static async ValueTask<string> GetFromOpenglAsync(string openglVersion, bool autoRefresh = true)
{
if (OpenglToDriver.TryGetValue(openglVersion, out var result))
return result;
@@ -111,7 +111,7 @@ internal static class AmdDriverVersionProvider
return openglVersion;
}
public static async Task<string> GetFromVulkanAsync(string vulkanVersion, bool autoRefresh = true)
public static async ValueTask<string> GetFromVulkanAsync(string vulkanVersion, bool autoRefresh = true)
{
if (!VulkanToDriver.TryGetValue(vulkanVersion, out var result))
await RefreshAsync().ConfigureAwait(false);

View File

@@ -59,7 +59,7 @@ internal static class ContentFilter
public static void RebuildMatcher()
{
var newFilters = new Dictionary<FilterContext, AhoCorasickDoubleArrayTrie<Piracystring>?>();
using var db = new BotDb();
using var db = BotDb.OpenRead();
foreach (FilterContext ctx in Enum.GetValues<FilterContext>())
{
var triggerList = db.Piracystring.Where(ps => ps.Disabled == false && ps.Context.HasFlag(ctx)).AsNoTracking()

View File

@@ -8,7 +8,7 @@ internal static class DisabledCommandsProvider
{
lock (DisabledCommands)
{
using var db = new BotDb();
using var db = BotDb.OpenRead();
foreach (var cmd in db.DisabledCommands.ToList())
DisabledCommands.Add(cmd.Command);
}
@@ -21,7 +21,7 @@ internal static class DisabledCommandsProvider
lock (DisabledCommands)
if (DisabledCommands.Add(command))
{
using var db = new BotDb();
using var db = BotDb.OpenWrite();
db.DisabledCommands.Add(new() {Command = command});
db.SaveChanges();
}
@@ -32,7 +32,7 @@ internal static class DisabledCommandsProvider
lock (DisabledCommands)
if (DisabledCommands.Remove(command))
{
using var db = new BotDb();
using var db = BotDb.OpenWrite();
var cmd = db.DisabledCommands.FirstOrDefault(c => c.Command == command);
if (cmd == null)
return;
@@ -47,7 +47,7 @@ internal static class DisabledCommandsProvider
lock (DisabledCommands)
{
DisabledCommands.Clear();
using var db = new BotDb();
using var db = BotDb.OpenWrite();
db.DisabledCommands.RemoveRange(db.DisabledCommands);
db.SaveChanges();
}

View File

@@ -76,8 +76,8 @@ internal static class HwInfoProvider
OsName = GetName(osType, items),
OsVersion = items["os_version"],
};
await using var db = new HardwareDb();
var existingItem = await db.HwInfo.FindAsync(info.InstallId).ConfigureAwait(false);
await using var db = HardwareDb.OpenWrite();
var existingItem = await db.HwInfo.FindAsync([info.InstallId], cancellationToken: cancellationToken).ConfigureAwait(false);
if (existingItem is null)
db.HwInfo.Add(info);
else if (existingItem.Timestamp <= info.Timestamp)

View File

@@ -7,7 +7,7 @@ internal static class InviteWhitelistProvider
{
public static bool IsWhitelisted(ulong guildId)
{
using var db = new BotDb();
using var db = BotDb.OpenRead();
return db.WhitelistedInvites.Any(i => i.GuildId == guildId);
}
@@ -15,9 +15,9 @@ internal static class InviteWhitelistProvider
{
var code = string.IsNullOrWhiteSpace(invite.Code) ? null : invite.Code;
var name = string.IsNullOrWhiteSpace(invite.Guild.Name) ? null : invite.Guild.Name;
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
var whitelistedInvite = await db.WhitelistedInvites.FirstOrDefaultAsync(i => i.GuildId == invite.Guild.Id);
if (whitelistedInvite == null)
if (whitelistedInvite is null)
return false;
if (name != null && name != whitelistedInvite.Name)
@@ -35,7 +35,7 @@ internal static class InviteWhitelistProvider
var code = invite.IsRevoked || string.IsNullOrWhiteSpace(invite.Code) ? null : invite.Code;
var name = string.IsNullOrWhiteSpace(invite.Guild.Name) ? null : invite.Guild.Name;
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
await db.WhitelistedInvites.AddAsync(new WhitelistedInvite { GuildId = invite.Guild.Id, Name = name, InviteCode = code }).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
return true;
@@ -46,7 +46,7 @@ internal static class InviteWhitelistProvider
if (IsWhitelisted(guildId))
return false;
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
await db.WhitelistedInvites.AddAsync(new WhitelistedInvite {GuildId = guildId}).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
return true;
@@ -54,7 +54,7 @@ internal static class InviteWhitelistProvider
public static async Task<bool> RemoveAsync(int id)
{
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
var dbItem = await db.WhitelistedInvites.FirstOrDefaultAsync(i => i.Id == id).ConfigureAwait(false);
if (dbItem == null)
return false;
@@ -68,13 +68,13 @@ internal static class InviteWhitelistProvider
{
while (!Config.Cts.IsCancellationRequested)
{
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
foreach (var invite in db.WhitelistedInvites.Where(i => i.InviteCode != null))
{
try
{
var result = await client.GetInviteByCodeAsync(invite.InviteCode).ConfigureAwait(false);
if (result?.IsRevoked == true)
if (result.IsRevoked)
invite.InviteCode = null;
}
catch (NotFoundException)

View File

@@ -6,12 +6,12 @@ namespace CompatBot.Database.Providers;
internal static class ModProvider
{
private static readonly Dictionary<ulong, Moderator> Moderators;
private static readonly BotDb Db = new();
public static ReadOnlyDictionary<ulong, Moderator> Mods => new(Moderators);
static ModProvider()
{
Moderators = Db.Moderator.AsNoTracking().ToDictionary(m => m.DiscordId, m => m);
using var db = BotDb.OpenRead();
Moderators = db.Moderator.AsNoTracking().ToDictionary(m => m.DiscordId, m => m);
}
public static bool IsMod(ulong userId) => Moderators.ContainsKey(userId);
@@ -23,8 +23,9 @@ internal static class ModProvider
return false;
var newMod = new Moderator {DiscordId = userId};
await Db.Moderator.AddAsync(newMod).ConfigureAwait(false);
await Db.SaveChangesAsync().ConfigureAwait(false);
await using var db = BotDb.OpenWrite();
await db.Moderator.AddAsync(newMod).ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
lock (Moderators)
{
if (IsMod(userId))
@@ -40,11 +41,12 @@ internal static class ModProvider
if (!Moderators.ContainsKey(userId))
return false;
var mod = await Db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
await using var db = BotDb.OpenWrite();
var mod = await db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
if (mod is not null)
{
Db.Moderator.Remove(mod);
await Db.SaveChangesAsync().ConfigureAwait(false);
db.Moderator.Remove(mod);
await db.SaveChangesAsync().ConfigureAwait(false);
}
lock (Moderators)
{
@@ -61,11 +63,12 @@ internal static class ModProvider
if (!Moderators.TryGetValue(userId, out var mod) || mod.Sudoer)
return false;
var dbMod = await Db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
await using var db = BotDb.OpenWrite();
var dbMod = await db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
if (dbMod is not null)
{
dbMod.Sudoer = true;
await Db.SaveChangesAsync().ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
}
mod.Sudoer = true;
return true;
@@ -76,14 +79,15 @@ internal static class ModProvider
if (!Moderators.TryGetValue(userId, out var mod) || !mod.Sudoer)
return false;
var dbMod = await Db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
await using var db = BotDb.OpenWrite();
var dbMod = await db.Moderator.FirstOrDefaultAsync(m => m.DiscordId == userId).ConfigureAwait(false);
if (dbMod is not null)
{
dbMod.Sudoer = false;
await Db.SaveChangesAsync().ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
}
mod.Sudoer = false;
await Db.SaveChangesAsync().ConfigureAwait(false);
await db.SaveChangesAsync().ConfigureAwait(false);
return true;
}

View File

@@ -13,7 +13,7 @@ internal static class ScrapeStateProvider
public static bool IsFresh(string locale, string? containerId = null)
{
var id = GetId(locale, containerId);
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var timestamp = string.IsNullOrEmpty(id) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == id);
if (timestamp is { Timestamp: long checkDate and > 0 })
return IsFresh(new DateTime(checkDate, DateTimeKind.Utc));
@@ -22,7 +22,7 @@ internal static class ScrapeStateProvider
public static bool IsFresh(string locale, DateTime dataTimestamp)
{
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var timestamp = string.IsNullOrEmpty(locale) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == locale);
if (timestamp is { Timestamp: long checkDate and > 0 })
return new DateTime(checkDate, DateTimeKind.Utc) > dataTimestamp;
@@ -35,18 +35,18 @@ internal static class ScrapeStateProvider
throw new ArgumentException("Locale is mandatory", nameof(locale));
var id = GetId(locale, containerId);
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var timestamp = db.State.FirstOrDefault(s => s.Locale == id);
if (timestamp == null)
await db.State.AddAsync(new State {Locale = id, Timestamp = DateTime.UtcNow.Ticks}).ConfigureAwait(false);
if (timestamp is null)
await db.State.AddAsync(new() {Locale = id, Timestamp = DateTime.UtcNow.Ticks}).ConfigureAwait(false);
else
timestamp.Timestamp = DateTime.UtcNow.Ticks;
await db.SaveChangesAsync().ConfigureAwait(false);
}
public static async Task CleanAsync(CancellationToken cancellationToken)
public static async ValueTask CleanAsync(CancellationToken cancellationToken)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var latestTimestamp = db.State.OrderByDescending(s => s.Timestamp).FirstOrDefault()?.Timestamp;
if (!latestTimestamp.HasValue)
return;

View File

@@ -10,7 +10,7 @@ internal static class SqlConfiguration
public static async Task RestoreAsync()
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var setVars = await db.BotState.AsNoTracking().Where(v => v.Key.StartsWith(ConfigVarPrefix)).ToListAsync().ConfigureAwait(false);
if (setVars.Any())
{

View File

@@ -64,14 +64,14 @@ internal static class StatsStorage
.ToList();
}
public static async Task SaveAsync(bool wait = false)
public static async ValueTask 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();
await using var db = BotDb.OpenWrite();
foreach (var (category, cache) in AllCaches)
{
var entries = cache.GetCacheEntries<string>();
@@ -126,7 +126,7 @@ internal static class StatsStorage
public static async Task RestoreAsync()
{
var now = DateTime.UtcNow;
await using var db = new BotDb();
await using var db = BotDb.OpenWrite();
foreach (var (category, cache) in AllCaches)
{
var entries = await db.Stats.Where(e => e.Category == category).ToListAsync().ConfigureAwait(false);

View File

@@ -17,7 +17,7 @@ internal static class SyscallInfoProvider
{
try
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
foreach (var productCodeMap in syscallInfo)
{
var product = db.Thumbnail.AsNoTracking().FirstOrDefault(t => t.ProductCode == productCodeMap.Key)
@@ -49,7 +49,7 @@ internal static class SyscallInfoProvider
{
var syscallStats = new TSyscallStats();
int funcs, links = 0;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var funcsToRemove = new List<SyscallInfo>(0);
try
{
@@ -106,7 +106,7 @@ internal static class SyscallInfoProvider
public static async Task<(int funcs, int links)> FixDuplicatesAsync()
{
int funcs = 0, links = 0;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var duplicateFunctionNames = await db.SyscallInfo.Where(sci => db.SyscallInfo.Count(isci => isci.Function == sci.Function) > 1).Distinct().ToListAsync().ConfigureAwait(false);
if (duplicateFunctionNames.Count == 0)
return (0, 0);

View File

@@ -22,7 +22,7 @@ internal static class ThumbnailProvider
if (tmdbInfo is { Icon.Url: string tmdbIconUrl })
return tmdbIconUrl;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false);
//todo: add search task if not found
if (thumb?.EmbeddableUrl is {Length: >0} embeddableUrl)
@@ -55,13 +55,13 @@ internal static class ThumbnailProvider
return embedUrl;
}
public static async Task<string?> GetTitleNameAsync(string? productCode, CancellationToken cancellationToken)
public static async ValueTask<string?> GetTitleNameAsync(string? productCode, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(productCode))
return null;
productCode = productCode.ToUpperInvariant();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var thumb = await db.Thumbnail.FirstOrDefaultAsync(
t => t.ProductCode == productCode,
cancellationToken: cancellationToken
@@ -93,13 +93,13 @@ internal static class ThumbnailProvider
return title;
}
public static async Task<(string? url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string? url = null)
public static async ValueTask<(string? url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string? url = null)
{
if (string.IsNullOrEmpty(contentId))
throw new ArgumentException("ContentID can't be empty", nameof(contentId));
contentId = contentId.ToUpperInvariant();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var info = await db.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false);
info ??= new() {Url = url};
if (info.Url is null)

View File

@@ -8,7 +8,7 @@ public static class TitleUpdateInfoProvider
{
private static readonly PsnClient.Client Client = new();
public static async Task<TitlePatch?> GetAsync(string? productId, CancellationToken cancellationToken)
public static async ValueTask<TitlePatch?> GetAsync(string? productId, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(productId))
return default;
@@ -18,7 +18,7 @@ public static class TitleUpdateInfoProvider
if (xml is {Length: > 10})
{
var xmlChecksum = xml.GetStableHash();
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenWrite();
var updateInfo = db.GameUpdateInfo.FirstOrDefault(ui => ui.ProductCode == productId);
if (updateInfo is null)
db.GameUpdateInfo.Add(new() {ProductCode = productId, MetaHash = xmlChecksum, MetaXml = xml, Timestamp = DateTime.UtcNow.Ticks});
@@ -34,7 +34,7 @@ public static class TitleUpdateInfoProvider
}
if (update?.Tag?.Packages?.Length is null or 0)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var updateInfo = db.GameUpdateInfo.FirstOrDefault(ui => ui.ProductCode == productId);
if (updateInfo is null)
return update;
@@ -52,7 +52,7 @@ public static class TitleUpdateInfoProvider
public static async Task RefreshGameUpdateInfoAsync(CancellationToken cancellationToken)
{
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
do
{
var productCodeList = await db.Thumbnail.AsNoTracking().Select(t => t.ProductCode).ToListAsync(cancellationToken).ConfigureAwait(false);

View File

@@ -6,6 +6,9 @@ namespace CompatBot.Database;
internal class ThumbnailDb : DbContext
{
private static ReaderWriterLockSlim dbLock = new();
private bool isWriteMode;
public DbSet<State> State { get; set; } = null!;
public DbSet<Thumbnail> Thumbnail { get; set; } = null!;
public DbSet<GameUpdateInfo> GameUpdateInfo { get; set; } = null!;
@@ -15,6 +18,20 @@ internal class ThumbnailDb : DbContext
public DbSet<Fortune> Fortune { get; set; } = null!;
public DbSet<NamePool> NamePool { get; set; } = null!;
private ThumbnailDb(bool writeMode = false) => isWriteMode = writeMode;
public static ThumbnailDb OpenRead()
{
dbLock.EnterReadLock();
return new();
}
public static ThumbnailDb OpenWrite()
{
dbLock.EnterWriteLock();
return new();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("thumbs.db", Environment.SpecialFolder.LocalApplicationData);
@@ -43,6 +60,24 @@ internal class ThumbnailDb : DbContext
//configure name conversion for all configured entities from CamelCase to snake_case
modelBuilder.ConfigureMapping(NamingStyles.Underscore);
}
public override void Dispose()
{
base.Dispose();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
if (isWriteMode)
dbLock.ExitWriteLock();
else
dbLock.ExitReadLock();
}
}
internal class State

View File

@@ -119,7 +119,7 @@ namespace CompatBot.EventHandlers
if (!string.IsNullOrEmpty(args.Message.Content) && Paws().Matches(args.Message.Content) is MatchCollection mc)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var matchedGroups = (from m in mc
from Group g in m.Groups
where g is { Success: true, Value.Length: > 0 }

View File

@@ -9,7 +9,7 @@ internal static class BotStatusMonitor
{
try
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var status = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-activity").ConfigureAwait(false);
var txt = await db.BotState.FirstOrDefaultAsync(s => s.Key == "bot-status-text").ConfigureAwait(false);
var msg = txt?.Value;

View File

@@ -7,7 +7,7 @@ internal static class Greeter
{
public static async Task OnMemberAdded(DiscordClient _, GuildMemberAddedEventArgs args)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
if (await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == "motd").ConfigureAwait(false) is {Text.Length: >0} explanation)
{
var dm = await args.Member.CreateDmChannelAsync().ConfigureAwait(false);

View File

@@ -58,7 +58,7 @@ internal static partial class PostLogHelpHandler
public static async Task<Explanation> GetExplanationAsync(string term)
{
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var result = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false);
return result ?? DefaultExplanation[term];
}

View File

@@ -15,7 +15,7 @@ internal static class ThumbnailCacheMonitor
if (!args.Message.Attachments.Any())
return;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var thumb = db.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content);
if (thumb is { EmbeddableUrl: { Length: > 0 } url } && args.Message.Attachments.Any(a => a.Url == url))
{

View File

@@ -19,7 +19,7 @@ public static class UsernameValidationMonitor
if (guild.Permissions?.HasFlag(DiscordPermission.ChangeNickname) is false)
return;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var forcedNickname = await db.ForcedNicknames.AsNoTracking().FirstOrDefaultAsync(x => x.UserId == guildMember.Id && x.GuildId == guildMember.Guild.Id).ConfigureAwait(false);
if (forcedNickname is null)
return;
@@ -54,7 +54,7 @@ public static class UsernameValidationMonitor
if (guild.Permissions?.HasFlag(DiscordPermission.ChangeNickname) is false)
continue;
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
var forcedNicknames = await db.ForcedNicknames
.Where(mem => mem.GuildId == guild.Id)
.ToListAsync()

View File

@@ -206,7 +206,7 @@ public static class UsernameZalgoMonitor
{
var hash = userId.GetHashCode();
var rng = new Random(hash);
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var count = db.NamePool.Count();
var name = db.NamePool.Skip(rng.Next(count)).First().Name;
return name + Config.RenameNameSuffix;

View File

@@ -192,7 +192,7 @@ internal static class Program
var msg = new StringBuilder($"Bot admin id{(owners.Count == 1 ? "": "s")}:");
if (owners.Count > 1)
msg.AppendLine();
await using var db = new BotDb();
await using var db = BotDb.OpenRead();
foreach (var owner in owners)
{
msg.AppendLine($"\t{owner.Id} ({owner.Username ?? "???"}#{owner.Discriminator ?? "????"})");
@@ -310,7 +310,7 @@ internal static class Program
ulong? channelId = null;
string? restartMsg = null;
await using (var db = new BotDb())
await using (var db = BotDb.OpenRead())
{
var chState = db.BotState.FirstOrDefault(k => k.Key == "bot-restart-channel");
if (chState != null)

View File

@@ -113,7 +113,7 @@ internal static partial class GameTdbScraper
if (string.IsNullOrEmpty(title))
continue;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var item = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productId, cancellationToken).ConfigureAwait(false);
if (item is null)
{

View File

@@ -23,6 +23,7 @@ internal sealed partial class PsnScraper
private static DateTime storeRefreshTimestamp = DateTime.MinValue;
private static readonly SemaphoreSlim QueueLimiter = new(32, 32);
[Obsolete]
public static async Task RunAsync(CancellationToken cancellationToken)
{
do
@@ -292,7 +293,7 @@ internal sealed partial class PsnScraper
private static bool NeedLookup(string contentId)
{
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
if (db.Thumbnail.FirstOrDefault(t => t.ContentId == contentId) is Thumbnail thumbnail)
if (!string.IsNullOrEmpty(thumbnail.Url))
if (ScrapeStateProvider.IsFresh(new DateTime(thumbnail.Timestamp, DateTimeKind.Utc)))
@@ -350,7 +351,7 @@ internal sealed partial class PsnScraper
return;
name = string.IsNullOrEmpty(name) ? null : name;
await using var db = new ThumbnailDb();
await using var db = ThumbnailDb.OpenRead();
var savedItem = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode);
if (savedItem == null)
{

View File

@@ -62,7 +62,7 @@ internal static class TitleInfoFormatter
var productCodePart = string.IsNullOrWhiteSpace(titleId) ? "" : $"[{titleId}] ";
if (!StatusColors.TryGetValue(info.Status, out _) && !string.IsNullOrEmpty(titleId))
{
using var db = new ThumbnailDb();
using var db = ThumbnailDb.OpenRead();
var thumb = db.Thumbnail.FirstOrDefault(t => t.ProductCode == titleId);
if (thumb?.CompatibilityStatus != null)
{