/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 | 25.2k | #define CURL_US_PER_SEC 1000000 |
30 | 1.70k | #define CURL_RLIMIT_MIN_RATE (4 * 1024) /* minimum step rate */ |
31 | 1.06k | #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 | 21.3M | { |
36 | 21.3M | timediff_t elapsed_us, elapsed_steps; |
37 | 21.3M | int64_t token_gain; |
38 | | |
39 | 21.3M | DEBUGASSERT(r->rate_per_step); |
40 | 21.3M | if((r->ts.tv_sec == pts->tv_sec) && (r->ts.tv_usec == pts->tv_usec)) |
41 | 1.85k | return; |
42 | | |
43 | 21.3M | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
44 | 21.3M | if(elapsed_us < 0) { /* not going back in time */ |
45 | 0 | DEBUGASSERT(0); |
46 | 0 | return; |
47 | 0 | } |
48 | | |
49 | 21.3M | elapsed_us += r->spare_us; |
50 | 21.3M | if(elapsed_us < r->step_us) |
51 | 21.3M | return; |
52 | | |
53 | | /* we do the update */ |
54 | 298 | r->ts = *pts; |
55 | 298 | elapsed_steps = elapsed_us / r->step_us; |
56 | 298 | r->spare_us = elapsed_us % r->step_us; |
57 | | |
58 | | /* How many tokens did we gain since the last update? */ |
59 | 298 | if(r->rate_per_step > (INT64_MAX / elapsed_steps)) |
60 | 0 | token_gain = INT64_MAX; |
61 | 298 | else { |
62 | 298 | token_gain = r->rate_per_step * elapsed_steps; |
63 | 298 | } |
64 | | |
65 | 298 | if((INT64_MAX - token_gain) > r->tokens) |
66 | 298 | 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 | 298 | if(r->burst_per_step && (r->tokens > r->burst_per_step)) { |
73 | 231 | r->tokens = r->burst_per_step; |
74 | 231 | } |
75 | 298 | } |
76 | | |
77 | | static void rlimit_tune_steps(struct Curl_rlimit *r, |
78 | | int64_t tokens_total) |
79 | 16.5k | { |
80 | 16.5k | 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 | 16.5k | if(!r->rate_per_step || |
102 | 3.38k | (tokens_total <= 1) || |
103 | 1.21k | (tokens_total > (INT64_MAX / 1000))) |
104 | 15.5k | return; |
105 | | |
106 | | /* Calculate tokens for the last step and the ones before. */ |
107 | 1.06k | tokens_last = tokens_total / 100; |
108 | 1.06k | if(!tokens_last) /* less than 100 total, use 1 */ |
109 | 121 | tokens_last = 1; |
110 | 944 | else if(tokens_last > CURL_RLIMIT_MIN_RATE) |
111 | 756 | tokens_last = CURL_RLIMIT_MIN_RATE; |
112 | 1.06k | DEBUGASSERT(tokens_last); |
113 | 1.06k | tokens_main = tokens_total - tokens_last; |
114 | 1.06k | 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.06k | DEBUGASSERT(r->step_us == CURL_US_PER_SEC); |
119 | | |
120 | 1.06k | msteps = (tokens_main * 1000 / r->rate_per_step); |
121 | 1.06k | if(msteps < CURL_RLIMIT_STEP_MIN_MS) { |
122 | | /* Steps this small will not work. Do not tune. */ |
123 | 98 | return; |
124 | 98 | } |
125 | 967 | 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 | 294 | r->step_us = (timediff_t)msteps * 1000; |
129 | 294 | r->rate_per_step = tokens_main; |
130 | 294 | r->tokens = r->rate_per_step; |
131 | 294 | } |
132 | 673 | 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 | 673 | curl_off_t ms_unaccounted = (msteps % 1000); |
137 | 673 | curl_off_t mstep_inc = (ms_unaccounted / (msteps / 1000)); |
138 | 673 | if(mstep_inc) { |
139 | 156 | curl_off_t rate_inc = ((r->rate_per_step * mstep_inc) / 1000); |
140 | 156 | if(rate_inc) { |
141 | 141 | r->step_us = CURL_US_PER_SEC + ((timediff_t)mstep_inc * 1000); |
142 | 141 | r->rate_per_step += rate_inc; |
143 | 141 | r->tokens = r->rate_per_step; |
144 | 141 | } |
145 | 156 | } |
146 | 673 | } |
147 | | |
148 | 967 | if(r->burst_per_step) |
149 | 967 | r->burst_per_step = r->rate_per_step; |
150 | 967 | } |
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 | 8.47k | { |
157 | 8.47k | DEBUGASSERT(rate_per_sec >= 0); |
158 | 8.47k | DEBUGASSERT(burst_per_sec >= rate_per_sec || !burst_per_sec); |
159 | 8.47k | DEBUGASSERT(pts); |
160 | 8.47k | r->rate_per_step = r->rate_per_sec = rate_per_sec; |
161 | 8.47k | r->burst_per_step = r->burst_per_sec = burst_per_sec; |
162 | 8.47k | r->step_us = CURL_US_PER_SEC; |
163 | 8.47k | r->spare_us = 0; |
164 | 8.47k | r->tokens = r->rate_per_step; |
165 | 8.47k | r->ts = *pts; |
166 | 8.47k | r->blocked = FALSE; |
167 | 8.47k | } |
168 | | |
169 | | void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts, |
170 | | int64_t total_tokens) |
171 | 16.5k | { |
172 | | /* A start always resets the values to initial defaults, then |
173 | | * fine tunes the intervals for the total_tokens expected. */ |
174 | 16.5k | r->rate_per_step = r->rate_per_sec; |
175 | 16.5k | r->burst_per_step = r->burst_per_sec; |
176 | 16.5k | r->step_us = CURL_US_PER_SEC; |
177 | 16.5k | r->spare_us = 0; |
178 | 16.5k | r->tokens = r->rate_per_step; |
179 | 16.5k | r->ts = *pts; |
180 | 16.5k | rlimit_tune_steps(r, total_tokens); |
181 | 16.5k | } |
182 | | |
183 | | int64_t Curl_rlimit_per_step(struct Curl_rlimit *r) |
184 | 32.6k | { |
185 | 32.6k | return r->rate_per_step; |
186 | 32.6k | } |
187 | | |
188 | | bool Curl_rlimit_active(struct Curl_rlimit *r) |
189 | 33.9M | { |
190 | 33.9M | return (r->rate_per_step > 0) || r->blocked; |
191 | 33.9M | } |
192 | | |
193 | | bool Curl_rlimit_is_blocked(struct Curl_rlimit *r) |
194 | 134M | { |
195 | 134M | return (bool)r->blocked; |
196 | 134M | } |
197 | | |
198 | | int64_t Curl_rlimit_avail(struct Curl_rlimit *r, |
199 | | const struct curltime *pts) |
200 | 88.9M | { |
201 | 88.9M | if(r->blocked) |
202 | 0 | return 0; |
203 | 88.9M | else if(r->rate_per_step) { |
204 | 13.2M | rlimit_update(r, pts); |
205 | 13.2M | return r->tokens; |
206 | 13.2M | } |
207 | 75.6M | else |
208 | 75.6M | return INT64_MAX; |
209 | 88.9M | } |
210 | | |
211 | | void Curl_rlimit_drain(struct Curl_rlimit *r, |
212 | | size_t tokens, |
213 | | const struct curltime *pts) |
214 | 2.18M | { |
215 | 2.18M | if(r->blocked || !r->rate_per_step) |
216 | 2.01M | return; |
217 | | |
218 | 171k | rlimit_update(r, pts); |
219 | 171k | #if 8 <= SIZEOF_SIZE_T |
220 | 171k | if(tokens > INT64_MAX) { |
221 | 0 | r->tokens = INT64_MAX; |
222 | 0 | } |
223 | 171k | else |
224 | 171k | #endif |
225 | 171k | { |
226 | 171k | int64_t val = (int64_t)tokens; |
227 | 171k | if((INT64_MIN + val) < r->tokens) |
228 | 171k | r->tokens -= val; |
229 | 0 | else |
230 | 0 | r->tokens = INT64_MIN; |
231 | 171k | } |
232 | 171k | } |
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 | 5.57M | return 0; |
241 | 7.88M | rlimit_update(r, pts); |
242 | 7.88M | if(r->tokens > 0) |
243 | 7.65M | 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 | 223k | wait_us = r->step_us - r->spare_us; |
248 | 223k | if(r->tokens < 0) { |
249 | 589 | curl_off_t debt_pct = ((-r->tokens) * 100 / r->rate_per_step); |
250 | 589 | if(debt_pct) |
251 | 544 | wait_us += (r->step_us * debt_pct / 100); |
252 | 589 | } |
253 | | |
254 | 223k | elapsed_us = curlx_ptimediff_us(pts, &r->ts); |
255 | 223k | if(elapsed_us >= wait_us) |
256 | 0 | return 0; |
257 | 223k | wait_us -= elapsed_us; |
258 | 223k | return (wait_us + 999) / 1000; /* in milliseconds */ |
259 | 223k | } |
260 | | |
261 | | timediff_t Curl_rlimit_next_step_ms(struct Curl_rlimit *r, |
262 | | const struct curltime *pts) |
263 | 13.0M | { |
264 | 13.0M | if(!r->blocked && r->rate_per_step) { |
265 | 7.65M | timediff_t elapsed_us, next_us; |
266 | | |
267 | 7.65M | elapsed_us = curlx_ptimediff_us(pts, &r->ts) + r->spare_us; |
268 | 7.65M | if(r->step_us > elapsed_us) { |
269 | 7.65M | next_us = r->step_us - elapsed_us; |
270 | 7.65M | return (next_us + 999) / 1000; /* in milliseconds */ |
271 | 7.65M | } |
272 | 7.65M | } |
273 | 5.35M | return 0; |
274 | 13.0M | } |
275 | | |
276 | | void Curl_rlimit_block(struct Curl_rlimit *r, |
277 | | bool activate, |
278 | | const struct curltime *pts) |
279 | 290k | { |
280 | 290k | if(!activate == !r->blocked) |
281 | 290k | 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 | } |