-
Notifications
You must be signed in to change notification settings - Fork 392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MSC4102: Clarifying precedence in threaded and unthreaded read receipts in EDUs #4102
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# MSC4102: Clarifying precedence in threaded and unthreaded read receipts in EDUs | ||
|
||
*This MSC assumes some knowledge around threaded receipts. Please read | ||
[MSC3771: Read receipts for threads](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3771-read-receipts-for-threads.md) | ||
as for brevity some concepts won't be re-explained.* | ||
|
||
This proposal clarifies previously undefined behaviour around how read receipts are expressed over | ||
the CSAPI and SSAPI. This clarification reliably provides clients with sufficient information to | ||
determine read receipts, without placing additional burdens on client implementations. | ||
Comment on lines
+7
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. element-hq/synapse#16927 doesn't seem to make any changes to the server-server API and Synapse currently will send multiple EDUs over federation if there's both an unthreaded and threaded receipt (or just multiple threads). This is since the EDU format is a bit different there. It maps room ID -> receipt type -> user ID -> {event IDs, data: {ts, thread ID (if applicable)}. (The client-server API maps event ID -> receipt type -> user ID -> {ts, thread ID (if applicable)}.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, which is fine. Federation can send multiple EDUs if it wants to, so long as at the end of the day the receiving server has the right receipts to combine them in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess my point is that there isn't undefined behavior of the SSAPI. |
||
|
||
## Background | ||
|
||
[MSC3771: Read receipts for threads](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3771-read-receipts-for-threads.md) | ||
defined an API shape for threaded read receipts over the `/sync` API. They look like this: | ||
```js | ||
{ | ||
"content": { | ||
"$thread_reply": { | ||
"m.read": { | ||
"@rikj:jki.re": { | ||
"ts": 1436451550453, | ||
"thread_id": "$thread_root" // or "main" or absent | ||
} | ||
} | ||
} | ||
}, | ||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", | ||
"type": "m.receipt" | ||
} | ||
``` | ||
|
||
This is problematic because it expresses receipt uniqueness based on the 3-uple (event ID, receipt type, user ID). | ||
In reality, uniqueness is based on the 4-uple of (event ID, receipt type, user ID, **thread ID**). This makes it | ||
impossible to express certain receipt aggregations, such as: | ||
``` | ||
Receipts Table | ||
room | user | event | thread | ||
-----+------+-------+-------- | ||
!foo | @bob | $abc | NULL // unthreaded receipt | ||
!foo | @bob | $abc | "some_id" // threaded receipt | ||
``` | ||
It is impossible to express these two receipts in a single `m.receipt` EDU, as the presence of `thread_id: "some_id"` | ||
by definition removes the absence of a thread ID, turning the receipt from an unthreaded receipt into a threaded receipt. | ||
|
||
[MSC3771: Read receipts for threads](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/3771-read-receipts-for-threads.md) | ||
does not provide rules around how to combine these receipts. This has led to undefined server behaviour. In practice, | ||
both Synapse and the Sliding Sync Proxy will combine them in a "last write wins" manner. If the unthreaded receipt is | ||
sent after the threaded receipt, then `/sync` will always return the unthreaded receipt and vice versa. This is not a | ||
hypothetical problem: Element-Web will send both threaded and unthreaded receipts for the same event if the event in | ||
question is the most recent event in the room, which is fairly common. | ||
|
||
This becomes a major problem when the threaded receipt "wins" and is returned _instead of_ the unthreaded receipt. | ||
Some clients, such as Element X, do not handle threads and will ignore receipts with a non-main `thread_id`, assuming that | ||
clients will also be sending unthreaded receipts. When the threaded receipt "wins" however, these clients will simply | ||
not see the receipt, despite the sender sending it. | ||
|
||
## Proposal | ||
|
||
When a server is combining receipts into an EDU, if there are multiple receipts for the same (user, event, receipt type), always | ||
choose the receipt which is unthreaded (has no `thread_id`) when aggregating into an EDU. | ||
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Combining leads to data loss, which is possibly why we still have stuck notifications/badge counts. If a threaded and unthreaded receipt are queued for sending, those should be independent EDUs because maps can't support both. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Losing the threaded receipt isn't really data loss though, which is what the "Potential Issues" section talks about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, fair point. I'm not sure this needs to be an MSC then - it feels like a clarification that can be made directly to the spec, given it's effectively talking about a data optimization and not functional specified behaviour. |
||
|
||
This change will apply to all `m.receipt` EDUs, which includes both CSAPI and Federation endpoints. | ||
|
||
For example, given these two EDUs: | ||
```js | ||
// EDU 1 | ||
{ | ||
"content": { | ||
"$1435641916114394fHBLK:matrix.org": { | ||
"m.read": { | ||
"@erikj:jki.re": { | ||
"ts": 1550000000000 | ||
} | ||
} | ||
} | ||
}, | ||
"type": "m.receipt" | ||
} | ||
|
||
// EDU 2 | ||
{ | ||
"content": { | ||
"$1435641916114394fHBLK:matrix.org": { | ||
"m.read": { | ||
"@erikj:jki.re": { | ||
"ts": 1559999999999, | ||
"thread_id": "foo" | ||
} | ||
}, | ||
"m.read.private": { | ||
"@self:example.org": { | ||
"ts": 1660000000000, | ||
"thread_id": "bar" | ||
} | ||
} | ||
} | ||
}, | ||
"type": "m.receipt" | ||
} | ||
``` | ||
The unthreaded receipt wins and the combined EDU should become: | ||
```js | ||
// Combined EDU | ||
{ | ||
"content": { | ||
"$1435641916114394fHBLK:matrix.org": { | ||
"m.read": { | ||
"@erikj:jki.re": { | ||
"ts": 1550000000000 | ||
} | ||
}, | ||
"m.read.private": { | ||
"@self:example.org": { | ||
"ts": 1660000000000, | ||
"thread_id": "bar" | ||
} | ||
} | ||
} | ||
}, | ||
"type": "m.receipt" | ||
} | ||
``` | ||
Note: | ||
- the data from the unthreaded receipt (the `ts`) is used. | ||
- the threaded receipt for type `m.read.private` is kept, as the 3-uple (user, event, receipt type) must match | ||
for the precedence rules to kick in. | ||
|
||
## Potential issues | ||
|
||
Some data is lost when aggregating EDUs, as the fact that a threaded receipt was sent will be lost. This is not a | ||
problem in practice because an unthreaded receipt always takes precedence over a threaded receipt due to it | ||
being a superset of the same data. | ||
|
||
Thread-unaware clients need to inspect `thread_id: "main"` receipts to accurately get read receipts from thread-aware | ||
clients, as that is how thread-aware clients send receipts for unthreaded events. Despite this, the | ||
`main` thread ID has no significance in this proposal, and should be treated as any other thread ID when determining | ||
precedence. This is because an unthreaded receipt (no thread ID) is still a superset of the `main` thread, just like | ||
any other thread. | ||
|
||
## Alternatives | ||
|
||
Servers could split `m.receipt` EDUs and send threaded receipts in one EDU and unthreaded EDUs in another. This | ||
creates additional EDUs, increasing bandwidth usage, processing time and increases complexity as servers need to | ||
handle cases where EDUs are sent incompletely to remote clients/servers. | ||
|
||
Servers could instead always choose the threaded receipt instead of the unthreaded receipt. This would make it | ||
harder to implement thread-unaware Matrix clients, as some receipt information is lost and can only be found by | ||
introspecting the threaded receipts. | ||
|
||
## Security considerations | ||
|
||
None. | ||
|
||
## Unstable prefix | ||
|
||
No unstable prefix required as no new endpoints / JSON keys are added in this proposal. | ||
|
||
## Dependencies | ||
|
||
None. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation requirements: