STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
SubscriptionTracker.hpp
Go to the documentation of this file.
1#pragma once
2/**
3 * Which table subscriptions the DB refused, so a reader can ask again for exactly those.
4 *
5 * Elodin DB refuses a VTableStream for a table nobody has registered yet, replying ErrorResponse
6 * (msg id [0xE0, 0x1D]) with the request id of the subscription it refused. Services start
7 * together, so a reader routinely subscribes a few hundred ms before the publisher registers:
8 * on 2026-11-05 calibration_service subscribed to the ball valve's raw tables before daq_bridge
9 * registered them, and published no ball valve position for the whole session, while every other
10 * table, registered a moment earlier, streamed normally.
11 *
12 * The blunt alternatives are both wrong. Waiting for "no packets at all" never fires when only some
13 * tables were refused. Re-asking for every table that has not delivered yet (PressureFeed) is
14 * fine for a reader that keeps only the latest value, but a table the DB ACCEPTED and that is
15 * merely silent (a board that is off) would gain a second stream per retry, and calibration would
16 * then process every sample several times. So: tag each subscription with its own request id, and
17 * retry only what was refused.
18 *
19 * Pure logic, no socket: the caller sends with ElodinClient::subscribe_table(hi, lo, id).
20 */
21#include <chrono>
22#include <cstddef>
23#include <cstdint>
24#include <map>
25#include <utility>
26#include <vector>
27
28namespace fsw {
29namespace elodin {
30
31/** ErrorResponse: header len(4) | ty(1) | msg id [0xE0, 0x1D] | request id(1). */
32inline bool is_error_response(const uint8_t* pkt, size_t len) {
33 return len >= 8 && pkt[5] == 0xE0 && pkt[6] == 0x1D;
34}
35
37public:
38 using Table = std::pair<uint8_t, uint8_t>;
39 using Clock = std::chrono::steady_clock;
40
41 explicit SubscriptionTracker(std::chrono::milliseconds retry_after = std::chrono::seconds(1))
42 : retry_after_(retry_after) {
43 }
44
45 /**
46 * The request id to subscribe `table` under. Ids rotate through 1..254: 0 is what untracked
47 * subscriptions use, and 255 is the backend's subscription fence.
48 */
49 uint8_t assign(Table table) {
50 const uint8_t id = next_id_;
51 next_id_ = next_id_ >= 254 ? 1 : static_cast<uint8_t>(next_id_ + 1);
52 by_id_[id] = table;
53 return id;
54 }
55
56 /**
57 * The DB refused the subscription sent under `request_id`. Returns the table it was for, so the
58 * caller can say which; {0, 0} for an id this tracker did not hand out.
59 */
60 Table rejected(uint8_t request_id, Clock::time_point now) {
61 auto it = by_id_.find(request_id);
62 if (it == by_id_.end())
63 return {0, 0};
64 const Table t = it->second;
65 by_id_.erase(it);
66 retry_at_[t] = now + retry_after_;
67 return t;
68 }
69
70 /** Refused tables whose wait is over; each is forgotten here until rejected again. */
71 std::vector<Table> due(Clock::time_point now) {
72 std::vector<Table> out;
73 for (auto it = retry_at_.begin(); it != retry_at_.end();) {
74 if (now >= it->second) {
75 out.push_back(it->first);
76 it = retry_at_.erase(it);
77 } else {
78 ++it;
79 }
80 }
81 return out;
82 }
83
84 /** Tables currently waiting to be asked for again. */
85 size_t refused() const {
86 return retry_at_.size();
87 }
88
89 /** A new connection: every subscription is sent afresh. */
90 void reset() {
91 by_id_.clear();
92 retry_at_.clear();
93 }
94
95private:
96 std::chrono::milliseconds retry_after_;
97 uint8_t next_id_ = 1;
98 std::map<uint8_t, Table> by_id_;
99 std::map<Table, Clock::time_point> retry_at_;
100};
101
102} // namespace elodin
103} // namespace fsw
std::map< uint8_t, Table > by_id_
std::chrono::milliseconds retry_after_
std::pair< uint8_t, uint8_t > Table
void reset()
A new connection: every subscription is sent afresh.
Table rejected(uint8_t request_id, Clock::time_point now)
The DB refused the subscription sent under request_id.
std::vector< Table > due(Clock::time_point now)
Refused tables whose wait is over; each is forgotten here until rejected again.
SubscriptionTracker(std::chrono::milliseconds retry_after=std::chrono::seconds(1))
size_t refused() const
Tables currently waiting to be asked for again.
std::map< Table, Clock::time_point > retry_at_
uint8_t assign(Table table)
The request id to subscribe table under.
bool is_error_response(const uint8_t *pkt, size_t len)
ErrorResponse: header len(4) | ty(1) | msg id [0xE0, 0x1D] | request id(1).
Motorized ball valve position: raw ADC counts -> % open.