7
7
8
8
using System ;
9
9
using System . Collections . Generic ;
10
+ using System . Linq ;
10
11
using Akka . Actor ;
11
12
using Akka . Event ;
12
13
using Akka . Pattern ;
@@ -18,8 +19,26 @@ namespace Akka.Persistence.Sql.Query;
18
19
/// </summary>
19
20
internal sealed class RequestQueryStart
20
21
{
21
- public static readonly RequestQueryStart Instance = new ( ) ;
22
- private RequestQueryStart ( ) { }
22
+ public DateTime DeadlineTime { get ; }
23
+
24
+ public RequestQueryStart ( TimeSpan timeout )
25
+ {
26
+ DeadlineTime = DateTime . UtcNow . Add ( timeout ) ;
27
+ }
28
+ }
29
+
30
+ internal sealed class PendingRequest
31
+ {
32
+ public PendingRequest ( IActorRef requester , DateTime deadlineTime )
33
+ {
34
+ Requester = requester ;
35
+ DeadlineTime = deadlineTime ;
36
+ }
37
+
38
+ public IActorRef Requester { get ; }
39
+ public DateTime DeadlineTime { get ; }
40
+
41
+ public bool IsExpired => DeadlineTime < DateTime . UtcNow ;
23
42
}
24
43
25
44
/// <summary>
@@ -40,31 +59,75 @@ internal sealed class ReturnQueryStart
40
59
private ReturnQueryStart ( ) { }
41
60
}
42
61
62
+ #region Test classes
63
+
64
+ /// <summary>
65
+ /// For testing purposes
66
+ /// </summary>
67
+ internal sealed class GetUsedPermits
68
+ {
69
+ public static readonly GetUsedPermits Instance = new ( ) ;
70
+ private GetUsedPermits ( ) { }
71
+ }
72
+
73
+ /// <summary>
74
+ /// For testing purposes
75
+ /// </summary>
76
+ internal sealed class GetPendingRequests
77
+ {
78
+ public static readonly GetPendingRequests Instance = new ( ) ;
79
+ private GetPendingRequests ( ) { }
80
+ }
81
+
82
+ internal sealed class GetWatchCount
83
+ {
84
+ public static readonly GetWatchCount Instance = new ( ) ;
85
+ private GetWatchCount ( ) { }
86
+ }
87
+
88
+ #endregion
89
+
43
90
/// <summary>
44
91
/// Token bucket throttler that grants queries permissions to run each iteration
45
92
/// </summary>
46
93
/// <remarks>
47
- /// Works identically to the RecoveryPermitter built into Akka.Persistence.
94
+ /// Works almost identically to the RecoveryPermitter built into Akka.Persistence.
95
+ ///
96
+ /// NOTE: Since this permitter works with Ask operation from outside an actor,
97
+ /// we can not rely on the actor termination as a signal for permit revocation.
98
+ ///
99
+ /// A query operation needs to be executed within the context of the permit
100
+ /// and an Ask temporary actor will always terminate before the actual
101
+ /// permits are used, making the Terminated message useless for this use case.
102
+ ///
103
+ /// ALWAYS USE A TRY...FINALLY BLOCK WHEN USING ASK AND RETURN THE PERMIT IN
104
+ /// THE FINALLY BLOCK
48
105
/// </remarks>
49
106
internal sealed class QueryThrottler : ReceiveActor
50
107
{
51
- private readonly LinkedList < IActorRef > _pending = new ( ) ;
108
+ private readonly LinkedList < PendingRequest > _pending = new ( ) ;
52
109
private readonly ILoggingAdapter _log = Context . GetLogger ( ) ;
110
+ private long _watchCount ;
53
111
private int _usedPermits ;
54
112
private int _maxPendingStats ;
55
113
56
114
public QueryThrottler ( int maxPermits )
57
115
{
58
116
MaxPermits = maxPermits ;
59
117
60
- Receive < RequestQueryStart > ( _ =>
118
+ Receive < RequestQueryStart > ( request =>
61
119
{
62
- Context . Watch ( Sender ) ;
120
+ if ( Sender is ActorRefWithCell )
121
+ {
122
+ _watchCount ++ ;
123
+ Context . Watch ( Sender ) ;
124
+ }
125
+
63
126
if ( _usedPermits >= MaxPermits )
64
127
{
65
128
if ( _pending . Count == 0 )
66
129
_log . Debug ( "Exceeded max-concurrent-queries[{0}]. First pending {1}" , MaxPermits , Sender ) ;
67
- _pending . AddLast ( Sender ) ;
130
+ _pending . AddLast ( new PendingRequest ( Sender , request . DeadlineTime ) ) ;
68
131
_maxPendingStats = Math . Max ( _maxPendingStats , _pending . Count ) ;
69
132
}
70
133
else
@@ -75,16 +138,34 @@ public QueryThrottler(int maxPermits)
75
138
76
139
Receive < ReturnQueryStart > ( _ =>
77
140
{
78
- ReturnQueryPermit ( Sender ) ;
141
+ if ( Sender is ActorRefWithCell )
142
+ Context . Unwatch ( Sender ) ;
143
+
144
+ ReturnQueryPermit ( ) ;
79
145
} ) ;
80
146
81
147
Receive < Terminated > ( terminated =>
82
148
{
83
- if ( ! _pending . Remove ( terminated . ActorRef ) )
149
+ var actor = terminated . ActorRef ;
150
+ if ( actor is ActorRefWithCell )
151
+ Context . Unwatch ( actor ) ;
152
+
153
+ var pending = _pending . FirstOrDefault ( p => p . Requester . Equals ( actor ) ) ;
154
+ if ( pending is not null )
84
155
{
85
- ReturnQueryPermit ( terminated . ActorRef ) ;
156
+ _pending . Remove ( pending ) ;
157
+ }
158
+ else
159
+ {
160
+ ReturnQueryPermit ( ) ;
86
161
}
87
162
} ) ;
163
+
164
+ #region Test handlers
165
+ Receive < GetUsedPermits > ( _ => Sender . Tell ( _usedPermits ) ) ;
166
+ Receive < GetPendingRequests > ( _ => Sender . Tell ( _pending . ToArray ( ) ) ) ;
167
+ Receive < GetWatchCount > ( _ => Sender . Tell ( _watchCount ) ) ;
168
+ #endregion
88
169
}
89
170
90
171
public int MaxPermits { get ; }
@@ -95,19 +176,36 @@ private void QueryStartGranted(IActorRef actorRef)
95
176
actorRef . Tell ( Query . QueryStartGranted . Instance ) ;
96
177
}
97
178
98
- private void ReturnQueryPermit ( IActorRef actorRef )
179
+ private void ReturnQueryPermit ( )
99
180
{
100
181
_usedPermits -- ;
101
- Context . Unwatch ( actorRef ) ;
102
182
183
+ // _usedPermits can go negative if a piece of code returns
184
+ // granted permits multiple times. This is not a critical
185
+ // error, the throttler should not stop working because of this.
186
+ //
187
+ // However, if this does trip, we will need to look into
188
+ // the query codes and figure out which code is over returning
189
+ // permits.
103
190
if ( _usedPermits < 0 )
104
- throw new IllegalStateException ( "Permits must not be negative" ) ;
191
+ {
192
+ _log . Warning ( "Permits must not be negative" ) ;
193
+ _usedPermits = 0 ;
194
+ return ;
195
+ }
105
196
106
- var popRef = _pending . First ? . Value ;
107
- if ( popRef is not null )
197
+ while ( _pending . First is not null )
108
198
{
199
+ var pending = _pending . First . Value ;
109
200
_pending . RemoveFirst ( ) ;
110
- QueryStartGranted ( popRef ) ;
201
+ if ( pending is not null && pending . IsExpired )
202
+ pending = null ;
203
+
204
+ if ( pending is null )
205
+ continue ;
206
+
207
+ QueryStartGranted ( pending . Requester ) ;
208
+ break ;
111
209
}
112
210
113
211
if ( _pending . Count != 0 || _maxPendingStats <= 0 )
0 commit comments