Версия 0.8.3 удерживала глобальный Store.slots mutex на весь durable
Test, build and publish / All tests and Linux binary (push) Failing after 19s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped

commit
(включая fsync/append). fsync одного канала задерживал SM-коммиты всех
остальных
сессий; при медленном диске и нескольких пользователях это выглядело как
«сервер
перестал отвечать на запросы» и секундная доставка сообщений.

- Глобальная блокировка разделена: короткий lock `slots` теперь только
  валидирует token/limits и резервирует слот; сам append+fsync идёт под
  per-session `wal` lock. Один медленный канал больше не блокирует
  остальных.
- Ротация/compaction/GC сериализуются отдельным `maintenance` lock вне
  любого
  per-session lock — очереди append и compaction для одного WAL не
  гонки,
  дедлоков нет, инварианты durable и crash-тесты сохранены.
- Все существующие crash/latency/recovery regressions зелёные без
  изменений.

Для genuinely медленного диска (VPS с медленным fsync) добавлена опция
как в
Prosody, где исходящие SM-stanza живого клиента идут в in-memory smqueue
без
fsync, а durable пишутся счётчик/h и переход в hibernation.

- `smacks_online_durable = false` (strict — default): live-сессии не
  fsync на
  каждый outbound/ack, а помечают `dirty`; фоновый проход
  `smacks_flush_interval` (default1с) и offline-переход фиксируют
  durable.
- Offline/hibernating цели в любом режиме фиксируются durable ДО
  доставки —
  сохраняются критичные инварианты («Alice отправила, Bob ещё не
  авторизован»)
  и crash-регрессии; отзыв токенов/expiry остаётся durable.
- Pre-ACK durable гарантию даёт строгий режим; deferred — компромисс
  Prosody
  «низкая задержка» в обмен на ограниченное окно потери для
  online-пиров.

- `cargo test --locked --release`: 54 binary, 336 passed, 11 ignored (0
  fail).
- Strict crash (`slow_storage_crash_network`), latency
  (`slow_storage_latency`),
  `sm_restart`, `journal_recovery_sync`, личная доставка — все зелёные.
- Новые регрессии: `tests/deferred_sm_online_network.py` (wire, deferred
  режим:
  offline durable до ACK, online доставка + flush persist), два Rust
  unit-теста
  в `tests/sm_journal.rs` (deferred online commit/flush, offline
  durable).
- `push_latency_network.py` в этой среде падает на TLS CA `keyUsage`
  из-за более
  строгого TLS в Python 3.14 (до SM-кода, не связано с этим изменением).

Документация: [docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md),
[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
This commit is contained in:
wt committed 2026-10-21 14:43:59 +07:00
1 parent bff609097b
commit 821e1c3e2f
11 files changed
+656 -88

No files matched your search

+20
View File
@@ -892,6 +892,12 @@ pub async fn serve(path: &Path) -> Result<()> {
} else {
Hub::default()
};
// Streaming durability policy. Defaults to the strict commit-before-ACK
// model; set `smacks_online_durable = false` to defer per-stanza fsync for
// live sessions (Prosody's streaming behavior), flushing on offline
// transitions and a periodic background pass. Offline/hibernating targets
// always commit durably before delivery regardless of this flag.
hub.set_defer_online(!boolean(&c, &domain, "smacks_online_durable", true));
// A persisted, unexpired SM session is still a logical room occupant while
// its transport is offline. Restore routing before accepting any clients so
// room traffic queues durably even before that occupant reauthenticates.
@@ -1242,6 +1248,9 @@ pub async fn serve(path: &Path) -> Result<()> {
let heartbeat_seconds = (number(&c, &domain, "smacks_hibernation_time", 300) / 4).clamp(1, 10);
let mut heartbeat = tokio::time::interval(Duration::from_secs(heartbeat_seconds));
let mut heartbeat_task: Option<tokio::task::JoinHandle<()>> = None;
let flush_seconds = number(&c, &domain, "smacks_flush_interval", 1).clamp(1, 3600);
let mut flush = tokio::time::interval(Duration::from_secs(flush_seconds));
let mut flush_task: Option<tokio::task::JoinHandle<()>> = None;
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
let mut hangup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())?;
loop {
@@ -1277,6 +1286,17 @@ pub async fn serve(path: &Path) -> Result<()> {
}));
}
},
_ = flush.tick() => {
if flush_task.as_ref().is_none_or(|task| task.is_finished()) {
let rt = runtime.clone();
flush_task = Some(tokio::task::spawn_blocking(move || {
if let Err(_error) = rt.hub.flush_dirty() {
rt.logger.event(Level::Error, "smacks", None,
"deferred-flush-failed", &[("reason", "storage-error")]);
}
}));
}
},
_ = tick.tick() => {
for jid in runtime.hub.expired() {
logger.event(Level::Info, "smacks", None, "session-expired", &[("jid", &jid)]);