Bound member snapshot events and sort roster without quadratic copies

This commit is contained in:
evgeny committed 2026-10-27 10:37:33 +01:00
1 parent 34a95b7733
commit 6104086d5b
3 files changed
+133 -21

No files matched your search

+39 -16
View File
@@ -21,10 +21,12 @@
#include "../../lib/platform_compat.h"
#include <stdlib.h>
#include <limits.h>
/* ─── live-онлайн: тот же живой путь, которым пользуется маршрутизатор ─── */
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
if (!inst) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member online: null instance"); return 0; }
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->inst || !ch_id || !ch_id[0]) return 0;
if (node_id == cc->my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
@@ -186,9 +188,16 @@ static void member_from_row(struct UTUN_INSTANCE* inst, const char* ch_id, sqlit
else snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
}
/* Онлайн выше offline, затем uint64 node_id по возрастанию без вычитания/знакового переполнения. */
static int compare_members(const void* left, const void* right) {
const struct chat_member_display *a = left, *b = right;
if (a->online != b->online) return a->online > b->online ? -1 : 1;
return a->node_id == b->node_id ? 0 : a->node_id < b->node_id ? -1 : 1;
}
static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int single, uint64_t node_id,
uint8_t** out, int* count) {
struct chat_core_ctx* cc = CC(inst);
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
*out = NULL; *count = 0;
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: invalid instance/channel"); return -1;
@@ -211,11 +220,19 @@ static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int singl
sqlite3_finalize(st); return -1;
}
struct chat_member_display* members = NULL;
size_t limit = UINT32_MAX / sizeof(*members); // u_realloc принимает uint32_t число байтов
if (limit > INT_MAX) limit = INT_MAX;
int cap = 0, cnt = 0;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
if (cnt == cap) {
cap = cap ? cap * 2 : (single ? 1 : 32);
struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members));
if ((size_t)cap >= limit) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation limit ch=%s count=%d max=%zu", ch_id, cnt, limit);
u_free(members); sqlite3_finalize(st); return -1;
}
size_t next = cap ? (size_t)cap * 2 : (single ? 1 : 32);
if (next > limit) next = limit;
cap = (int)next;
struct chat_member_display* tmp = u_realloc(members, (uint32_t)(next * sizeof(*members)));
if (!tmp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation ch=%s count=%d", ch_id, cnt);
u_free(members); sqlite3_finalize(st); return -1;
@@ -229,15 +246,7 @@ static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int singl
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: read ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
u_free(members); return -1;
}
for (int i = 1; i < cnt; i++) {
struct chat_member_display key = members[i];
int j = i - 1;
while (j >= 0 && (members[j].online < key.online ||
(members[j].online == key.online && members[j].node_id > key.node_id))) {
members[j + 1] = members[j]; j--;
}
members[j + 1] = key;
}
if (cnt > 1) qsort(members, (size_t)cnt, sizeof(*members), compare_members);
if (cnt) {
*out = u_malloc((size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE);
if (!*out) {
@@ -300,7 +309,9 @@ void chat_core_toggle_member_radio_mute_trampoline(void* arg) {
void chat_core_request_member_list_trampoline(void* arg) {
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
if (!req || !req->inst || !CC(req->inst) || !CC(req->inst)->initialized) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member list request rejected: chat unavailable"); u_free(arg); return;
}
struct UTUN_INSTANCE* inst = req->inst;
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
@@ -310,6 +321,10 @@ void chat_core_request_member_list_trampoline(void* arg) {
if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build list ch=%s", ch_id); return;
}
if (count < 0 || count > UINT16_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list event rejected ch=%s count=%d wire_limit=%u", ch_id, count, (unsigned)UINT16_MAX);
u_free(members); return;
}
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 2 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE;
@@ -335,19 +350,27 @@ void chat_core_request_member_list_trampoline(void* arg) {
void chat_core_request_member_rtt_trampoline(void* arg) {
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
if (!req || !req->inst || !CC(req->inst) || !CC(req->inst)->initialized) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member RTT request rejected: chat unavailable"); u_free(arg); return;
}
struct UTUN_INSTANCE* inst = req->inst;
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
uint8_t* members = NULL;
int count = 0;
if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0 || !members) return;
if (chat_core_get_member_list(inst, ch_id, &members, &count) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build RTT snapshots ch=%s", ch_id); return;
}
if (!count) { u_free(members); return; }
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 1 + CHAT_MEMBER_DISPLAY_SIZE;
uint8_t* evt = u_malloc(evt_sz);
if (!evt) { u_free(members); return; }
if (!evt) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: RTT event allocation failed ch=%s count=%d", ch_id, count);
u_free(members); return;
}
/* шлём мемберов по одному (RTT мог измениться у каждого) */
for (int i = 0; i < count; i++) {
+9 -5
View File
@@ -48,7 +48,10 @@ extern "C" {
#define CHAT_MEMBER_DISPLAY_SIZE 164
/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */
/* Получить всех мемберов канала в uasync. out/count обязательны; 0 — успех, -1 с логом.
* out — собственный u_malloc-массив count*CHAT_MEMBER_DISPLAY_SIZE, освобождается u_free;
* пустой список даёт NULL/0. Ошибка сохраняет NULL/0. Порядок: online первыми, затем uint64 node_id.
* Это API не ограничено 16-битным count GUI-события; рост массива ограничен размером u_realloc. */
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count);
/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */
@@ -81,13 +84,14 @@ struct chat_member_display {
void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst);
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst);
/* Трамплин для gui_bridge: GUI запрашивает полный список мемберов канала.
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_LIST. */
/* Трамплин для gui_bridge, только uasync: принимает собственный u_malloc-блок arg и освобождает его.
* ch_id — NUL-строка до 63 байт. Успех: CHAT_EVT_MEMBER_LIST с uint16 native count (0..65535).
* Непредставимый count/ошибка чата/памяти логируются, события нет; ложный/усечённый снимок не отправляется. */
struct chat_member_list_req { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void chat_core_request_member_list_trampoline(void* arg);
/* Трамплин для gui_bridge: GUI запрашивает RTT мемберов канала.
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
/* Владение/поток/аргумент как у list trampoline. Для каждого участника — один полный
* CHAT_EVT_MEMBER_UPDATED, включая остальные live-поля. Пустой список не даёт событий. */
void chat_core_request_member_rtt_trampoline(void* arg);
/* Локальный переключатель рации; каждый запрос меняет актуальное состояние в uasync. */
+85
View File
@@ -1,5 +1,7 @@
/* Реальный адаптер, SQLite и транзакции; без копии алгоритма и без сетевых таймингов. */
#include "../src/chat/member_sync.c"
#include "../src/chat/chat_member.h"
#include "../src/member_files/protocol.h"
#include <assert.h>
void test_member_purge(struct UASYNC* ua);
@@ -18,6 +20,88 @@ static void put(struct UTUN_INSTANCE* inst, uint64_t id) {
assert(member_sync_put(inst, "1001", id, key, key, NULL, 0, NULL, 0, "{}", NULL, NULL, 0, 0, NULL) >= 0);
}
static int snapshot_list_events, snapshot_updates, snapshot_bad;
static uint64_t snapshot_first, snapshot_last;
/* Событие проверяем синхронно, не сохраняем заимствованный буфер и не печатаем тысячи записей. */
static void snapshot_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
(void)inst;
if (type == CHAT_EVT_MEMBER_UPDATED) { snapshot_updates++; return; }
if (type != CHAT_EVT_MEMBER_LIST) return;
snapshot_list_events++;
if (!data || len < 7 || data[0] != 4 || memcmp(data + 1, "1001", 4)) { snapshot_bad++; return; }
uint16_t count;
memcpy(&count, data + 5, sizeof(count));
if ((size_t)len != 7 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE) { snapshot_bad++; return; }
snapshot_first = count ? mf_get64(data + 7) : 0;
snapshot_last = count ? mf_get64(data + 7 + (size_t)(count - 1) * CHAT_MEMBER_DISPLAY_SIZE) : 0;
}
/* Настоящие SQLite-запрос/сериализатор/trampoline: count не должен оборачиваться и чистить GUI ложным снимком. */
static int test_snapshot_event_bounds(struct UASYNC* ua) {
struct UTUN_INSTANCE inst; setup(&inst, ua);
struct chat_core_ctx cc = { .inst = &inst, .db = inst.topo_sqlite_db, .initialized = 1 };
inst.chat_core = &cc;
chat_event_set_handler(&inst, snapshot_event);
const int sizes[] = {0, 1, UINT16_MAX, UINT16_MAX + 1};
int result = 0;
for (unsigned test = 0; test < sizeof(sizes) / sizeof(sizes[0]); test++) {
const int count = sizes[test]; inst.node_id = count; cc.my_node_id = count;
assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001", NULL, NULL, NULL) == SQLITE_OK);
if (count) {
char sql[512];
snprintf(sql, sizeof(sql), "WITH RECURSIVE n(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM n WHERE id<%d) "
"INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) "
"SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", count);
assert(sqlite3_exec(cc.db, sql, NULL, NULL, NULL) == SQLITE_OK);
}
snapshot_list_events = snapshot_updates = snapshot_bad = 0; snapshot_first = snapshot_last = 0;
const size_t before = u_get_allocated_count();
struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request);
request->inst = &inst; strcpy(request->ch_id, "1001");
chat_core_request_member_list_trampoline(request);
if (snapshot_bad || snapshot_list_events != (count <= UINT16_MAX) ||
(count <= UINT16_MAX && count && (snapshot_first != (uint64_t)count || snapshot_last != (uint64_t)(count > 1 ? count - 1 : 1)))) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member event boundary failed count=%d events=%d malformed=%d first=%llu last=%llu",
count, snapshot_list_events, snapshot_bad, (unsigned long long)snapshot_first, (unsigned long long)snapshot_last);
result = -1;
}
assert(u_get_allocated_count() == before);
if (count <= 1) {
request = u_calloc(1, sizeof(*request)); assert(request);
request->inst = &inst; strcpy(request->ch_id, "1001");
chat_core_request_member_rtt_trampoline(request);
assert(snapshot_updates == count && u_get_allocated_count() == before);
}
}
if (!result) {
uint8_t* wire = NULL; int count = -1;
assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == UINT16_MAX + 1);
u_free(wire); wire = NULL;
assert(sqlite3_exec(cc.db, "DELETE FROM peers_1001;"
"WITH n(id) AS (VALUES(1),(2),(-1),(-9223372036854775808)) "
"INSERT INTO peers_1001(node_id,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,source) "
"SELECT id,zeroblob(32),zeroblob(32),zeroblob(64),0,1 FROM n", NULL, NULL, NULL) == SQLITE_OK);
inst.node_id = cc.my_node_id = 2;
assert(chat_core_get_member_list(&inst, "1001", &wire, &count) == 0 && count == 4);
const uint64_t order[] = {2, 1, UINT64_C(1) << 63, UINT64_MAX};
for (int i = 0; i < 4; i++) {
assert(mf_get64(wire + i * CHAT_MEMBER_DISPLAY_SIZE) == order[i]);
assert(wire[i * CHAT_MEMBER_DISPLAY_SIZE + 8] == (i == 0));
}
u_free(wire); wire = NULL;
assert(chat_core_get_member_list(NULL, "1001", &wire, &count) < 0 && !wire && count == 0);
struct chat_member_list_req* request = u_calloc(1, sizeof(*request)); assert(request);
chat_core_request_member_list_trampoline(request);
request = u_calloc(1, sizeof(*request)); assert(request);
chat_core_request_member_rtt_trampoline(request);
}
inst.chat_core = NULL;
member_sync_destroy(&inst);
assert(sqlite3_close(inst.topo_sqlite_db) == SQLITE_OK);
return result;
}
static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) {
assert(merkle_sync_read_hash(inst, MT_DATA_MEMBERS, "1001", 0, 0, hash) == 0);
}
@@ -196,6 +280,7 @@ int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
size_t baseline = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua);
if (test_snapshot_event_bounds(ua) < 0) { uasync_destroy(ua, 0); return 1; }
test_member_purge(ua);
test_invitation_certificate(ua);
test_owner_sizes(ua);