Exercise radio invitations over routed network with deadlines and packet loss
This commit is contained in:
1 parent
a831cf6295
commit
a73d829080
1 file changed
+136
-3
+136
-3
@@ -17,6 +17,8 @@
|
||||
#include "chat_member.h"
|
||||
#include "member_sync.h"
|
||||
#include "../radio/radio.h"
|
||||
#include "../radio/radio_call.h"
|
||||
#include "../routing_layer/etcp_router.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
@@ -48,13 +50,14 @@
|
||||
#define IDX_C 2
|
||||
|
||||
#define TICK_TB 100 /* 10 ms на такт */
|
||||
#define MAX_TICKS 4000 /* глобальный таймаут ≈ 40 c */
|
||||
#define MAX_TICKS 12000 /* включает минуту ожидания решения по вызову */
|
||||
#define N_FRAMES 5
|
||||
|
||||
enum rad_phase {
|
||||
P_WAIT_CONN,
|
||||
P_SETUP,
|
||||
P_WAIT_BGP,
|
||||
P_RADIO_CALLS,
|
||||
P_RADIO_ON,
|
||||
P_WAIT_FLAGS,
|
||||
P_TALK1,
|
||||
@@ -81,6 +84,8 @@ struct rad_ctx {
|
||||
int result; /* 0=running, 1=ok, 2=fail */
|
||||
int ticks;
|
||||
void* timer;
|
||||
int call_step;
|
||||
uint64_t call_started;
|
||||
};
|
||||
|
||||
/* приёмник кадров (per-узел): проверка порядка seq и FIN */
|
||||
@@ -104,9 +109,48 @@ static int g_member_radio[3][3];
|
||||
static int g_member_lists[3];
|
||||
static int g_member_event_bad;
|
||||
static int g_talk_on[3], g_talk_starts[3], g_talk_stops[3];
|
||||
static struct { int state, reason, finished, incoming; uint64_t finished_tb; } g_calls[6][3];
|
||||
static int g_call_ready, g_drop_result, g_drop_ack;
|
||||
static etcp_recv_fn g_call_recv[3];
|
||||
|
||||
/* Потеря приложения после router ACK: восстановить результат обязан сервис, не транспорт. */
|
||||
static void call_packet_loss(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
int idx = -1;
|
||||
for (int i = 0; i < 3; ++i) if (conn->instance->node_id == g_sh.nid[i]) idx = i;
|
||||
if (idx < 0 || !entry || entry->len <= ROUTER_SVC_PAYLOAD_OFF) abort();
|
||||
int cmd = entry->dgram[ROUTER_SVC_PAYLOAD_OFF];
|
||||
if ((idx == IDX_A && cmd == 3 && g_drop_result-- > 0) || (idx == IDX_C && cmd == 4 && g_drop_ack-- > 0)) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test drops radio result/ACK dst=%d command=%d", idx, cmd);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return;
|
||||
}
|
||||
g_call_recv[idx](conn, entry);
|
||||
}
|
||||
|
||||
static void radio_action(struct UTUN_INSTANCE* inst, uint64_t id, int action, uint64_t peer) {
|
||||
struct radio_call_request req = {inst, id, strtoull(CH_ID, NULL, 10), peer, action};
|
||||
radio_call_execute(&req);
|
||||
}
|
||||
|
||||
/* Проверяем именно UI-события: self, прямой пир и пир за посредником. */
|
||||
static void member_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
||||
if (type == CHAT_EVT_RADIO_CALL) {
|
||||
uint64_t id, group, peer;
|
||||
if (!data || len != 27) { g_member_event_bad = 1; return; }
|
||||
memcpy(&id, data, 8); memcpy(&group, data + 8, 8); memcpy(&peer, data + 16, 8);
|
||||
int idx = -1;
|
||||
for (int i = 0; i < 3; ++i) if (inst->node_id == g_sh.nid[i]) idx = i;
|
||||
if (idx < 0 || id < 100 || id > 105 || group != strtoull(CH_ID, NULL, 10)) { g_member_event_bad = 1; return; }
|
||||
g_calls[id - 100][idx].state = data[24]; g_calls[id - 100][idx].reason = data[25];
|
||||
if (data[24] == RADIO_CALL_INCOMING) {
|
||||
++g_calls[id - 100][idx].incoming;
|
||||
if (g_call_ready) radio_action(inst, id, RADIO_CALL_READY, 0);
|
||||
}
|
||||
if (data[24] == RADIO_CALL_FINISHED) {
|
||||
++g_calls[id - 100][idx].finished; g_calls[id - 100][idx].finished_tb = get_time_tb();
|
||||
if (data[25] == RADIO_CALL_ACCEPTED) radio_set_active(inst, group, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == CHAT_EVT_RADIO_TALK) {
|
||||
uint64_t group, source;
|
||||
if (!data || len != 19 || data[18] > 1) {
|
||||
@@ -327,9 +371,10 @@ static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
|
||||
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id,
|
||||
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm));
|
||||
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig);
|
||||
uint8_t tree_sig[64]; member_sync_sign_pubkey(g_sh.ch_ed_priv, inst->my_keys.public_key, tree_sig);
|
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
|
||||
member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
||||
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
|
||||
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, tree_sig);
|
||||
}
|
||||
|
||||
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) {
|
||||
@@ -401,8 +446,96 @@ static void rad_tick(void* arg) {
|
||||
break;
|
||||
case P_WAIT_BGP:
|
||||
if (bgp_has(A, nb) && bgp_has(A, nc) && bgp_has(B, na) && bgp_has(B, nc) && bgp_has(C, na) && bgp_has(C, nb))
|
||||
t->phase = P_RADIO_ON;
|
||||
t->phase = P_RADIO_CALLS;
|
||||
break;
|
||||
case P_RADIO_CALLS: {
|
||||
uint64_t now = get_time_tb();
|
||||
if (g_member_event_bad) { t->result = 2; uasync_stop(t->ua); return; }
|
||||
switch (t->call_step) {
|
||||
case 0: {
|
||||
uint8_t key[32];
|
||||
if (member_sync_verified_ed25519(A, CH_ID, nc, key) || member_sync_verified_ed25519(C, CH_ID, na, key)) break;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
g_call_recv[i] = t->inst[i]->router_bindings.callbacks[ETCP_RT_ID_RADIO_CALL];
|
||||
t->inst[i]->router_bindings.callbacks[ETCP_RT_ID_RADIO_CALL] = call_packet_loss;
|
||||
}
|
||||
g_call_ready = 0; t->call_started = now;
|
||||
radio_action(A, 100, RADIO_CALL_START, nc); ++t->call_step;
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
if (g_calls[0][IDX_A].finished && g_calls[0][IDX_C].finished) {
|
||||
uint64_t elapsed = g_calls[0][IDX_A].finished_tb - t->call_started;
|
||||
if (g_calls[0][IDX_A].reason != RADIO_CALL_UNAVAILABLE || elapsed < RADIO_CALL_RESPONSE_TB ||
|
||||
elapsed > RADIO_CALL_RESPONSE_TB + 1000 || g_calls[0][IDX_C].incoming != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test first response deadline failed reason=%d elapsed_tb=%llu incoming=%d",
|
||||
g_calls[0][IDX_A].reason, (unsigned long long)elapsed, g_calls[0][IDX_C].incoming);
|
||||
t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
g_call_ready = 1; g_drop_result = 1; g_drop_ack = 1;
|
||||
radio_action(A, 101, RADIO_CALL_START, nc); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (g_calls[1][IDX_A].state == RADIO_CALL_RINGING) {
|
||||
radio_action(C, 101, RADIO_CALL_ACCEPT, 0); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (g_calls[1][IDX_A].finished && g_calls[1][IDX_C].finished) {
|
||||
if (g_calls[1][IDX_A].reason != RADIO_CALL_ACCEPTED || !radio_is_active(A, gid) || !radio_is_active(C, gid)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio acceptance failed"); t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
radio_set_active(A, gid, 0); radio_set_active(C, gid, 0);
|
||||
radio_action(A, 102, RADIO_CALL_START, nc); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if (g_calls[2][IDX_A].state == RADIO_CALL_RINGING) { radio_action(C, 102, RADIO_CALL_DECLINE, 0); ++t->call_step; }
|
||||
break;
|
||||
case 5:
|
||||
if (g_calls[2][IDX_A].finished) {
|
||||
if (g_calls[2][IDX_A].reason != RADIO_CALL_DECLINED || radio_is_active(A, gid) || radio_is_active(C, gid)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio decline failed"); t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
radio_action(A, 103, RADIO_CALL_START, nc); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
if (g_calls[3][IDX_A].state == RADIO_CALL_RINGING) {
|
||||
radio_action(C, 104, RADIO_CALL_START, nb);
|
||||
radio_action(A, 103, RADIO_CALL_CANCEL, 0); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
if (g_calls[3][IDX_C].finished && g_calls[4][IDX_C].finished) {
|
||||
if (g_calls[3][IDX_C].reason != RADIO_CALL_CANCELLED || g_calls[4][IDX_C].reason != RADIO_CALL_BUSY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test radio cancel/busy failed"); t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
t->call_started = now;
|
||||
radio_action(A, 105, RADIO_CALL_START, nc); ++t->call_step;
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
if (g_calls[5][IDX_A].finished && g_calls[5][IDX_C].finished) {
|
||||
uint64_t elapsed = g_calls[5][IDX_A].finished_tb - t->call_started;
|
||||
if (g_calls[5][IDX_A].reason != RADIO_CALL_NO_ANSWER || elapsed < RADIO_CALL_RING_TB || elapsed > RADIO_CALL_RING_TB + 1000) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test one minute deadline failed reason=%d elapsed_tb=%llu",
|
||||
g_calls[5][IDX_A].reason, (unsigned long long)elapsed); t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
for (int n = 0; n < 6; ++n) for (int i = 0; i < 3; ++i) {
|
||||
if (g_calls[n][i].finished > 1 || g_calls[n][i].incoming > 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "test duplicated radio call event id=%d node=%d", n + 100, i);
|
||||
t->result = 2; uasync_stop(t->ua); return;
|
||||
}
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "test radio calls passed: 2s/60s deadlines, accept, decline, cancel, busy, lost result/ACK");
|
||||
t->phase = P_RADIO_ON;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case P_RADIO_ON:
|
||||
radio_set_active(A, gid, 1);
|
||||
radio_set_active(B, gid, 1);
|
||||
|
||||
Reference in new issue
Block a user