feat(jsonrpc2): add manual receive mode to prevent message loss - #835
feat(jsonrpc2): add manual receive mode to prevent message loss#835strichter wants to merge 2 commits into
Conversation
In the default 'on-start' receive mode, signal-cli's daemon
auto-pulls inbound messages and pushes them to its JSON-RPC clients
the moment they arrive from the Signal servers. If no websocket
subscriber is currently attached to /v1/receive/{number} — for
example during a brief subscriber redeploy — the receiver loop in
signal-cli-rest-api drops the message via a non-blocking channel
send (see the 'no receiver' debug log in ReceiveData). signal-cli
has already acknowledged the message to Signal, so it is never
redelivered.
Add support for signal-cli's --receive-mode=manual, opt-in via the
new JSON_RPC_RECEIVE_MODE=manual environment variable. In manual
mode signal-cli does NOT auto-receive; signal-cli-rest-api now
issues subscribeReceive when a websocket subscriber attaches and
unsubscribeReceive when the last subscriber detaches. While no
subscriber is attached, signal-cli does not pull messages from the
Signal servers, so they remain buffered server-side under Signal's
normal retention rules and are delivered to the next subscriber
that attaches.
Implementation:
* jsonrpc2-helper.go: thread JSON_RPC_RECEIVE_MODE through to the
daemon launch command line.
* jsonrpc2.go: add per-account subscription tracking with
refcounting; subscribeReceive/unsubscribeReceive helpers; an
unwrap step in ReceiveData so manual-mode notifications
({subscription,result}) and auto-mode notifications (envelope
directly) reach downstream consumers in the same shape.
* client.go, api.go: thread the account number through
GetReceiveChannel so the JsonRpc2Client can attach the right
subscription.
The change is fully backward compatible: when JSON_RPC_RECEIVE_MODE
is unset or set to 'on-start', signal-cli runs in auto mode and the
new subscribeReceive code path is bypassed (account is empty, no
RPC issued).
Closes bbernhard#255 (the same root cause: no subscriber → 'no receiver'
drop).
| } | ||
|
|
||
| if account != "" { | ||
| if err := r.acquireReceiveSubscription(account); err != nil { |
There was a problem hiding this comment.
I think I don't understand that code part here. Isn't account always non-empty? So, I guess we would end up always in this code branch, no matter which mode is selected via JSON_RPC_RECEIVE_MODE.
There was a problem hiding this comment.
Looking further into the code, I'd say that even if gin, for any reason, calls this method with an empty string, this method shouldn't be responsible for deciding whether the value of account is a valid account. If signal-cli does not return an error for its subscribeReceive call, then account is a valid account.
So if @strichter has no objections and there are no other remarks from @bbernhard, I’d push a fix to this comment to get the PR merged and released.
There was a problem hiding this comment.
@strichter, I've created a PR to your fork just so that the code you've implemented stays as your PR (otherwise I'll need to push a commit into my fork and create a new PR to the bbernard's repo, which will look like I've implemented the actual receive-mode fix).
Fixes #834. Same root cause as the long-standing #255 ("Couldn't send message to golang channel, as there's no receiver").
Summary
In `MODE=json-rpc` with default `--receive-mode=on-start`, signal-cli auto-pulls inbound messages and pushes them as JSON-RPC notifications regardless of whether any websocket subscriber is currently attached. If no subscriber is reading the channel at the moment a notification arrives, the receiver loop in `src/client/jsonrpc2.go` drops the message via a non-blocking channel send and only logs a debug line. signal-cli has already acked the message to Signal at that point, so it is never redelivered.
This PR adds opt-in support for signal-cli's `--receive-mode=manual`, where the daemon does NOT auto-receive and clients must explicitly call `subscribeReceive` to start receiving. While no subscription is active, messages stay queued server-side under Signal's normal retention rules.
See #834 for the full reproduction, root cause analysis, and validation results.
Changes
Backward compatibility
Fully backward compatible:
Validation
Local validation against a real Signal account, real signal-cli-rest-api container in `MODE=json-rpc` mode, and a real bot consumer (nora-ng-2). Test scenario: stop the websocket client, send 10 messages from another Signal account into the group during a 60-second outage, restart the client, count delivered messages.
The signal-api log trace clearly shows the new lifecycle:
```
20:22:25 Subscribed to receive notifications for account +1XXXXXXXXXX (subscription=0)
20:22:47 Unsubscribed from receive notifications for account +1XXXXXXXXXX (subscription=0)
20:28:32 Subscribed to receive notifications for account +1XXXXXXXXXX (subscription=1)
```
Between unsubscribe (20:22:47) and resubscribe (20:28:32), 10 messages were sent into the group. After resubscribe, all 10 were delivered to the websocket client within ~10 seconds.
Notes for review
Open questions