Skip to content

Commit 071b489

Browse files
committed
Add try/finally around cache locking
1 parent b4dfb79 commit 071b489

5 files changed

Lines changed: 225 additions & 186 deletions

File tree

src/server.ts

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,47 @@ const hook = new Webhooks({
2020
hook.on("push", async ({ payload: data }) => {
2121
if (data.ref === "refs/heads/dev") {
2222
const key = await Cache.lock();
23-
const cache = await Cache.read(key);
24-
const counts: Record<string, number> = {};
25-
const users: Array<string> = [];
26-
for (const commit of data.commits) {
27-
const name = commit.author.username ?? commit.author.name;
28-
if (cache.connections[name.toLowerCase()]) {
29-
counts[name] = (counts[name.toLowerCase()] || 0) + 1;
30-
if (!users.includes(name)) {
31-
users.push(name);
23+
try {
24+
const cache = await Cache.read(key);
25+
const counts: Record<string, number> = {};
26+
const users: Array<string> = [];
27+
for (const commit of data.commits) {
28+
const name = commit.author.username ?? commit.author.name;
29+
if (cache.connections[name.toLowerCase()]) {
30+
counts[name] = (counts[name.toLowerCase()] || 0) + 1;
31+
if (!users.includes(name)) {
32+
users.push(name);
33+
}
3234
}
3335
}
34-
}
3536

36-
let modified = false;
37-
for (const user of users) {
38-
const conn = cache.connections[user.toLowerCase()];
39-
if (conn) {
40-
const helper = client.rest.oauth.getHelper(`Bearer ${EncryptionHandler.decrypt(conn.accessToken)}`);
41-
try {
42-
await helper.updateRoleConnection(Config.client.id, {
43-
metadata: {
44-
commits: String(conn.commits + counts[user])
45-
},
46-
platformName: "Github",
47-
platformUsername: user
48-
});
49-
cache.connections[user.toLowerCase()].commits += counts[user];
50-
} catch {
51-
delete cache.connections[user.toLowerCase()];
37+
let modified = false;
38+
for (const user of users) {
39+
const conn = cache.connections[user.toLowerCase()];
40+
if (conn) {
41+
const helper = client.rest.oauth.getHelper(`Bearer ${EncryptionHandler.decrypt(conn.accessToken)}`);
42+
try {
43+
await helper.updateRoleConnection(Config.client.id, {
44+
metadata: {
45+
commits: String(conn.commits + counts[user])
46+
},
47+
platformName: "Github",
48+
platformUsername: user
49+
});
50+
cache.connections[user.toLowerCase()].commits += counts[user];
51+
} catch {
52+
delete cache.connections[user.toLowerCase()];
53+
}
54+
modified = true;
5255
}
53-
modified = true;
5456
}
55-
}
5657

57-
if (modified) {
58-
await Cache.write(cache, key);
58+
if (modified) {
59+
await Cache.write(cache, key);
60+
}
61+
} finally {
62+
await Cache.unlock(key);
5963
}
60-
await Cache.unlock(key);
6164
} else if (data.ref.startsWith("refs/tags/")) {
6265
const version = data.ref.slice(11);
6366
void generate(version);
@@ -118,13 +121,16 @@ const app = express()
118121
});
119122

120123
const key = await Cache.lock();
121-
const cache = await Cache.read(key);
122-
cache.connections[name.toLowerCase()] = {
123-
accessToken: EncryptionHandler.encrypt(token.accessToken),
124-
commits: commitCount
125-
};
126-
await Cache.write(cache, key);
127-
await Cache.unlock(key);
124+
try {
125+
const cache = await Cache.read(key);
126+
cache.connections[name.toLowerCase()] = {
127+
accessToken: EncryptionHandler.encrypt(token.accessToken),
128+
commits: commitCount
129+
};
130+
await Cache.write(cache, key);
131+
} finally {
132+
await Cache.unlock(key);
133+
}
128134
return res.status(200).end(`Successfully linked via @${name}`);
129135
} else {
130136
return res.status(400).end("You have not contributed. Please make sure your github account is linked to your Discord account before attempting this.");

src/util/Cache.ts

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,65 +26,86 @@ export interface Snipe {
2626
// this definitely has problems, but it'll work good enough
2727
export default class Cache {
2828
private static lockKey: string | null = null;
29+
private static releaseLock: (() => Promise<void>) | null = null;
2930

3031
static async lock() {
31-
await lock(`${Config.dataDir}/cache.json`, { retries: { retries: 10, factor: 1, minTimeout: 1000, maxTimeout: 10000 } });
32+
if (this.lockKey !== null) {
33+
throw new Error("Attempted to lock cache while another cache lock is already active in this process");
34+
}
35+
36+
const release = await lock(`${Config.dataDir}/cache.json`, { retries: { retries: 10, factor: 1, minTimeout: 1000, maxTimeout: 10000 } });
3237

3338
this.lockKey = randomBytes(16).toString("hex");
39+
this.releaseLock = release;
3440
return this.lockKey;
3541
}
3642

3743
static async read(key?: string) {
3844
let didLock = false;
45+
let lockKey = key;
3946
if (key === undefined) {
40-
key = await this.lock();
47+
lockKey = await this.lock();
4148
didLock = true;
4249
}
4350

44-
if (key !== this.lockKey) {
51+
if (lockKey !== this.lockKey) {
4552
throw new Error("Attempted to read cache with invalid key");
4653
}
4754

48-
let data: ICache | undefined;
49-
if (await exists(`${Config.dataDir}/cache.json`)) {
50-
try {
51-
data = JSON.parse(await readFile(`${Config.dataDir}/cache.json`, "utf8")) as ICache;
52-
} catch (err) {
53-
await handleError("Failed To Load Cache", err as Error);
54-
throw err; // rethrow so we don't accidentally delete the file or something dumb
55+
try {
56+
let data: ICache | undefined;
57+
if (await exists(`${Config.dataDir}/cache.json`)) {
58+
try {
59+
data = JSON.parse(await readFile(`${Config.dataDir}/cache.json`, "utf8")) as ICache;
60+
} catch (err) {
61+
await handleError("Failed To Load Cache", err as Error);
62+
throw err; // rethrow so we don't accidentally delete the file or something dumb
63+
}
5564
}
56-
}
5765

58-
if (didLock) {
59-
await this.unlock(key);
66+
return data ?? { commands: [], commandIDs: {}, commit: null, connections: {}, pulls: [], snipes: [] } satisfies ICache;
67+
} finally {
68+
if (didLock) {
69+
await this.unlock(lockKey);
70+
}
6071
}
61-
62-
return data ?? { commands: [], commandIDs: {}, commit: null, connections: {}, pulls: [], snipes: [] } satisfies ICache;
6372
}
6473

6574
static async unlock(key: string) {
6675
if (this.lockKey !== key) {
6776
throw new Error("Attempted to unlock cache with invalid key");
6877
}
6978

70-
await unlock(`${Config.dataDir}/cache.json`);
79+
const release = this.releaseLock;
80+
this.releaseLock = null;
7181
this.lockKey = null;
82+
83+
if (release) {
84+
await release();
85+
return;
86+
}
87+
88+
await unlock(`${Config.dataDir}/cache.json`);
7289
}
7390

7491
static async write(data: ICache, key?: string) {
7592
let didLock = false;
93+
let lockKey = key;
7694
if (key === undefined) {
77-
key = await this.lock();
95+
lockKey = await this.lock();
7896
didLock = true;
7997
}
8098

81-
if (key !== this.lockKey) {
99+
if (lockKey !== this.lockKey) {
82100
throw new Error("Attempted to write cache with invalid key");
83101
}
84102

85-
await writeFile(`${Config.dataDir}/cache.json`, JSON.stringify(data, null, 2));
86-
if (didLock) {
87-
await this.unlock(key);
103+
try {
104+
await writeFile(`${Config.dataDir}/cache.json`, JSON.stringify(data, null, 2));
105+
} finally {
106+
if (didLock) {
107+
await this.unlock(lockKey);
108+
}
88109
}
89110
}
90111
}

src/util/Commands.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,24 @@ export default class Commands {
3030
static async register(client: Client) {
3131
const commands = this.toJSON();
3232
const key = await Cache.lock();
33-
const cache = await Cache.read(key);
34-
if (JSON.stringify(commands) !== JSON.stringify(cache.commands)) {
35-
let ids: Record<string, string>;
36-
try {
37-
ids = Object.fromEntries((await client.application.bulkEditGuildCommands(Config.guild, commands)).map(b => [b.name, b.id]));
38-
} catch (err) {
39-
console.log("Command registration error, index list:");
40-
console.log(commands.map((c, i) => `${i}: ${c.name}`).join("\n"));
41-
throw err;
33+
try {
34+
const cache = await Cache.read(key);
35+
if (JSON.stringify(commands) !== JSON.stringify(cache.commands)) {
36+
let ids: Record<string, string>;
37+
try {
38+
ids = Object.fromEntries((await client.application.bulkEditGuildCommands(Config.guild, commands)).map(b => [b.name, b.id]));
39+
} catch (err) {
40+
console.log("Command registration error, index list:");
41+
console.log(commands.map((c, i) => `${i}: ${c.name}`).join("\n"));
42+
throw err;
43+
}
44+
cache.commands = commands;
45+
cache.commandIDs = ids;
46+
await Cache.write(cache, key);
4247
}
43-
cache.commands = commands;
44-
cache.commandIDs = ids;
45-
await Cache.write(cache, key);
48+
} finally {
49+
await Cache.unlock(key);
4650
}
47-
await Cache.unlock(key);
4851
}
4952

5053
static toJSON() {

0 commit comments

Comments
 (0)