SendRateLimiter answers "may I send now?". When the answer was no, the
bulk worker called `break` -- it abandoned the rest of the list and told
the user "Stopped 20 short of 50, try the rest later". That protects the
carrier ceiling but is useless as a way to deliver 200 messages: the
user has to babysit the app and re-run it seven times.
SendPacing is the other half: a fixed gap between sends, so a batch
stays under the ceiling by construction and runs to completion.
* MAX_DELAY_MS caps at 10 minutes -- past that a batch of any size
takes days and this app is not a scheduler.
* RECOMMENDED_DELAY_MS is derived, not hardcoded: window / capacity,
i.e. one per minute for the default 30-per-30-minutes.
* Default is the recommended gap, NOT zero. A user who never touches
the setting should get a batch that completes rather than one that
dies a third of the way through.
* from_millis clamps instead of rejecting: it is fed by a text field,
and refusing to send because someone typed 9999 is worse than
quietly using the maximum. from_seconds uses saturating_mul so
i64::MAX seconds cannot wrap to a negative delay.
* total_duration_ms counts n-1 gaps, not n. Nothing waits before the
first message or after the last, and the off-by-one is visible to
the user on small batches.
Pure arithmetic with no sleeping, so the schedule is asserted in unit
tests rather than observed. 10 new tests, the important pair being:
the_recommended_gap_keeps_a_long_batch_under_the_limiter
walks 200 sends through a real SendRateLimiter at the paced
timestamps and asserts none is refused
without_pacing_the_same_batch_is_refused_at_the_cap
the same 200 sends with no gap stop at exactly 30
robius-sms: 37 -> 47 lib tests. Negative-tested by changing the default
back to zero, which fails 2 tests.