The FreeRADIUS server $Id: 15bac2a4c627c01d1aa2047687b3418955ac7f00 $
Loading...
Searching...
No Matches
retry_tests.c
Go to the documentation of this file.
1/*
2 * This library is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU Lesser General Public
4 * License as published by the Free Software Foundation; either
5 * version 2.1 of the License, or (at your option) any later version.
6 *
7 * This library is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 * Lesser General Public License for more details.
11 *
12 * You should have received a copy of the GNU Lesser General Public
13 * License along with this library; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17/** Tests for the retransmission timer
18 *
19 * @file src/lib/util/test/retry_tests.c
20 *
21 * @copyright 2026 Network RADIUS SAS (legal@networkradius.com)
22 */
23#include "acutest.h"
24#include "acutest_helpers.h"
25
26#include <freeradius-devel/util/retry.h>
27
28/*
29 * Test basic initialization.
30 */
31static void test_retry_init(void)
32{
33 fr_retry_t r;
35 .irt = fr_time_delta_from_sec(1),
36 .mrt = fr_time_delta_from_sec(30),
37 .mrd = fr_time_delta_from_sec(60),
38 .mrc = 10,
39 };
40 fr_time_t now = fr_time_wrap(1000000000);
41
42 fr_retry_init(&r, now, &config);
43
44 TEST_CASE("Initial state is CONTINUE");
46
47 TEST_CASE("Count starts at 1");
48 TEST_CHECK(r.count == 1);
49
50 TEST_CASE("Start is set to now");
52
53 TEST_CASE("Next retransmission is after start");
54 TEST_CHECK(fr_time_gt(r.next, now));
55
56 TEST_CASE("End is set to start + MRD");
58}
59
60/*
61 * RT = IRT + RAND*IRT
62 */
63static void test_retry_irt(void)
64{
65 fr_retry_t r;
67 .irt = fr_time_delta_from_msec(100),
68 .mrt = fr_time_delta_from_sec(100), /* effectively no limit */
69 .mrd = fr_time_delta_from_sec(0),
70 .mrc = 0,
71 };
72 fr_time_t now = fr_time_wrap(1000000000);
73 fr_time_delta_t rt_12;
74 fr_time_delta_t rt_08;
75
76 fr_retry_init(&r, now, &config);
77
78 rt_12 = fr_time_delta_wrap((fr_time_delta_unwrap(config.irt) * 12) / 10);
79 rt_08 = fr_time_delta_wrap((fr_time_delta_unwrap(config.irt) * 8) / 10);
80
81 TEST_CASE("Initial bounds for RT = IRT * (1 + RAND[-0.1,+0.1])");
83 TEST_MSG("rt should be < IRT * 1.2 (i.e. %" PRIi64 ") < (%" PRIi64 ", for IRT %" PRIi64 ")",
85
87 TEST_MSG("rt should be > IRT * 0.8 (i.e. %" PRIi64 ") > (%" PRIi64 ", for RTprev %" PRIi64 ")",
89}
90
91/*
92 * Test RT = RTprev * (2 + RAND[-0.1,+0.1])
93 *
94 * We don't set MRC or MRD here.
95 */
96static void test_retry_rt(void)
97{
98 fr_retry_t r;
100 .irt = fr_time_delta_from_msec(100),
101 .mrt = fr_time_delta_from_sec(100), /* effectively no limit */
102 .mrd = fr_time_delta_from_sec(0),
103 .mrc = 0,
104 };
105 fr_time_t now = fr_time_wrap(1000000000);
107 fr_retry_state_t state;
108 int i;
109
110 fr_retry_init(&r, now, &config);
111
112 /*
113 * 1.1+2.2x^4 =~ 25, which is smaller than MRT=100.
114 */
115 for (i = 0; i < 4; i++) {
116 now = r.next;
117 state = fr_retry_next(&r, now);
119 TEST_MSG("retry %d should be CONTINUE, got %d", i + 2, state);
120
121 /*
122 * For zero, RT = IRT + RAND*IRT
123 */
124 if (i > 0) {
126 (fr_time_delta_unwrap(rt_prev) * 22) / 10
127 );
129 (fr_time_delta_unwrap(rt_prev) * 18) / 10
130 );
131
132 TEST_CASE("Retry interval bounds RT = RTprev * (2 + RAND[-0.1,+0.1])");
133 TEST_CHECK(fr_time_delta_lt(r.rt, rt_220));
134 TEST_MSG("rt should be < RT_prev * 2.2 (i.e. %" PRIi64 ") < (%" PRIi64 ", for RTprev %" PRIi64 ")",
136
137 TEST_CHECK(fr_time_delta_gt(r.rt, rt_180));
138 TEST_MSG("rt should be > RT * 1.8 (i.e. %" PRIi64 ") > (%" PRIi64 ", for RTprev %" PRIi64 ")",
140 }
141
142 rt_prev = r.rt;
143 }
144}
145
146/*
147 * Test that MRC (max retransmission count) is respected.
148 */
149static void test_retry_mrc(void)
150{
151 fr_retry_t r;
153 .irt = fr_time_delta_from_msec(100),
154 .mrt = fr_time_delta_from_sec(5),
155 .mrd = fr_time_delta_from_sec(300),
156 .mrc = 3,
157 };
158 fr_time_t now = fr_time_wrap(1000000000);
159 fr_retry_state_t state;
160 int i;
161
162 fr_retry_init(&r, now, &config);
163
164 /*
165 * We should get CONTINUE for retransmissions 1 and 2,
166 * then MRC on the 3th (count > mrc).
167 */
168 for (i = 0; i < 2; i++) {
169 now = r.next;
170 state = fr_retry_next(&r, now);
172 TEST_MSG("retry %d should be CONTINUE, got %d", i + 2, state);
173
174 }
175
176 now = r.next;
177 state = fr_retry_next(&r, now);
178 TEST_CHECK(state == FR_RETRY_MRC);
179 TEST_MSG("retry 3 should be MRC, got %d", state);
180
182}
183
184/*
185 * Test that MRD (max retransmission duration) is respected.
186 */
187static void test_retry_mrd(void)
188{
189 fr_retry_t r;
191 .irt = fr_time_delta_from_msec(500),
192 .mrt = fr_time_delta_from_sec(2),
193 .mrd = fr_time_delta_from_sec(3),
194 .mrc = 0, /* no count limit */
195 };
196 fr_time_t now = fr_time_wrap(1000000000);
197 fr_retry_state_t state;
198 int attempts = 0;
199
200 fr_retry_init(&r, now, &config);
201
202 /*
203 * Keep retrying until we hit MRD.
204 * With no MRC, we should hit MRD.
205 */
206 for (;;) {
207 now = r.next;
208 state = fr_retry_next(&r, now);
209 attempts++;
210
211 if (state != FR_RETRY_CONTINUE) break;
212
213 /*
214 * Safety valve: shouldn't take more than 10 attempts
215 * for a 3-second MRD with 500ms IRT.
216 */
217 TEST_CHECK(attempts < 10);
218 if (attempts >= 10) break;
219 }
220
221 TEST_CHECK(state == FR_RETRY_MRD);
222 TEST_MSG("expected MRD, got %d after %d attempts", state, attempts);
224}
225
226/*
227 * Test single retry (MRC=1) which is just a simple duration timer.
228 */
229static void test_retry_single(void)
230{
231 fr_retry_t r;
233 .irt = fr_time_delta_from_sec(0),
234 .mrt = fr_time_delta_from_sec(0),
235 .mrd = fr_time_delta_from_sec(5),
236 .mrc = 1,
237 };
238 fr_time_t now = fr_time_wrap(1000000000);
239 fr_retry_state_t state;
240
241 fr_retry_init(&r, now, &config);
242
243 TEST_CASE("MRC=1 sets next to end");
245
246 TEST_CASE("First retry exceeds count, returns MRD");
247 now = r.next;
248 state = fr_retry_next(&r, now);
249 TEST_CHECK(state == FR_RETRY_MRD);
250}
251
252/*
253 * Test exponential backoff behavior.
254 */
255static void test_retry_backoff(void)
256{
257 fr_retry_t r;
259 .irt = fr_time_delta_from_msec(100),
260 .mrt = fr_time_delta_from_sec(60),
261 .mrd = fr_time_delta_from_sec(600),
262 .mrc = 20,
263 };
264 fr_time_t now = fr_time_wrap(1000000000);
265 fr_time_delta_t prev_rt;
266 int i;
267 int grew = 0;
268
269 fr_retry_init(&r, now, &config);
270 prev_rt = r.rt;
271
272 /*
273 * The retry interval should generally grow (with some randomness).
274 * Check that it grows in at least half the cases over several iterations.
275 */
276 for (i = 0; i < 8; i++) {
277 now = r.next;
278 fr_retry_next(&r, now);
279
280 if (fr_time_delta_gt(r.rt, prev_rt)) grew++;
281 prev_rt = r.rt;
282 }
283
284 TEST_CASE("Retry interval grows over time (exponential backoff)");
285 TEST_CHECK(grew >= 2);
286 TEST_MSG("expected interval to grow at least 2 times, grew %d times", grew);
287}
288
289/*
290 * Test MRT capping.
291 */
292static void test_retry_mrt_cap(void)
293{
294 fr_retry_t r;
296 .irt = fr_time_delta_from_sec(1),
297 .mrt = fr_time_delta_from_sec(4),
298 .mrd = fr_time_delta_from_sec(600),
299 .mrc = 100,
300 };
301 fr_time_t now = fr_time_wrap(1000000000);
302 int i;
303
304 fr_retry_init(&r, now, &config);
305
306 /*
307 * After enough doublings, the interval should be capped near MRT.
308 * With IRT=1s and doubling, after ~3 iterations we'd exceed MRT=4s.
309 * Allow some randomness headroom: check rt <= MRT * 1.2.
310 */
311 for (i = 0; i < 10; i++) {
312 now = r.next;
313 if (fr_retry_next(&r, now) != FR_RETRY_CONTINUE) break;
314 }
315
316 TEST_CASE("Retry interval bounds RT <= MRT * (1 + RAND[-0.1,+0.1])");
317 {
319 (fr_time_delta_unwrap(config.mrt) * 12) / 10
320 );
322 (fr_time_delta_unwrap(config.mrt) * 8) / 10
323 );
324 TEST_CHECK(fr_time_delta_lt(r.rt, mrt_120));
325 TEST_MSG("rt should be < MRT * 1.2 (i.e. %" PRIi64 ") < (%" PRIi64 ", for MRT %" PRIi64 ")",
327
328 TEST_CHECK(fr_time_delta_gt(r.rt, mrt_08));
329 TEST_MSG("rt should be > MRT * 0.8 (i.e. %" PRIi64 ") > (%" PRIi64 ", for MRT %" PRIi64 ")",
331 }
332}
333
334/*
335 * Test with no MRD and no MRC (defaults to 1-day MRD).
336 */
337static void test_retry_no_limits(void)
338{
339 fr_retry_t r;
341 .irt = fr_time_delta_from_sec(1),
342 .mrt = fr_time_delta_from_sec(30),
343 .mrd = fr_time_delta_from_sec(0),
344 .mrc = 0,
345 };
346 fr_time_t now = fr_time_wrap(1000000000);
347 fr_time_t expected_end = fr_time_add(now, fr_time_delta_from_sec(86400));
348
349 fr_retry_init(&r, now, &config);
350
351 TEST_CASE("With no MRD/MRC, end defaults to start + 1 day");
352 TEST_CHECK(fr_time_eq(r.end, expected_end));
353
354 TEST_CASE("Retransmissions continue normally");
355 now = r.next;
357}
358
360 { "retry_init", test_retry_init },
361 { "retry_irt", test_retry_irt },
362 { "retry_rt", test_retry_rt },
363 { "retry_mrc", test_retry_mrc },
364 { "retry_mrd", test_retry_mrd },
365 { "retry_single", test_retry_single },
366 { "retry_backoff", test_retry_backoff },
367 { "retry_mrt_cap", test_retry_mrt_cap },
368 { "retry_no_limits", test_retry_no_limits },
370};
#define TEST_CHECK(cond)
Definition acutest.h:87
#define TEST_CASE(name)
Definition acutest.h:186
#define TEST_TERMINATOR
Definition acutest.h:64
#define TEST_MSG(...)
Definition acutest.h:217
static const conf_parser_t config[]
Definition base.c:169
static void test_retry_mrc(void)
TEST_LIST
static void test_retry_init(void)
Definition retry_tests.c:31
static void test_retry_single(void)
static void test_retry_mrd(void)
static void test_retry_mrt_cap(void)
static void test_retry_irt(void)
Definition retry_tests.c:63
static void test_retry_no_limits(void)
static void test_retry_backoff(void)
static void test_retry_rt(void)
Definition retry_tests.c:96
static fr_time_delta_t fr_time_delta_from_msec(int64_t msec)
Definition time.h:575
static int64_t fr_time_delta_unwrap(fr_time_delta_t time)
Definition time.h:154
#define fr_time_delta_lt(_a, _b)
Definition time.h:285
static fr_time_delta_t fr_time_delta_from_sec(int64_t sec)
Definition time.h:590
#define fr_time_delta_wrap(_time)
Definition time.h:152
#define fr_time_wrap(_time)
Definition time.h:145
#define fr_time_eq(_a, _b)
Definition time.h:241
#define fr_time_add(_a, _b)
Add a time/time delta together.
Definition time.h:196
#define fr_time_gt(_a, _b)
Definition time.h:237
#define fr_time_delta_gt(_a, _b)
Definition time.h:283
A time delta, a difference in time measured in nanoseconds.
Definition time.h:80
"server local" time.
Definition time.h:69
fr_retry_state_t fr_retry_next(fr_retry_t *r, fr_time_t now)
Initialize a retransmission counter.
Definition retry.c:110
void fr_retry_init(fr_retry_t *r, fr_time_t now, fr_retry_config_t const *config)
Initialize a retransmission counter.
Definition retry.c:36
fr_time_t start
when we started the retransmission
Definition retry.h:53
fr_time_delta_t rt
retransmit interval
Definition retry.h:57
fr_retry_state_t state
so callers can see what state it's in.
Definition retry.h:60
fr_retry_state_t
Definition retry.h:45
@ FR_RETRY_MRC
reached maximum retransmission count
Definition retry.h:47
@ FR_RETRY_CONTINUE
Definition retry.h:46
@ FR_RETRY_MRD
reached maximum retransmission duration
Definition retry.h:48
uint32_t count
number of sent packets
Definition retry.h:58
fr_time_t end
when we will end the retransmissions
Definition retry.h:54
fr_time_t next
when the next timer should be set
Definition retry.h:55