Skip to content

Commit 5d9f920

Browse files
committed
Making IgnoredInvites mostly sync.
Experience and feedback from matrix-org/matrix-react-sdk#9255 indicates that we cannot integrate an async `IgnoredInvites.getRuleForInvite`. This PR: - rewrites `getRuleForInvite` to be sync; - adds some locking within `IgnoredInvites` to avoid race conditions noticed by @t3chguy.
1 parent 81c3668 commit 5d9f920

File tree

4 files changed

+78
-54
lines changed

4 files changed

+78
-54
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"dependencies": {
5656
"@babel/runtime": "^7.12.5",
5757
"another-json": "^0.2.0",
58+
"await-lock": "^2.2.2",
5859
"bs58": "^5.0.0",
5960
"content-type": "^1.0.4",
6061
"loglevel": "^1.7.1",

spec/unit/matrix-client.spec.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,12 +1500,25 @@ describe("MatrixClient", function() {
15001500
expect(target1).toBe(target2);
15011501
});
15021502

1503-
it("should initialize and return the same `sources` consistently", async () => {
1504-
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
1505-
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
1506-
expect(sources1).toBeTruthy();
1503+
it("if there are no source rooms, it should return an empty list consistently", async () => {
1504+
const sources1 = client.ignoredInvites.getSourceRooms();
1505+
const sources2 = client.ignoredInvites.getSourceRooms();
1506+
expect(sources1).toHaveLength(0);
1507+
expect(sources1).toEqual(sources2);
1508+
});
1509+
1510+
it("if there are any source rooms, it should the same list consistently", async () => {
1511+
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
1512+
1513+
// Make sure that everything is initialized.
1514+
await client.joinRoom(NEW_SOURCE_ROOM_ID);
1515+
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
1516+
1517+
const sources1 = client.ignoredInvites.getSourceRooms();
1518+
const sources2 = client.ignoredInvites.getSourceRooms();
15071519
expect(sources1).toHaveLength(1);
15081520
expect(sources1).toEqual(sources2);
1521+
expect(sources1.map(room => room.roomId)).toContain(NEW_SOURCE_ROOM_ID);
15091522
});
15101523

15111524
it("should initially not reject any invite", async () => {
@@ -1517,6 +1530,7 @@ describe("MatrixClient", function() {
15171530
});
15181531

15191532
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
1533+
await client.ignoredInvites.getOrCreateTargetRoom();
15201534
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
15211535

15221536
// We should reject this invite.
@@ -1606,7 +1620,6 @@ describe("MatrixClient", function() {
16061620
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
16071621

16081622
// Make sure that everything is initialized.
1609-
await client.ignoredInvites.getOrCreateSourceRooms();
16101623
await client.joinRoom(NEW_SOURCE_ROOM_ID);
16111624
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
16121625

@@ -1669,8 +1682,8 @@ describe("MatrixClient", function() {
16691682
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
16701683

16711684
// Make sure that everything is initialized.
1672-
await client.ignoredInvites.getOrCreateSourceRooms();
16731685
await client.joinRoom(NEW_SOURCE_ROOM_ID);
1686+
await client.ignoredInvites.getOrCreateTargetRoom();
16741687
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
16751688

16761689
// Fetch the list of sources and check that we do not have the new room yet.

src/models/invites-ignorer.ts

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import AwaitLock from "await-lock";
1718
import { UnstableValue } from "matrix-events-sdk";
1819

1920
import { MatrixClient } from "../client";
@@ -73,6 +74,16 @@ export enum PolicyScope {
7374
* our data structures.
7475
*/
7576
export class IgnoredInvites {
77+
// A lock around method `getOrCreateTargetRoom`.
78+
// Used to ensure that only one async task of this class
79+
// is creating a new target room and modifying the
80+
// `target` property of account key `IGNORE_INVITES_POLICIES`.
81+
private _getOrCreateTargetRoomLock = new AwaitLock();
82+
83+
// A lock around method `withIgnoreInvitesPoliciesLock`.
84+
// Used to ensure that only one async task of this class is
85+
// modifying `IGNORE_INVITES_POLICIES` at any point in time.
86+
private _withIgnoreInvitesPoliciesLock = new AwaitLock();
7687
constructor(
7788
private readonly client: MatrixClient,
7889
) {
@@ -85,6 +96,12 @@ export class IgnoredInvites {
8596
* @param entity The entity covered by this rule. Globs are supported.
8697
* @param reason A human-readable reason for introducing this new rule.
8798
* @return The event id for the new rule.
99+
*
100+
* # Safety
101+
*
102+
* This method will rewrite the `Policies` object in the user's account data.
103+
* This rewrite is inherently racy and could overwrite or be overwritten by
104+
* other concurrent rewrites of the same object.
88105
*/
89106
public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> {
90107
const target = await this.getOrCreateTargetRoom();
@@ -121,11 +138,10 @@ export class IgnoredInvites {
121138
*/
122139
public async addSource(roomId: string): Promise<boolean> {
123140
// We attempt to join the room *before* calling
124-
// `await this.getOrCreateSourceRooms()` to decrease the duration
141+
// `await this.getSourceRooms()` to decrease the duration
125142
// of the racy section.
126143
await this.client.joinRoom(roomId);
127-
// Race starts.
128-
const sources = (await this.getOrCreateSourceRooms())
144+
const sources = this.getSourceRooms()
129145
.map(room => room.roomId);
130146
if (sources.includes(roomId)) {
131147
return false;
@@ -135,7 +151,6 @@ export class IgnoredInvites {
135151
ignoreInvitesPolicies.sources = sources;
136152
});
137153

138-
// Race ends.
139154
return true;
140155
}
141156

@@ -146,10 +161,10 @@ export class IgnoredInvites {
146161
* @param roomId The room to which the user is invited.
147162
* @returns A rule matching the entity, if any was found, `null` otherwise.
148163
*/
149-
public async getRuleForInvite({ sender, roomId }: {
164+
public getRuleForInvite({ sender, roomId }: {
150165
sender: string;
151166
roomId: string;
152-
}): Promise<Readonly<MatrixEvent | null>> {
167+
}): Readonly<MatrixEvent | null> {
153168
// In this implementation, we perform a very naive lookup:
154169
// - search in each policy room;
155170
// - turn each (potentially glob) rule entity into a regexp.
@@ -160,7 +175,7 @@ export class IgnoredInvites {
160175
// - match several entities per go;
161176
// - pre-compile each rule entity into a regexp;
162177
// - pre-compile entire rooms into a single regexp.
163-
const policyRooms = await this.getOrCreateSourceRooms();
178+
const policyRooms = this.getSourceRooms();
164179
const senderServer = sender.split(":")[1];
165180
const roomServer = roomId.split(":")[1];
166181
for (const room of policyRooms) {
@@ -229,21 +244,30 @@ export class IgnoredInvites {
229244
const room = this.client.getRoom(target);
230245
if (room) {
231246
return room;
232-
} else {
233-
target = null;
234247
}
235248
}
236-
// We need to create our own policy room for ignoring invites.
237-
target = (await this.client.createRoom({
238-
name: "Individual Policy Room",
239-
preset: Preset.PrivateChat,
240-
})).room_id;
241-
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
242-
ignoreInvitesPolicies.target = target;
243-
});
249+
try {
250+
// We need to create our own policy room for ignoring invites.
251+
await this._getOrCreateTargetRoomLock.acquireAsync();
252+
target = (await this.client.createRoom({
253+
name: "Individual Policy Room",
254+
preset: Preset.PrivateChat,
255+
})).room_id;
256+
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
257+
ignoreInvitesPolicies.target = target;
258+
if (!("sources" in ignoreInvitesPolicies)) {
259+
// `[target]` is a reasonable default for `sources`.
260+
ignoreInvitesPolicies.sources = [target];
261+
}
262+
});
244263

245-
// Since we have just called `createRoom`, `getRoom` should not be `null`.
246-
return this.client.getRoom(target)!;
264+
// Since we have just called `createRoom`, `getRoom` should not be `null`.
265+
// Note that this is unavoidably racy, e.g. another client could have left
266+
// the room during the call to `this.withIgnoreInvitesPolicies`.
267+
return this.client.getRoom(target)!;
268+
} finally {
269+
this._getOrCreateTargetRoomLock.release();
270+
}
247271
}
248272

249273
/**
@@ -260,40 +284,21 @@ export class IgnoredInvites {
260284
* This rewrite is inherently racy and could overwrite or be overwritten by
261285
* other concurrent rewrites of the same object.
262286
*/
263-
public async getOrCreateSourceRooms(): Promise<Room[]> {
287+
public getSourceRooms(): Room[] {
264288
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
265289
let sources = ignoreInvitesPolicies.sources;
266290

267291
// Validate `sources`. If it is invalid, trash out the current `sources`
268292
// and create a new list of sources from `target`.
269-
let hasChanges = false;
270293
if (!Array.isArray(sources)) {
271294
// `sources` could not be an array.
272-
hasChanges = true;
273295
sources = [];
274296
}
275-
let sourceRooms: Room[] = sources
297+
const sourceRooms: Room[] = sources
276298
// `sources` could contain non-string / invalid room ids
277299
.filter(roomId => typeof roomId === "string")
278300
.map(roomId => this.client.getRoom(roomId))
279301
.filter(room => !!room);
280-
if (sourceRooms.length != sources.length) {
281-
hasChanges = true;
282-
}
283-
if (sourceRooms.length == 0) {
284-
// `sources` could be empty (possibly because we've removed
285-
// invalid content)
286-
const target = await this.getOrCreateTargetRoom();
287-
hasChanges = true;
288-
sourceRooms = [target];
289-
}
290-
if (hasChanges) {
291-
// Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
292-
// during or by our call to `this.getTargetRoom()`.
293-
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
294-
ignoreInvitesPolicies.sources = sources;
295-
});
296-
}
297302
return sourceRooms;
298303
}
299304

@@ -315,10 +320,15 @@ export class IgnoredInvites {
315320
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
316321
*/
317322
private async withIgnoreInvitesPolicies(cb: (ignoreInvitesPolicies: {[key: string]: any}) => void) {
318-
const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
319-
cb(ignoreInvitesPolicies);
320-
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
321-
await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
323+
await this._withIgnoreInvitesPoliciesLock.acquireAsync();
324+
try {
325+
const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
326+
cb(ignoreInvitesPolicies);
327+
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
328+
await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
329+
} finally {
330+
this._withIgnoreInvitesPoliciesLock.release();
331+
}
322332
}
323333

324334
/**

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,10 +1367,10 @@
13671367
"@jridgewell/resolve-uri" "3.1.0"
13681368
"@jridgewell/sourcemap-codec" "1.4.14"
13691369

1370-
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz":
1371-
version "3.2.13"
1372-
uid "0109fde93bcc61def851f79826c9384c073b5175"
1373-
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz#0109fde93bcc61def851f79826c9384c073b5175"
1370+
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz":
1371+
version "3.2.12"
1372+
uid "0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9"
1373+
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz#0bce3c86f9d36a4984d3c3e07df1c3fb4c679bd9"
13741374

13751375
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
13761376
version "2.1.8-no-fsevents.3"

0 commit comments

Comments
 (0)