STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_subscription_tracker.cpp
Go to the documentation of this file.
1/**
2 * SubscriptionTracker + ElodinClient::subscribe_table: a subscription the DB refuses is asked for
3 * again, and only that one.
4 *
5 * On 2026-11-05 calibration_service subscribed to the ball valve's raw tables a moment before
6 * daq_bridge registered them. The DB refused (ErrorResponse, echoing the request id), nothing
7 * noticed, and no ball valve position was published for the whole session. Calibration's only
8 * recovery was "no packets at all for 5 s", which never fires while other tables stream.
9 */
10#include <arpa/inet.h>
11#include <netinet/in.h>
12#include <sys/socket.h>
13#include <unistd.h>
14
15#include <atomic>
16#include <chrono>
17#include <cstring>
18#include <iostream>
19#include <set>
20#include <thread>
21#include <vector>
22
25
27using namespace std::chrono_literals;
28
29static int g_failures = 0;
30#define CHECK(cond) \
31 do { \
32 if (!(cond)) { \
33 std::cerr << "FAIL " << __LINE__ << ": " #cond << std::endl; \
34 g_failures++; \
35 } \
36 } while (0)
37
39 // len(4) | ty | [0xE0, 0x1D] | request id | postcard string
40 const uint8_t err[] = {5, 0, 0, 0, 0, 0xE0, 0x1D, 7, 4, 'n', 'o', 'p', 'e'};
41 const uint8_t data[] = {16, 0, 0, 0, 1, 0x33, 0x01, 0, 1, 2, 3, 4};
42 CHECK(fsw::elodin::is_error_response(err, sizeof(err)));
43 CHECK(!fsw::elodin::is_error_response(data, sizeof(data)));
44 CHECK(!fsw::elodin::is_error_response(err, 7)); // too short to carry a request id
45}
46
48 SubscriptionTracker tr(1000ms);
49 const auto t0 = SubscriptionTracker::Clock::now();
50 const uint8_t pt = tr.assign({0x20, 0x01});
51 const uint8_t bv = tr.assign({0x33, 0x01});
52 CHECK(pt != bv);
53
54 const auto t = tr.rejected(bv, t0);
55 CHECK(t.first == 0x33 && t.second == 0x01);
56 CHECK(tr.refused() == 1);
57 CHECK(tr.due(t0 + 999ms).empty()); // waits before asking again
58
59 const auto again = tr.due(t0 + 1000ms);
60 CHECK(again.size() == 1 && again[0].first == 0x33 && again[0].second == 0x01);
61 CHECK(tr.due(t0 + 5000ms).empty()); // asked once; only another refusal brings it back
62}
63
65 // The reason this is not PressureFeed's "re-ask for anything silent": a table the DB accepted
66 // for a board that is off is silent too, and re-asking would give it a second stream.
67 SubscriptionTracker tr(1000ms);
68 const auto t0 = SubscriptionTracker::Clock::now();
69 tr.assign({0x23, 0x01}); // e.g. a TC board that is powered down
70 CHECK(tr.due(t0 + 1h).empty());
71 CHECK(tr.refused() == 0);
72}
73
76 const auto t = tr.rejected(200, SubscriptionTracker::Clock::now());
77 CHECK(t.first == 0 && t.second == 0);
78 CHECK(tr.refused() == 0);
79}
80
83 std::set<int> seen;
84 for (int i = 0; i < 600; i++) {
85 const uint8_t id = tr.assign({0x20, static_cast<uint8_t>(i)});
86 CHECK(id != 0 && id != 255);
87 seen.insert(id);
88 }
89 CHECK(seen.size() == 254);
90}
91
93 SubscriptionTracker tr(1ms);
94 const auto t0 = SubscriptionTracker::Clock::now();
95 const uint8_t id = tr.assign({0x33, 0x02});
96 tr.reset();
97 CHECK(tr.rejected(id, t0).first == 0); // pre-reset id means nothing
98 tr.rejected(tr.assign({0x33, 0x02}), t0);
99 tr.reset();
100 CHECK(tr.due(t0 + 1s).empty());
101}
102
103/** subscribe_table puts the request id where the DB echoes it from: header byte 7. */
105 std::atomic<uint16_t> port{0};
106 std::vector<uint8_t> got;
107 std::thread db([&]() {
108 int srv = socket(AF_INET, SOCK_STREAM, 0);
109 int opt = 1;
110 setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
111 struct sockaddr_in a{};
112 a.sin_family = AF_INET;
113 a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
114 a.sin_port = 0;
115 bind(srv, reinterpret_cast<struct sockaddr*>(&a), sizeof(a));
116 socklen_t sl = sizeof(a);
117 getsockname(srv, reinterpret_cast<struct sockaddr*>(&a), &sl);
118 listen(srv, 1);
119 port = ntohs(a.sin_port);
120 int c = accept(srv, nullptr, nullptr);
121 uint8_t buf[64];
122 while (got.size() < 20) {
123 ssize_t n = read(c, buf, sizeof(buf));
124 if (n <= 0)
125 break;
126 got.insert(got.end(), buf, buf + n);
127 }
128 close(c);
129 close(srv);
130 });
131 for (int i = 0; i < 500 && port.load() == 0; i++)
132 std::this_thread::sleep_for(2ms);
133
135 CHECK(client.connect("127.0.0.1", port.load()));
136 CHECK(client.subscribe_table(0x33, 0x01, 42));
137 CHECK(client.subscribe_tables({{0x20, 0x05}})); // untracked: request id 0
138 db.join();
139
140 CHECK(got.size() == 20);
141 if (got.size() == 20) {
142 CHECK(got[5] == 0x11 && got[6] == 0x0d); // VTableStream
143 CHECK(got[7] == 42);
144 CHECK(got[8] == 0x33 && got[9] == 0x01);
145 CHECK(got[17] == 0);
146 CHECK(got[18] == 0x20 && got[19] == 0x05);
147 }
148}
149
150int main() {
158 if (g_failures) {
159 std::cerr << g_failures << " failure(s)" << std::endl;
160 return 1;
161 }
162 std::cout << "test_subscription_tracker: all passed" << std::endl;
163 return 0;
164}
Standalone Elodin database client.
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.
size_t refused() const
Tables currently waiting to be asked for again.
uint8_t assign(Table table)
The request id to subscribe table under.
static const emmc_port_t port
Definition emmc_stm32.c:210
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
bool is_error_response(const uint8_t *pkt, size_t len)
ErrorResponse: header len(4) | ty(1) | msg id [0xE0, 0x1D] | request id(1).
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
static int g_failures
static double read(int32_t code)
static void test_error_response_is_recognised()
static void test_unknown_request_id_names_nothing()
static void test_reset_forgets_everything()
static void test_refused_table_is_asked_for_again()
static void test_accepted_table_is_never_asked_for_twice()
#define CHECK(cond)
static void test_ids_rotate_and_skip_0_and_255()
static void test_request_id_is_on_the_wire()
subscribe_table puts the request id where the DB echoes it from: header byte 7.