/src/curl/lib/ratelimit.c
Line | Count | Source |
1 | | /*************************************************************************** |
2 | | * _ _ ____ _ |
3 | | * Project ___| | | | _ \| | |
4 | | * / __| | | | |_) | | |
5 | | * | (__| |_| | _ <| |___ |
6 | | * \___|\___/|_| \_\_____| |
7 | | * |
8 | | * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. |
9 | | * |
10 | | * This software is licensed as described in the file COPYING, which |
11 | | * you should have received as part of this distribution. The terms |
12 | | * are also available at https://curl.se/docs/copyright.html. |
13 | | * |
14 | | * You may opt to use, copy, modify, merge, publish, distribute and/or sell |
15 | | * copies of the Software, and permit persons to whom the Software is |
16 | | * furnished to do so, under the terms of the COPYING file. |
17 | | * |
18 | | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
19 | | * KIND, either express or implied. |
20 | | * |
21 | | * SPDX-License-Identifier: curl |
22 | | * |
23 | | ***************************************************************************/ |
24 | | #include "curl_setup.h" |
25 | | |
26 | | #include "urldata.h" |
27 | | #include "ratelimit.h" |
28 | | |
29 | 34.6k | #define CURL_US_PER_SEC 1000000 |
30 | 1.71k | #define CURL_RLIMIT_MIN_RATE (4 * 1024) /* minimum step rate */ |
31 | 1.15k | #define CURL_RLIMIT_STEP_MIN_MS 2 /* minimum step duration */ |
32 | | |
33 | | static void rlimit_update(struct Curl_rlimit *r, |
34 | | const struct curltime *pts) |
35 | 1.08M | { |
36 | 1.08M | timediff_t elapsed_us, elapsed_steps; |
37 | 1.08M | int64_t token_gain; |
38 | | |
39 | 1.08M | DEBUGASSERT(r->rate_per_step); |
40 | 1.08M | if((r->ts.tv_sec == pts->tv_sec) && (r->ts.tv_usec == pts->tv_usec)) |
41 | 1.65k | return; |
42 | | |
43 | 1.08M | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
44 | 1.08M | if(elapsed_us < 0) { /* not going back in time */ |
45 | 0 | DEBUGASSERT(0); |
46 | 0 | return; |
47 | 0 | } |
48 | | |
49 | 1.08M | elapsed_us += r->spare_us; |
50 | 1.08M | if(elapsed_us < r->step_us) |
51 | 1.08M | return; |
52 | | |
53 | | /* we do the update */ |
54 | 1.31k | r->ts = *pts; |
55 | 1.31k | elapsed_steps = elapsed_us / r->step_us; |
56 | 1.31k | r->spare_us = elapsed_us % r->step_us; |
57 | | |
58 | | /* How many tokens did we gain since the last update? */ |
59 | 1.31k | if(r->rate_per_step > (INT64_MAX / elapsed_steps)) |
60 | 0 | token_gain = INT64_MAX; |
61 | 1.31k | else { |
62 | 1.31k | token_gain = r->rate_per_step * elapsed_steps; |
63 | 1.31k | } |
64 | | |
65 | 1.31k | if((INT64_MAX - token_gain) > r->tokens) |
66 | 1.31k | r->tokens += token_gain; |
67 | 0 | else |
68 | 0 | r->tokens = INT64_MAX; |
69 | | |
70 | | /* Limit the token again by the burst rate (if set), so we |
71 | | * do not suddenly have a huge number of tokens after inactivity. */ |
72 | 1.31k | if(r->burst_per_step && (r->tokens > r->burst_per_step)) { |
73 | 48 | r->tokens = r->burst_per_step; |
74 | 48 | } |
75 | 1.31k | } |
76 | | |
77 | | static void rlimit_tune_steps(struct Curl_rlimit *r, |
78 | | int64_t tokens_total) |
79 | 17.3k | { |
80 | 17.3k | int64_t tokens_last, tokens_main, msteps; |
81 | | |
82 | | /* Tune the ratelimit at the start *if* we know how many tokens |
83 | | * are expected to be consumed in total. |
84 | | * The reason for tuning is that rlimit provides tokens to be consumed |
85 | | * per "step" which starts out to be a second. The tokens may be consumed |
86 | | * in full at the beginning of a step. The remainder of the second will |
87 | | * have no tokens available, effectively blocking the consumption and |
88 | | * so keeping the "step average" in line. |
89 | | * This works will up to the last step. When no more tokens are needed, |
90 | | * no wait will happen and the last step would be too fast. This is |
91 | | * especially noticeable when only a few steps are needed. |
92 | | * |
93 | | * Example: downloading 1.5kb with a ratelimit of 1k could be done in |
94 | | * roughly 1 second (1k in the first second and the 0.5 at the start of |
95 | | * the second one). |
96 | | * |
97 | | * The tuning tries to make the last step small, using only |
98 | | * 1 percent of the total tokens (at least 1). The rest of the tokens |
99 | | * are to be consumed in the steps before by adjusting the duration of |
100 | | * the step and the amount of tokens it provides. */ |
101 | 17.3k | if(!r->rate_per_step || |
102 | 4.39k | (tokens_total <= 1) || |
103 | 1.30k | (tokens_total > (INT64_MAX / 1000))) |
104 | 16.1k | return; |
105 | | |
106 | | /* Calculate tokens for the last step and the ones before. */ |
107 | 1.15k | tokens_last = tokens_total / 100; |
108 | 1.15k | if(!tokens_last) /* less than 100 total, use 1 */ |
109 | 198 | tokens_last = 1; |
110 | 960 | else if(tokens_last > CURL_RLIMIT_MIN_RATE) |
111 | 753 | tokens_last = CURL_RLIMIT_MIN_RATE; |
112 | 1.15k | DEBUGASSERT(tokens_last); |
113 | 1.15k | tokens_main = tokens_total - tokens_last; |
114 | 1.15k | DEBUGASSERT(tokens_main); |
115 | | |
116 | | /* how many milli-steps will it take to consume those, give the |
117 | | * original rate limit per second? */ |
118 | 1.15k | DEBUGASSERT(r->step_us == CURL_US_PER_SEC); |
119 | | |
120 | 1.15k | msteps = (tokens_main * 1000 / r->rate_per_step); |
121 | 1.15k | if(msteps < CURL_RLIMIT_STEP_MIN_MS) { |
122 | | /* Steps this small will not work. Do not tune. */ |
123 | 168 | return; |
124 | 168 | } |
125 | 990 | else if(msteps < 1000) { |
126 | | /* It needs less than one step to provide the needed tokens. |
127 | | * Make it exactly that long and with exactly those tokens. */ |
128 | 256 | r->step_us = (timediff_t)msteps * 1000; |
129 | 256 | r->rate_per_step = tokens_main; |
130 | 256 | r->tokens = r->rate_per_step; |
131 | 256 | } |
132 | 734 | else { |
133 | | /* More than 1 step. Spread the remainder milli steps and |
134 | | * the tokens they need to provide across all steps. If integer |
135 | | * arithmetic can do it. */ |
136 | 734 | curl_off_t ms_unaccounted = (msteps % 1000); |
137 | 734 | curl_off_t mstep_inc = (ms_unaccounted / (msteps / 1000)); |
138 | 734 | if(mstep_inc) { |
139 | 205 | curl_off_t rate_inc = ((r->rate_per_step * mstep_inc) / 1000); |
140 | 205 | if(rate_inc) { |
141 | 192 | r->step_us = CURL_US_PER_SEC + ((timediff_t)mstep_inc * 1000); |
142 | 192 | r->rate_per_step += rate_inc; |
143 | 192 | r->tokens = r->rate_per_step; |
144 | 192 | if(r->burst_per_step) { |
145 | 192 | curl_off_t burst_inc = ((r->burst_per_step * mstep_inc) / 1000); |
146 | 192 | if(burst_inc) |
147 | 192 | r->burst_per_step += burst_inc; |
148 | 192 | } |
149 | 192 | } |
150 | 205 | } |
151 | 734 | } |
152 | 1.15k | } |
153 | | |
154 | | void Curl_rlimit_init(struct Curl_rlimit *r, |
155 | | int64_t rate_per_sec, |
156 | | int64_t burst_per_sec, |
157 | | const struct curltime *pts) |
158 | 17.1k | { |
159 | 17.1k | DEBUGASSERT(rate_per_sec >= 0); |
160 | 17.1k | DEBUGASSERT(burst_per_sec >= rate_per_sec || !burst_per_sec); |
161 | 17.1k | DEBUGASSERT(pts); |
162 | 17.1k | r->rate_per_step = r->rate_per_sec = rate_per_sec; |
163 | 17.1k | r->burst_per_step = r->burst_per_sec = burst_per_sec; |
164 | 17.1k | r->step_us = CURL_US_PER_SEC; |
165 | 17.1k | r->spare_us = 0; |
166 | 17.1k | r->tokens = r->rate_per_step; |
167 | 17.1k | r->ts = *pts; |
168 | 17.1k | r->blocked = FALSE; |
169 | 17.1k | } |
170 | | |
171 | | void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts, |
172 | | int64_t total_tokens) |
173 | 17.3k | { |
174 | | /* A start always resets the values to initial defaults, then |
175 | | * fine tunes the intervals for the total_tokens expected. */ |
176 | 17.3k | r->rate_per_step = r->rate_per_sec; |
177 | 17.3k | r->burst_per_step = r->burst_per_sec; |
178 | 17.3k | r->step_us = CURL_US_PER_SEC; |
179 | 17.3k | r->spare_us = 0; |
180 | 17.3k | r->tokens = r->rate_per_step; |
181 | 17.3k | r->ts = *pts; |
182 | 17.3k | rlimit_tune_steps(r, total_tokens); |
183 | 17.3k | } |
184 | | |
185 | | int64_t Curl_rlimit_per_step(struct Curl_rlimit *r) |
186 | 36.0k | { |
187 | 36.0k | return r->rate_per_step; |
188 | 36.0k | } |
189 | | |
190 | | bool Curl_rlimit_active(struct Curl_rlimit *r) |
191 | 716k | { |
192 | 716k | return (r->rate_per_step > 0) || r->blocked; |
193 | 716k | } |
194 | | |
195 | | bool Curl_rlimit_is_blocked(struct Curl_rlimit *r) |
196 | 780k | { |
197 | 780k | return (bool)r->blocked; |
198 | 780k | } |
199 | | |
200 | | int64_t Curl_rlimit_avail(struct Curl_rlimit *r, |
201 | | const struct curltime *pts) |
202 | 769k | { |
203 | 769k | struct curltime ts; |
204 | | |
205 | 769k | if(r->blocked) |
206 | 0 | return 0; |
207 | 769k | else if(r->rate_per_step) { |
208 | 415k | if(!pts) { |
209 | 415k | curlx_pnow(&ts); |
210 | 415k | pts = &ts; |
211 | 415k | } |
212 | 415k | rlimit_update(r, pts); |
213 | 415k | return r->tokens; |
214 | 415k | } |
215 | 354k | else |
216 | 354k | return INT64_MAX; |
217 | 769k | } |
218 | | |
219 | | void Curl_rlimit_drain(struct Curl_rlimit *r, size_t tokens, |
220 | | const struct curltime *pts) |
221 | 441k | { |
222 | 441k | struct curltime ts; |
223 | | |
224 | 441k | if(r->blocked || !r->rate_per_step) |
225 | 79.3k | return; |
226 | | |
227 | 362k | if(!pts) { |
228 | 362k | curlx_pnow(&ts); |
229 | 362k | pts = &ts; |
230 | 362k | } |
231 | 362k | rlimit_update(r, pts); |
232 | 362k | #if 8 <= SIZEOF_SIZE_T |
233 | 362k | if(tokens > INT64_MAX) { |
234 | 0 | r->tokens = INT64_MAX; |
235 | 0 | } |
236 | 362k | else |
237 | 362k | #endif |
238 | 362k | { |
239 | 362k | int64_t val = (int64_t)tokens; |
240 | 362k | if((INT64_MIN + val) < r->tokens) |
241 | 362k | r->tokens -= val; |
242 | 0 | else |
243 | 0 | r->tokens = INT64_MIN; |
244 | 362k | } |
245 | 362k | } |
246 | | |
247 | | timediff_t Curl_rlimit_wait_ms(struct Curl_rlimit *r, |
248 | | const struct curltime *pts) |
249 | 615k | { |
250 | 615k | timediff_t wait_us, elapsed_us; |
251 | | |
252 | 615k | if(r->blocked || !r->rate_per_step) |
253 | 305k | return 0; |
254 | 309k | rlimit_update(r, pts); |
255 | 309k | if(r->tokens > 0) |
256 | 306k | return 0; |
257 | | |
258 | | /* How much time will it take tokens to become positive again? |
259 | | * Deduct `spare_us` and check against already elapsed time */ |
260 | 2.51k | wait_us = r->step_us - r->spare_us; |
261 | 2.51k | if(r->tokens < 0) { |
262 | 463 | curl_off_t debt_pct = ((-r->tokens) * 100 / r->rate_per_step); |
263 | 463 | if(debt_pct) |
264 | 436 | wait_us += (r->step_us * debt_pct / 100); |
265 | 463 | } |
266 | | |
267 | 2.51k | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
268 | 2.51k | if(elapsed_us >= wait_us) |
269 | 0 | return 0; |
270 | 2.51k | wait_us -= elapsed_us; |
271 | 2.51k | return (wait_us + 999) / 1000; /* in milliseconds */ |
272 | 2.51k | } |
273 | | |
274 | | timediff_t Curl_rlimit_next_step_ms(struct Curl_rlimit *r, |
275 | | const struct curltime *pts) |
276 | 609k | { |
277 | 609k | if(!r->blocked && r->rate_per_step) { |
278 | 306k | timediff_t elapsed_us, next_us; |
279 | | |
280 | 306k | elapsed_us = curlx_ptimediff_us(pts, &r->ts) + r->spare_us; |
281 | 306k | if(r->step_us > elapsed_us) { |
282 | 306k | next_us = r->step_us - elapsed_us; |
283 | 306k | return (next_us + 999) / 1000; /* in milliseconds */ |
284 | 306k | } |
285 | 306k | } |
286 | 303k | return 0; |
287 | 609k | } |
288 | | |
289 | | void Curl_rlimit_block(struct Curl_rlimit *r, |
290 | | bool activate, |
291 | | const struct curltime *pts) |
292 | 336k | { |
293 | 336k | if(!activate == !r->blocked) |
294 | 336k | return; |
295 | | |
296 | 0 | r->ts = *pts; |
297 | 0 | r->blocked = activate; |
298 | 0 | if(!r->blocked) { |
299 | | /* Start rate limiting fresh. The amount of time this was blocked |
300 | | * does not generate extra tokens. */ |
301 | 0 | Curl_rlimit_start(r, pts, -1); |
302 | 0 | } |
303 | 0 | else { |
304 | 0 | r->tokens = 0; |
305 | 0 | } |
306 | 0 | } |