|
8 | 8 | // discards the received bytes before logRx() when allocNew() fails. Under duty-cycle |
9 | 9 | // throttling the outbound queue can park the entire pool waiting on TX budget, which |
10 | 10 | // starves RX allocation and silently caps MQTT capture at the TX rate (each completed |
11 | | -// TX frees exactly one packet for exactly one more RX). |
| 11 | +// TX frees exactly one packet for exactly one more RX). Parked retransmissions also |
| 12 | +// absorb every budget refill, starving the node's own CLI responses/ACKs and making |
| 13 | +// a heavily-throttled node un-administrable over the mesh. |
12 | 14 | // |
13 | | -// This manager sheds *retransmissions* instead: once the free pool drops below the |
14 | | -// reserve, outbound packets are refused (freed straight back to the pool) so RX |
15 | | -// allocation — and therefore capture — continues at full rate. The node was already |
16 | | -// dropping traffic in that state; this chooses to drop repeats it has no TX budget |
17 | | -// for anyway, rather than capture. |
| 15 | +// Two policies fix this, both confined to this manager: |
| 16 | +// |
| 17 | +// 1. Priority-aware shedding. Below the RX reserve, only low-priority outbound |
| 18 | +// (priority > 1: multi-hop flood repeats, adverts, trace) is refused; the node's |
| 19 | +// own responses/ACKs (pri 0) and login/PATH replies (pri 1) still queue. Below the |
| 20 | +// smaller emergency floor everything is shed to keep capture alive. |
| 21 | +// 2. Stale-packet expiry. A queued packet still untransmitted STALE_OUTBOUND_MS past |
| 22 | +// its scheduled time is dropped at the next dequeue — a repeat delayed that long is |
| 23 | +// noise (the flood has long since propagated), and a CLI response that old has |
| 24 | +// already timed out at the client. Under normal load the queue drains in |
| 25 | +// milliseconds and this never triggers; under throttle it frees the pool and lets |
| 26 | +// fresh traffic (including admin responses) compete for the trickle of TX budget. |
18 | 27 | class RxReservePacketManager : public StaticPoolPacketManager { |
19 | | - int _rx_reserve; |
| 28 | + int _rx_reserve, _emergency_floor; |
| 29 | + int _cap; |
| 30 | + // scheduled_for per queued packet, keyed by packet pointer. The pool is a fixed set |
| 31 | + // of _cap Packet objects, so _cap slots cover every possible key with no eviction. |
| 32 | + struct AgeEntry { mesh::Packet* pkt; uint32_t scheduled_for; }; |
| 33 | + AgeEntry* _ages; |
| 34 | + |
| 35 | + static const uint32_t STALE_OUTBOUND_MS = 30000; |
| 36 | + static const uint8_t MAX_PROTECTED_PRI = 1; // pri 0-1 = own responses/ACKs/replies |
| 37 | + |
| 38 | + void recordAge(mesh::Packet* packet, uint32_t scheduled_for) { |
| 39 | + int empty = -1; |
| 40 | + for (int i = 0; i < _cap; i++) { |
| 41 | + if (_ages[i].pkt == packet) { _ages[i].scheduled_for = scheduled_for; return; } |
| 42 | + if (empty < 0 && _ages[i].pkt == NULL) empty = i; |
| 43 | + } |
| 44 | + if (empty >= 0) { _ages[empty].pkt = packet; _ages[empty].scheduled_for = scheduled_for; } |
| 45 | + } |
| 46 | + |
| 47 | + bool lookupAge(const mesh::Packet* packet, uint32_t* scheduled_for) const { |
| 48 | + for (int i = 0; i < _cap; i++) { |
| 49 | + if (_ages[i].pkt == packet) { *scheduled_for = _ages[i].scheduled_for; return true; } |
| 50 | + } |
| 51 | + return false; |
| 52 | + } |
| 53 | + |
20 | 54 | public: |
21 | 55 | RxReservePacketManager(int pool_size, int rx_reserve) |
22 | | - : StaticPoolPacketManager(pool_size), _rx_reserve(rx_reserve) {} |
| 56 | + : StaticPoolPacketManager(pool_size), _rx_reserve(rx_reserve), |
| 57 | + _emergency_floor(rx_reserve / 2), _cap(pool_size) { |
| 58 | + _ages = new AgeEntry[pool_size]; |
| 59 | + for (int i = 0; i < pool_size; i++) { _ages[i].pkt = NULL; _ages[i].scheduled_for = 0; } |
| 60 | + } |
23 | 61 |
|
24 | 62 | void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override { |
25 | | - if (getFreeCount() < _rx_reserve) { |
26 | | - MESH_DEBUG_PRINTLN("RxReservePacketManager: pool below RX reserve, shedding outbound"); |
| 63 | + int free_count = getFreeCount(); |
| 64 | + if (free_count < _emergency_floor |
| 65 | + || (free_count < _rx_reserve && priority > MAX_PROTECTED_PRI)) { |
| 66 | + MESH_DEBUG_PRINTLN("RxReservePacketManager: pool below RX reserve, shedding outbound (pri %d)", (int)priority); |
27 | 67 | free(packet); |
28 | 68 | return; |
29 | 69 | } |
| 70 | + recordAge(packet, scheduled_for); |
30 | 71 | StaticPoolPacketManager::queueOutbound(packet, priority, scheduled_for); |
31 | 72 | } |
| 73 | + |
| 74 | + mesh::Packet* getNextOutbound(uint32_t now) override { |
| 75 | + // Expire queued packets that have waited too long past their scheduled time. |
| 76 | + for (int i = getOutboundTotal() - 1; i >= 0; i--) { |
| 77 | + mesh::Packet* pkt = getOutboundByIdx(i); |
| 78 | + uint32_t scheduled_for; |
| 79 | + if (pkt && lookupAge(pkt, &scheduled_for) |
| 80 | + && (int32_t)(now - scheduled_for) > (int32_t)STALE_OUTBOUND_MS) { |
| 81 | + MESH_DEBUG_PRINTLN("RxReservePacketManager: dropping stale queued outbound"); |
| 82 | + removeOutboundByIdx(i); |
| 83 | + free(pkt); |
| 84 | + } |
| 85 | + } |
| 86 | + return StaticPoolPacketManager::getNextOutbound(now); |
| 87 | + } |
32 | 88 | }; |
33 | 89 |
|
34 | 90 | // The packet manager for an app build: observer builds reserve a quarter of the pool |
|
0 commit comments