mirror of
https://github.com/matrix-org/dendrite.git
synced 2026-01-09 15:13:12 -06:00
Merge branch 'main' into s7evink/hisvismessages
This commit is contained in:
commit
9295bf0b1f
30
CHANGES.md
30
CHANGES.md
|
|
@ -1,5 +1,35 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Dendrite 0.9.0 (2022-08-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite now uses Ristretto for managing in-memory caches
|
||||||
|
* Should improve cache utilisation considerably over time by more intelligently selecting and managing cache entries compared to the previous LRU-based cache
|
||||||
|
* Defaults to a 1GB cache size if not configured otherwise
|
||||||
|
* The estimated cache size in memory and maximum age can now be configured with new [configuration options](https://github.com/matrix-org/dendrite/blob/e94ef84aaba30e12baf7f524c4e7a36d2fdeb189/dendrite-sample.monolith.yaml#L44-L61) to prevent unbounded cache growth
|
||||||
|
* Added support for serving the `/.well-known/matrix/client` hint directly from Dendrite
|
||||||
|
* Configurable with the new [configuration option](https://github.com/matrix-org/dendrite/blob/e94ef84aaba30e12baf7f524c4e7a36d2fdeb189/dendrite-sample.monolith.yaml#L67-L69)
|
||||||
|
* Refactored membership updater, which should eliminate some bugs caused by the membership table getting out of sync with the room state
|
||||||
|
* The User API is now responsible for sending account data updates to other components, which may fix some races and duplicate account data events
|
||||||
|
* Optimised database query for checking whether a remote server is allowed to request an event over federation without using anywhere near as much CPU time (PostgreSQL only)
|
||||||
|
* Database migrations have been refactored to eliminate some problems that were present with `goose` and upgrading from older Dendrite versions
|
||||||
|
* Media fetching will now use the `/v3` endpoints for downloading media from remote homeservers
|
||||||
|
* HTTP 404 and HTTP 405 errors from the client-facing APIs should now be returned with CORS headers so that web-based clients do not produce incorrect access control warnings for unknown endpoints
|
||||||
|
* Some preparation work for full history visibility support
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Fixes a crash that could occur during event redaction
|
||||||
|
* The `/members` endpoint will no longer incorrectly return HTTP 500 as a result of some invite events
|
||||||
|
* Send-to-device messages should now be ordered more reliably and the last position in the stream updated correctly
|
||||||
|
* Parsing of appservice configuration files is now less strict (contributed by [Kab1r](https://github.com/Kab1r))
|
||||||
|
* The sync API should now identify shared users correctly when waking up for E2EE key changes
|
||||||
|
* The federation `/state` endpoint will now return a HTTP 403 when the state before an event isn't known instead of a HTTP 500
|
||||||
|
* Presence timestamps should now be calculated with the correct precision
|
||||||
|
* A race condition in the roomserver's room info has been fixed
|
||||||
|
* A race condition in the sync API has been fixed
|
||||||
|
|
||||||
## Dendrite 0.8.9 (2022-07-01)
|
## Dendrite 0.8.9 (2022-07-01)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ var build string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionMajor = 0
|
VersionMajor = 0
|
||||||
VersionMinor = 8
|
VersionMinor = 9
|
||||||
VersionPatch = 9
|
VersionPatch = 0
|
||||||
VersionTag = "" // example: "rc1"
|
VersionTag = "" // example: "rc1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,14 +50,14 @@ func CheckForSoftFail(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("db.RoomNID: %w", err)
|
return false, fmt.Errorf("db.RoomNID: %w", err)
|
||||||
}
|
}
|
||||||
if roomInfo == nil || roomInfo.IsStub {
|
if roomInfo == nil || roomInfo.IsStub() {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then get the state entries for the current state snapshot.
|
// Then get the state entries for the current state snapshot.
|
||||||
// We'll use this to check if the event is allowed right now.
|
// We'll use this to check if the event is allowed right now.
|
||||||
roomState := state.NewStateResolution(db, roomInfo)
|
roomState := state.NewStateResolution(db, roomInfo)
|
||||||
authStateEntries, err = roomState.LoadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID)
|
authStateEntries, err = roomState.LoadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, fmt.Errorf("roomState.LoadStateAtSnapshot: %w", err)
|
return true, fmt.Errorf("roomState.LoadStateAtSnapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,13 +242,34 @@ func LoadStateEvents(
|
||||||
func CheckServerAllowedToSeeEvent(
|
func CheckServerAllowedToSeeEvent(
|
||||||
ctx context.Context, db storage.Database, info *types.RoomInfo, eventID string, serverName gomatrixserverlib.ServerName, isServerInRoom bool,
|
ctx context.Context, db storage.Database, info *types.RoomInfo, eventID string, serverName gomatrixserverlib.ServerName, isServerInRoom bool,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
|
stateAtEvent, err := db.GetHistoryVisibilityState(ctx, info, eventID, string(serverName))
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
// No error, so continue normally
|
||||||
|
case tables.OptimisationNotSupportedError:
|
||||||
|
// The database engine didn't support this optimisation, so fall back to using
|
||||||
|
// the old and slow method
|
||||||
|
stateAtEvent, err = slowGetHistoryVisibilityState(ctx, db, info, eventID, serverName)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Something else went wrong
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return auth.IsServerAllowed(serverName, isServerInRoom, stateAtEvent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func slowGetHistoryVisibilityState(
|
||||||
|
ctx context.Context, db storage.Database, info *types.RoomInfo, eventID string, serverName gomatrixserverlib.ServerName,
|
||||||
|
) ([]*gomatrixserverlib.Event, error) {
|
||||||
roomState := state.NewStateResolution(db, info)
|
roomState := state.NewStateResolution(db, info)
|
||||||
stateEntries, err := roomState.LoadStateAtEvent(ctx, eventID)
|
stateEntries, err := roomState.LoadStateAtEvent(ctx, eventID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return false, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("roomState.LoadStateAtEvent: %w", err)
|
return nil, fmt.Errorf("roomState.LoadStateAtEvent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all of the event state key NIDs from the room state.
|
// Extract all of the event state key NIDs from the room state.
|
||||||
|
|
@ -260,7 +281,7 @@ func CheckServerAllowedToSeeEvent(
|
||||||
// Then request those state key NIDs from the database.
|
// Then request those state key NIDs from the database.
|
||||||
stateKeys, err := db.EventStateKeys(ctx, stateKeyNIDs)
|
stateKeys, err := db.EventStateKeys(ctx, stateKeyNIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("db.EventStateKeys: %w", err)
|
return nil, fmt.Errorf("db.EventStateKeys: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the event state key doesn't match the given servername
|
// If the event state key doesn't match the given servername
|
||||||
|
|
@ -283,15 +304,10 @@ func CheckServerAllowedToSeeEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filteredEntries) == 0 {
|
if len(filteredEntries) == 0 {
|
||||||
return false, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stateAtEvent, err := LoadStateEvents(ctx, db, filteredEntries)
|
return LoadStateEvents(ctx, db, filteredEntries)
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth.IsServerAllowed(serverName, isServerInRoom, stateAtEvent), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove this when we have tests to assert correctness of this function
|
// TODO: Remove this when we have tests to assert correctness of this function
|
||||||
|
|
@ -399,7 +415,7 @@ func QueryLatestEventsAndState(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if roomInfo == nil || roomInfo.IsStub {
|
if roomInfo == nil || roomInfo.IsStub() {
|
||||||
response.RoomExists = false
|
response.RoomExists = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ func (r *Admin) PerformAdminEvacuateRoom(
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if roomInfo == nil || roomInfo.IsStub {
|
if roomInfo == nil || roomInfo.IsStub() {
|
||||||
res.Error = &api.PerformError{
|
res.Error = &api.PerformError{
|
||||||
Code: api.PerformErrorNoRoom,
|
Code: api.PerformErrorNoRoom,
|
||||||
Msg: fmt.Sprintf("Room %s not found", req.RoomID),
|
Msg: fmt.Sprintf("Room %s not found", req.RoomID),
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func (r *Backfiller) PerformBackfill(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return fmt.Errorf("PerformBackfill: missing room info for room %s", request.RoomID)
|
return fmt.Errorf("PerformBackfill: missing room info for room %s", request.RoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ func (r *Backfiller) backfillViaFederation(ctx context.Context, req *api.Perform
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return fmt.Errorf("backfillViaFederation: missing room info for room %s", req.RoomID)
|
return fmt.Errorf("backfillViaFederation: missing room info for room %s", req.RoomID)
|
||||||
}
|
}
|
||||||
requester := newBackfillRequester(r.DB, r.FSAPI, r.ServerName, req.BackwardsExtremities, r.PreferServers)
|
requester := newBackfillRequester(r.DB, r.FSAPI, r.ServerName, req.BackwardsExtremities, r.PreferServers)
|
||||||
|
|
@ -434,7 +434,7 @@ FindSuccessor:
|
||||||
logrus.WithError(err).WithField("room_id", roomID).Error("ServersAtEvent: failed to get RoomInfo for room")
|
logrus.WithError(err).WithField("room_id", roomID).Error("ServersAtEvent: failed to get RoomInfo for room")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
logrus.WithField("room_id", roomID).Error("ServersAtEvent: failed to get RoomInfo for room, room is missing")
|
logrus.WithField("room_id", roomID).Error("ServersAtEvent: failed to get RoomInfo for room, room is missing")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (r *InboundPeeker) PerformInboundPeek(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
response.RoomExists = true
|
response.RoomExists = true
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ func (r *Inviter) PerformInvite(
|
||||||
return outputUpdates, nil
|
return outputUpdates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info == nil || info.IsStub) && !isOriginLocal && isTargetLocal {
|
if (info == nil || info.IsStub()) && !isOriginLocal && isTargetLocal {
|
||||||
// The invite came in over federation for a room that we don't know about
|
// The invite came in over federation for a room that we don't know about
|
||||||
// yet. We need to handle this a bit differently to most invites because
|
// yet. We need to handle this a bit differently to most invites because
|
||||||
// we don't know the room state, therefore the roomserver can't process
|
// we don't know the room state, therefore the roomserver can't process
|
||||||
|
|
@ -276,7 +276,7 @@ func buildInviteStrippedState(
|
||||||
}
|
}
|
||||||
roomState := state.NewStateResolution(db, info)
|
roomState := state.NewStateResolution(db, info)
|
||||||
stateEntries, err := roomState.LoadStateAtSnapshotForStringTuples(
|
stateEntries, err := roomState.LoadStateAtSnapshotForStringTuples(
|
||||||
ctx, info.StateSnapshotNID, stateWanted,
|
ctx, info.StateSnapshotNID(), stateWanted,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ func (r *Queryer) QueryStateAfterEvents(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,7 +350,7 @@ func (r *Queryer) QueryServerJoinedToRoom(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("r.DB.RoomInfo: %w", err)
|
return fmt.Errorf("r.DB.RoomInfo: %w", err)
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
response.RoomExists = true
|
response.RoomExists = true
|
||||||
|
|
@ -438,7 +438,7 @@ func (r *Queryer) QueryMissingEvents(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return fmt.Errorf("missing RoomInfo for room %s", events[0].RoomID())
|
return fmt.Errorf("missing RoomInfo for room %s", events[0].RoomID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,7 +477,7 @@ func (r *Queryer) QueryStateAndAuthChain(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info == nil || info.IsStub {
|
if info == nil || info.IsStub() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
response.RoomExists = true
|
response.RoomExists = true
|
||||||
|
|
@ -822,7 +822,7 @@ func (r *Queryer) QueryRestrictedJoinAllowed(ctx context.Context, req *api.Query
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("r.DB.RoomInfo: %w", err)
|
return fmt.Errorf("r.DB.RoomInfo: %w", err)
|
||||||
}
|
}
|
||||||
if roomInfo == nil || roomInfo.IsStub {
|
if roomInfo == nil || roomInfo.IsStub() {
|
||||||
return nil // fmt.Errorf("room %q doesn't exist or is stub room", req.RoomID)
|
return nil // fmt.Errorf("room %q doesn't exist or is stub room", req.RoomID)
|
||||||
}
|
}
|
||||||
// If the room version doesn't allow restricted joins then don't
|
// If the room version doesn't allow restricted joins then don't
|
||||||
|
|
@ -885,7 +885,7 @@ func (r *Queryer) QueryRestrictedJoinAllowed(ctx context.Context, req *api.Query
|
||||||
// See if the room exists. If it doesn't exist or if it's a stub
|
// See if the room exists. If it doesn't exist or if it's a stub
|
||||||
// room entry then we can't check memberships.
|
// room entry then we can't check memberships.
|
||||||
targetRoomInfo, err := r.DB.RoomInfo(ctx, rule.RoomID)
|
targetRoomInfo, err := r.DB.RoomInfo(ctx, rule.RoomID)
|
||||||
if err != nil || targetRoomInfo == nil || targetRoomInfo.IsStub {
|
if err != nil || targetRoomInfo == nil || targetRoomInfo.IsStub() {
|
||||||
res.Resident = false
|
res.Resident = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ func (v *StateResolution) LoadMembershipAtEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
stateBlockNIDLists, err := v.db.StateBlockNIDs(ctx, snapshotNIDs)
|
stateBlockNIDLists, err := v.db.StateBlockNIDs(ctx, snapshotNIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,6 +176,29 @@ func (v *StateResolution) LoadMembershipAtEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadStateAtEvent loads the full state of a room before a particular event.
|
||||||
|
func (v *StateResolution) LoadStateAtEventForHistoryVisibility(
|
||||||
|
ctx context.Context, eventID string,
|
||||||
|
) ([]types.StateEntry, error) {
|
||||||
|
span, ctx := opentracing.StartSpanFromContext(ctx, "StateResolution.LoadStateAtEvent")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
snapshotNID, err := v.db.SnapshotNIDFromEventID(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LoadStateAtEvent.SnapshotNIDFromEventID failed for event %s : %w", eventID, err)
|
||||||
|
}
|
||||||
|
if snapshotNID == 0 {
|
||||||
|
return nil, fmt.Errorf("LoadStateAtEvent.SnapshotNIDFromEventID(%s) returned 0 NID, was this event stored?", eventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateEntries, err := v.LoadStateAtSnapshot(ctx, snapshotNID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateEntries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadCombinedStateAfterEvents loads a snapshot of the state after each of the events
|
// LoadCombinedStateAfterEvents loads a snapshot of the state after each of the events
|
||||||
|
|
|
||||||
|
|
@ -166,4 +166,6 @@ type Database interface {
|
||||||
GetKnownRooms(ctx context.Context) ([]string, error)
|
GetKnownRooms(ctx context.Context) ([]string, error)
|
||||||
// ForgetRoom sets a flag in the membership table, that the user wishes to forget a specific room
|
// ForgetRoom sets a flag in the membership table, that the user wishes to forget a specific room
|
||||||
ForgetRoom(ctx context.Context, userID, roomID string, forget bool) error
|
ForgetRoom(ctx context.Context, userID, roomID string, forget bool) error
|
||||||
|
|
||||||
|
GetHistoryVisibilityState(ctx context.Context, roomInfo *types.RoomInfo, eventID string, domain string) ([]*gomatrixserverlib.Event, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,14 +147,16 @@ func (s *roomStatements) InsertRoomNID(
|
||||||
func (s *roomStatements) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID string) (*types.RoomInfo, error) {
|
func (s *roomStatements) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID string) (*types.RoomInfo, error) {
|
||||||
var info types.RoomInfo
|
var info types.RoomInfo
|
||||||
var latestNIDs pq.Int64Array
|
var latestNIDs pq.Int64Array
|
||||||
|
var stateSnapshotNID types.StateSnapshotNID
|
||||||
stmt := sqlutil.TxStmt(txn, s.selectRoomInfoStmt)
|
stmt := sqlutil.TxStmt(txn, s.selectRoomInfoStmt)
|
||||||
err := stmt.QueryRowContext(ctx, roomID).Scan(
|
err := stmt.QueryRowContext(ctx, roomID).Scan(
|
||||||
&info.RoomVersion, &info.RoomNID, &info.StateSnapshotNID, &latestNIDs,
|
&info.RoomVersion, &info.RoomNID, &stateSnapshotNID, &latestNIDs,
|
||||||
)
|
)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
info.IsStub = len(latestNIDs) == 0
|
info.SetStateSnapshotNID(stateSnapshotNID)
|
||||||
|
info.SetIsStub(len(latestNIDs) == 0)
|
||||||
return &info, err
|
return &info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,35 @@ const bulkSelectStateBlockNIDsSQL = "" +
|
||||||
"SELECT state_snapshot_nid, state_block_nids FROM roomserver_state_snapshots" +
|
"SELECT state_snapshot_nid, state_block_nids FROM roomserver_state_snapshots" +
|
||||||
" WHERE state_snapshot_nid = ANY($1) ORDER BY state_snapshot_nid ASC"
|
" WHERE state_snapshot_nid = ANY($1) ORDER BY state_snapshot_nid ASC"
|
||||||
|
|
||||||
|
// Looks up both the history visibility event and relevant membership events from
|
||||||
|
// a given domain name from a given state snapshot. This is used to optimise the
|
||||||
|
// helpers.CheckServerAllowedToSeeEvent function.
|
||||||
|
// TODO: There's a sequence scan here because of the hash join strategy, which is
|
||||||
|
// probably O(n) on state key entries, so there must be a way to avoid that somehow.
|
||||||
|
// Event type NIDs are:
|
||||||
|
// - 5: m.room.member as per https://github.com/matrix-org/dendrite/blob/c7f7aec4d07d59120d37d5b16a900f6d608a75c4/roomserver/storage/postgres/event_types_table.go#L40
|
||||||
|
// - 7: m.room.history_visibility as per https://github.com/matrix-org/dendrite/blob/c7f7aec4d07d59120d37d5b16a900f6d608a75c4/roomserver/storage/postgres/event_types_table.go#L42
|
||||||
|
const bulkSelectStateForHistoryVisibilitySQL = `
|
||||||
|
SELECT event_nid FROM (
|
||||||
|
SELECT event_nid, event_type_nid, event_state_key_nid FROM roomserver_events
|
||||||
|
WHERE (event_type_nid = 5 OR event_type_nid = 7)
|
||||||
|
AND event_nid = ANY(
|
||||||
|
SELECT UNNEST(event_nids) FROM roomserver_state_block
|
||||||
|
WHERE state_block_nid = ANY(
|
||||||
|
SELECT UNNEST(state_block_nids) FROM roomserver_state_snapshots
|
||||||
|
WHERE state_snapshot_nid = $1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) AS roomserver_events
|
||||||
|
INNER JOIN roomserver_event_state_keys
|
||||||
|
ON roomserver_events.event_state_key_nid = roomserver_event_state_keys.event_state_key_nid
|
||||||
|
AND (event_type_nid = 7 OR event_state_key LIKE '%:' || $2);
|
||||||
|
`
|
||||||
|
|
||||||
type stateSnapshotStatements struct {
|
type stateSnapshotStatements struct {
|
||||||
insertStateStmt *sql.Stmt
|
insertStateStmt *sql.Stmt
|
||||||
bulkSelectStateBlockNIDsStmt *sql.Stmt
|
bulkSelectStateBlockNIDsStmt *sql.Stmt
|
||||||
|
bulkSelectStateForHistoryVisibilityStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateStateSnapshotTable(db *sql.DB) error {
|
func CreateStateSnapshotTable(db *sql.DB) error {
|
||||||
|
|
@ -88,6 +114,7 @@ func PrepareStateSnapshotTable(db *sql.DB) (tables.StateSnapshot, error) {
|
||||||
return s, sqlutil.StatementList{
|
return s, sqlutil.StatementList{
|
||||||
{&s.insertStateStmt, insertStateSQL},
|
{&s.insertStateStmt, insertStateSQL},
|
||||||
{&s.bulkSelectStateBlockNIDsStmt, bulkSelectStateBlockNIDsSQL},
|
{&s.bulkSelectStateBlockNIDsStmt, bulkSelectStateBlockNIDsSQL},
|
||||||
|
{&s.bulkSelectStateForHistoryVisibilityStmt, bulkSelectStateForHistoryVisibilitySQL},
|
||||||
}.Prepare(db)
|
}.Prepare(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,3 +163,23 @@ func (s *stateSnapshotStatements) BulkSelectStateBlockNIDs(
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stateSnapshotStatements) BulkSelectStateForHistoryVisibility(
|
||||||
|
ctx context.Context, txn *sql.Tx, stateSnapshotNID types.StateSnapshotNID, domain string,
|
||||||
|
) ([]types.EventNID, error) {
|
||||||
|
stmt := sqlutil.TxStmt(txn, s.bulkSelectStateForHistoryVisibilityStmt)
|
||||||
|
rows, err := stmt.QueryContext(ctx, stateSnapshotNID, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close() // nolint: errcheck
|
||||||
|
results := make([]types.EventNID, 0, 16)
|
||||||
|
for rows.Next() {
|
||||||
|
var eventNID types.EventNID
|
||||||
|
if err = rows.Scan(&eventNID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, eventNID)
|
||||||
|
}
|
||||||
|
return results, rows.Err()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,8 @@ func (u *RoomUpdater) SetLatestEvents(
|
||||||
// Since it's entirely possible that this types.RoomInfo came from the
|
// Since it's entirely possible that this types.RoomInfo came from the
|
||||||
// cache, we should make sure to update that entry so that the next run
|
// cache, we should make sure to update that entry so that the next run
|
||||||
// works from live data.
|
// works from live data.
|
||||||
u.roomInfo.StateSnapshotNID = currentStateSnapshotNID
|
u.roomInfo.SetStateSnapshotNID(currentStateSnapshotNID)
|
||||||
u.roomInfo.IsStub = false
|
u.roomInfo.SetIsStub(false)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -988,6 +988,38 @@ func (d *Database) loadEvent(ctx context.Context, eventID string) *types.Event {
|
||||||
return &evs[0]
|
return &evs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetHistoryVisibilityState(ctx context.Context, roomInfo *types.RoomInfo, eventID string, domain string) ([]*gomatrixserverlib.Event, error) {
|
||||||
|
eventStates, err := d.EventsTable.BulkSelectStateAtEventByID(ctx, nil, []string{eventID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stateSnapshotNID := eventStates[0].BeforeStateSnapshotNID
|
||||||
|
if stateSnapshotNID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
eventNIDs, err := d.StateSnapshotTable.BulkSelectStateForHistoryVisibility(ctx, nil, stateSnapshotNID, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
eventIDs, _ := d.EventsTable.BulkSelectEventID(ctx, nil, eventNIDs)
|
||||||
|
if err != nil {
|
||||||
|
eventIDs = map[types.EventNID]string{}
|
||||||
|
}
|
||||||
|
events := make([]*gomatrixserverlib.Event, 0, len(eventNIDs))
|
||||||
|
for _, eventNID := range eventNIDs {
|
||||||
|
data, err := d.EventJSONTable.BulkSelectEventJSON(ctx, nil, []types.EventNID{eventNID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ev, err := gomatrixserverlib.NewEventFromTrustedJSONWithEventID(eventIDs[eventNID], data[0].EventJSON, false, roomInfo.RoomVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
events = append(events, ev)
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetStateEvent returns the current state event of a given type for a given room with a given state key
|
// GetStateEvent returns the current state event of a given type for a given room with a given state key
|
||||||
// If no event could be found, returns nil
|
// If no event could be found, returns nil
|
||||||
// If there was an issue during the retrieval, returns an error
|
// If there was an issue during the retrieval, returns an error
|
||||||
|
|
@ -1000,7 +1032,7 @@ func (d *Database) GetStateEvent(ctx context.Context, roomID, evType, stateKey s
|
||||||
return nil, fmt.Errorf("room %s doesn't exist", roomID)
|
return nil, fmt.Errorf("room %s doesn't exist", roomID)
|
||||||
}
|
}
|
||||||
// e.g invited rooms
|
// e.g invited rooms
|
||||||
if roomInfo.IsStub {
|
if roomInfo.IsStub() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
eventTypeNID, err := d.EventTypesTable.SelectEventTypeNID(ctx, nil, evType)
|
eventTypeNID, err := d.EventTypesTable.SelectEventTypeNID(ctx, nil, evType)
|
||||||
|
|
@ -1019,7 +1051,7 @@ func (d *Database) GetStateEvent(ctx context.Context, roomID, evType, stateKey s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries, err := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID)
|
entries, err := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1065,7 +1097,7 @@ func (d *Database) GetStateEventsWithEventType(ctx context.Context, roomID, evTy
|
||||||
return nil, fmt.Errorf("room %s doesn't exist", roomID)
|
return nil, fmt.Errorf("room %s doesn't exist", roomID)
|
||||||
}
|
}
|
||||||
// e.g invited rooms
|
// e.g invited rooms
|
||||||
if roomInfo.IsStub {
|
if roomInfo.IsStub() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
eventTypeNID, err := d.EventTypesTable.SelectEventTypeNID(ctx, nil, evType)
|
eventTypeNID, err := d.EventTypesTable.SelectEventTypeNID(ctx, nil, evType)
|
||||||
|
|
@ -1076,7 +1108,7 @@ func (d *Database) GetStateEventsWithEventType(ctx context.Context, roomID, evTy
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries, err := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID)
|
entries, err := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1193,10 +1225,10 @@ func (d *Database) GetBulkStateContent(ctx context.Context, roomIDs []string, tu
|
||||||
return nil, fmt.Errorf("GetBulkStateContent: failed to load room info for room %s : %w", roomID, err2)
|
return nil, fmt.Errorf("GetBulkStateContent: failed to load room info for room %s : %w", roomID, err2)
|
||||||
}
|
}
|
||||||
// for unknown rooms or rooms which we don't have the current state, skip them.
|
// for unknown rooms or rooms which we don't have the current state, skip them.
|
||||||
if roomInfo == nil || roomInfo.IsStub {
|
if roomInfo == nil || roomInfo.IsStub() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entries, err2 := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID)
|
entries, err2 := d.loadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID())
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return nil, fmt.Errorf("GetBulkStateContent: failed to load state for room %s : %w", roomID, err2)
|
return nil, fmt.Errorf("GetBulkStateContent: failed to load state for room %s : %w", roomID, err2)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,9 +129,10 @@ func (s *roomStatements) SelectRoomIDsWithEvents(ctx context.Context, txn *sql.T
|
||||||
func (s *roomStatements) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID string) (*types.RoomInfo, error) {
|
func (s *roomStatements) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID string) (*types.RoomInfo, error) {
|
||||||
var info types.RoomInfo
|
var info types.RoomInfo
|
||||||
var latestNIDsJSON string
|
var latestNIDsJSON string
|
||||||
|
var stateSnapshotNID types.StateSnapshotNID
|
||||||
stmt := sqlutil.TxStmt(txn, s.selectRoomInfoStmt)
|
stmt := sqlutil.TxStmt(txn, s.selectRoomInfoStmt)
|
||||||
err := stmt.QueryRowContext(ctx, roomID).Scan(
|
err := stmt.QueryRowContext(ctx, roomID).Scan(
|
||||||
&info.RoomVersion, &info.RoomNID, &info.StateSnapshotNID, &latestNIDsJSON,
|
&info.RoomVersion, &info.RoomNID, &stateSnapshotNID, &latestNIDsJSON,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
|
@ -143,7 +144,8 @@ func (s *roomStatements) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID
|
||||||
if err = json.Unmarshal([]byte(latestNIDsJSON), &latestNIDs); err != nil {
|
if err = json.Unmarshal([]byte(latestNIDsJSON), &latestNIDs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
info.IsStub = len(latestNIDs) == 0
|
info.SetStateSnapshotNID(stateSnapshotNID)
|
||||||
|
info.SetIsStub(len(latestNIDs) == 0)
|
||||||
return &info, err
|
return &info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,3 +140,9 @@ func (s *stateSnapshotStatements) BulkSelectStateBlockNIDs(
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stateSnapshotStatements) BulkSelectStateForHistoryVisibility(
|
||||||
|
ctx context.Context, txn *sql.Tx, stateSnapshotNID types.StateSnapshotNID, domain string,
|
||||||
|
) ([]types.EventNID, error) {
|
||||||
|
return nil, tables.OptimisationNotSupportedError
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,15 @@ package tables
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var OptimisationNotSupportedError = errors.New("optimisation not supported")
|
||||||
|
|
||||||
type EventJSONPair struct {
|
type EventJSONPair struct {
|
||||||
EventNID types.EventNID
|
EventNID types.EventNID
|
||||||
EventJSON []byte
|
EventJSON []byte
|
||||||
|
|
@ -80,6 +83,10 @@ type Rooms interface {
|
||||||
type StateSnapshot interface {
|
type StateSnapshot interface {
|
||||||
InsertState(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, stateBlockNIDs types.StateBlockNIDs) (stateNID types.StateSnapshotNID, err error)
|
InsertState(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, stateBlockNIDs types.StateBlockNIDs) (stateNID types.StateSnapshotNID, err error)
|
||||||
BulkSelectStateBlockNIDs(ctx context.Context, txn *sql.Tx, stateNIDs []types.StateSnapshotNID) ([]types.StateBlockNIDList, error)
|
BulkSelectStateBlockNIDs(ctx context.Context, txn *sql.Tx, stateNIDs []types.StateSnapshotNID) ([]types.StateBlockNIDList, error)
|
||||||
|
// BulkSelectStateForHistoryVisibility is a PostgreSQL-only optimisation for finding
|
||||||
|
// which users are in a room faster than having to load the entire room state. In the
|
||||||
|
// case of SQLite, this will return tables.OptimisationNotSupportedError.
|
||||||
|
BulkSelectStateForHistoryVisibility(ctx context.Context, txn *sql.Tx, stateSnapshotNID types.StateSnapshotNID, domain string) ([]types.EventNID, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateBlock interface {
|
type StateBlock interface {
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,12 @@ func TestRoomsTable(t *testing.T) {
|
||||||
|
|
||||||
roomInfo, err := tab.SelectRoomInfo(ctx, nil, room.ID)
|
roomInfo, err := tab.SelectRoomInfo(ctx, nil, room.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &types.RoomInfo{
|
expected := &types.RoomInfo{
|
||||||
RoomNID: wantRoomNID,
|
RoomNID: wantRoomNID,
|
||||||
RoomVersion: room.Version,
|
RoomVersion: room.Version,
|
||||||
StateSnapshotNID: 0,
|
}
|
||||||
IsStub: true, // there are no latestEventNIDs
|
expected.SetIsStub(true) // there are no latestEventNIDs
|
||||||
}, roomInfo)
|
assert.Equal(t, expected, roomInfo)
|
||||||
|
|
||||||
roomInfo, err = tab.SelectRoomInfo(ctx, nil, "!doesnotexist:localhost")
|
roomInfo, err = tab.SelectRoomInfo(ctx, nil, "!doesnotexist:localhost")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
@ -103,12 +103,12 @@ func TestRoomsTable(t *testing.T) {
|
||||||
|
|
||||||
roomInfo, err = tab.SelectRoomInfo(ctx, nil, room.ID)
|
roomInfo, err = tab.SelectRoomInfo(ctx, nil, room.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, &types.RoomInfo{
|
expected = &types.RoomInfo{
|
||||||
RoomNID: wantRoomNID,
|
RoomNID: wantRoomNID,
|
||||||
RoomVersion: room.Version,
|
RoomVersion: room.Version,
|
||||||
StateSnapshotNID: 1,
|
}
|
||||||
IsStub: false,
|
expected.SetStateSnapshotNID(1)
|
||||||
}, roomInfo)
|
assert.Equal(t, expected, roomInfo)
|
||||||
|
|
||||||
eventNIDs, snapshotNID, err := tab.SelectLatestEventNIDs(ctx, nil, wantRoomNID)
|
eventNIDs, snapshotNID, err := tab.SelectLatestEventNIDs(ctx, nil, wantRoomNID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,15 @@ func mustCreateStateSnapshotTable(t *testing.T, dbType test.DBType) (tab tables.
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
switch dbType {
|
switch dbType {
|
||||||
case test.DBTypePostgres:
|
case test.DBTypePostgres:
|
||||||
|
// for the PostgreSQL history visibility optimisation to work,
|
||||||
|
// we also need some other tables to exist
|
||||||
|
err = postgres.CreateEventStateKeysTable(db)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = postgres.CreateEventsTable(db)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = postgres.CreateStateBlockTable(db)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// ... and then the snapshot table itself
|
||||||
err = postgres.CreateStateSnapshotTable(db)
|
err = postgres.CreateStateSnapshotTable(db)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
tab, err = postgres.PrepareStateSnapshotTable(db)
|
tab, err = postgres.PrepareStateSnapshotTable(db)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
@ -279,8 +280,33 @@ func (e RejectedError) Error() string { return string(e) }
|
||||||
|
|
||||||
// RoomInfo contains metadata about a room
|
// RoomInfo contains metadata about a room
|
||||||
type RoomInfo struct {
|
type RoomInfo struct {
|
||||||
|
mu sync.RWMutex
|
||||||
RoomNID RoomNID
|
RoomNID RoomNID
|
||||||
RoomVersion gomatrixserverlib.RoomVersion
|
RoomVersion gomatrixserverlib.RoomVersion
|
||||||
StateSnapshotNID StateSnapshotNID
|
stateSnapshotNID StateSnapshotNID
|
||||||
IsStub bool
|
isStub bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RoomInfo) StateSnapshotNID() StateSnapshotNID {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.stateSnapshotNID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RoomInfo) IsStub() bool {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.isStub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RoomInfo) SetStateSnapshotNID(nid StateSnapshotNID) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.stateSnapshotNID = nid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RoomInfo) SetIsStub(isStub bool) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.isStub = isStub
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,11 @@ func (p *PDUStreamProvider) CompleteSync(
|
||||||
p.queue(func() {
|
p.queue(func() {
|
||||||
defer reqWaitGroup.Done()
|
defer reqWaitGroup.Done()
|
||||||
|
|
||||||
var jr *types.JoinResponse
|
jr, jerr := p.getJoinResponseForCompleteSync(
|
||||||
jr, err = p.getJoinResponseForCompleteSync(
|
|
||||||
ctx, roomID, r, &stateFilter, &eventFilter, req.WantFullState, req.Device, false,
|
ctx, roomID, r, &stateFilter, &eventFilter, req.WantFullState, req.Device, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if jerr != nil {
|
||||||
req.Log.WithError(err).Error("p.getJoinResponseForCompleteSync failed")
|
req.Log.WithError(jerr).Error("p.getJoinResponseForCompleteSync failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,9 +264,9 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse(
|
||||||
var pos types.StreamPosition
|
var pos types.StreamPosition
|
||||||
if _, pos, err = p.DB.PositionInTopology(ctx, mostRecentEventID); err == nil {
|
if _, pos, err = p.DB.PositionInTopology(ctx, mostRecentEventID); err == nil {
|
||||||
switch {
|
switch {
|
||||||
case r.Backwards && pos > latestPosition:
|
case r.Backwards && pos < latestPosition:
|
||||||
fallthrough
|
fallthrough
|
||||||
case !r.Backwards && pos < latestPosition:
|
case !r.Backwards && pos > latestPosition:
|
||||||
latestPosition = pos
|
latestPosition = pos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue