fix(messaging): deliver Appwrite push topic campaigns on the topic id by ArnabChatterjee20k · Pull Request #14580 · appwrite/appwrite · GitHub
Skip to content

fix(messaging): deliver Appwrite push topic campaigns on the topic id - #14580

Merged
ArnabChatterjee20k merged 8 commits into
mainfrom
fix/mqtt-topic-send-by-topic-id
Oct 16, 2026
Merged

ArnabChatterjee20k merged 8 commits into
mainfrom
fix/mqtt-topic-send-by-topic-id

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Includes #14586, merged into this branch: targetless users reach users/<userId> through the enabled Appwrite provider whatever the default, and message.providers lists only providers a send used. See #14586 for its description and tests.

What does this PR do?

Topic campaigns sent through the Appwrite push (MQTT) provider were published on each subscribed target's identifier instead of the topic ID.

streamRecipients keyed topic pages by target identifier and dropped the topic ID. Topic sends skip the per-user branch, so recipients = array_keys($identifiers). The Appwrite adapter then treats each to as a topic ID (Push/Appwrite.php persist() → getDocument('topics', $to)). The result:

  • Delivery only worked when the target was created with identifier = <topicId>, which is what the e2e fixture did.
  • Any other identifier, such as ID.unique(), failed with Topic <identifier> does not exist and counted as a failed delivery.
  • A user in N topics needed N Appwrite targets.

Fix

  • streamRecipients now yields each page with its topic ID (null for user, target or email addressed pages) instead of a perUser bool.
  • A topic page on an Appwrite push provider sends to [$topicId]. The subscribed Appwrite targets only mark that the topic has MQTT subscribers.
  • Each topic is published once per message, across pages and across multiple Appwrite providers.

What each recipient source yields

$perUser is now $topicId === null. Only topic pages carry an ID:

Recipient source in streamRecipients Yields $perUser
topics: [...]: subscriber pages for each topic [$page, $topic->getId()] false
users: [...]: those users' push targets [$page, null] true
users: [...] with no live target: fallback onto users/<id> (only when the default provider is Appwrite) [[$default->getId() => $identifiers], null] true
targets: [...]: explicitly named targets [$page, null] true
Raw emails (email messages only) [[$default->getId() => ...], null] true

For an Appwrite provider page:

  • $perUser true: sends to users/<userId>, unchanged.
  • $perUser false: sends to [$topicId] (new).

FCM and APNs pages still send to device tokens. A mixed send such as topics: ['news'], users: ['U'] routes each page by its own value.

One Appwrite target with any identifier now covers every topic it is subscribed to. Targets that already use identifier = topicId keep working.

Test Plan

E2E in tests/e2e/Services/Mqtt/MqttServerTest.php:

  • The shared setupPushTopic fixture now uses a placeholder identifier, so every existing campaign, replay and QoS test runs against the fixed routing.
  • testCampaignReachesEveryTopicOfOneTarget: one target subscribed to two topics gets the message exactly once on each topic ID, and the message ends sent.
  • testCampaignPublishesTopicOncePerMessage: two users on two different Appwrite providers each get the message exactly once.
  • testCampaignIsNotDeliveredOnTargetIdentifier: subscribing to the target identifier is refused, and the only delivery is on the topic ID.

.github/ci/e2e.json: Workers/Messaging.php and Messaging/Adapter/Push/ now also trigger the Mqtt lane.

Locally I ran php -l, Pint and PHPStan on the changed files. PHPStan reports no new errors; the only ones are the Utopia\Database\Id not-found errors that main also shows because the local vendor is stale. E2E was not run locally; the CI e2e lanes ran it.

CI

The earlier rust-toolchain repin is superseded by #14577 on main. The merge from main takes its v1 pin, so this PR no longer changes that workflow.

Related PRs and Issues

🤖 Generated with Claude Code

A topic campaign sent through the Appwrite push provider was published on
each subscribed target's identifier rather than the topic id, so it only
reached devices when the target identifier happened to equal the topic id.
Any other identifier failed with "Topic <identifier> does not exist".

Carry the topic id through the recipient stream and publish an Appwrite
topic page on it, once per message, so one Appwrite target with any
identifier covers every topic it subscribes to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tenki-reviewer

tenki-reviewer Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review complete. No blocking issues — approved ✅; 1 nitpick below.

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 expected+1 consume always burns full timeout (MqttServerTest.php:869) — MqttSubscriber::consume() loops until $limit is reached or the deadline expires, so asking for one more than the expected deliveries makes every successful run block the whole timeout: testCampaignReachesEveryTopicOfOneTarget waits a guaranteed 10s (tests/e2e/Services/Mqtt/MqttServerTest.php:869) and testCampaignPublishesTopicOncePerMessage waits 10s plus 5s sequentially (lines 913-914).

This PR reworks recipient streaming in the messaging worker so each yielded page carries its topic id, routing per-user and topic campaigns to the correct MQTT wire topic with a dedup map guarding against double publishes, and adds MQTT e2e tests plus CI lane wiring.

Files Change
src/Appwrite/Platform/Workers/Messaging.php Generator contract changes from bool to ?string topic id; per-topic publish dedup across streamed recipient pages.
tests/e2e/Services/Mqtt/MqttServerTest.php New e2e helpers (topic, target, subscriber) and duplicate-delivery assertions against a live MQTT broker.
.github/ci/e2e.json Registers the MQTT service so its e2e lane runs in CI.

Reviewed commit: f864fa7

@hansi-codes

hansi-codes Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Tier S · Ready to merge

The incremental changes have no actionable issues.

Messaging recipient pages retain topic IDs so Appwrite push campaigns publish to each topic ID once per message rather than to target identifiers. The worker also routes targetless users through an enabled Appwrite provider when another push provider is the default, and builds message search terms from providers actually used. MQTT e2e coverage exercises topic routing and provider selection, the CI path map includes messaging and push-adapter changes, and the console snippets workflow repins its Rust toolchain action.

Latest changes: The new commits route targetless user sends through enabled Appwrite even when it is not the default, track only providers actually used in message search, and add MQTT e2e cases for enabled and disabled Appwrite-provider selection.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 4
File Change
.github/ci/e2e.json Routes messaging worker and push-adapter changes to the MQTT e2e lane.
.github/workflows/console-snippets-qa-server.yml Repins the Rust toolchain action to the current stable-branch commit.
src/Appwrite/Platform/Workers/Messaging.php Routes topic campaigns by topic ID, handles targetless users with a non-default enabled Appwrite provider, and records only providers used by delivery batches.
tests/e2e/Services/Mqtt/MqttServerTest.php Covers topic-ID delivery, multi-topic and multi-provider deduplication, and targetless-user routing with and without an enabled Appwrite provider.

Reviewed the commits since ea41069 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

@github-actions

Copy link
Copy Markdown

Security rules

No new WARNING or ERROR findings from security rules.

19 existing findings tracked in .semgrep/baseline.json
  • php.appwrite.guest-write-without-abuse-limit (12)
  • php.appwrite.secret-compare-timing (5)
  • php.appwrite.weak-secret-env-default (2)

Posted by Checks / Rules. Re-runs update this comment in place. Rule details and baseline: .semgrep/README.md.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✨ Benchmark results

Comparing main (before) → fix/mqtt-topic-send-by-topic-id (after).

Metric Before After Change
🚀 Requests/sec 205.24 222.75 🟢 +8.5%
⏱️ Latency P50 82.64 ms 78.04 ms 🟢 -5.6%
⏱️ Latency P95 192.76 ms 179.82 ms 🟢 -6.7%
Per-scenario breakdown & investigation details

Metrics below reflect the current branch (after). Δ P95 compares against the base.

Scenario P50 (ms) P95 (ms) Requests RPS Δ P95 (ms)
API total 78.04 179.82 13,680 222.75 -12.94
Account 145.5 281.96 720 12.38 -25.64
TablesDB 77.18 148.35 7,440 123.37 -6.14
Storage 67.83 157.65 3,600 61.67 -23.99
Functions 107.97 204.05 1,920 33.61 -12.62

Top API waits (after)

API request Max wait (ms)
account.name.update 502.08
account.prefs.update 416.52
functions.variables.update 400.91
tablesdb.rows.create 356.15
functions.create 337.06

dtolnay/rust-toolchain rewrote its stable branch, leaving the pinned commit
outside the repo's history, which fails the zizmor workflow audit on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ArnabChatterjee20k and others added 2 commits October 16, 2026 13:16
…isn't the default

A users[] push reached a user with no push target on users/<userId> only
when the default (first enabled) push provider was Appwrite. With FCM or
APNs set up first, the message failed with "No valid recipients found."
even though an Appwrite provider was enabled.

Resolve the enabled Appwrite push provider once per send and use it for
the targetless fallback, whichever provider is the default. Users holding
other push targets are unchanged, so no notification is sent twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worker appended every provider in its resolved-provider cache to the
message's search text and the message.providers span. That cache is
seeded with the default provider, and now the enabled Appwrite provider,
so a send delivered entirely through another provider was still
searchable by providers it never used.

Record the provider of each batch result, wake-ups included, and build
the search and span from that set. The cache keeps its seeding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ArnabChatterjee20k ArnabChatterjee20k mentioned this pull request Oct 16, 2026
4 tasks done
ArnabChatterjee20k and others added 2 commits October 16, 2026 13:34
…akes unobservable

The messageSearch attribute filter recomputes a message's search text on
every write from its id, description, status, title and type, so provider
names are never stored and cannot be asserted through the search API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te-provider

fix(messaging): reach targetless users through Appwrite push when it isn't the default
…y-topic-id

# Conflicts:
#	.github/workflows/console-snippets-qa-server.yml
@ArnabChatterjee20k
ArnabChatterjee20k merged commit c3abfba into main Oct 16, 2026
66 of 67 checks passed
@ChiragAgg5k ChiragAgg5k mentioned this pull request Oct 21, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant