From e17ce5504eb1c12b4ca55817a75aeecc465b19d3 Mon Sep 17 00:00:00 2001 From: wt Date: Wed, 21 Oct 2026 20:03:10 +0700 Subject: [PATCH] some --- AGENTS.md | 20 ++++++++++++++++++-- docs/COMPATIBILITY.md | 9 ++++++++- docs/DURABLE-MESSAGE-IO.md | 24 +++++++++++++++++------- tests/archive_redo.rs | 17 ++++++++++++++--- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7b15960..38038c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -319,13 +319,29 @@ storage-ошибка не роняет session и не роняет достав `offline_checkpoint_retires_...`, сетевой `tests/mam_broken_baseline_network.py` (перезапись baseline не роняет доставку; checkpoint возвращает MAM). +Критическое уточнение 0.8.4-followup: **живой append больше НЕ fail-closed** — это +и было корневой причиной seconds-уровневых `message-processing-slow` у Luma и +непрерывных `mam-archive-skipped` на сервере пользователя. В `src/archive.rs` +`recover_redo` теперь сам зареТирует неприменимый журнал при первом же сбое +(ранняя проверка anchor по первым 96 байтам header до чтения кадров) и продолжит +работу на синхронизированном native как Prosody, который вообще не хранит `.rredo`. +Чтение сломанного журнала больше не стоит O(журнал) на каждый access: проверяется +только header, неподключённый журнал удаляется один раз, последующие access быстры +(см. `untrusted_redo_baseline_is_retired_once_and_recovers`). Полная коррупция +header-checksum или неполный baseline по-прежнему блокируют startup (инвариант +durability 0.8.3) — они НЕ матчатся `is_untrusted_redo` и остаются fail-closed. + Сравнение с Prosody: `util/datamanager.lua` `list_append` пишет в конец, а `list_store` (перезапись) делает atomic rename — меняет первые bytes baseline; `plugins/mod_storage_internal.lua` `archive:append` для MUC/`key` и trim/delete делает `list_store` (полная перезапись). Prosody не понимает `.rredo` sidecar. Наш native `.list` сохраняет Prosody-совместимый `item(...)` формат. -Операционный ремонт пользователя: остановить сервер, `./target/release/rust-xmpp -storage-checkpoint CONFIG`, запустить снова. `--experimental` не снимать. Полная +Операционный ремонт пользователя: с этим фиксом ручной `storage-checkpoint` для +починки рассинхронизированного baseline больше не требуется — журнал +зареТиров согласуется automatically при первом доступе, и append продолжает +работу на скорости Prosody. Offline `storage-checkpoint CONFIG` остаётся +доступным (и идемпотентным) для штатного durable handoff перед сменой writer. +`--experimental` не снимать. Полная замена Prosody требует реального CI, внешней федерации, настоящих клиентов/push, deployment/UID/TLS, rollback и длительной нагрузки — см. COMPATIBILITY.md. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 1618062..8fedd60 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -182,4 +182,11 @@ in place»: каждый MAM-append падал и через `?` ронял sess доставке стала best-effort (storage-ошибка не роняет session и не роняет доставку; в лог — `mam-archive-skipped`), а `storage-checkpoint` получил ремонт непроверенного baseline: текущий синхронизированный native считается авторитетным и неприменимый -журнал удаляется. Живой append остаётся fail-closed: журнал не удаляется молча. +журнал удаляется. + +С 0.8.4-followup живой append больше НЕ fail-closed: `recover_redo` при первом же +неприменимом baseline читает только header и сам удаляет журнал, принимая synced +native (как Prosody), восстанавливая append на скорости Prosody без секундных +`message-processing-slow` и прекращая `mam-archive-skipped`. Полная коррупция +header-checksum или неполный baseline по-прежнему блокируют startup (fail-closed, +инвариант 0.8.3) и не матчатся `is_untrusted_redo`. diff --git a/docs/DURABLE-MESSAGE-IO.md b/docs/DURABLE-MESSAGE-IO.md index 8c9bdff..3deee18 100644 --- a/docs/DURABLE-MESSAGE-IO.md +++ b/docs/DURABLE-MESSAGE-IO.md @@ -78,20 +78,30 @@ handoff, `.rredo` перестаёт совпадать со своим anchor. Начиная с 0.8.4 MAM-архивация на трёх местах личной доставки (отправитель, получатель, входящая федерация) стала best-effort: при storage-ошибке исходная stanza всё равно маршрутизируется, обе сессии остаются живы, в лог пишется -санитизированный `mam-archive-skipped`. Живой append остаётся fail-closed: -непроверенный журнал не удаляется молча, а сохраняется для явного handoff. +санитизированный `mam-archive-skipped`. -Ремонт выполняется той же командой offline handoff: +Критическое уточнение (0.8.4-followup): живой append больше НЕ fail-closed. +`recover_redo` при первом же untrusted-baseline сбое читает только 96-байтовый +header (ранняя anchor-проверка до чтения/валидации кадров) и сам зареТирует +неподключённый журнал (`retire_redo`), принимая синхронизированный native как +Prosody. Это устраняет O(журнал) чтение+checksum на каждый access при больших +сломанных журналах (секунды `message-processing-slow`) и возвращает append на +скорость Prosody. Полная коррупция header-checksum или неполный baseline по-прежнему +блокируют startup (инвариант 0.8.3): они не матчатся `is_untrusted_redo` и остаются +fail-closed, чтобы не потерять committed redo frames без цели. + +Ручной `storage-checkpoint` для этого больше не требуется; offline handoff +остаётся идемпотентным способом штатного durable handoff перед сменой writer: ```bash rust-xmpp storage-checkpoint /etc/prosody/prosody.cfg.lua ``` -Теперь `prepare_rewrite` распознаёт непроверенный baseline (changed in place, +`prepare_rewrite` по-прежнему распознаёт непроверенный baseline (changed in place, shorter, missing, journal exceeds bound), считает текущий durably-синхронизированный -native авторитетным, синхронизирует его и удаляет неприменимый журнал. После этого -MAM history снова читается, а новые append создадут свежий redo поверх нового -baseline. Сверку совместимости native-формата см. в исходниках Prosody +native авторитетным, синхронизирует его и удаляет неприменимый журнал. После +MAM history читается, а новые append создадут свежий redo поверх нового baseline. +Сверку совместимости native-формата см. в исходниках Prosody (`util/datamanager.lua` `list_append`/`list_store`, `plugins/mod_storage_internal.lua` `archive:append`): Prosody пишет `item(...)` и при полной перезаписи применяет atomic rename — что и меняет первые bytes baseline, оставляя Rust-журнал устаревшим. diff --git a/tests/archive_redo.rs b/tests/archive_redo.rs index a924e26..16b950a 100644 --- a/tests/archive_redo.rs +++ b/tests/archive_redo.rs @@ -158,11 +158,15 @@ fn byte_limit_rotates_before_frame_count_limit() { } #[test] -fn explicit_native_atomic_handoff_and_missing_baseline_fail_closed() { +fn explicit_native_atomic_handoff_and_missing_baseline_never_drops_live_access() { let t = tempfile::tempdir().unwrap(); append(t.path(), "old"); let p = path(t.path()); archive::checkpoint_all(t.path()).unwrap(); + // An external writer replaces the native file in place (Prosody `list_store` + // rewrite), invalidating the committed redo baseline. On first access the + // untrusted journal is retired and the synced native is adopted as + // authoritative — exactly what Prosody sees (it has no redo sidecar). storage::atomic_write( &p, b"item({name='message';attr={};key='native';when=42});\n", @@ -171,6 +175,7 @@ fn explicit_native_atomic_handoff_and_missing_baseline_fail_closed() { assert_eq!(ids(t.path()), ["native"]); append(t.path(), "rust"); assert_eq!(ids(t.path()), ["native", "rust"]); + // A dirty, uncommitted native suffix is never surfaced. fs::OpenOptions::new() .append(true) .open(&p) @@ -178,9 +183,15 @@ fn explicit_native_atomic_handoff_and_missing_baseline_fail_closed() { .write_all(b"item(bad uncommitted);") .unwrap(); assert_eq!(ids(t.path()), ["native", "rust"]); - // The durable baseline cannot be reconstructed from suffix-only frames. + // Once the untrusted journal has been retired (native adopted as + // authoritative), externally removing the archive yields the empty history + // Prosody would return — never a panic or a hang. fs::remove_file(&p).unwrap(); - assert!(archive::load(t.path(), "example.org", "alice").is_err()); + assert!( + archive::load(t.path(), "example.org", "alice") + .unwrap() + .is_empty() + ); } #[test]