14
14
15
15
import java .time .Duration ;
16
16
import java .util .UUID ;
17
- import java .util .concurrent .ArrayBlockingQueue ;
18
- import java .util .concurrent .CompletableFuture ;
19
- import java .util .concurrent .TimeUnit ;
17
+ import java .util .concurrent .*;
20
18
import java .util .concurrent .atomic .AtomicBoolean ;
19
+ import java .util .concurrent .locks .Condition ;
20
+ import java .util .concurrent .locks .ReentrantLock ;
21
21
22
22
public class ReactorHttpClient implements HypixelHttpClient {
23
+
23
24
private final HttpClient httpClient ;
24
25
private final UUID apiKey ;
25
26
@@ -33,8 +34,10 @@ public class ReactorHttpClient implements HypixelHttpClient {
33
34
34
35
// For shutting down the flux that emits request callbacks
35
36
private final Disposable requestCallbackFluxDisposable ;
37
+ private final ExecutorService requestCallbackFluxExecutorService = Executors .newSingleThreadExecutor ();
36
38
37
- private final Object lock = new Object ();
39
+ private final ReentrantLock lock = new ReentrantLock (true );
40
+ private final Condition limitResetCondition = lock .newCondition ();
38
41
39
42
/*
40
43
* How many requests we can send before reaching the limit
@@ -62,18 +65,13 @@ public ReactorHttpClient(UUID apiKey, long minDelayBetweenRequests, int bufferCa
62
65
callback = blockingQueue .take ();
63
66
}
64
67
65
- synchronized (lock ) {
66
- while (this .actionsLeftThisMinute <= 0 ) {
67
- lock .wait ();
68
- }
68
+ this .decrementActionsOrWait ();
69
69
70
- actionsLeftThisMinute --;
71
- }
72
70
synchronousSink .next (callback );
73
71
} catch (InterruptedException e ) {
74
72
throw new AssertionError ("This should not have been possible" , e );
75
73
}
76
- }).subscribeOn (Schedulers .boundedElastic ( )).delayElements (Duration .ofMillis (minDelayBetweenRequests ), Schedulers .boundedElastic ()).subscribe (RequestCallback ::sendRequest );
74
+ }).subscribeOn (Schedulers .fromExecutorService ( this . requestCallbackFluxExecutorService )).delayElements (Duration .ofMillis (minDelayBetweenRequests ), Schedulers .boundedElastic ()).subscribe (RequestCallback ::sendRequest );
77
75
}
78
76
79
77
public ReactorHttpClient (UUID apiKey , long minDelayBetweenRequests ) {
@@ -89,15 +87,15 @@ public ReactorHttpClient(UUID apiKey) {
89
87
}
90
88
91
89
/**
92
- * Canceling the returned future will result in canceling the request if possible
90
+ * Canceling the returned future will result in canceling the sending of the request if still possible
93
91
*/
94
92
@ Override
95
93
public CompletableFuture <HypixelHttpResponse > makeRequest (String url ) {
96
94
return toHypixelResponseFuture (makeRequest (url , false ));
97
95
}
98
96
99
97
/**
100
- * Canceling the returned future will result in canceling the request if possible
98
+ * Canceling the returned future will result in canceling the sending of the request if still possible
101
99
*/
102
100
@ Override
103
101
public CompletableFuture <HypixelHttpResponse > makeAuthenticatedRequest (String url ) {
@@ -112,17 +110,18 @@ private static CompletableFuture<HypixelHttpResponse> toHypixelResponseFuture(Mo
112
110
@ Override
113
111
public void shutdown () {
114
112
this .requestCallbackFluxDisposable .dispose ();
113
+ this .requestCallbackFluxExecutorService .shutdown ();
115
114
}
116
115
117
116
/**
118
- * Makes a request to the Hypixel api and returns a {@link Mono<Tuple2<String, Integer>> } containing
117
+ * Makes a request to the Hypixel api and returns a {@link Mono} containing
119
118
* the response body and status code, canceling this mono will prevent the request from being sent if possible
120
119
*
121
120
* @param path full url
122
121
* @param isAuthenticated whether to enable authentication or not
123
122
*/
124
123
public Mono <Tuple2 <String , Integer >> makeRequest (String path , boolean isAuthenticated ) {
125
- return Mono .< Tuple2 < String , Integer >> create (sink -> {
124
+ return Mono .create (sink -> {
126
125
RequestCallback callback = new RequestCallback (path , sink , isAuthenticated , this );
127
126
128
127
try {
@@ -131,7 +130,40 @@ public Mono<Tuple2<String, Integer>> makeRequest(String path, boolean isAuthenti
131
130
sink .error (e );
132
131
throw new AssertionError ("Queue insertion interrupted. This should not have been possible" , e );
133
132
}
134
- }).subscribeOn (Schedulers .boundedElastic ());
133
+ });
134
+ }
135
+
136
+ private void decrementActionsOrWait () throws InterruptedException {
137
+ this .lock .lock ();
138
+ try {
139
+ while (this .actionsLeftThisMinute <= 0 ) {
140
+ this .limitResetCondition .await ();
141
+ }
142
+ this .actionsLeftThisMinute --;
143
+ } finally {
144
+ this .lock .unlock ();
145
+ }
146
+ }
147
+
148
+
149
+ private void incrementActionsLeftThisMinute () {
150
+ this .lock .lock ();
151
+ try {
152
+ this .actionsLeftThisMinute ++;
153
+ this .limitResetCondition .signal ();
154
+ } finally {
155
+ this .lock .unlock ();
156
+ }
157
+ }
158
+
159
+ private void setActionsLeftThisMinute (int value ) {
160
+ this .lock .lock ();
161
+ try {
162
+ this .actionsLeftThisMinute = Math .max (0 , value );
163
+ this .limitResetCondition .signal ();
164
+ } finally {
165
+ this .lock .unlock ();
166
+ }
135
167
}
136
168
137
169
/**
@@ -144,12 +176,11 @@ public Mono<Tuple2<String, Integer>> makeRequest(String path, boolean isAuthenti
144
176
*/
145
177
private ResponseHandlingResult handleResponse (HttpClientResponse response , RequestCallback requestCallback ) throws InterruptedException {
146
178
if (response .status () == HttpResponseStatus .TOO_MANY_REQUESTS ) {
179
+ System .out .println ("Too many requests were sent, is something else using the same API Key?!!" );
147
180
int timeRemaining = Math .max (1 , response .responseHeaders ().getInt ("ratelimit-reset" , 10 ));
148
181
149
182
if (this .overflowStartedNewClock .compareAndSet (false , true )) {
150
- synchronized (lock ) {
151
- this .actionsLeftThisMinute = 0 ;
152
- }
183
+ this .setActionsLeftThisMinute (0 );
153
184
resetForFirstRequest (timeRemaining );
154
185
}
155
186
@@ -162,10 +193,7 @@ private ResponseHandlingResult handleResponse(HttpClientResponse response, Reque
162
193
int timeRemaining = Math .max (1 , response .responseHeaders ().getInt ("ratelimit-reset" , 10 ));
163
194
int requestsRemaining = response .responseHeaders ().getInt ("ratelimit-remaining" , 110 );
164
195
165
- synchronized (lock ) {
166
- this .actionsLeftThisMinute = requestsRemaining ;
167
- lock .notifyAll ();
168
- }
196
+ this .setActionsLeftThisMinute (requestsRemaining );
169
197
170
198
resetForFirstRequest (timeRemaining );
171
199
}
@@ -182,10 +210,7 @@ private void resetForFirstRequest(int timeRemaining) {
182
210
Schedulers .parallel ().schedule (() -> {
183
211
this .firstRequestReturned .set (false );
184
212
this .overflowStartedNewClock .set (false );
185
- synchronized (lock ) {
186
- this .actionsLeftThisMinute = 1 ;
187
- lock .notifyAll ();
188
- }
213
+ this .setActionsLeftThisMinute (1 );
189
214
}, timeRemaining + 2 , TimeUnit .SECONDS );
190
215
}
191
216
@@ -194,37 +219,43 @@ private void resetForFirstRequest(int timeRemaining) {
194
219
*/
195
220
private static class RequestCallback {
196
221
private final String url ;
197
- private final MonoSink <Tuple2 <String , Integer >> monoSink ;
222
+ private final MonoSink <Tuple2 <String , Integer >> requestResultSink ;
198
223
private final ReactorHttpClient requestRateLimiter ;
199
224
private final boolean isAuthenticated ;
225
+ private final ReentrantLock lock = new ReentrantLock ();
200
226
private boolean isCanceled = false ;
201
227
202
- private RequestCallback (String url , MonoSink <Tuple2 <String , Integer >> monoSink , boolean isAuthenticated , ReactorHttpClient requestRateLimiter ) {
228
+ private RequestCallback (String url , MonoSink <Tuple2 <String , Integer >> requestResultSink , boolean isAuthenticated , ReactorHttpClient requestRateLimiter ) {
203
229
this .url = url ;
204
- this .monoSink = monoSink ;
230
+ this .requestResultSink = requestResultSink ;
205
231
this .requestRateLimiter = requestRateLimiter ;
206
232
this .isAuthenticated = isAuthenticated ;
207
233
208
- this .monoSink .onCancel (() -> {
209
- synchronized (this ) {
210
- this .isCanceled = true ;
211
- }
212
- });
234
+ this .requestResultSink .onCancel (this ::setCanceled );
235
+ }
236
+
237
+ private void setCanceled () {
238
+ this .lock .lock ();
239
+ try {
240
+ this .isCanceled = true ;
241
+ } finally {
242
+ this .lock .unlock ();
243
+ }
213
244
}
214
245
215
246
public boolean isCanceled () {
216
- return this .isCanceled ;
247
+ this .lock .lock ();
248
+ try {
249
+ return this .isCanceled ;
250
+ } finally {
251
+ this .lock .unlock ();
252
+ }
217
253
}
218
254
219
255
private void sendRequest () {
220
- synchronized (this ) {
221
- if (isCanceled ) {
222
- synchronized (this .requestRateLimiter .lock ) {
223
- this .requestRateLimiter .actionsLeftThisMinute ++;
224
- this .requestRateLimiter .lock .notifyAll ();
225
- }
226
- return ;
227
- }
256
+ if (this .isCanceled ()) {
257
+ this .requestRateLimiter .incrementActionsLeftThisMinute ();
258
+ return ;
228
259
}
229
260
230
261
(this .isAuthenticated ? requestRateLimiter .httpClient .headers (headers -> headers .add ("API-Key" , requestRateLimiter .apiKey .toString ())) : requestRateLimiter .httpClient ).get ()
@@ -238,10 +269,10 @@ private void sendRequest() {
238
269
}
239
270
return Mono .empty ();
240
271
} catch (InterruptedException e ) {
241
- monoSink .error (e );
272
+ this . requestResultSink .error (e );
242
273
throw new AssertionError ("ERROR: Queue insertion got interrupted, serious problem! (this should not happen!!)" , e );
243
274
}
244
- }).subscribe (this .monoSink ::success );
275
+ }).subscribe (this .requestResultSink ::success );
245
276
}
246
277
}
247
278
0 commit comments