STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_abort_ordering.cpp
Go to the documentation of this file.
1/**
2 * The physical ABORT broadcast must not queue behind the sequencer's own housekeeping.
3 *
4 * transitionTo() used to fire abort_broadcaster_.triggerAbort() near the END of the function,
5 * after stopContinuousLoop() (joins the republish thread, up to ~100 ms), after
6 * fire_manager_.stop() — which notifies controller_service over TCP — and after a full actuator
7 * batch. None of that is a precondition for telling the boards to abort, and the controller leg
8 * was the worst of it: connect() on a blocking socket with no connect timeout, so a controller
9 * host that drops SYNs rather than refusing them (powered off but routable, or a partition) held
10 * the abort for the kernel's full SYN retry budget. Measured on this machine: 133,348 ms.
11 *
12 * The abort broadcast is now the first thing transitionTo() does. This test pins that ordering by
13 * making the controller unreachable in exactly the way that used to hurt, and asserting the ABORT
14 * datagram still lands promptly.
15 *
16 * ── Why 192.0.2.1 ──────────────────────────────────────────────────────────────────────────────
17 * TEST-NET-1 (RFC 5737) is guaranteed not to be routed anywhere, so SYNs are dropped rather than
18 * refused. A closed port on localhost is NOT a substitute: it answers RST immediately, so
19 * connect() fails fast and the regression is invisible.
20 *
21 * ── Destination, not just ordering ─────────────────────────────────────────────────────────────
22 * The broadcaster now takes its destination and port from [server_heartbeat] rather than sending
23 * to the limited broadcast 255.255.255.255 on a hardcoded 5005. This test writes those keys and
24 * binds what it wrote, so it also covers the routing: if configure() were skipped, or the address
25 * plumbed through wrongly, the datagram would go to the shipped default 192.168.2.255 and never
26 * arrive here.
27 *
28 * A high port is used rather than 5005 so the test does not collide with a real board stack (or
29 * another service) on the developer's machine.
30 */
31#include <arpa/inet.h>
32#include <netinet/in.h>
33#include <sys/socket.h>
34#include <unistd.h>
35
36#include <atomic>
37#include <chrono>
38#include <cstdio>
39#include <cstring>
40#include <fstream>
41#include <iostream>
42#include <string>
43#include <thread>
44
47
50using Clock = std::chrono::steady_clock;
51
52static int g_failures = 0;
53
54static void check(bool ok, const std::string& what) {
55 std::cout << (ok ? " ok " : " FAIL ") << what << std::endl;
56 if (!ok)
57 g_failures++;
58}
59
60// PacketType::ABORT — the first header byte of the broadcast.
61static constexpr uint8_t kAbortPacketType = 7;
62static constexpr uint16_t kAbortPort = 15005;
63// Loopback, so the packet is observable on a box with no board LAN — which is every dev box and
64// every CI runner.
65static constexpr const char* kAbortIp = "127.0.0.1";
66
67int main() {
68 std::cout << "=== Abort ordering (abort must not wait on controller_service) ===" << std::endl;
69
70 // Listener for the abort broadcast, on the address the config below tells the sequencer to
71 // send to.
72 int udp = socket(AF_INET, SOCK_DGRAM, 0);
73 int opt = 1;
74 setsockopt(udp, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
75 struct sockaddr_in a{};
76 a.sin_family = AF_INET;
77 a.sin_addr.s_addr = INADDR_ANY;
78 a.sin_port = htons(kAbortPort);
79 if (bind(udp, reinterpret_cast<struct sockaddr*>(&a), sizeof(a)) < 0) {
80 std::cerr << "SKIP: cannot bind UDP " << kAbortPort << " (" << strerror(errno)
81 << ") — another process is using the abort port" << std::endl;
82 close(udp);
83 return 0; // environmental, not a code failure
84 }
85
86 std::atomic<bool> got_abort{false};
87 std::atomic<long long> abort_at_ms{-1};
88 std::atomic<bool> listening{true};
89 const auto t0 = Clock::now();
90 std::thread listener([&]() {
91 while (listening) {
92 struct timeval tv{.tv_sec = 0, .tv_usec = 100000};
93 setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
94 uint8_t buf[64];
95 ssize_t n = recv(udp, buf, sizeof(buf), 0);
96 if (n > 0 && buf[0] == kAbortPacketType && !got_abort.exchange(true)) {
97 abort_at_ms =
98 std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0)
99 .count();
100 }
101 }
102 });
103
104 // Controller pointed at a blackhole; a burn configured so we can abort out of FIRE, which is
105 // the path that calls fire_manager_.stop() -> notifyControllerFire().
106 const std::string cfg_path = "test_abort_ordering_cfg.toml";
107 {
108 std::ofstream f(cfg_path);
109 f << "[database]\nhost=\"127.0.0.1\"\nport=1\n\n"
110 << "[server_heartbeat]\nbroadcast_ip=\"" << kAbortIp
111 << "\"\nbroadcast_port=" << kAbortPort << "\n\n"
112 << "[controller_service]\nhost=\"192.0.2.1\"\nport=8000\n\n"
113 << "[fire]\nstate=\"Fire\"\nexpiry_target=\"Armed\"\n"
114 << "duration_ms=30000\nextended_ms=60000\n";
115 }
116
118 if (!svc.init(cfg_path)) {
119 std::cerr << "FAILED: init (run from the build dir, with config/ reachable)" << std::endl;
120 listening = false;
121 listener.join();
122 close(udp);
123 std::remove(cfg_path.c_str());
124 return 1;
125 }
126 svc.setDebugMode(true); // the ordering is the subject here, not the CSV's allowed transitions
127
128 // Enter FIRE. This already pays one blackholed connect (FIRE_START).
129 svc.transitionTo(std::string("Fire"));
130 got_abort = false;
131 abort_at_ms = -1;
132
133 // Now abort out of it. Pre-fix, the broadcast sat behind the republish-thread join, the
134 // FIRE_STOP connect, and the actuator batch.
135 const auto abort_issued = Clock::now();
136 svc.transitionTo(std::string("Engine Abort"));
137
138 // Give the datagram a moment to be seen even on a slow box.
139 for (int i = 0; i < 20 && !got_abort; ++i)
140 std::this_thread::sleep_for(std::chrono::milliseconds(50));
141
142 const long long issued_ms =
143 std::chrono::duration_cast<std::chrono::milliseconds>(abort_issued - t0).count();
144 const long long latency = got_abort ? (abort_at_ms.load() - issued_ms) : -1;
145
146 check(got_abort.load(), "ABORT broadcast was sent");
147 if (got_abort) {
148 std::cout << " abort latency: " << latency << " ms" << std::endl;
149 // Generous bound, but far below what the pre-fix ordering costs with an unreachable
150 // controller: the FIRE_STOP connect alone is 300 ms now (and was ~133 s before it was
151 // bounded), and it used to happen BEFORE the broadcast.
152 check(latency >= 0 && latency < 200,
153 "ABORT went out ahead of the controller notification and the actuator batch");
154 }
155
156 listening = false;
157 listener.join();
158 close(udp);
159 std::remove(cfg_path.c_str());
160
161 std::cout << (g_failures == 0 ? "✅ PASS" : "❌ FAIL") << std::endl;
162 return g_failures == 0 ? 0 : 1;
163}
Top-level sequencer service.
bool setDebugMode(bool enabled)
Enable or disable debug mode.
bool init(const std::string &config_path="config/config.toml")
Load config, CSVs, connect to Elodin.
bool transitionTo(const std::string &state_name)
Request a state transition by name (e.g.
Parses state_transitions.csv and validates state transitions.
EthernetUDP udp
Definition main.cpp:40
static bool ok
Definition flight_e2e.c:435
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
std::chrono::steady_clock Clock
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
static int g_failures
static void check(bool ok, const std::string &what)
static constexpr uint8_t kAbortPacketType
static constexpr const char * kAbortIp
int main()
static constexpr uint16_t kAbortPort