/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 | 35.3k | #define CURL_US_PER_SEC 1000000 |
30 | 1.68k | #define CURL_RLIMIT_MIN_RATE (4 * 1024) /* minimum step rate */ |
31 | 1.14k | #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 | 23.3M | { |
36 | 23.3M | timediff_t elapsed_us, elapsed_steps; |
37 | 23.3M | int64_t token_gain; |
38 | | |
39 | 23.3M | DEBUGASSERT(r->rate_per_step); |
40 | 23.3M | if((r->ts.tv_sec == pts->tv_sec) && (r->ts.tv_usec == pts->tv_usec)) |
41 | 1.70k | return; |
42 | | |
43 | 23.3M | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
44 | 23.3M | if(elapsed_us < 0) { /* not going back in time */ |
45 | 0 | DEBUGASSERT(0); |
46 | 0 | return; |
47 | 0 | } |
48 | | |
49 | 23.3M | elapsed_us += r->spare_us; |
50 | 23.3M | if(elapsed_us < r->step_us) |
51 | 23.3M | return; |
52 | | |
53 | | /* we do the update */ |
54 | 1.19k | r->ts = *pts; |
55 | 1.19k | elapsed_steps = elapsed_us / r->step_us; |
56 | 1.19k | r->spare_us = elapsed_us % r->step_us; |
57 | | |
58 | | /* How many tokens did we gain since the last update? */ |
59 | 1.19k | if(r->rate_per_step > (INT64_MAX / elapsed_steps)) |
60 | 0 | token_gain = INT64_MAX; |
61 | 1.19k | else { |
62 | 1.19k | token_gain = r->rate_per_step * elapsed_steps; |
63 | 1.19k | } |
64 | | |
65 | 1.19k | if((INT64_MAX - token_gain) > r->tokens) |
66 | 1.19k | 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.19k | if(r->burst_per_step && (r->tokens > r->burst_per_step)) { |
73 | 1.14k | r->tokens = r->burst_per_step; |
74 | 1.14k | } |
75 | 1.19k | } |
76 | | |
77 | | static void rlimit_tune_steps(struct Curl_rlimit *r, |
78 | | int64_t tokens_total) |
79 | 18.9k | { |
80 | 18.9k | 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 | 18.9k | if(!r->rate_per_step || |
102 | 4.81k | (tokens_total <= 1) || |
103 | 1.36k | (tokens_total > (INT64_MAX / 1000))) |
104 | 17.7k | return; |
105 | | |
106 | | /* Calculate tokens for the last step and the ones before. */ |
107 | 1.14k | tokens_last = tokens_total / 100; |
108 | 1.14k | if(!tokens_last) /* less than 100 total, use 1 */ |
109 | 215 | tokens_last = 1; |
110 | 929 | else if(tokens_last > CURL_RLIMIT_MIN_RATE) |
111 | 751 | tokens_last = CURL_RLIMIT_MIN_RATE; |
112 | 1.14k | DEBUGASSERT(tokens_last); |
113 | 1.14k | tokens_main = tokens_total - tokens_last; |
114 | 1.14k | 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.14k | DEBUGASSERT(r->step_us == CURL_US_PER_SEC); |
119 | | |
120 | 1.14k | msteps = (tokens_main * 1000 / r->rate_per_step); |
121 | 1.14k | if(msteps < CURL_RLIMIT_STEP_MIN_MS) { |
122 | | /* Steps this small will not work. Do not tune. */ |
123 | 113 | return; |
124 | 113 | } |
125 | 1.03k | 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 | 302 | r->step_us = (timediff_t)msteps * 1000; |
129 | 302 | r->rate_per_step = tokens_main; |
130 | 302 | r->tokens = r->rate_per_step; |
131 | 302 | } |
132 | 729 | 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 | 729 | curl_off_t ms_unaccounted = (msteps % 1000); |
137 | 729 | curl_off_t mstep_inc = (ms_unaccounted / (msteps / 1000)); |
138 | 729 | if(mstep_inc) { |
139 | 143 | curl_off_t rate_inc = ((r->rate_per_step * mstep_inc) / 1000); |
140 | 143 | if(rate_inc) { |
141 | 120 | r->step_us = CURL_US_PER_SEC + ((timediff_t)mstep_inc * 1000); |
142 | 120 | r->rate_per_step += rate_inc; |
143 | 120 | r->tokens = r->rate_per_step; |
144 | 120 | } |
145 | 143 | } |
146 | 729 | } |
147 | | |
148 | 1.03k | if(r->burst_per_step) |
149 | 1.03k | r->burst_per_step = r->rate_per_step; |
150 | 1.03k | } |
151 | | |
152 | | void Curl_rlimit_init(struct Curl_rlimit *r, |
153 | | int64_t rate_per_sec, |
154 | | int64_t burst_per_sec, |
155 | | const struct curltime *pts) |
156 | 16.3k | { |
157 | 16.3k | DEBUGASSERT(rate_per_sec >= 0); |
158 | 16.3k | DEBUGASSERT(burst_per_sec >= rate_per_sec || !burst_per_sec); |
159 | 16.3k | DEBUGASSERT(pts); |
160 | 16.3k | r->rate_per_step = r->rate_per_sec = rate_per_sec; |
161 | 16.3k | r->burst_per_step = r->burst_per_sec = burst_per_sec; |
162 | 16.3k | r->step_us = CURL_US_PER_SEC; |
163 | 16.3k | r->spare_us = 0; |
164 | 16.3k | r->tokens = r->rate_per_step; |
165 | 16.3k | r->ts = *pts; |
166 | 16.3k | r->blocked = FALSE; |
167 | 16.3k | } |
168 | | |
169 | | void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts, |
170 | | int64_t total_tokens) |
171 | 18.9k | { |
172 | | /* A start always resets the values to initial defaults, then |
173 | | * fine tunes the intervals for the total_tokens expected. */ |
174 | 18.9k | r->rate_per_step = r->rate_per_sec; |
175 | 18.9k | r->burst_per_step = r->burst_per_sec; |
176 | 18.9k | r->step_us = CURL_US_PER_SEC; |
177 | 18.9k | r->spare_us = 0; |
178 | 18.9k | r->tokens = r->rate_per_step; |
179 | 18.9k | r->ts = *pts; |
180 | 18.9k | rlimit_tune_steps(r, total_tokens); |
181 | 18.9k | } |
182 | | |
183 | | int64_t Curl_rlimit_per_step(struct Curl_rlimit *r) |
184 | 38.5k | { |
185 | 38.5k | return r->rate_per_step; |
186 | 38.5k | } |
187 | | |
188 | | bool Curl_rlimit_active(struct Curl_rlimit *r) |
189 | 28.6M | { |
190 | 28.6M | return (r->rate_per_step > 0) || r->blocked; |
191 | 28.6M | } |
192 | | |
193 | | bool Curl_rlimit_is_blocked(struct Curl_rlimit *r) |
194 | 43.5M | { |
195 | 43.5M | return (bool)r->blocked; |
196 | 43.5M | } |
197 | | |
198 | | int64_t Curl_rlimit_avail(struct Curl_rlimit *r, |
199 | | const struct curltime *pts) |
200 | 36.9M | { |
201 | 36.9M | if(r->blocked) |
202 | 0 | return 0; |
203 | 36.9M | else if(r->rate_per_step) { |
204 | 14.5M | rlimit_update(r, pts); |
205 | 14.5M | return r->tokens; |
206 | 14.5M | } |
207 | 22.4M | else |
208 | 22.4M | return INT64_MAX; |
209 | 36.9M | } |
210 | | |
211 | | void Curl_rlimit_drain(struct Curl_rlimit *r, |
212 | | size_t tokens, |
213 | | const struct curltime *pts) |
214 | 1.83M | { |
215 | 1.83M | if(r->blocked || !r->rate_per_step) |
216 | 1.72M | return; |
217 | | |
218 | 101k | rlimit_update(r, pts); |
219 | 101k | #if 8 <= SIZEOF_SIZE_T |
220 | 101k | if(tokens > INT64_MAX) { |
221 | 0 | r->tokens = INT64_MAX; |
222 | 0 | } |
223 | 101k | else |
224 | 101k | #endif |
225 | 101k | { |
226 | 101k | int64_t val = (int64_t)tokens; |
227 | 101k | if((INT64_MIN + val) < r->tokens) |
228 | 101k | r->tokens -= val; |
229 | 0 | else |
230 | 0 | r->tokens = INT64_MIN; |
231 | 101k | } |
232 | 101k | } |
233 | | |
234 | | timediff_t Curl_rlimit_wait_ms(struct Curl_rlimit *r, |
235 | | const struct curltime *pts) |
236 | 13.4M | { |
237 | 13.4M | timediff_t wait_us, elapsed_us; |
238 | | |
239 | 13.4M | if(r->blocked || !r->rate_per_step) |
240 | 4.74M | return 0; |
241 | 8.74M | rlimit_update(r, pts); |
242 | 8.74M | if(r->tokens > 0) |
243 | 8.74M | return 0; |
244 | | |
245 | | /* How much time will it take tokens to become positive again? |
246 | | * Deduct `spare_us` and check against already elapsed time */ |
247 | 1.59k | wait_us = r->step_us - r->spare_us; |
248 | 1.59k | if(r->tokens < 0) { |
249 | 597 | curl_off_t debt_pct = ((-r->tokens) * 100 / r->rate_per_step); |
250 | 597 | if(debt_pct) |
251 | 549 | wait_us += (r->step_us * debt_pct / 100); |
252 | 597 | } |
253 | | |
254 | 1.59k | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
255 | 1.59k | if(elapsed_us >= wait_us) |
256 | 0 | return 0; |
257 | 1.59k | wait_us -= elapsed_us; |
258 | 1.59k | return (wait_us + 999) / 1000; /* in milliseconds */ |
259 | 1.59k | } |
260 | | |
261 | | timediff_t Curl_rlimit_next_step_ms(struct Curl_rlimit *r, |
262 | | const struct curltime *pts) |
263 | 13.4M | { |
264 | 13.4M | if(!r->blocked && r->rate_per_step) { |
265 | 8.74M | timediff_t elapsed_us, next_us; |
266 | | |
267 | 8.74M | elapsed_us = curlx_ptimediff_us(pts, &r->ts) + r->spare_us; |
268 | 8.74M | if(r->step_us > elapsed_us) { |
269 | 8.74M | next_us = r->step_us - elapsed_us; |
270 | 8.74M | return (next_us + 999) / 1000; /* in milliseconds */ |
271 | 8.74M | } |
272 | 8.74M | } |
273 | 4.74M | return 0; |
274 | 13.4M | } |
275 | | |
276 | | void Curl_rlimit_block(struct Curl_rlimit *r, |
277 | | bool activate, |
278 | | const struct curltime *pts) |
279 | 322k | { |
280 | 322k | if(!activate == !r->blocked) |
281 | 322k | return; |
282 | | |
283 | 0 | r->ts = *pts; |
284 | 0 | r->blocked = activate; |
285 | 0 | if(!r->blocked) { |
286 | | /* Start rate limiting fresh. The amount of time this was blocked |
287 | | * does not generate extra tokens. */ |
288 | 0 | Curl_rlimit_start(r, pts, -1); |
289 | 0 | } |
290 | 0 | else { |
291 | 0 | r->tokens = 0; |
292 | 0 | } |
293 | 0 | } |