Skip to content
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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions proposals/4102-unthreaded-read-receipts.md
Copy link
Member

Choose a reason for hiding this comment

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

Implementation requirements:

  • Demonstration of a bug fixed (likely a client implementation)

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
Copy link
Member

Choose a reason for hiding this comment

The 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)}.)

Copy link
Member Author

Choose a reason for hiding this comment

The 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 /sync. The Complement test does check that this MSC works over federation.

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
Loading