Repository navigation
fix(pjsua): honour the retry interval when it is below the jitter - #5319
Conversation
schedule_reregistration() randomises the retry delay by reg_retry_random_interval. For an interval below it, the else branch cleared delay.sec before using it in the modulo, so delay.sec * 1000 was always 0 and the delay was a uniform 0 .. reg_retry_random_interval whatever the interval was: 1 and 9 behaved alike, and 9 averaged shorter than 10. Compute the delay before clearing delay.sec, so it is uniform in 0 .. interval + reg_retry_random_interval. That is what the code from pjsip#1802 (e9268d2) wrote, it matches the other branch at interval == reg_retry_random_interval, and it grows with the interval. An interval of 0, including the default reg_first_retry_interval, is unchanged. The documented "interval +/- reg_retry_random_interval" cannot apply below the jitter, where the lower end would be negative. Say in the pjsua and pjsua2 comments what range applies there.
| delay.msec = -rand_ms + (pj_rand() % (rand_ms * 2)); | ||
| } else { | ||
| delay.sec = 0; | ||
| delay.msec = (pj_rand() % (delay.sec * 1000 + rand_ms)); |
There was a problem hiding this comment.
The divisor now depends on delay.sec, which is a signed long copied from the unsigned reg_retry_interval / reg_first_retry_interval. Where long is 32-bit (Windows, 32-bit Linux), a huge value, e.g. (unsigned)-10, makes delay.sec negative. It then takes this branch, delay.sec * 1000 + rand_ms can become 0, and the % crashes with SIGFPE. Values like 0x80000000 also overflow delay.sec * 1000. Before this change the divisor was always rand_ms, so this is new.
It's an odd config, but the guard is cheap. Could you clamp delay.sec to [0, reg_retry_random_interval) before using it here, e.g. by testing delay.sec < 0 too, or by doing the comparison in unsigned?
There was a problem hiding this comment.
You're right that this is new: the divisor used to be rand_ms alone. Fixed in 0e1ad28 by clamping a negative delay.sec to 0 in that branch, so such a value gets the same 0 to reg_retry_random_interval delay as before this PR. I didn't do the comparison in unsigned: those values would then take the other branch and come out negative, and since the timer adds the delay to the current time, every retry would fire at once. I can't build with a 32-bit long here, so I checked the arithmetic with long emulated as 32-bit under UBSan: division by zero before the fix, the old 0–10 s after it. With the clamp the divisor stays below 2 * rand_ms, so it can only overflow for a reg_retry_random_interval at which the other branch's rand_ms * 2 already does (above about 12 days); I've left that pre-existing limit alone.
| @@ -5527,8 +5527,8 @@ static void schedule_reregistration(pjsua_acc *acc) | |||
| if (delay.sec >= (long)acc->cfg.reg_retry_random_interval) { | |||
There was a problem hiding this comment.
Two nearby comments no longer match the behaviour (both lines are outside the diff, so I'm commenting here):
- L5524
/* Randomize interval by +/- reg_retry_random_interval, if configured */: the else branch now uses 0 to interval + jitter, not +/- jitter. Please mention that the lower bound is clamped at 0. - L5519
The first attempt will be done immediately.(and the same statement in the function header at L5471): the first retry actually waitsreg_first_retry_intervalplus jitter. That predates this PR, but the new header docs now contradict it, so it'd be good to fix it here too.
There was a problem hiding this comment.
Both fixed in 0e1ad28. The randomisation comment now says the lower bound is clamped at 0 and gives the resulting range for an interval below reg_retry_random_interval. The function header and the in-body comment now say that, until a registration succeeds again, the first retry waits reg_first_retry_interval and later ones reg_retry_interval, since attempt_cnt is only reset on a 2xx.
| * shorter than this value, it cannot be reduced by that much, and the | ||
| * actual interval will be in the range of 0 to the retry interval plus | ||
| * this value instead: for example 0 to 15 seconds for a retry interval | ||
| * of 5 seconds. |
There was a problem hiding this comment.
Thanks for documenting the new window here. The reg_retry_interval (L5147) and reg_first_retry_interval (L5161) docs still say the interval is "randomized slightly by some seconds", though, and with this change a 1 s interval can become anything from 0 to 11 s. Could you update those two to point to the clamped window described here?
There was a problem hiding this comment.
Done in 0e1ad28: both now say the interval is randomized by reg_retry_random_interval and point there for the resulting range, rather than repeating it. The pjsua2 retryIntervalSec and firstRetryIntervalSec docs got the same change, and pjsip/pjproject_docs#70 adds reg_retry_random_interval to the retry settings listed in the server affinity guide.
| * and this is set to 10 seconds, the actual registration retry interval | ||
| * will be in the range of 90 to 110 seconds. | ||
| * will be in the range of 90 to 110 seconds. If the retry interval is | ||
| * shorter than this value, it cannot be reduced by that much, and the |
There was a problem hiding this comment.
Minor, PJSUA2 naming: "this value" here, and the retryIntervalSec doc at L118, refer to the C field reg_retry_random_interval. In this struct the field is randomRetryIntervalSec, so please use that name (e.g. \a randomRetryIntervalSec).
There was a problem hiding this comment.
Done in 0e1ad28, together with the other C names in the same three field docs: retryIntervalSec and firstRetryIntervalSec now refer to randomRetryIntervalSec, and randomRetryIntervalSec refers to retryIntervalSec and firstRetryIntervalSec.
Where long is 32-bit, a reg_retry_interval or reg_first_retry_interval too large for a long makes delay.sec negative. It then takes the branch below reg_retry_random_interval, whose divisor now depends on delay.sec, and the divisor can become 0 or overflow. Clamp a negative delay.sec to 0 there, which gives such a value the same 0 .. reg_retry_random_interval delay as before. Also correct the comments: the lower bound of the randomized range is clamped at 0, and the first retry since the last successful registration waits reg_first_retry_interval rather than happening immediately. Point the retry interval docs at reg_retry_random_interval for the resulting range, and use the pjsua2 field names in AccountRegConfig.
Fixes #5318.
schedule_reregistration()randomises the retry delay byreg_retry_random_interval(10 s by default). For a retry interval below that value, the else branch cleareddelay.secbefore using it in the modulo, sodelay.sec * 1000was always 0 and the delay was a uniform 0 toreg_retry_random_intervalwhatever the interval was. An interval of 1 and one of 9 behaved alike, and the mean was 5 s for every value below 10 but 10 s at 10. This was pointed out in the review of #5306, where it became reachable from pjsua through--rereg-delay.The
delay.sec * 1000term came with #1802 (e9268d2), butdelay.secwas cleared on the line before, so it has been 0 ever since. The behaviour is the one the code has had since 2011 (#1375), not a regression.This computes the delay before clearing
delay.sec, so for an interval belowreg_retry_random_intervalit is uniform in 0 to the interval plusreg_retry_random_interval. That is the whole documented range, "interval ±reg_retry_random_interval" with the negative part cut off at 0. It is continuous with the>=branch (at an interval equal toreg_retry_random_intervalboth give 0 to twice that), it grows with the interval, and it keeps the default first retry (interval 0) at 0–10 s. I did not clamp the documented range at 0 instead, because that puts a point mass at 0: with the defaults, about half of all retries at an interval of 0 or 1 would be immediate.The documentation cannot be followed literally below the jitter, where the lower end would be negative, so the
pjsua.handpjsua2comments now say what range applies there.A second commit, from review: where
longis 32-bit, an interval too large for alongmakesdelay.secnegative, and in this branch that made the divisor 0. It is now clamped to 0 there, so such a value keeps the old 0 toreg_retry_random_intervaldelay. The same commit corrects the comments that said the first retry is immediate (it waitsreg_first_retry_interval), points thereg_retry_intervalandreg_first_retry_intervaldocs atreg_retry_random_intervalfor the range, and uses the pjsua2 field names inAccountRegConfig. The docs side is pjsip/pjproject_docs#70.With the default jitter of 10 s, the delay window becomes:
reg_first_retry_interval)This is a behaviour change for anyone who sets
reg_retry_intervalorreg_first_retry_intervalto 1–9 s: the average delay grows from 5 s to (interval + 10) / 2 s, so a 2 s first retry goes from 0–10 s to 0–12 s. It is what the documentation says for such an interval, and I found nothing in the repository that depends on the old window.Testing
pjsua with #5306 applied, so that
--rereg-delayreaches the account, against a closed TCP port,--rereg-delay=9, 155 s, reading the "Scheduling re-registration retry for acc 1 in N seconds" lines after the first retry (the app's own first retry is 60 s):The logged seconds are truncated, and the sample is small. The windows in the table above are the exact ones: I computed the minimum, maximum and mean over a million draws for intervals 0, 1, 2, 5, 9, 10, 11, 15 and 100, and the "after" delay stays inside the documented range for every one of them.
makebuilds without new warnings, andtests/pjsuascripts-run/100_simple.pypasses.I can't build with a 32-bit
longhere. Withlongemulated as 32-bit under UBSan, the arithmetic divides by zero for(unsigned)-10before the clamp and gives the old 0–10 s after it, and the new divisor overflows only for areg_retry_random_intervalat which the other branch'srand_ms * 2already does (above about 12 days).There is no regression test, and I would rather say so. The delay is computed inline in a static function that takes a live account, so testing it means extracting the calculation into a small helper first. Happy to do that if you would like one.