Repository navigation
Conversation
Some LoRa Basics Station gateways do not extend the concentrator time beyond 32 bits, so their concentrator time restarts from zero every 2^32 microseconds. The concentrator time base can also change, for example after the clock was synchronized from an uplink token of a previous gateway connection. In both cases, the recorded emissions remained relative to the old time base and appeared to be in the future, blocking their sub-band for up to hours. Shift the emissions to the new time base when the synchronized concentrator time lags more than a minute behind the concentrator time expected from the server time. The emissions keep counting towards the duty-cycle at their actual position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LoRa Basics Station gateways may start a new concentrator time session when they reconnect. A downlink scheduled right after a reconnect could carry an uplink token of an uplink message received on the previous connection, and synchronize the clock of the new connection to the old concentrator time base. Add a connection option that makes the scheduler ignore uplink tokens of uplink messages received before the connection was established, and use it in the LoRa Basics Station frontend. Other frontends keep using such uplink tokens, as their concentrator time does not depend on the connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
A critical race in scheduler garbage collection allows synchronization to shift emissions after the cutoff is calculated, potentially deleting recent transmissions and future reservations and losing duty-cycle and collision accounting.
1 open issue
What changed in this PR
This PR addresses prolonged downlink blocking when a LoRa Basics Station gateway’s concentrator clock jumps backwards, while preserving duty-cycle accounting.
Changes:
- Shift recorded emissions into the new time base when the clock falls more than one minute behind expectations.
- Ignore pre-connection uplink tokens when synchronizing LoRa Basics Station gateway clocks.
- Add regression tests and document the fixes in the changelog.
| File | Description |
|---|---|
pkg/gatewayserver/scheduling/sub_band.go |
Add synchronized shifting of sub-band emissions. |
pkg/gatewayserver/scheduling/scheduler.go |
Detect backward clock jumps, shift emissions, and filter stale uplink tokens. |
pkg/gatewayserver/scheduling/scheduler_test.go |
Cover clock resets, rollover, jitter, and stale tokens. |
pkg/gatewayserver/scheduling/emission.go |
Add an operation to shift emission timestamps. |
pkg/gatewayserver/io/semtechws/ws.go |
Enable stale-token filtering for LoRa Basics Station connections. |
pkg/gatewayserver/io/io.go |
Forward the connection option to the scheduler. |
pkg/gatewayserver/io/io_test.go |
Test connection-level stale-token filtering. |
CHANGELOG.md |
Document clock-jump recovery and stale-token handling. |
Review effort: Balanced
The garbage collection determined the cutoff time under a read lock and released it before removing the emissions. A clock synchronization that shifts the emissions to a new time base could run in between, after which the emissions were removed using the cutoff time of the old time base. After a concentrator time jump, this could remove all emissions, including the ones that still count towards the duty-cycle and the ones scheduled in the future. Hold the lock while determining the cutoff time and removing the emissions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. I reviewed the code and it looks fine. I don't understand the cause.
They all rollover right? In case of Basic Station, we have two fields in lorawan-stack/pkg/gatewayserver/io/semtechws/time.go Lines 109 to 119 in 71f91c5 Since you cannot reproduce the issue, I think what we need to collect is better data: we need full logging on the LBS LNS, including
When they reconnect, we create a new scheduler and start all over with empty emissions right? |
Agreed, we need more data to better understand the issue. Especially if it only happens on one kind of gateway.
Correct, a reconnect creates a fresh scheduler. But what could happen is that a fresh scheduler has no clock sync, so a downlink right after the reconnect whose uplink token comes from the previous connection syncs the new clock to the old time base, and the emission is recorded there. The first uplink then moves the clock back, and if the difference was big between the two timestamps then that emission blocks the sub-band. |
Yes, that is a good point and that is a good fix here. This is basically relevant for all frontends, not just LBS LNS. Maybe that is the only culprit here. And it's an understandable one, i.e. when using Class C downlink with an uplink token that was from before a restart + reconnect. Can you carve that out in a new PR, or reduce the scope of this PR? And also add debug logging so we can collect the right information. |
Yes, I opened a new PR for it #8080 |

Summary
References: https://github.com/TheThingsIndustries/lorawan-stack-support/issues/1517
Downlink scheduling on LoRa Basics Station gateways could fail with
sub band is blockedfor up to hours after the concentrator time jumped backwards. This happens on gateways that do not extend the concentrator time beyond 32 bits (the time restarts from zero every 2^32 µs, ~71.6 minutes), and on gateways whose concentrator time base changes when they reconnect. The emissions recorded in the scheduler stayed in the old time base, so they appeared to be in the future and blocked their sub-band.Changes
WithIgnorePriorUplinkTokensconnection option, which makes the scheduler ignore uplink tokens of uplink messages received before the connection was established. The LoRa Basics Station frontend uses it, so that a downlink scheduled right after a reconnect cannot synchronize the new connection's clock to the previous connection's time base. Other frontends are unchanged, because their concentrator time does not depend on the connection.Testing
Steps
go test ./pkg/gatewayserver/scheduling/... ./pkg/gatewayserver/io/...TestScheduleConcentratorTimeJumpcovers a 32-bit rollover that the gateway did not account for, a time base reset, and normal jitter that must not trigger a shift.TestScheduleSyncViaUplinkTokenandTestIgnorePriorUplinkTokenscover ignoring uplink tokens from before the connection was established.Results
All tests in
pkg/gatewayserver/schedulingandpkg/gatewayserver/io/...pass. With the emission shift disabled,TestScheduleConcentratorTimeJumpfails withsub band is blocked for 1h11m34.9s(rollover) andsub band is blocked for 94h0m1.95s(time base reset).Regressions
RolloverClock.Sync, which already treats a backwards timestamp as a rollover.xtimecan go back up to ~100 ms) stays below the one-minute tolerance and does not trigger a shift.Notes for Reviewers
The shift happens during clock synchronization, before any scheduling, so the
MaxScheduleAheadchecks see the emissions in the new time base.Checklist
README.mdfor the chosen target branch.CHANGELOG.md.CONTRIBUTING.md, there are no fixup commits left.🤖 Generated with Claude Code