Recipient subscription stuck in pendingSubscriptions after unclean shutdown until unrelated traffic arrives
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- haskell
- Lĩnh vực
- distributed-systems, networking
Hướng nghiên cứu
Start with the agent subscription handling exposed by /get subs details, then trace the acknowledgment path after newChatItems during an unclean shutdown. Reproduce with the listed chatMigrateInit/_start and force-stop sequence; done means the same queue returns from pendingSubscriptions to activeSubscriptions and clears the queued message without unrelated traffic.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Environment
- Server (smp-server): Docker image
simplexchat/smp-server:v6.5.0, built from simplexmq
commitba6af65(tagv6.5.0, 2026-04-29). - Client (simplex-chat core library):
v6.5.6, extracted from the signed
simplex-aarch64.apkrelease (arm64-v8alibsimplex.so/libsupport.so). This version of
simplex-chat pins its own simplexmq dependency at commit92598c2viacabal.project
(stablebranch as of 2026-06-21, not itself a tagged release). - Version skew, disclosed because it may be relevant to the bug: the client's simplexmq
dependency (92598c2, 2026-06-21) is 16 commits and about two months newer than the server's
(ba6af65, 2026-04-29). Reviewed the titles of those 16 commits — none reference subscription
or agent-connection handling by name, but this was a commit-message review, not a full diff
audit, so a protocol/agent-state mismatch between server and client versions is not ruled out
as a contributing factor. - Client harness: a minimal third-party Android app driving the core directly via C FFI.
Verified against thev6.5.6source (apps/multiplatform/common/src/commonMain/cpp/android/ simplex-api.c) that the four functions used —chatMigrateInit,chatSendCmdRetry,
chatRecvMsgWait,chatCloseStore— are exactly the same functions and signatures the
official app itself calls (only the JNI package qualifier differs, since this harness uses its
own package rather thanchat.simplex.common.platform). Same command/response protocol
throughout (bots/api/COMMANDS.md,bots/api/TYPES.md), not a different or reduced API surface. - Server config: self-hosted, default
[INACTIVE_CLIENTS](ttl=21600,
check_interval=3600),store_messages = memory,restore_messages = on,store_queues = memory,expire_messages_days = 21. Server and client on the same local network (Android
emulator via the10.0.2.2host alias).
Summary
If a client process is killed uncleanly (no chatCloseStore() call) immediately after receiving
a message, the SMP acknowledgment for that message is never sent. On every subsequent clean
start, the client's subscription to that specific queue is stuck in pendingSubscriptions (per
/get subs details) and never transitions to activeSubscriptions on its own — confirmed idle
for at least 60 minutes with no self-resolution. The subscription only completes when unrelated
new traffic arrives at the same queue (confirmed to be the same queue throughout, by watching a
fixed rcvId transition live — not a distinct queue picking up traffic while an abandoned one
leaks separately), at which point it activates and delivers the entire backlog at once, including
the message from the original unclean shutdown. The same scenario with a clean shutdown
instead of an unclean kill does not reproduce the bug — the subscription completes normally within
seconds either way.
Severity/triage note: in every reproduction here, the message that triggers the stuck state
was already delivered to and durably stored in the recipient's local database before the process
was killed, so it is not lost or invisible to the recipient — the confirmed impact is a
relay-side bookkeeping issue (an indefinitely unacknowledged message occupying a queue slot,
bounded by the 128-message-per-queue quota and 21-day message expiry), not confirmed data loss.
A more severe variant — a kill landing during receipt, before the local database write commits,
which would leave the recipient genuinely unaware a message was even sent — was tested for
statistically (80 randomized-kill cycles) and not observed; see "What we tested" below for why
that's a bound and not a proof.
This is not an edge case for a mobile client: Android routinely kills app processes (low memory,
force-stop, backgrounding) without running any graceful-shutdown callback, so "unclean kill right
after receiving a message" is a normal operating condition, not a rare failure.
Reproduction steps
- Two users,
aliceandbob, each with their ownChatCtrl(chatMigrateInit+
/_create user ...+/_start), both configured (/_servers) against the same self-hosted
SMP relay. - Establish a contact connection between them (
/_connectinvite from alice,/_connect plan/_connectaccept from bob) and confirm both sides reachcontactConnected.
- Stop bob's process cleanly (
chatCloseStore+ process exit — connection itself is healthy at
this point). - With bob's process not running, alice sends a text message (
/_send @<bobContactId> ...).
Confirmed server-side: the message is queued (relay log on restart:
messages stored: 1, expired: 0, queues: 2). - Start bob's process fresh (
chatMigrateIniton the same existing on-disk database +
/_start). The message is delivered normally — anewChatItemsevent fires with the correct
content, i.e. it is now durably stored in bob's local database. - Do not call
chatCloseStore(). Kill bob's process immediately after step 5's
newChatItemsevent (in our repro:adb shell am force-stop, i.e.SIGKILL-equivalent, no
graceful shutdown hook runs). - Restart the relay and check its log: the message is still present
(messages stored: 1, expired: 0, queues: 2) — confirms the acknowledgment was never sent
before the kill in step 6. - Start bob's process again (same on-disk database,
chatMigrateInit+/_start). The message
is not redelivered, and/get subs detailsshows the queue'srcvIdpermanently in
pendingSubscriptions, neveractiveSubscriptions:
This persists across repeated fresh-process attempts, and for at least 60 minutes of idle time{"result":{"type":"agentSubsDetails","agentSubs":{ "activeSubscriptions":[], "pendingSubscriptions":[{"userId":1,"server":"smp://<fp>@<host>","rcvId":"XsuyWw_UHI0YCDYvDZih1nRY3ub9m6pW"}], "removedSubscriptions":[] }}}
with zero client or network activity in between. - Sending a new, unrelated message from alice to the same contact resolves it: the same
rcvIdtransitions toactiveSubscriptions, and both the new message and the original
stuck one are delivered together. Confirmed server-side too — the relay's message count drops
to 0 at that point.
What we tested
Ruled out: INACTIVE_CLIENTS server-side timeout. Hypothesized the server might be holding
the queue "claimed" by bob's dead TCP session until its own inactivity timeout expired. Tested
directly: reproduced the same scenario against a separate relay instance with [INACTIVE_CLIENTS]
ttl=8 / check_interval=5 (vs. the default ttl=21600 / check_interval=3600), confirmed via
the relay's own startup log that the faster settings were active (expiring clients inactive for 8 seconds every 5 seconds), and waited well past several sweep cycles (20+ seconds, then again
90+ seconds). The message stayed stuck on that relay too. This rules out the inactive-client
sweep as the mechanism.
No self-resolution after 60 minutes idle. Left the stuck state completely untouched (no
traffic, no client activity of any kind) for a full hour, then reopened bob. Identical result to
the shorter tests (events_on_reopen=0) — not a delay that eventually resolves on its own.
Specific to unclean shutdown. Reproduced the same scenario but with a genuinely clean
chatCloseStore() before bob goes offline (instead of an unclean kill), then sent a new message
while he's closed. The reopened subscription shows the same brief pendingSubscriptions state
immediately after /_start (normal startup lag), then transitions to activeSubscriptions on
its own within the observation window and delivers normally. A clean shutdown does not reproduce
the stuck-forever state.
Same queue confirmed via live tracking, not inferred. With the subscription already stuck,
watched /get subs details repeatedly across a send of a new message from alice: the same
rcvId was in pendingSubscriptions immediately before and after the send, then present in
activeSubscriptions at the exact snapshot where the new message's newChatItems event fired.
The relay's message count went to 0 at that same point (both messages delivered and acked
together).
Recovery commands tested — neither is a reliable fix. /_resubscribe all
(ResubscribeAllConnections) flips the reported status from pending to active but does not
flush the backlogged message — relay still reports it queued afterward, no delivery within a
20-second window. /reconnect (ReconnectAllServers) had no effect at all — no status change,
no delivery.
Fault-injection test: statistical bound on the more severe "kill mid-receive" variant. The
window between the server delivering a message and the agent's local database write committing
is inside the Haskell core and not deterministically injectable from the client side, so this was
tested statistically: sent a numbered 100-message sequence, then ran 80 cycles of starting bob's
process, letting it receive for a randomized delay (0.5–6 seconds), and externally killing it
(adb shell am force-stop) at that point — never calling chatCloseStore. Aggregated every
message received across all 80 cycles and compared against the sent sequence.
Result, stated precisely: zero gaps and zero duplicates across 80 kill cycles, at a sequence
length (100) kept safely under the relay's 128-message-per-queue quota, after correcting an
initial harness artifact. That artifact: the first attempt used 200 messages sent in one burst
before bob ever started receiving, which silently exceeded the 128-message quota — messages
130–200 were rejected at send time and never entered the queue at all. This initially looked like
a 71-message gap and would have been a dramatic (and wrong) result; caught by checking the
relay's own message count rather than trusting the client-side tally alone, then corrected by
re-running under-quota. Noted here deliberately, since anyone else attempting to reproduce at a
higher message count should watch for the same failure mode.
This is a statistical bound, not a debugger-verified proof: 80 external, randomly-timed kills
against a fast local relay is a real result, not a single clean pass, but it does not establish
that the exact byte-level window between the server sending a message and the local database
write committing is unreachable or safe. A negative result here means "not observed in 80
trials," not "impossible."
Impact
- Not head-of-line blocking. A new message sent while an old one sits stuck is delivered to
the recipient normally and promptly — confirmed both client-side and via the relay's message
count (leaked message stays put, new message's slot clears independently). - Confirmed impact is relay-side, not recipient-visible, in every scenario reproduced here: a
queue slot occupied indefinitely by an unacknowledged message, bounded in practice by the
128-message quota (defaultMsgQueueQuotainsimplexmq, a compile-time constant not
configurable viasmp-server.ini) and 21-day message expiry. - Asymmetric failure mode when it does surface: a sender whose send hits a full queue gets an
immediate, visibleQUOTAerror; a recipient with a stuck-but-already-delivered message has no
client-visible indication their queue has an outstanding ack, and neither side can clear it
from the client (see recovery commands above). - The more severe variant — genuine recipient-side message loss from a kill during the local
database write — remains unconfirmed. 80 fault-injection cycles targeting this found no gaps
or duplicates, a meaningful bound, not proof it cannot happen.
Prior art
Searched simplex-chat/simplexmq and simplex-chat/simplex-chat issues (open and closed) and
GitHub Discussions across a wide range of phrasings (pending subscription, resubscribe,
ResubscribeAllConnections, chatCloseStore, unclean shutdown, force stop, messages stuck, queue quota, Android-lifecycle-specific terms, and more) — no existing report of this
behavior found. One issue reviewed in detail and ruled out as a different mechanism:
simplex-chat/simplex-chat#6487
("Messages not received on groups") describes group membership/read-receipt propagation
reliability across many members, not a single 1:1 queue's subscription getting stuck after one
client's unclean shutdown.
Not covered by this report
- Exact root cause of why the subscription doesn't self-complete on a bare reopen is not
established — this report describes reproducible behavior and what's been ruled out
(INACTIVE_CLIENTS, delayed self-resolution, clean-shutdown reproduction), not a diagnosed
cause in the agent's subscription-handling code. - The precise byte-level kill-mid-commit window was not deterministically reproduced (not
possible from outside the Haskell core with the tools available here) — the fault-injection
test above is a statistical bound over 80 trials, not an exhaustive test or a debugger-verified
negative result.
- Ngôn ngữ chính
- Haskell
- Star
- 852
- Fork
- 112
- Merge trung bình
- 2 ngày 5 giờ
- Pull request đã merge (30 ngày)
- 37
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của simplex-chat/simplexmq
-
xftp CLI: Generated web link is missing the /file pathCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
simplex-chat/simplexmq#1853 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Setting a password on smp-server with an '@' character breaks the Docker entrypoint scriptCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
simplex-chat/simplexmq#1781 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 30/100
simplex-chat/simplexmq#1864 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 20/100
simplex-chat/simplexmq#1857 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 35/100
simplex-chat/simplexmq#1855 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của simplex-chat/simplexmq
Issue tương tự
-
An empty line splits a signature where an ordinary comment is right above an argument's HaddockĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
mrkkrp/tilia#213 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
dhall-lang/dhall-haskell#2870 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
alunduil/collection-json.hs#391 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
plutus
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
IntersectMBO/cardano-ledger#6152 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
fmidue/prolog-programming-task#55 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày