ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
handoff.c
Go to the documentation of this file.
3#include <ascii-chat/otel.h>
5#include <string.h>
6
7static uint32_t read32(const uint8_t *p) {
8 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
9}
10static void write32(uint8_t *p, uint32_t n) {
11 p[0] = (uint8_t)(n >> 24);
12 p[1] = (uint8_t)(n >> 16);
13 p[2] = (uint8_t)(n >> 8);
14 p[3] = (uint8_t)n;
15}
17 if (!data || !m || length < CONSENSUS_HANDOFF_HEADER)
18 return SET_ERRNO(ERROR_INVALID_PARAM, "Truncated handoff frame");
19 const uint8_t *p = data;
22 : length != CONSENSUS_HANDOFF_HEADER))
23 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Invalid handoff version, kind or length");
24 memset(m, 0, sizeof(*m));
25 m->kind = p[1];
26 memcpy(m->session, p + 4, SESSION_ID_SIZE);
27 memcpy(m->participant, p + 20, SESSION_ID_SIZE);
28 m->generation = read32(p + 36);
29 m->sequence = read32(p + 40);
30 m->upload_kbps = read32(p + 44);
31 m->rtt_ms = read32(p + 48);
32 m->capabilities = p[52];
33 m->nat_tier = p[53];
34 m->port = (uint16_t)((p[54] << 8) | p[55]);
35 return ASCIICHAT_OK;
36}
38 memset(out, 0, CONSENSUS_HANDOFF_HEADER);
40 out[1] = m->kind;
41 memcpy(out + 4, m->session, SESSION_ID_SIZE);
42 memcpy(out + 20, m->participant, SESSION_ID_SIZE);
43 write32(out + 36, m->generation);
44 write32(out + 40, m->sequence);
45 write32(out + 44, m->upload_kbps);
46 write32(out + 48, m->rtt_ms);
47 out[52] = m->capabilities;
48 out[53] = m->nat_tier;
49 out[54] = (uint8_t)(m->port >> 8);
50 out[55] = (uint8_t)m->port;
51}
53 const uint8_t host[SESSION_ID_SIZE]) {
54 memset(s, 0, sizeof(*s));
55 memcpy(s->session, session, SESSION_ID_SIZE);
56 memcpy(s->host, host, SESSION_ID_SIZE);
57 s->generation = 1;
58}
60 for (size_t i = 0; i < s->count; ++i)
61 if (memcmp(s->peers[i].id, id, SESSION_ID_SIZE) == 0)
62 return &s->peers[i];
63 return NULL;
64}
66 if (find_peer(s, id))
67 return ASCIICHAT_OK;
69 return SET_ERRNO(ERROR_INVALID_STATE, "Handoff membership full");
71 memset(p, 0, sizeof(*p));
72 memcpy(p->id, id, SESSION_ID_SIZE);
73 p->nat_tier = 255;
74 ++s->generation;
75 s->ready = false;
76 memset(s->successor, 0, SESSION_ID_SIZE);
77 return ASCIICHAT_OK;
78}
80 // The incumbent can leave before its first measurement creates a peer entry.
81 if (memcmp(s->host, id, SESSION_ID_SIZE) == 0)
82 s->host_lost = true;
83 for (size_t i = 0; i < s->count; ++i) {
84 if (memcmp(s->peers[i].id, id, SESSION_ID_SIZE) != 0)
85 continue;
86 memmove(&s->peers[i], &s->peers[i + 1], (s->count - i - 1) * sizeof(s->peers[0]));
87 --s->count;
88 ++s->generation;
89 if (memcmp(s->host, id, SESSION_ID_SIZE) == 0)
90 s->host_lost = true;
91 // Pending acknowledgements carry the old membership generation. Reissue
92 // their preparation instead of rejecting the late reply and exhausting
93 // recovery without trying the still-eligible successor again.
94 if (!s->ready || memcmp(s->successor, id, SESSION_ID_SIZE) == 0) {
95 memset(s->successor, 0, SESSION_ID_SIZE);
96 s->ready = false;
97 }
98 return;
99 }
100}
101
106 if (!memcmp(s->host, host, sizeof(s->host)))
107 s->host_lost = true;
108}
109
117 uint64_t now) {
118 if (memcmp(s->session, m->session, SESSION_ID_SIZE) != 0)
119 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Wrong handoff session");
120 consensus_handoff_peer_t *p = find_peer(s, m->participant);
121 if (!p)
122 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Handoff sender is not a member");
123 if (m->kind == HANDOFF_SAMPLE_START) {
124 if (m->sequence <= p->sequence || (p->measured_ms && now - p->measured_ms < 1000))
125 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Repeated or excessive handoff probe");
126 p->sequence = m->sequence;
127 p->probe_started_ms = now;
128 p->measuring = true;
130 p->pending_nat_tier = m->nat_tier <= 4 ? m->nat_tier : 255;
131 p->pending_port = m->port;
132 } else if (m->kind == HANDOFF_SAMPLE_DATA) {
133 if (!p->measuring || m->sequence != p->sequence ||
135 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Unmatched handoff probe");
136 p->measuring = false;
137 uint64_t elapsed = now - p->probe_started_ms;
138 if (elapsed == 0)
139 elapsed = 1;
140 p->valid = elapsed <= 5000;
141 if (p->valid) {
144 p->port = p->pending_port;
145 // A quality refresh cannot extend a failed recovery by retrying the same host.
146 if (!s->host_lost)
147 p->rejected = false;
148 }
150 p->measured_ms = now;
151 } else if (m->kind == HANDOFF_READY || m->kind == HANDOFF_REJECT) {
152 if (m->generation != s->generation || m->sequence != s->round ||
153 memcmp(m->participant, s->successor, SESSION_ID_SIZE))
154 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Obsolete handoff preparation");
155 if (m->kind == HANDOFF_READY && now <= s->prepare_deadline_ms)
156 s->ready = true;
157 else {
158 p->rejected = true;
159 s->ready = false;
160 memset(s->successor, 0, SESSION_ID_SIZE);
161 }
162 } else
163 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Client cannot announce a handoff decision");
164 return ASCIICHAT_OK;
165}
166// Record decisions, not periodic ticks or per-peer measurements.
167static void handoff_record_action(consensus_handoff_t *state, const consensus_handoff_message_t *action, uint64_t now,
168 size_t candidates) {
169 ASCII_OTEL_SCOPE(DISCOVERY_HOST_SELECTION);
170 const char *outcome = action->kind == HANDOFF_PREPARE ? "selected"
171 : action->kind == HANDOFF_COMMIT ? "committed"
172 : "failed";
173 ASCII_OTEL_ATTR_STRING("host.selection.phase", "successor");
174 ASCII_OTEL_ATTR_STRING("host.selection.outcome", outcome);
175 ASCII_OTEL_ATTR_STRING("host.selection.reason", action->kind == HANDOFF_PREPARE ? "best_eligible_quality"
176 : action->kind == HANDOFF_COMMIT ? "prepared_successor"
177 : "no_eligible_successor");
178 ASCII_OTEL_ATTR_INT("host.generation", state->generation);
179 ASCII_OTEL_ATTR_INT("host.selection.round", state->round);
180 if (action->kind != HANDOFF_COMMIT)
181 ASCII_OTEL_ATTR_INT("host.selection.eligible_candidates", (int64_t)candidates);
182 ASCII_OTEL_ATTR_INT("host.selection.host_lost", state->host_lost);
183 consensus_handoff_peer_t *peer = find_peer(state, action->participant);
184 if (peer) {
185 ASCII_OTEL_ATTR_INT("host.selection.upload_kbps", peer->upload_kbps);
186 ASCII_OTEL_ATTR_INT("host.selection.nat_tier", peer->nat_tier);
187 }
188 ascii_otel_metric(action->kind == HANDOFF_PREPARE ? ASCII_OTEL_METRIC_HOST_SELECTIONS
189 : action->kind == HANDOFF_COMMIT ? ASCII_OTEL_METRIC_HOST_COMMITS
190 : ASCII_OTEL_METRIC_HOST_FAILURES,
191 1);
192 if (action->kind != HANDOFF_PREPARE && state->failure_deadline_ms >= CONSENSUS_HANDOFF_RECOVERY_MS) {
193 uint64_t started = state->failure_deadline_ms - CONSENSUS_HANDOFF_RECOVERY_MS;
194 if (now >= started)
195 ascii_otel_metric(ASCII_OTEL_METRIC_HOST_RECOVERY_DURATION, (double)(now - started) / 1000.0);
196 }
197}
198
208 memset(a, 0, sizeof(*a));
209 memcpy(a->session, s->session, SESSION_ID_SIZE);
210 a->generation = s->generation;
211 if (s->failure_announced)
212 return false; // Terminal until the session is recreated.
213 if (!s->monitoring) {
214 s->monitoring = true;
215 s->monitoring_started_ms = now;
216 }
217 consensus_handoff_peer_t *incumbent = find_peer(s, s->host);
218 uint64_t last_host_sample = incumbent && incumbent->valid ? incumbent->measured_ms : s->monitoring_started_ms;
219 if (now - last_host_sample > CONSENSUS_HANDOFF_FRESH_MS)
220 s->host_lost = true;
221 if (s->host_lost && !s->failure_deadline_ms)
223 // The recovery budget is shared by all candidates, including silent/rejecting
224 // candidates that continue sending fresh probes. A late READY cannot revive it.
225 if (s->host_lost && now >= s->failure_deadline_ms) {
226 s->failure_announced = true;
227 s->ready = false;
228 memset(s->successor, 0, sizeof(s->successor));
229 a->kind = HANDOFF_FAILED;
230 a->sequence = s->round;
231 handoff_record_action(s, a, now, 0);
232 return true;
233 }
234 if (s->ready && s->host_lost) {
235 consensus_handoff_peer_t *p = find_peer(s, s->successor);
236 if (p && p->valid && p->capabilities && now - p->measured_ms <= CONSENSUS_HANDOFF_FRESH_MS) {
237 a->kind = HANDOFF_COMMIT;
238 memcpy(a->participant, s->successor, SESSION_ID_SIZE);
239 a->sequence = s->round;
240 a->port = p->port;
242 handoff_record_action(s, a, now, 1);
243 s->host_sequence = a->sequence;
244 memcpy(s->host, s->successor, SESSION_ID_SIZE);
245 s->host_lost = false;
246 s->monitoring_started_ms = now;
247 s->ready = false;
248 s->failure_deadline_ms = 0;
249 s->failure_announced = false;
250 for (size_t i = 0; i < s->count; ++i)
251 s->peers[i].rejected = false;
252 memset(s->successor, 0, SESSION_ID_SIZE);
253 return true;
254 }
255 s->ready = false;
256 memset(s->successor, 0, SESSION_ID_SIZE);
257 }
258 uint8_t zero[SESSION_ID_SIZE] = {0};
259 if (memcmp(s->successor, zero, sizeof(s->successor))) {
260 consensus_handoff_peer_t *p = find_peer(s, s->successor);
261 if (p && p->valid && p->capabilities && now - p->measured_ms <= CONSENSUS_HANDOFF_FRESH_MS &&
262 (s->ready || now < s->prepare_deadline_ms))
263 return false;
264 if (p && !s->ready)
265 p->rejected = true;
266 memset(s->successor, 0, SESSION_ID_SIZE);
267 s->ready = false;
268 }
269 participant_metrics_t metrics[MAX_SESSION_PARTICIPANTS];
270 size_t count = 0;
271 for (size_t i = 0; i < s->count; ++i) {
273 if (!p->valid || p->rejected || !p->capabilities || now - p->measured_ms > CONSENSUS_HANDOFF_FRESH_MS ||
274 memcmp(p->id, s->host, SESSION_ID_SIZE) == 0)
275 continue;
276 participant_metrics_t *m = &metrics[count++];
277 memset(m, 0, sizeof(*m));
278 memcpy(m->participant_id, p->id, SESSION_ID_SIZE);
279 m->nat_tier = p->nat_tier <= 4 ? p->nat_tier : 4;
280 m->upload_kbps = p->upload_kbps;
281 m->rtt_ns = 500000000; /* Unknown RTT earns no latency bonus. */
282 }
283 if (!count)
284
285 return false;
286 int best = -1, backup = -1;
287 if (consensus_election_choose_hosts(metrics, (int)count, &best, &backup) != ASCIICHAT_OK)
288 return false;
289 memcpy(s->successor, metrics[best].participant_id, SESSION_ID_SIZE);
290 ++s->round;
293 memcpy(a->participant, s->successor, SESSION_ID_SIZE);
294 a->sequence = s->round;
295 handoff_record_action(s, a, now, count);
296 return true;
297}
deferred_action_t action
Definition actions.c:77
#define SET_ERRNO(code,...)
Application limits and constraints.
asciichat_error_t consensus_election_choose_hosts(const participant_metrics_t *metrics, int num_metrics, int *out_best_index, int *out_backup_index)
Elect best and backup host from metrics.
Definition election.c:79
unsigned short uint16_t
Definition common.h:61
unsigned int uint32_t
Definition common.h:62
unsigned long long uint64_t
Definition common.h:63
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_NETWORK_PROTOCOL
Definition error_codes.h:76
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define MAX_SESSION_PARTICIPANTS
#define SESSION_ID_SIZE
void consensus_handoff_leave(consensus_handoff_t *s, const uint8_t id[SESSION_ID_SIZE])
Definition handoff.c:79
void consensus_handoff_host_lost(consensus_handoff_t *s, const uint8_t host[SESSION_ID_SIZE])
Record loss of the incumbent, ignoring delayed reports about old hosts. The caller authenticates the ...
Definition handoff.c:105
void consensus_handoff_init(consensus_handoff_t *s, const uint8_t session[SESSION_ID_SIZE], const uint8_t host[SESSION_ID_SIZE])
Definition handoff.c:52
bool consensus_handoff_tick(consensus_handoff_t *s, uint64_t now, consensus_handoff_message_t *a)
Advance measured successor selection and bounded host-loss recovery. Fresh eligible peers prepare wit...
Definition handoff.c:207
void consensus_handoff_encode(const consensus_handoff_message_t *m, uint8_t out[CONSENSUS_HANDOFF_HEADER])
Definition handoff.c:37
asciichat_error_t consensus_handoff_join(consensus_handoff_t *s, const uint8_t id[SESSION_ID_SIZE])
Definition handoff.c:65
asciichat_error_t consensus_handoff_decode(const void *data, size_t length, consensus_handoff_message_t *m)
Definition handoff.c:16
asciichat_error_t consensus_handoff_receive(consensus_handoff_t *s, const consensus_handoff_message_t *m, size_t length, uint64_t now)
Accept a member's probe or preparation reply using receiver-clock time. Probe metadata becomes eligib...
Definition handoff.c:116
#define CONSENSUS_HANDOFF_HEADER
Definition handoff.h:11
#define CONSENSUS_HANDOFF_PROBE_BYTES
Definition handoff.h:12
#define CONSENSUS_HANDOFF_PREPARE_MS
Definition handoff.h:14
#define CONSENSUS_HANDOFF_FRESH_MS
Definition handoff.h:13
#define CONSENSUS_HANDOFF_VERSION
Definition handoff.h:10
#define CONSENSUS_HANDOFF_RECOVERY_MS
Definition handoff.h:15
@ HANDOFF_REJECT
Definition handoff.h:24
@ HANDOFF_SAMPLE_DATA
Definition handoff.h:19
@ HANDOFF_COMMIT
Definition handoff.h:23
@ HANDOFF_FAILED
Definition handoff.h:25
@ HANDOFF_SAMPLE_ACK
Definition handoff.h:26
@ HANDOFF_READY
Definition handoff.h:21
@ HANDOFF_PREPARE
Definition handoff.h:20
@ HANDOFF_SAMPLE_START
Definition handoff.h:18
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
static bool size_t gotel_query_t * out
Definition queries.h:8
🔤 String Manipulation and Shell Escaping Utilities
uint8_t session[SESSION_ID_SIZE]
Definition handoff.h:31
uint8_t participant[SESSION_ID_SIZE]
Definition handoff.h:32
consensus handoff message is intentionally a small, concrete record. Fields such as kind,...
Definition handoff.h:47
uint64_t probe_started_ms
Definition handoff.h:57
uint8_t pending_capabilities
Definition handoff.h:52
uint8_t id[SESSION_ID_SIZE]
Definition handoff.h:48
uint32_t host_sequence
Last committed round; preparation does not change host authority.
Definition handoff.h:72
uint8_t session[SESSION_ID_SIZE]
Definition handoff.h:67
uint8_t successor[SESSION_ID_SIZE]
Definition handoff.h:69
uint64_t monitoring_started_ms
Definition handoff.h:78
uint64_t prepare_deadline_ms
Definition handoff.h:73
uint32_t generation
Definition handoff.h:70
consensus_handoff_peer_t peers[MAX_SESSION_PARTICIPANTS]
Definition handoff.h:80
uint64_t failure_deadline_ms
Definition handoff.h:77
uint8_t host[SESSION_ID_SIZE]
Definition handoff.h:68