diff --git a/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs b/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs
index 02571f7..70f5f33 100644
--- a/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs
+++ b/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs
@@ -1,5 +1,11 @@
-using Microsoft.EntityFrameworkCore;
+using System.Diagnostics;
+using log4net;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage;
+
+using Nullinside.Api.Common;
using Nullinside.Api.Common.Twitch;
using Nullinside.Api.Model;
using Nullinside.Api.Model.Ddl;
@@ -10,6 +16,16 @@ namespace Nullinside.Api.TwitchBot.Model;
/// Extensions for and its ERMs.
///
public static class NullinsideContextExtensions {
+ ///
+ /// The database lock name to use as a lock in the MySQL database.
+ ///
+ private const string BOT_REFRESH_TOKEN_LOCK_NAME = "bot_refresh_token";
+
+ ///
+ /// The logger.
+ ///
+ private static readonly ILog _log = LogManager.GetLogger(typeof(NullinsideContextExtensions));
+
///
/// Gets a twitch api proxy.
///
@@ -17,7 +33,7 @@ public static class NullinsideContextExtensions {
/// The twitch api object currently in use.
/// The twitch api.
public static void Configure(this ITwitchApiProxy api, User user) {
- api.OAuth = new() {
+ api.OAuth = new TwitchAccessToken {
AccessToken = user.TwitchToken,
RefreshToken = user.TwitchRefreshToken,
ExpiresUtc = user.TwitchTokenExpiration
@@ -36,17 +52,63 @@ public static void Configure(this ITwitchApiProxy api, User user) {
ITwitchApiProxy api, CancellationToken stoppingToken = new()) {
api.Configure(user);
- // Refresh its token if necessary.
+ // Use the token we have, if it hasn't expired.
if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > user.TwitchTokenExpiration)) {
return api;
}
- if (null == await api.RefreshAccessToken(stoppingToken) || null == api.OAuth) {
- return api;
- }
+ // Database locking requires ExecutionStrategy
+ IExecutionStrategy strat = db.Database.CreateExecutionStrategy();
+ return await strat.ExecuteAsync(async () => {
+ bool failed = false;
+
+ // Database locking requires transaction scope
+ await using IDbContextTransaction scope = await db.Database.BeginTransactionAsync(stoppingToken);
+
+ // Perform the database lock
+ using var dbLock = new DatabaseLock(db);
+ var sw = new Stopwatch();
+ sw.Start();
+ await dbLock.GetLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken);
+ _log.Info($"bot_refresh_token: {sw.Elapsed}");
+ sw.Stop();
+
+ try {
+ // Get the user with the database lock acquired.
+ User? updatedUser = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == user.Id, stoppingToken);
+ if (null == updatedUser) {
+ return null;
+ }
+
+ // Use the token we have, if it hasn't expired.
+ if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > updatedUser.TwitchTokenExpiration)) {
+ api.Configure(updatedUser);
+ return api;
+ }
+
+ // Refresh the token with the Twitch API.
+ TwitchAccessToken? newToken = await api.RefreshAccessToken(stoppingToken);
+ if (null == newToken) {
+ return null;
+ }
+
+ // Update the credentials in the database.
+ await db.UpdateOAuthInDatabase(user.Id, newToken, stoppingToken);
+ return api;
+ }
+ catch {
+ failed = true;
+ }
+ finally {
+ await dbLock.ReleaseLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken);
+
+ if (!failed) {
+ scope.Commit();
+ }
+ }
- await db.UpdateOAuthInDatabase(user.Id, api.OAuth, stoppingToken);
- return api;
+ return null;
+ });
}
///
@@ -57,7 +119,7 @@ public static void Configure(this ITwitchApiProxy api, User user) {
/// The OAuth information.
/// The stopping token.
/// The number of state entries written to the database.
- public static async Task UpdateOAuthInDatabase(this INullinsideContext db, int userId,
+ private static async Task UpdateOAuthInDatabase(this INullinsideContext db, int userId,
TwitchAccessToken oAuth, CancellationToken stoppingToken = new()) {
User? row = await db.Users.FirstOrDefaultAsync(u => u.Id == userId, stoppingToken);
if (null == row) {
diff --git a/src/nullinside-api b/src/nullinside-api
index 55f80e6..aa08d21 160000
--- a/src/nullinside-api
+++ b/src/nullinside-api
@@ -1 +1 @@
-Subproject commit 55f80e6b981e8f39c991d0cb44112678d99d64d8
+Subproject commit aa08d219a5cc12fbef0c1a420aee92cde549c7a8