Skip to content

Commit b80ebe6

Browse files
committed
Merge branch 'develop' of https://github.com/AbstractPlay/node-backend into develop
2 parents 1863699 + 58899ba commit b80ebe6

File tree

2 files changed

+60
-27
lines changed

2 files changed

+60
-27
lines changed

api/abstractplay.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6956,6 +6956,9 @@ async function sendPush(opts: PushOptions) {
69566956
}
69576957

69586958
if(subscription !== undefined) {
6959+
// treat both 410 and 404 as permanent. There are a LOT of 404 web push errors in the logs and apparently these are because since June 2024 the Chrome/FCM push service has changed the way it reports stale or deleted web‑push registrations.
6960+
const PERMANENT_FAILURES = new Set([404, 410]);
6961+
69596962
try {
69606963
const options: RequestOptions = {
69616964
vapidDetails: {
@@ -6971,7 +6974,7 @@ async function sendPush(opts: PushOptions) {
69716974
console.log(`Result of webpush:`);
69726975
console.log(result);
69736976
} catch (err: any) {
6974-
if ( ("statusCode" in err) && (err.statusCode === 410) ) {
6977+
if ( ("statusCode" in err) && PERMANENT_FAILURES.has(err.statusCode)) {
69756978
await ddbDocClient.send(
69766979
new DeleteCommand({
69776980
TableName: process.env.ABSTRACT_PLAY_TABLE,

utils/starttournaments.ts

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ const headers = {
3535
'Access-Control-Allow-Origin': '*'
3636
};
3737

38+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
39+
40+
async function sendCommandWithRetry(command: any, maxRetries = 8, initialDelay = 100, maxDelay = 5000) {
41+
let retries = 0;
42+
while (retries < maxRetries) {
43+
try {
44+
// @ts-ignore
45+
return await ddbDocClient.send(command);
46+
} catch (err: any) {
47+
if (['ThrottlingException', 'ProvisionedThroughputExceededException', 'InternalServerError', 'ServiceUnavailable'].includes(err.name)) {
48+
retries++;
49+
if (retries >= maxRetries) {
50+
console.error(`Command failed after ${maxRetries} retries.`);
51+
throw err;
52+
}
53+
const delay = Math.min(initialDelay * Math.pow(2, retries - 1), maxDelay);
54+
const jitter = delay * 0.1 * Math.random();
55+
console.log(`Retryable error (${err.name}) caught. Retrying in ${Math.round(delay + jitter)}ms...`);
56+
await sleep(delay + jitter);
57+
} else {
58+
throw err;
59+
}
60+
}
61+
}
62+
}
63+
3864
// Types
3965
export type UserSettings = {
4066
[k: string]: any;
@@ -234,14 +260,14 @@ async function getPlayersSlowly(playerIDs: string[]) {
234260
const players: FullUser[] = [];
235261
for (const id of playerIDs) {
236262
try {
237-
const playerData = await ddbDocClient.send(
263+
const playerData = await sendCommandWithRetry(
238264
new GetCommand({
239265
TableName: process.env.ABSTRACT_PLAY_TABLE,
240266
Key: {
241267
"pk": "USER", "sk": id
242268
},
243269
})
244-
);
270+
) as { Item?: any };
245271
players.push(playerData.Item as FullUser);
246272
} catch (error) {
247273
logGetItemError(error);
@@ -255,29 +281,29 @@ function addToGameLists(type: string, game: Game, now: number, keepgame: boolean
255281
const work: Promise<any>[] = [];
256282
const sk = now + "#" + game.id;
257283
if (type === "COMPLETEDGAMES" && keepgame) {
258-
work.push(ddbDocClient.send(new PutCommand({
284+
work.push(sendCommandWithRetry(new PutCommand({
259285
TableName: process.env.ABSTRACT_PLAY_TABLE,
260286
Item: {
261287
"pk": type,
262288
"sk": sk,
263289
...game}
264290
})));
265-
work.push(ddbDocClient.send(new PutCommand({
291+
work.push(sendCommandWithRetry(new PutCommand({
266292
TableName: process.env.ABSTRACT_PLAY_TABLE,
267293
Item: {
268294
"pk": type + "#" + game.metaGame,
269295
"sk": sk,
270296
...game}
271297
})));
272298
game.players.forEach((player: { id: string; }) => {
273-
work.push(ddbDocClient.send(new PutCommand({
299+
work.push(sendCommandWithRetry(new PutCommand({
274300
TableName: process.env.ABSTRACT_PLAY_TABLE,
275301
Item: {
276302
"pk": type + "#" + player.id,
277303
"sk": sk,
278304
...game}
279305
})));
280-
work.push(ddbDocClient.send(new PutCommand({
306+
work.push(sendCommandWithRetry(new PutCommand({
281307
TableName: process.env.ABSTRACT_PLAY_TABLE,
282308
Item: {
283309
"pk": type + "#" + game.metaGame + "#" + player.id,
@@ -287,7 +313,7 @@ function addToGameLists(type: string, game: Game, now: number, keepgame: boolean
287313
});
288314
}
289315
if (type === "CURRENTGAMES") {
290-
work.push(ddbDocClient.send(new UpdateCommand({
316+
work.push(sendCommandWithRetry(new UpdateCommand({
291317
TableName: process.env.ABSTRACT_PLAY_TABLE,
292318
Key: { "pk": "METAGAMES", "sk": "COUNTS" },
293319
ExpressionAttributeNames: { "#g": game.metaGame },
@@ -301,7 +327,7 @@ function addToGameLists(type: string, game: Game, now: number, keepgame: boolean
301327
update += ", #g.completedgames :n";
302328
eavObj[":n"] = 1
303329
}
304-
work.push(ddbDocClient.send(new UpdateCommand({
330+
work.push(sendCommandWithRetry(new UpdateCommand({
305331
TableName: process.env.ABSTRACT_PLAY_TABLE,
306332
Key: { "pk": "METAGAMES", "sk": "COUNTS" },
307333
ExpressionAttributeNames: { "#g": game.metaGame },
@@ -352,7 +378,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
352378
// Cancel tournament. Everyone is gone.
353379
try {
354380
console.log(`Deleting tournament ${tournament.id}`);
355-
await ddbDocClient.send(
381+
await sendCommandWithRetry(
356382
new DeleteCommand({
357383
TableName: process.env.ABSTRACT_PLAY_TABLE,
358384
Key: {
@@ -361,7 +387,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
361387
},
362388
}));
363389
const sk = tournament.metaGame + "#" + tournament.variants.sort().join("|");
364-
await ddbDocClient.send(
390+
await sendCommandWithRetry(
365391
new UpdateCommand({
366392
TableName: process.env.ABSTRACT_PLAY_TABLE,
367393
Key: {"pk": "TOURNAMENTSCOUNTER", "sk": sk},
@@ -417,7 +443,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
417443
if (tournament.waiting !== true) {
418444
try {
419445
console.log(`Updating tournament ${tournament.id} to waiting`);
420-
await ddbDocClient.send(new UpdateCommand({
446+
await sendCommandWithRetry(new UpdateCommand({
421447
TableName: process.env.ABSTRACT_PLAY_TABLE,
422448
Key: { "pk": "TOURNAMENT", "sk": tournament.id },
423449
ExpressionAttributeValues: { ":t": true },
@@ -463,7 +489,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
463489
player.sk = tournament.id + "#" + division.toString() + '#' + player.playerid;
464490
try {
465491
console.log(`Adding player ${player.playerid} to tournament ${tournament.id} in division ${division}`);
466-
await ddbDocClient.send(new PutCommand({
492+
await sendCommandWithRetry(new PutCommand({
467493
TableName: process.env.ABSTRACT_PLAY_TABLE,
468494
Item: player
469495
}));
@@ -476,7 +502,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
476502
if (division > 1) {
477503
try {
478504
console.log(`Deleting player ${player.playerid} from tournament ${tournament.id} with division 1 (so they can be put in the right division)`);
479-
await ddbDocClient.send(new DeleteCommand({
505+
await sendCommandWithRetry(new DeleteCommand({
480506
TableName: process.env.ABSTRACT_PLAY_TABLE,
481507
Key: {
482508
"pk": "TOURNAMENTPLAYER", "sk": tournament.id + "#1#" + player.playerid
@@ -537,7 +563,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
537563
const state = engine.serialize();
538564
try {
539565
console.log(`Creating game ${gameId} for tournament ${tournament.id} with division ${division}`);
540-
await ddbDocClient.send(new PutCommand({
566+
await sendCommandWithRetry(new PutCommand({
541567
TableName: process.env.ABSTRACT_PLAY_TABLE,
542568
Item: {
543569
"pk": "GAME",
@@ -588,7 +614,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
588614
"player2": gamePlayers[1].id
589615
};
590616
console.log(`Adding game ${gameId} to TOURNAMENTGAME list`);
591-
await ddbDocClient.send(new PutCommand({
617+
await sendCommandWithRetry(new PutCommand({
592618
TableName: process.env.ABSTRACT_PLAY_TABLE,
593619
Item: tournamentGame
594620
}));
@@ -607,7 +633,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
607633
}
608634
const newTournamentid = uuid();
609635
console.log(`Updating tournament ${tournament.id} to started`);
610-
await ddbDocClient.send(new UpdateCommand({
636+
await sendCommandWithRetry(new UpdateCommand({
611637
TableName: process.env.ABSTRACT_PLAY_TABLE,
612638
Key: { "pk": "TOURNAMENT", "sk": tournament.id },
613639
ExpressionAttributeValues: { ":dt": now, ":t": true, ":nextid": newTournamentid, ":ds": divisions },
@@ -616,7 +642,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
616642
// open next tournament for sign-up.
617643
console.log(`Opening next tournament ${newTournamentid} for sign-up. Update TOURNAMENTSCOUNTER for '${tournament.metaGame}#${tournament.variants.sort().join("|")}'`);
618644
try {
619-
await ddbDocClient.send(new UpdateCommand({
645+
await sendCommandWithRetry(new UpdateCommand({
620646
TableName: process.env.ABSTRACT_PLAY_TABLE,
621647
Key: { "pk": "TOURNAMENTSCOUNTER", "sk": tournament.metaGame + "#" + tournament.variants.sort().join("|") },
622648
ExpressionAttributeValues: { ":inc": 1, ":f": false },
@@ -641,7 +667,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
641667
};
642668
console.log(`Creating new tournament ${newTournamentid}`);
643669
try {
644-
await ddbDocClient.send(new PutCommand({
670+
await sendCommandWithRetry(new PutCommand({
645671
TableName: process.env.ABSTRACT_PLAY_TABLE,
646672
Item: data
647673
}));
@@ -667,7 +693,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
667693
};
668694
try {
669695
console.log(`Adding player ${player.playerid} to new tournament ${newTournamentid}`);
670-
await ddbDocClient.send(new PutCommand({
696+
await sendCommandWithRetry(new PutCommand({
671697
TableName: process.env.ABSTRACT_PLAY_TABLE,
672698
Item: playerdata
673699
}));
@@ -704,7 +730,7 @@ async function startTournament(users: UserLastSeen[], tournament: Tournament) {
704730
if (remove.length > 0) {
705731
for (const player of remove) {
706732
console.log(`Deleting tournament player record for ${player.playerid} from tournament ${tournament.id}`);
707-
await ddbDocClient.send(
733+
await sendCommandWithRetry(
708734
new DeleteCommand({
709735
TableName: process.env.ABSTRACT_PLAY_TABLE,
710736
Key: {
@@ -754,7 +780,7 @@ async function updateUserGames(userId: string, gamesUpdate: undefined | number,
754780
gameIDsChanged.length = 0;
755781
if (gamesUpdate === undefined) {
756782
// Update "old" users. This is a one-time update.
757-
return ddbDocClient.send(new UpdateCommand({
783+
return sendCommandWithRetry(new UpdateCommand({
758784
TableName: process.env.ABSTRACT_PLAY_TABLE,
759785
Key: { "pk": "USER", "sk": userId },
760786
ExpressionAttributeValues: { ":val": 1, ":gs": games },
@@ -763,7 +789,7 @@ async function updateUserGames(userId: string, gamesUpdate: undefined | number,
763789
} else {
764790
console.log(`updateUserGames: optimistically updating games for ${userId}`);
765791
try {
766-
await ddbDocClient.send(new UpdateCommand({
792+
await sendCommandWithRetry(new UpdateCommand({
767793
TableName: process.env.ABSTRACT_PLAY_TABLE,
768794
Key: { "pk": "USER", "sk": userId },
769795
ExpressionAttributeValues: { ":val": gamesUpdate, ":inc": 1, ":gs": games },
@@ -777,14 +803,14 @@ async function updateUserGames(userId: string, gamesUpdate: undefined | number,
777803
console.log(`updateUserGames: games has been modified by another process for ${userId}`);
778804
let count = 0;
779805
while (count < 3) {
780-
const userData = await ddbDocClient.send(
806+
const userData = await sendCommandWithRetry(
781807
new GetCommand({
782808
TableName: process.env.ABSTRACT_PLAY_TABLE,
783809
Key: {
784810
"pk": "USER",
785811
"sk": userId
786812
},
787-
}));
813+
})) as { Item?: any };
788814
const user = userData.Item as FullUser;
789815
const dbGames = user.games;
790816
const gamesUpdate = user.gamesUpdate;
@@ -801,7 +827,7 @@ async function updateUserGames(userId: string, gamesUpdate: undefined | number,
801827
}
802828
try {
803829
console.log(`updateUserGames: Update ${count} of games for user`, userId, newgames);
804-
await ddbDocClient.send(new UpdateCommand({
830+
await sendCommandWithRetry(new UpdateCommand({
805831
TableName: process.env.ABSTRACT_PLAY_TABLE,
806832
Key: { "pk": "USER", "sk": userId },
807833
ExpressionAttributeValues: { ":val": gamesUpdate, ":inc": 1, ":gs": newgames },
@@ -810,7 +836,11 @@ async function updateUserGames(userId: string, gamesUpdate: undefined | number,
810836
}));
811837
return;
812838
} catch (err: any) {
813-
count++;
839+
if (err.name === 'ConditionalCheckFailedException') {
840+
count++;
841+
} else {
842+
throw err;
843+
}
814844
}
815845
}
816846
new Error(`updateUserGames: Unable to update games for user ${userId} after 3 retries`);

0 commit comments

Comments
 (0)