Files
simplexmq/plans/20260401_01_batch_queue_associations.md
f0b7a4be73 messaging services (#1667)
* smp server: messaging services (#1565)

* smp server: refactor message delivery to always respond SOK to subscriptions

* refactor ntf subscribe

* cancel subscription thread and reduce service subscription count when queue is deleted

* subscribe rcv service, deliver sent messages to subscribed service

* subscribe rcv service to messages (TODO delivery on subscription)

* WIP

* efficient initial delivery of messages to subscribed service

* test: delivery to client with service certificate

* test: upgrade/downgrade to/from service subscriptions

* remove service association from agent API, add per-user flag to use the service

* agent client (WIP)

* service certificates in the client

* rfc about drift detection, and SALL to mark end of message delivery

* fix test

* fix test

* add function for postgresql message storage

* update migration

* servers: maintain xor-hash of all associated queue IDs in PostgreSQL (#1668)

* servers: maintain xor-hash of all associated queue IDs in PostgreSQL (#1615)

* ntf server: maintain xor-hash of all associated queue IDs via PostgreSQL triggers

* smp server: xor hash with triggers

* fix sql and using pgcrypto extension in tests

* track counts and hashes in smp/ntf servers via triggers, smp server stats for service subscription, update SMP protocol to pass expected count and hash in SSUB/NSSUB commands

* agent migrations with functions/triggers

* remove agent triggers

* try tracking service subs in the agent (WIP, does not compile)

* Revert "try tracking service subs in the agent (WIP, does not compile)"

This reverts commit 59e908100d.

* comment

* agent database triggers

* service subscriptions in the client

* test / fix client services

* update schema

* fix postgres migration

* update schema

* move schema test to the end

* use static function with SQLite to avoid dynamic wrapper

* agent: fail when per-connection transport isolation is used with services (#1670)

* agent: service subscription events (#1671)

* agent: use server keyhash when loading service record

* agent: process queue/service associations with delayed subscription results

* agent: service subscription events

* agent: finalize initial service subscriptions, remove associations on service ID changes (#1672)

* agent: remove service/queue associations when service ID changes

* agent: check that service ID in NEW response matches session ID in transport session

* agent subscription WIP

* test

* comment

* enable tests

* update queries

* agent: option to add SQLite aggregates to DB connection  (#1673)

* agent: add build_relations_vector function to sqlite

* update aggregate

* use static aggregate

* remove relations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* add test, treat BAD_SERVICE as temp error, only remove queue associations on service errors

* add packZipWith for backward compatibility with GHC 8.10.7

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* servers: service stats and logging, allow services without option (removed), report errors during service message delivery, remove threads when service subscription ended (#1676)

* smp server: always allow services without option

* smp server: maintain IDs hash in session subscription states

* smp server: service message delivery error handling

* ntf server: log subscription count and hash differences

* smp server: remove delivery threads when service subscription ended/client disconnected

* agent: remove service queue association when service ID changed, process ENDS event, test migrating to/from service (#1677)

* agent: remove service queue association when service ID changed

* agent: process ENDS event

* agent: send service subscription error event

* agent: test migrating to/from service subscriptions, fixes

* agent: always remove service when disabled, fix service subscriptions

* ntf server: use different client certs for each SMP server, remove support for store log (#1681)

* ntf server: remove support for store log

* ntf server: use different client certificates for each SMP server

* smp protocol: fix encoding for SOKS/ENDS responses (#1683)

* agent: create user with option to enable client service (#1684)

* agent: create user with option to enable client service

* handle HTTP2 errors

* do not catch async exceptions

* agent: minor fixes

* docs: update protocol (#1705)

* docs: agent threat model

* update protocol docs

* update RFCs (#1730)

* update RFCs

* update

* update overview

* update terminology

* original language in threat model

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* docs: fix minor issues in protocols

* docs: add e2e encrypted message wire encoding to PQDR spec

* docs: add missing encodings and other protocol corrections

* docs: move implemented rfcs

* smp: service fixes (#1737)

* smp: deliver service subscription to correct client

* tests: more resilient to concurrency

* optimize PostgreSQL query

* fix service re-association after server "downgrade"

* correctly handle service removed from server (and ID changed)

* remove unused

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* prometheus: fix metrics names (#1747)

* test: rcv service re-association on restart (#1746)

* agent: correct log message

* docs: update whitepaper

* smp: fix messaging client service issues (#1751)

* services: fix minor issues

* fix accounting for subscribed service queues, add prometheus stats

* fix uncorrelated subquery

* fix potential race condition when inserting service defensively, as it is also prevented by how client is created

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* agent: refactor cleanup if no pending subs (#1757)

* smp server: batch processing of subscription messages (#1753)

* smp server: batch processing of subscription messages

* refactor

* empty line

* fix

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* smp: batch queue association updates on subscriptions (#1760)

* smp: batch queue association updates on subscriptions

* refactor to fused batching

* simpler

* batch assoc functions

* clean up

* fix

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* agent: use primary key index in setRcvServiceAssocs (#1783)

* agent: use primary key index in setRcvServiceAssocs

Previous WHERE rcv_id = ? did not match the (host, port, rcv_id)
primary key prefix and fell back to a table scan via
idx_rcv_queues_client_notice_id. With ~390k rows per queue, each
update in a 1350-row batch scanned the whole table, yielding ~290s
per batch and a multi-hour rcv-services migration.

* agent: pass SMPServer explicitly to setRcvServiceAssocs

Avoid extracting host/port from the first queue inside setRcvServiceAssocs.
The caller already has SMPServer in scope (from tSess) and the call chain
is short, so threading it through is simpler than inspecting the list.
Removes the empty-list guard from setRcvServiceAssocs (it remains in
processRcvServiceAssocs).

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
2026-05-21 14:14:03 +01:00

5.8 KiB

Server: batch queue service associations

When a batch of SUB or NSUB commands arrives from a service client, each command that needs a new or removed service association calls setQueueService individually - one DB write per command. For 135 commands per batch, that's 135 individual UPDATE msg_queues queries.

Goal

Reduce to at most 2 DB queries per batch (one for rcv associations, one for ntf associations), using UPDATE ... RETURNING recipient_id to identify which queues were actually updated.

Also fuse message pre-fetch and association batching into a single batch preparation step with a clean contract.

Contract

prepareBatch :: Maybe ServiceId -> NonEmpty (VerifiedTransmission s) -> M s (Either ErrorType (Map RecipientId (Maybe Message, Maybe (Either ErrorType ()))))

Left e = batch-level failure (message pre-fetch or association query failed entirely). All SUBs/NSUBs in the batch get this error.

Right map = per-queue results as a tuple:

  • Maybe Message - pre-fetched message for SUB queues, Nothing for NSUB or no message
  • Maybe (Either ErrorType ()) - association result. Nothing = no update needed. Just (Right ()) = update succeeded. Just (Left e) = update failed for this queue.

One map, one lookup per queue. processCommand passes both values to subscribeQueueAndDeliver / subscribeNotifications -> sharedSubscribeQueue.

Queues not in the map (non-SUB/NSUB commands, failed verification) are not affected.

prepareBatch implementation

One accumulating fold over the batch, collecting three lists:

  • subMsgQs :: [StoreQueue s] - SUB queues for message pre-fetch
  • rcvAssocQs :: [StoreQueue s] - SUB queues needing rcv_service_id update (clntServiceId /= rcvServiceId qr)
  • ntfAssocQs :: [StoreQueue s] - NSUB queues needing ntf_service_id update (clntServiceId /= ntfServiceId from NtfCreds)

Classification reads from the already-loaded QueueRec in VerifiedTransmission - no extra DB query.

Then three store calls (each skipped if its list is empty):

  1. tryPeekMsgs ms subMsgQs -> Map RecipientId Message
  2. setRcvQueueServices (queueStore ms) clntServiceId rcvAssocQs -> Set RecipientId
  3. setNtfQueueServices (queueStore ms) clntServiceId ntfAssocQs -> Set RecipientId

Then one pass to merge results into Map RecipientId (Maybe Message, Maybe (Either ErrorType ())):

  • For each SUB queue: (M.lookup rId msgMap, assocResult rId rcvUpdated rcvAssocQs)
  • For each NSUB queue: (Nothing, assocResult rId ntfUpdated ntfAssocQs)

Where assocResult rId updated assocQs = if the queue was in assocQs (needed update), then Just (Right ()) if rId is in updated, else Just (Left AUTH). If not in assocQs (no update needed), Nothing.

If any of the three calls fails entirely, return Left e.

Store interface

Replace the polymorphic setQueueServices with two plain functions in QueueStoreClass:

setRcvQueueServices :: s -> Maybe ServiceId -> [q] -> IO (Set RecipientId)
setNtfQueueServices :: s -> Maybe ServiceId -> [q] -> IO (Set RecipientId)

No SParty p polymorphism. Each function knows its column.

Postgres implementation

setRcvQueueServices:

UPDATE msg_queues SET rcv_service_id = ?
WHERE recipient_id IN ? AND deleted_at IS NULL
RETURNING recipient_id

setNtfQueueServices:

UPDATE msg_queues SET ntf_service_id = ?
WHERE recipient_id IN ? AND notifier_id IS NOT NULL AND deleted_at IS NULL
RETURNING recipient_id

After each batch query, for each queue in the returned set:

  1. Read QueueRec TVar, update with new serviceId
  2. Write store log entry

STM implementation

Loop over queues, call existing per-item logic, collect succeeded RecipientIds into a Set.

Downstream changes in Server.hs

processCommand

Gains one parameter: Map RecipientId (Maybe Message, Maybe (Either ErrorType ())).

SUB case: M.lookup entId prepared gives Just (msg_, assocResult) or Nothing. Pass both to subscribeQueueAndDeliver.

NSUB case: M.lookup entId prepared gives Just (Nothing, assocResult) or Nothing. Pass assocResult to subscribeNotifications.

Forwarded commands: pass M.empty.

subscribeQueueAndDeliver

Takes Maybe Message and Maybe (Either ErrorType ()) as before. No change in how it uses them.

sharedSubscribeQueue

Takes Maybe (Either ErrorType ()). On paths needing association update:

  • Just (Left e) -> return error
  • Just (Right ()) -> skip setQueueService, proceed with STM work
  • Nothing -> no update needed, proceed with existing logic

Implementation order (top-down)

  1. Define the prepareBatch contract and thread one map through processCommand -> subscribeQueueAndDeliver / subscribeNotifications -> sharedSubscribeQueue (Server.hs)
  2. Implement prepareBatch with the fold, three calls, and merge (Server.hs)
  3. Add setRcvQueueServices and setNtfQueueServices to QueueStoreClass (Types.hs)
  4. Implement for Postgres with batch UPDATE ... RETURNING (Postgres.hs)
  5. Implement for STM as loop (STM.hs)
  6. Implement for Journal as delegation (Journal.hs)

At step 2, store functions can initially be stubs returning empty sets. Steps 3-6 fill in the real implementations.

Files changed

File Change
src/Simplex/Messaging/Server.hs prepareBatch with fold + merge; one map parameter through processCommand -> subscribeQueueAndDeliver / subscribeNotifications -> sharedSubscribeQueue
src/Simplex/Messaging/Server/QueueStore/Types.hs Add setRcvQueueServices, setNtfQueueServices to QueueStoreClass
src/Simplex/Messaging/Server/QueueStore/Postgres.hs Implement with batch UPDATE ... RETURNING + per-item TVar/log updates
src/Simplex/Messaging/Server/QueueStore/STM.hs Implement as loop
src/Simplex/Messaging/Server/MsgStore/Journal.hs Delegate to underlying store