Bound member snapshot events and sort roster without quadratic copies
This commit is contained in:
1 parent
34a95b7733
commit
6104086d5b
3 files changed
+133
-21
No files matched your search
+39
-16
@@ -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++) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user