Skip to content

Commit 622511c

Browse files
committed
Merge branch 'master' of https://github.com/ajayyy/SponsorBlockServer into more-coverage
2 parents 9286f16 + c2acc62 commit 622511c

16 files changed

+137
-77
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ RUN npm ci && npm run tsc
66

77
FROM node:16-alpine as app
88
WORKDIR /usr/src/app
9-
RUN apk add git postgresql-client
9+
RUN apk add --no-cache git postgresql-client
1010
COPY --from=builder ./node_modules ./node_modules
1111
COPY --from=builder ./dist ./dist
1212
COPY ./.git ./.git
1313
COPY entrypoint.sh .
1414
COPY databases/*.sql databases/
1515
EXPOSE 8080
16-
CMD ./entrypoint.sh
16+
CMD ./entrypoint.sh

src/config.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ addDefaults(config, {
7676
port: 5432,
7777
max: 10,
7878
idleTimeoutMillis: 10000,
79-
maxTries: 3,
80-
maxConcurrentRequests: 3500
79+
maxTries: 3
8180
},
8281
postgresReadOnly: {
8382
enabled: false,
@@ -91,7 +90,7 @@ addDefaults(config, {
9190
idleTimeoutMillis: 10000,
9291
maxTries: 3,
9392
fallbackOnFail: true,
94-
maxConcurrentRequests: 3500
93+
stopRetryThreshold: 800
9594
},
9695
dumpDatabase: {
9796
enabled: false,
@@ -139,6 +138,15 @@ addDefaults(config, {
139138
expiryTime: 24 * 60 * 60,
140139
getTimeout: 40
141140
},
141+
redisRead: {
142+
enabled: false,
143+
socket: {
144+
host: "",
145+
port: 0
146+
},
147+
disableOfflineQueue: true,
148+
weight: 1
149+
},
142150
patreon: {
143151
clientId: "",
144152
clientSecret: "",

src/databases/Postgres.ts

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ export class Postgres implements IDatabase {
3333
private poolRead: Pool;
3434
private lastPoolReadFail = 0;
3535

36-
private concurrentRequests = 0;
37-
private concurrentReadRequests = 0;
36+
activePostgresRequests = 0;
3837

3938
constructor(private config: DatabaseConfig) {}
4039

@@ -54,19 +53,23 @@ export class Postgres implements IDatabase {
5453
});
5554

5655
if (this.config.postgresReadOnly && this.config.postgresReadOnly.enabled) {
57-
this.poolRead = new Pool({
58-
...this.config.postgresReadOnly
59-
});
60-
this.poolRead.on("error", (err, client) => {
61-
Logger.error(err.stack);
62-
this.lastPoolReadFail = Date.now();
63-
64-
try {
65-
client.release(true);
66-
} catch (err) {
67-
Logger.error(`poolRead (postgres): ${err}`);
68-
}
69-
});
56+
try {
57+
this.poolRead = new Pool({
58+
...this.config.postgresReadOnly
59+
});
60+
this.poolRead.on("error", (err, client) => {
61+
Logger.error(err.stack);
62+
this.lastPoolReadFail = Date.now();
63+
64+
try {
65+
client.release(true);
66+
} catch (err) {
67+
Logger.error(`poolRead (postgres): ${err}`);
68+
}
69+
});
70+
} catch (e) {
71+
Logger.error(`poolRead (postgres): ${e}`);
72+
}
7073
}
7174

7275
if (!this.config.readOnly) {
@@ -102,22 +105,6 @@ export class Postgres implements IDatabase {
102105

103106
Logger.debug(`prepare (postgres): type: ${type}, query: ${query}, params: ${params}`);
104107

105-
if (this.config.readOnly) {
106-
if (this.concurrentReadRequests > this.config.postgresReadOnly?.maxConcurrentRequests) {
107-
Logger.error(`prepare (postgres): cancelling read query because too many concurrent requests, query: ${query}`);
108-
throw new Error("Too many concurrent requests");
109-
}
110-
111-
this.concurrentReadRequests++;
112-
} else {
113-
if (this.concurrentRequests > this.config.postgres.maxConcurrentRequests) {
114-
Logger.error(`prepare (postgres): cancelling query because too many concurrent requests, query: ${query}`);
115-
throw new Error("Too many concurrent requests");
116-
}
117-
118-
this.concurrentRequests++;
119-
}
120-
121108
const pendingQueries: PromiseWithState<QueryResult<any>>[] = [];
122109
let tries = 0;
123110
let lastPool: Pool = null;
@@ -127,19 +114,15 @@ export class Postgres implements IDatabase {
127114
tries++;
128115

129116
try {
117+
this.activePostgresRequests++;
130118
lastPool = this.getPool(type, options);
131119

132120
pendingQueries.push(savePromiseState(lastPool.query({ text: query, values: params })));
133121
const currentPromises = [...pendingQueries];
134122
if (options.useReplica && maxTries() - tries > 1) currentPromises.push(savePromiseState(timeoutPomise(this.config.postgresReadOnly.readTimeout)));
135123
const queryResult = await nextFulfilment(currentPromises);
136124

137-
if (this.config.readOnly) {
138-
this.concurrentReadRequests--;
139-
} else {
140-
this.concurrentRequests--;
141-
}
142-
125+
this.activePostgresRequests--;
143126
switch (type) {
144127
case "get": {
145128
const value = queryResult.rows[0];
@@ -159,30 +142,30 @@ export class Postgres implements IDatabase {
159142
if (lastPool === this.pool) {
160143
// Only applies if it is get or all request
161144
options.forceReplica = true;
162-
} else if (lastPool === this.poolRead && maxTries() - tries <= 1) {
163-
options.useReplica = false;
145+
} else if (lastPool === this.poolRead) {
146+
this.lastPoolReadFail = Date.now();
147+
148+
if (maxTries() - tries <= 1) {
149+
options.useReplica = false;
150+
}
164151
}
165152

166153
Logger.error(`prepare (postgres) try ${tries}: ${err}`);
167154
}
168-
} while (this.isReadQuery(type) && tries < maxTries());
169-
170-
if (this.config.readOnly) {
171-
this.concurrentReadRequests--;
172-
} else {
173-
this.concurrentRequests--;
174-
}
155+
} while (this.isReadQuery(type) && tries < maxTries()
156+
&& this.activePostgresRequests < this.config.postgresReadOnly.stopRetryThreshold);
175157

158+
this.activePostgresRequests--;
176159
throw new Error(`prepare (postgres): ${type} ${query} failed after ${tries} tries`);
177160
}
178161

179162
private getPool(type: string, options: QueryOption): Pool {
180163
const readAvailable = this.poolRead && options.useReplica && this.isReadQuery(type);
181-
const ignroreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail
164+
const ignoreReadDueToFailure = this.config.postgresReadOnly.fallbackOnFail
182165
&& this.lastPoolReadFail > Date.now() - 1000 * 30;
183166
const readDueToFailure = this.config.postgresReadOnly.fallbackOnFail
184167
&& this.lastPoolFail > Date.now() - 1000 * 30;
185-
if (readAvailable && !ignroreReadDueToFailure && (options.forceReplica || readDueToFailure ||
168+
if (readAvailable && !ignoreReadDueToFailure && (options.forceReplica || readDueToFailure ||
186169
Math.random() > 1 / (this.config.postgresReadOnly.weight + 1))) {
187170
return this.poolRead;
188171
} else {

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ async function init() {
1212
process.exit(1);
1313
});
1414

15-
await initDb();
15+
try {
16+
await initDb();
17+
} catch (e) {
18+
Logger.error(`Init Db: ${e}`);
19+
process.exit(1);
20+
}
21+
1622
// edge case clause for creating compatible .db files, do not enable
1723
if (config.mode === "init-db-and-exit") process.exit(0);
1824
// do not enable init-db-only mode for usage.
@@ -27,4 +33,4 @@ async function init() {
2733
}).setTimeout(15000);
2834
}
2935

30-
init().catch((err) => Logger.error(err));
36+
init().catch((err) => Logger.error(`Index.js: ${err}`));

src/routes/generateToken.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Request, Response } from "express";
22
import { config } from "../config";
33
import { createAndSaveToken, TokenType } from "../utils/tokenUtils";
4-
4+
import { getHashCache } from "../utils/getHashCache";
55

66
interface GenerateTokenRequest extends Request {
77
query: {
@@ -15,12 +15,13 @@ interface GenerateTokenRequest extends Request {
1515

1616
export async function generateTokenRequest(req: GenerateTokenRequest, res: Response): Promise<Response> {
1717
const { query: { code, adminUserID }, params: { type } } = req;
18+
const adminUserIDHash = adminUserID ? (await getHashCache(adminUserID)) : null;
1819

1920
if (!code || !type) {
2021
return res.status(400).send("Invalid request");
2122
}
2223

23-
if (type === TokenType.patreon || (type === TokenType.local && adminUserID === config.adminUserID)) {
24+
if (type === TokenType.patreon || (type === TokenType.local && adminUserIDHash === config.adminUserID)) {
2425
const licenseKey = await createAndSaveToken(type, code);
2526

2627
/* istanbul ignore else */

src/routes/getSearchSegments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type searchSegmentResponse = {
1414
function getSegmentsFromDBByVideoID(videoID: VideoID, service: Service): Promise<DBSegment[]> {
1515
return db.prepare(
1616
"all",
17-
`SELECT "UUID", "timeSubmitted", "startTime", "endTime", "category", "actionType", "votes", "views", "locked", "hidden", "shadowHidden", "userID" FROM "sponsorTimes"
17+
`SELECT "UUID", "timeSubmitted", "startTime", "endTime", "category", "actionType", "votes", "views", "locked", "hidden", "shadowHidden", "userID", "description" FROM "sponsorTimes"
1818
WHERE "videoID" = ? AND "service" = ? ORDER BY "timeSubmitted"`,
1919
[videoID, service]
2020
) as Promise<DBSegment[]>;

src/routes/getStatus.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,31 @@ import { db } from "../databases/databases";
22
import { Logger } from "../utils/logger";
33
import { Request, Response } from "express";
44
import os from "os";
5-
import redis from "../utils/redis";
5+
import redis, { getRedisActiveRequests } from "../utils/redis";
66
import { promiseOrTimeout } from "../utils/promise";
7+
import { Postgres } from "../databases/Postgres";
78

89
export async function getStatus(req: Request, res: Response): Promise<Response> {
910
const startTime = Date.now();
1011
let value = req.params.value as string[] | string;
1112
value = Array.isArray(value) ? value[0] : value;
1213
let processTime, redisProcessTime = -1;
1314
try {
15+
const dbStartTime = Date.now();
1416
const dbVersion = await promiseOrTimeout(db.prepare("get", "SELECT key, value FROM config where key = ?", ["version"]), 5000)
1517
.then(e => {
16-
processTime = Date.now() - startTime;
18+
processTime = Date.now() - dbStartTime;
1719
return e.value;
1820
})
1921
.catch(e => /* istanbul ignore next */ {
2022
Logger.error(`status: SQL query timed out: ${e}`);
2123
return -1;
2224
});
2325
let statusRequests: unknown = 0;
26+
const redisStartTime = Date.now();
2427
const numberRequests = await promiseOrTimeout(redis.increment("statusRequest"), 5000)
2528
.then(e => {
26-
redisProcessTime = Date.now() - startTime;
29+
redisProcessTime = Date.now() - redisStartTime;
2730
return e;
2831
}).catch(e => /* istanbul ignore next */ {
2932
Logger.error(`status: redis increment timed out ${e}`);
@@ -40,7 +43,9 @@ export async function getStatus(req: Request, res: Response): Promise<Response>
4043
redisProcessTime,
4144
loadavg: os.loadavg().slice(1), // only return 5 & 15 minute load average
4245
statusRequests,
43-
hostname: os.hostname()
46+
hostname: os.hostname(),
47+
activePostgresRequests: (db as Postgres)?.activePostgresRequests,
48+
activeRedisRequests: getRedisActiveRequests(),
4449
};
4550
return value ? res.send(JSON.stringify(statusValues[value])) : res.send(statusValues);
4651
} catch (err) /* istanbul ignore next */ {

src/routes/getTopCategoryUsers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async function generateTopCategoryUsersStats(sortBy: string, category: string) {
2626
SUM("votes") as "userVotes", COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
2727
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
2828
WHERE "sponsorTimes"."category" = ? AND "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
29-
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
29+
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 2
3030
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds, category]);
3131

3232
if (rows) {

src/routes/getTopUsers.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
2828
SUM(CASE WHEN category = 'poi_highlight' THEN 1 ELSE 0 END) as "categorySumHighlight",
2929
SUM(CASE WHEN category = 'filler' THEN 1 ELSE 0 END) as "categorySumFiller",
3030
SUM(CASE WHEN category = 'exclusive_access' THEN 1 ELSE 0 END) as "categorySumExclusiveAccess",
31+
SUM(CASE WHEN category = 'chapter' THEN 1 ELSE 0 END) as "categorySumChapter",
3132
`;
3233
}
3334

3435
const rows = await db.prepare("all", `SELECT COUNT(*) as "totalSubmissions", SUM(views) as "viewCount",
35-
SUM(((CASE WHEN "sponsorTimes"."endTime" - "sponsorTimes"."startTime" > ? THEN ? ELSE "sponsorTimes"."endTime" - "sponsorTimes"."startTime" END) / 60) * "sponsorTimes"."views") as "minutesSaved",
36+
SUM(CASE WHEN "sponsorTimes"."actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "sponsorTimes"."endTime" - "sponsorTimes"."startTime" > ? THEN ? ELSE "sponsorTimes"."endTime" - "sponsorTimes"."startTime" END) / 60) * "sponsorTimes"."views" END) as "minutesSaved",
3637
SUM("votes") as "userVotes", ${additionalFields} COALESCE("userNames"."userName", "sponsorTimes"."userID") as "userName" FROM "sponsorTimes" LEFT JOIN "userNames" ON "sponsorTimes"."userID"="userNames"."userID"
3738
LEFT JOIN "shadowBannedUsers" ON "sponsorTimes"."userID"="shadowBannedUsers"."userID"
38-
WHERE "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "sponsorTimes"."actionType" != 'chapter' AND "shadowBannedUsers"."userID" IS NULL
39+
WHERE "sponsorTimes"."votes" > -1 AND "sponsorTimes"."shadowHidden" != 1 AND "shadowBannedUsers"."userID" IS NULL
3940
GROUP BY COALESCE("userName", "sponsorTimes"."userID") HAVING SUM("votes") > 20
4041
ORDER BY "${sortBy}" DESC LIMIT 100`, [maxRewardTimePerSegmentInSeconds, maxRewardTimePerSegmentInSeconds]);
4142

@@ -55,7 +56,8 @@ async function generateTopUsersStats(sortBy: string, categoryStatsEnabled = fals
5556
row.categorySumPreview,
5657
row.categorySumHighlight,
5758
row.categorySumFiller,
58-
row.categorySumExclusiveAccess
59+
row.categorySumExclusiveAccess,
60+
row.categorySumChapter
5961
]);
6062
}
6163
}

src/routes/getUserInfo.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ async function getPermissions(userID: HashedUserID): Promise<Record<string, bool
118118

119119
async function getFreeChaptersAccess(userID: HashedUserID): Promise<boolean> {
120120
return await oneOf([isUserVIP(userID),
121-
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "reputation" > 0 AND "timeSubmitted" < 1663872563000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))(),
122-
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "timeSubmitted" < 1590969600000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))()
121+
(async () => !!(await db.prepare("get", `SELECT "timeSubmitted" FROM "sponsorTimes" WHERE "timeSubmitted" < 1666126187000 AND "userID" = ? LIMIT 1`, [userID], { useReplica: true })))()
123122
]);
124123
}
125124

0 commit comments

Comments
 (0)