From 1bb824fb8a55029b51e2c817c3aaba69d5bdcc4e Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 05:24:47 +0100 Subject: [PATCH] fix(chat): commit radio mute before applying cache and reporting success --- src/chat/chat_member.c | 9 ++++--- src/radio/radio.c | 59 ++++++++++++++++++++++++++++-------------- src/radio/radio.h | 4 +++ 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/chat/chat_member.c b/src/chat/chat_member.c index 30f639fa..767f17ad 100644 --- a/src/chat/chat_member.c +++ b/src/chat/chat_member.c @@ -274,14 +274,15 @@ int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, u int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) { uint8_t member[CHAT_MEMBER_DISPLAY_SIZE]; - if (!ch_id || !ch_id[0] || strlen(ch_id) >= 64 || chat_core_get_single_member(inst, ch_id, node_id, member) < 0) { + if (!chat_channel_id(ch_id) || chat_core_get_single_member(inst, ch_id, node_id, member) < 0) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: mute toggle member not found node=%016llx", (unsigned long long)node_id); return -1; } int muted = !(member[10] & CHAT_MEMBER_FLAG_RADIO_MUTED); if (radio_set_member_muted(inst, strtoull(ch_id, NULL, 10), node_id, muted) < 0) return -1; - if (chat_core_get_single_member(inst, ch_id, node_id, member) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: cannot publish local mute ch=%s node=%016llx", - ch_id, (unsigned long long)node_id); return -1; + /* Снимок уже готов: после committed mute нет нового чтения, способного превратить успех в ошибку. */ + member[10] = (uint8_t)((member[10] & ~CHAT_MEMBER_FLAG_RADIO_MUTED) | (muted ? CHAT_MEMBER_FLAG_RADIO_MUTED : 0)); + if (!inst->topo_groups || !topo_groups_find(inst->topo_groups, strtoull(ch_id, NULL, 10))) { + DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: mute committed, group removed by audio callback ch=%s", ch_id); return 0; } size_t cl = strlen(ch_id); uint8_t evt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; diff --git a/src/radio/radio.c b/src/radio/radio.c index 1a24a6b1..f375c442 100644 --- a/src/radio/radio.c +++ b/src/radio/radio.c @@ -478,9 +478,10 @@ static int radio_load_muted_members(struct radio_ctx* ctx) { } members = next; members[count++] = (uint64_t)sqlite3_column_int64(st, 0); } - sqlite3_finalize(st); - if (rc != SQLITE_DONE) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read muted members: %s", sqlite3_errmsg(db)); u_free(members); return -1; + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read muted members rc=%d finalize=%d: %s", rc, final, sqlite3_errmsg(db)); + u_free(members); return -1; } u_free(ctx->muted_members); ctx->muted_members = members; ctx->muted_count = count; ctx->muted_loaded = 1; DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio: loaded local mutes grp=%016llx count=%d", @@ -524,34 +525,54 @@ int radio_set_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64 DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: invalid local mute grp=%016llx node=%016llx muted=%d", (unsigned long long)group_id, (unsigned long long)node_id, muted); return -1; } - if (!ctx->muted_loaded && radio_load_muted_members(ctx) < 0) return -1; - int slot = -1; + sqlite3* db = inst->topo_sqlite_db; + if (!db || !sqlite3_get_autocommit(db)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute requires database without external transaction grp=%016llx", + (unsigned long long)group_id); return -1; + } + int rc = sqlite3_exec(db, "BEGIN IMMEDIATE /* radio mute */", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute begin grp=%016llx rc=%d: %s", + (unsigned long long)group_id, rc, sqlite3_errmsg(db)); return -1; + } + sqlite3_stmt* st = NULL; const char* stage = "load cache"; int slot = -1; + if (!ctx->muted_loaded && radio_load_muted_members(ctx) < 0) { rc = SQLITE_ERROR; goto error; } for (int i = 0; i < ctx->muted_count; i++) if (ctx->muted_members[i] == node_id) { slot = i; break; } /* Выделить память до записи БД: после commit применение не может отказать. */ if (muted && slot < 0) { uint64_t* next = u_realloc(ctx->muted_members, (size_t)(ctx->muted_count + 1) * sizeof(*next)); - if (!next) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: grow muted member cache failed"); return -1; } + if (!next) { stage = "reserve cache"; rc = SQLITE_NOMEM; goto error; } ctx->muted_members = next; } - sqlite3* db = inst->topo_sqlite_db; char sql[160]; snprintf(sql, sizeof(sql), "UPDATE \"peers_%llu\" SET radio_muted=? WHERE node_id=?", (unsigned long long)group_id); - sqlite3_stmt* st = NULL; - if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: prepare local mute: %s", db ? sqlite3_errmsg(db) : "no database"); return -1; - } - sqlite3_bind_int(st, 1, muted); sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id); - int rc = sqlite3_step(st); sqlite3_finalize(st); - if (rc != SQLITE_DONE || sqlite3_changes(db) != 1) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: save local mute grp=%016llx node=%016llx rc=%d changed=%d: %s", - (unsigned long long)group_id, (unsigned long long)node_id, rc, sqlite3_changes(db), sqlite3_errmsg(db)); return -1; - } + stage = "prepare"; rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); if (rc != SQLITE_OK) goto error; + stage = "bind mute"; rc = sqlite3_bind_int(st, 1, muted); if (rc != SQLITE_OK) goto error; + stage = "bind member"; rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id); if (rc != SQLITE_OK) goto error; + stage = "step"; rc = sqlite3_step(st); if (rc != SQLITE_DONE) goto error; + stage = "changes"; if (sqlite3_changes(db) != 1) { rc = SQLITE_CONSTRAINT; goto error; } + stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error; + stage = "commit"; rc = sqlite3_exec(db, "COMMIT /* radio mute */", NULL, NULL, NULL); if (rc != SQLITE_OK) goto error; if (muted && slot < 0) ctx->muted_members[ctx->muted_count++] = node_id; else if (!muted && slot >= 0) ctx->muted_members[slot] = ctx->muted_members[--ctx->muted_count]; - radio_refresh_member_mute(inst, group_id, node_id); - DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: local member mute grp=%016llx node=%016llx muted=%d", + DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: local member mute grp=%016llx node=%016llx muted=%d committed=1", (unsigned long long)group_id, (unsigned long long)node_id, muted); + radio_refresh_member_mute(inst, group_id, node_id); return 0; +error: + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute grp=%016llx node=%016llx muted=%d stage=%s rc=%d: %s", + (unsigned long long)group_id, (unsigned long long)node_id, muted, stage, rc, sqlite3_errmsg(db)); + rc = sqlite3_finalize(st); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute cleanup finalize rc=%d", rc); + if (!sqlite3_get_autocommit(db)) { + rc = sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL); + if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute rollback rc=%d: %s", rc, sqlite3_errmsg(db)); + if (!sqlite3_get_autocommit(db)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: local mute transaction remains open; requesting owner stop/DB close"); + utun_instance_stop(inst); + } + } + return -1; } /* ── подписка (вкл/выкл прослушивание) ── */ diff --git a/src/radio/radio.h b/src/radio/radio.h index c5a21b00..4df7e3f9 100644 --- a/src/radio/radio.h +++ b/src/radio/radio.h @@ -39,6 +39,10 @@ int radio_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t n int radio_member_silenced(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id); /* Изменилось временное подавление; настройки в БД и подписка остаются прежними. */ void radio_refresh_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id); +/* Сохранить muted=0/1 для другого участника существующей CHAT-группы;0 — committed,-1 — отказ. + * Собственная BEGIN IMMEDIATE,чужая транзакция запрещена. Память резервируется до записи; + * cache и mute callback меняются после commit. При отказе настройка/звук сохраняются; + * неудачный rollback с открытой транзакцией запрашивает stop/закрытие БД владельцем. */ int radio_set_member_muted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id, int muted); /* После удаления записи участника из БД удалить её из локального кэша. */ void radio_forget_member_mute(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id);