mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-03-20 09:01:05 +08:00
# Description of Changes This bug was introduced when we added the v2 subscription protocol. Before this change, when a v2 client disconnected or unsubscribed, other clients could stop receiving updates for sender-scoped views they were subscribed to. This happened because `SubscriptionManager::remove_all_subscriptions()` was cleaning up v2 subscription state correctly, but we would also run the legacy v1 cleanup loop which mixed v1 and v2 queries. This meant the same query hash could be processed again in the v1 cleanup loop after the v2 removal had already happened. In the sender-view case, that second pass could remove metadata (`indexes` / `search_args`) for active subscribers. Specifically, we could have the following sequence: 1. Client B disconnects. 2. The v2 loop removes B’s v2 subscription for `query_hash`. 3. `query_hash` now has no subscribers, so `remove_query_from_tables(query_hash)` runs once. 4. That compatibility loop then visits `query_hash` again via `subscription_ref_count.keys()`. 5. `query_hash` still has no subscribers, so `remove_query_from_tables(query_hash)` runs a second time. and when `remove_query_from_tables(query_hash)` runs a 2nd time, it calls `index_ids.delete_index_ids_for_query()` which is refcounted, not idempotent. So calling it twice for the same query decrements the shared index count twice, and the 2nd decrement drops an index that client A’s query still needs. The fix was to: 1. Collect the disconnected client’s v1 query hashes from `client_info.v1_subscriptions` 2. Run the v2 cleanup loop over `client_info.v2_subscriptions` (as we did before this change) 3. Run the old compatibility cleanup loop only over the deduplicated v1 hashes # API and ABI breaking changes None # Expected complexity level and risk 2 Small fix to a rather intricate bug. Most of the code in this patch is testing code. # Testing This bug was not reproducible with the current smoketest harness, because `spacetime subscribe` still uses the `v1` subscription protocol. I added a Rust SDK test, which did reproduce the bug before the fix, because the Rust SDK uses the `v2` subscription protocol.