1- using Microsoft . EntityFrameworkCore ;
1+ using System . Diagnostics ;
22
3+ using log4net ;
4+
5+ using Microsoft . EntityFrameworkCore ;
6+ using Microsoft . EntityFrameworkCore . Storage ;
7+
8+ using Nullinside . Api . Common ;
39using Nullinside . Api . Common . Twitch ;
410using Nullinside . Api . Model ;
511using Nullinside . Api . Model . Ddl ;
@@ -10,14 +16,24 @@ namespace Nullinside.Api.TwitchBot.Model;
1016/// Extensions for <see cref="NullinsideContext" /> and its ERMs.
1117/// </summary>
1218public static class NullinsideContextExtensions {
19+ /// <summary>
20+ /// The database lock name to use as a lock in the MySQL database.
21+ /// </summary>
22+ private const string BOT_REFRESH_TOKEN_LOCK_NAME = "bot_refresh_token" ;
23+
24+ /// <summary>
25+ /// The logger.
26+ /// </summary>
27+ private static readonly ILog _log = LogManager . GetLogger ( typeof ( NullinsideContextExtensions ) ) ;
28+
1329 /// <summary>
1430 /// Gets a twitch api proxy.
1531 /// </summary>
1632 /// <param name="user">The user to configure the proxy as.</param>
1733 /// <param name="api">The twitch api object currently in use.</param>
1834 /// <returns>The twitch api.</returns>
1935 public static void Configure ( this ITwitchApiProxy api , User user ) {
20- api . OAuth = new ( ) {
36+ api . OAuth = new TwitchAccessToken {
2137 AccessToken = user . TwitchToken ,
2238 RefreshToken = user . TwitchRefreshToken ,
2339 ExpiresUtc = user . TwitchTokenExpiration
@@ -36,17 +52,63 @@ public static void Configure(this ITwitchApiProxy api, User user) {
3652 ITwitchApiProxy api , CancellationToken stoppingToken = new ( ) ) {
3753 api . Configure ( user ) ;
3854
39- // Refresh its token if necessary .
55+ // Use the token we have, if it hasn't expired .
4056 if ( ! ( DateTime . UtcNow + TimeSpan . FromHours ( 1 ) > user . TwitchTokenExpiration ) ) {
4157 return api ;
4258 }
4359
44- if ( null == await api . RefreshAccessToken ( stoppingToken ) || null == api . OAuth ) {
45- return api ;
46- }
60+ // Database locking requires ExecutionStrategy
61+ IExecutionStrategy strat = db . Database . CreateExecutionStrategy ( ) ;
62+ return await strat . ExecuteAsync ( async ( ) => {
63+ bool failed = false ;
64+
65+ // Database locking requires transaction scope
66+ await using IDbContextTransaction scope = await db . Database . BeginTransactionAsync ( stoppingToken ) ;
67+
68+ // Perform the database lock
69+ using var dbLock = new DatabaseLock ( db ) ;
70+ var sw = new Stopwatch ( ) ;
71+ sw . Start ( ) ;
72+ await dbLock . GetLock ( BOT_REFRESH_TOKEN_LOCK_NAME , stoppingToken ) ;
73+ _log . Info ( $ "bot_refresh_token: { sw . Elapsed } ") ;
74+ sw . Stop ( ) ;
75+
76+ try {
77+ // Get the user with the database lock acquired.
78+ User ? updatedUser = await db . Users . AsNoTracking ( ) . FirstOrDefaultAsync ( u => u . Id == user . Id , stoppingToken ) ;
79+ if ( null == updatedUser ) {
80+ return null ;
81+ }
82+
83+ // Use the token we have, if it hasn't expired.
84+ if ( ! ( DateTime . UtcNow + TimeSpan . FromHours ( 1 ) > updatedUser . TwitchTokenExpiration ) ) {
85+ api . Configure ( updatedUser ) ;
86+ return api ;
87+ }
88+
89+ // Refresh the token with the Twitch API.
90+ TwitchAccessToken ? newToken = await api . RefreshAccessToken ( stoppingToken ) ;
91+ if ( null == newToken ) {
92+ return null ;
93+ }
94+
95+ // Update the credentials in the database.
96+ await db . UpdateOAuthInDatabase ( user . Id , newToken , stoppingToken ) ;
97+ return api ;
98+ }
99+ catch {
100+ failed = true ;
101+ }
102+ finally {
103+ await dbLock . ReleaseLock ( BOT_REFRESH_TOKEN_LOCK_NAME , stoppingToken ) ;
104+
105+ if ( ! failed ) {
106+ scope . Commit ( ) ;
107+ }
108+ }
47109
48- await db . UpdateOAuthInDatabase ( user . Id , api . OAuth , stoppingToken ) ;
49- return api ;
110+ return null ;
111+ } ) ;
50112 }
51113
52114 /// <summary>
@@ -57,7 +119,7 @@ public static void Configure(this ITwitchApiProxy api, User user) {
57119 /// <param name="oAuth">The OAuth information.</param>
58120 /// <param name="stoppingToken">The stopping token.</param>
59121 /// <returns>The number of state entries written to the database.</returns>
60- public static async Task < int > UpdateOAuthInDatabase ( this INullinsideContext db , int userId ,
122+ private static async Task < int > UpdateOAuthInDatabase ( this INullinsideContext db , int userId ,
61123 TwitchAccessToken oAuth , CancellationToken stoppingToken = new ( ) ) {
62124 User ? row = await db . Users . FirstOrDefaultAsync ( u => u . Id == userId , stoppingToken ) ;
63125 if ( null == row ) {
0 commit comments