Skip to content

Commit 223ee0d

Browse files
committed
Add locking to avoid index corruption
When a new room is added there's a fairly good chance that the other events being dispatched will happen in the middle of (for example) the room list being re-sorted. This commit wraps the entire handleRoomUpdate() function for the underlying algorithms in a lock so that if we're unlucky enough to get an update while we're sorting (as the ImportanceAlgorithm splices out what it is sorting) we won't scream about invalid index errors.
1 parent c7a83e6 commit 223ee0d

File tree

3 files changed

+62
-48
lines changed

3 files changed

+62
-48
lines changed

src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -179,45 +179,51 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
179179
}
180180

181181
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
182-
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
183-
return this.handleSplice(room, cause);
184-
}
182+
try {
183+
await this.updateLock.acquireAsync();
185184

186-
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
187-
throw new Error(`Unsupported update cause: ${cause}`);
188-
}
185+
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
186+
return this.handleSplice(room, cause);
187+
}
189188

190-
const category = this.getRoomCategory(room);
191-
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
192-
return; // Nothing to do here.
193-
}
189+
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
190+
throw new Error(`Unsupported update cause: ${cause}`);
191+
}
194192

195-
const roomIdx = this.getRoomIndex(room);
196-
if (roomIdx === -1) {
197-
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
198-
}
193+
const category = this.getRoomCategory(room);
194+
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
195+
return; // Nothing to do here.
196+
}
199197

200-
// Try to avoid doing array operations if we don't have to: only move rooms within
201-
// the categories if we're jumping categories
202-
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
203-
if (oldCategory !== category) {
204-
// Move the room and update the indices
205-
this.moveRoomIndexes(1, oldCategory, category, this.indices);
206-
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
207-
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
208-
// Note: if moveRoomIndexes() is called after the splice then the insert operation
209-
// will happen in the wrong place. Because we would have already adjusted the index
210-
// for the category, we don't need to determine how the room is moving in the list.
211-
// If we instead tried to insert before updating the indices, we'd have to determine
212-
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
213-
// current position, as it'll affect the category's start index after we remove the
214-
// room from the array.
215-
}
198+
const roomIdx = this.getRoomIndex(room);
199+
if (roomIdx === -1) {
200+
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
201+
}
216202

217-
// Sort the category now that we've dumped the room in
218-
await this.sortCategory(category);
203+
// Try to avoid doing array operations if we don't have to: only move rooms within
204+
// the categories if we're jumping categories
205+
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
206+
if (oldCategory !== category) {
207+
// Move the room and update the indices
208+
this.moveRoomIndexes(1, oldCategory, category, this.indices);
209+
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
210+
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
211+
// Note: if moveRoomIndexes() is called after the splice then the insert operation
212+
// will happen in the wrong place. Because we would have already adjusted the index
213+
// for the category, we don't need to determine how the room is moving in the list.
214+
// If we instead tried to insert before updating the indices, we'd have to determine
215+
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
216+
// current position, as it'll affect the category's start index after we remove the
217+
// room from the array.
218+
}
219219

220-
return true; // change made
220+
// Sort the category now that we've dumped the room in
221+
await this.sortCategory(category);
222+
223+
return true; // change made
224+
} finally {
225+
await this.updateLock.release();
226+
}
221227
}
222228

223229
private async sortCategory(category: Category) {

src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,29 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
3838
}
3939

4040
public async handleRoomUpdate(room, cause): Promise<boolean> {
41-
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
42-
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
43-
if (!isSplice && !isInPlace) {
44-
throw new Error(`Unsupported update cause: ${cause}`);
45-
}
41+
try {
42+
await this.updateLock.acquireAsync();
4643

47-
if (cause === RoomUpdateCause.NewRoom) {
48-
this.cachedOrderedRooms.push(room);
49-
} else if (cause === RoomUpdateCause.RoomRemoved) {
50-
const idx = this.cachedOrderedRooms.indexOf(room);
51-
if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1);
52-
}
44+
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
45+
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
46+
if (!isSplice && !isInPlace) {
47+
throw new Error(`Unsupported update cause: ${cause}`);
48+
}
5349

54-
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14035
55-
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
56-
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
50+
if (cause === RoomUpdateCause.NewRoom) {
51+
this.cachedOrderedRooms.push(room);
52+
} else if (cause === RoomUpdateCause.RoomRemoved) {
53+
const idx = this.cachedOrderedRooms.indexOf(room);
54+
if (idx >= 0) this.cachedOrderedRooms.splice(idx, 1);
55+
}
5756

58-
return true;
57+
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14035
58+
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
59+
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
60+
61+
return true;
62+
} finally {
63+
await this.updateLock.release();
64+
}
5965
}
6066
}

src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
import { Room } from "matrix-js-sdk/src/models/room";
1818
import { RoomUpdateCause, TagID } from "../../models";
1919
import { SortAlgorithm } from "../models";
20+
import AwaitLock from "await-lock";
2021

2122
/**
2223
* Represents a list ordering algorithm. Subclasses should populate the
@@ -25,6 +26,7 @@ import { SortAlgorithm } from "../models";
2526
export abstract class OrderingAlgorithm {
2627
protected cachedOrderedRooms: Room[];
2728
protected sortingAlgorithm: SortAlgorithm;
29+
protected readonly updateLock = new AwaitLock();
2830

2931
protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
3032
// noinspection JSIgnoredPromiseFromCall

0 commit comments

Comments
 (0)