1
- using Microsoft . EntityFrameworkCore ;
1
+ using System . Diagnostics ;
2
2
3
+ using log4net ;
4
+
5
+ using Microsoft . EntityFrameworkCore ;
6
+ using Microsoft . EntityFrameworkCore . Storage ;
7
+
8
+ using Nullinside . Api . Common ;
3
9
using Nullinside . Api . Common . Twitch ;
4
10
using Nullinside . Api . Model ;
5
11
using Nullinside . Api . Model . Ddl ;
@@ -10,14 +16,24 @@ namespace Nullinside.Api.TwitchBot.Model;
10
16
/// Extensions for <see cref="NullinsideContext" /> and its ERMs.
11
17
/// </summary>
12
18
public 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
+
13
29
/// <summary>
14
30
/// Gets a twitch api proxy.
15
31
/// </summary>
16
32
/// <param name="user">The user to configure the proxy as.</param>
17
33
/// <param name="api">The twitch api object currently in use.</param>
18
34
/// <returns>The twitch api.</returns>
19
35
public static void Configure ( this ITwitchApiProxy api , User user ) {
20
- api . OAuth = new ( ) {
36
+ api . OAuth = new TwitchAccessToken {
21
37
AccessToken = user . TwitchToken ,
22
38
RefreshToken = user . TwitchRefreshToken ,
23
39
ExpiresUtc = user . TwitchTokenExpiration
@@ -36,17 +52,63 @@ public static void Configure(this ITwitchApiProxy api, User user) {
36
52
ITwitchApiProxy api , CancellationToken stoppingToken = new ( ) ) {
37
53
api . Configure ( user ) ;
38
54
39
- // Refresh its token if necessary .
55
+ // Use the token we have, if it hasn't expired .
40
56
if ( ! ( DateTime . UtcNow + TimeSpan . FromHours ( 1 ) > user . TwitchTokenExpiration ) ) {
41
57
return api ;
42
58
}
43
59
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
+ }
47
109
48
- await db . UpdateOAuthInDatabase ( user . Id , api . OAuth , stoppingToken ) ;
49
- return api ;
110
+ return null ;
111
+ } ) ;
50
112
}
51
113
52
114
/// <summary>
@@ -57,7 +119,7 @@ public static void Configure(this ITwitchApiProxy api, User user) {
57
119
/// <param name="oAuth">The OAuth information.</param>
58
120
/// <param name="stoppingToken">The stopping token.</param>
59
121
/// <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 ,
61
123
TwitchAccessToken oAuth , CancellationToken stoppingToken = new ( ) ) {
62
124
User ? row = await db . Users . FirstOrDefaultAsync ( u => u . Id == userId , stoppingToken ) ;
63
125
if ( null == row ) {
0 commit comments