Skip to content

Commit fa501b1

Browse files
authored
Save hopefully ~$200/month (#183)
* Save hopefully ~$200/month Turns out that this consumes a lot of bandwidth from RTDB, I'm seeing like 8 GB/day. Which is kind of insane :( I think the bandwidth is being counted even for rows that are never returned from the query due to being filtered out. Firebase is just really inefficient. So it's iterating through basically the entire `gameData` database field every time I run this function, ugh. Making some changes to reduce expenditure. * Actually save money by improving indexing Why doesn't Firebase have query iterators…
1 parent 96277e0 commit fa501b1

File tree

4 files changed

+52
-29
lines changed

4 files changed

+52
-29
lines changed

database.rules.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
}
2929
},
3030
"gameData": {
31+
".indexOn": "populatedAt",
3132
"$gameId": {
3233
".read": "auth != null",
3334
"events": {

functions/src/index.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import PQueue from "p-queue";
77
import Stripe from "stripe";
88

99
import { GameMode, findSet, generateDeck, replayEvents } from "./game";
10-
import { databaseIterator, gzip } from "./utils";
10+
import { gzip } from "./utils";
1111

1212
initializeApp(); // Sets the default Firebase app.
1313

@@ -511,10 +511,24 @@ export const archiveStaleGames = functions
511511
const cutoff = Date.now() - 14 * 86400 * 1000; // 14 days ago
512512
const queue = new PQueue({ concurrency: 200 });
513513

514-
for await (const [gameId, gameState] of databaseIterator("gameData")) {
514+
const snap = await getDatabase()
515+
.ref("gameData")
516+
.orderByChild("populatedAt")
517+
.endBefore(cutoff)
518+
.get();
519+
520+
const childKeys: string[] = [];
521+
snap.forEach((child) => {
522+
childKeys.push(child.key);
523+
});
524+
525+
let archiveCount = 0;
526+
for (const gameId of childKeys) {
527+
const gameState = snap.child(gameId);
515528
const populatedAt: number | null = gameState.child("populatedAt").val();
516529
if (!populatedAt || populatedAt < cutoff) {
517530
await queue.onEmpty();
531+
archiveCount += 1;
518532
queue.add(async () => {
519533
console.log(`Archiving stale game state for ${gameId}`);
520534
await archiveGameState(gameId, gameState);
@@ -523,4 +537,5 @@ export const archiveStaleGames = functions
523537
}
524538

525539
await queue.onIdle();
540+
console.log(`Completed archive of ${archiveCount} games`);
526541
});

functions/src/utils.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,22 @@ export const gzip = {
3030
export async function* databaseIterator(
3131
path: string,
3232
batchSize = 1000,
33+
start?: string, // inclusive
34+
end?: string, // inclusive
3335
): AsyncGenerator<[string, DataSnapshot]> {
34-
let lastKey = null;
36+
let lastKey: string | undefined = undefined;
3537
while (true) {
36-
const snap = lastKey
37-
? await getDatabase()
38-
.ref(path)
39-
.orderByKey()
40-
.startAfter(lastKey)
41-
.limitToFirst(batchSize)
42-
.get()
43-
: await getDatabase()
44-
.ref(path)
45-
.orderByKey()
46-
.limitToFirst(batchSize)
47-
.get();
38+
let query = getDatabase().ref(path).orderByKey();
39+
if (lastKey !== undefined) {
40+
query = query.startAfter(lastKey);
41+
} else if (start !== undefined) {
42+
query = query.startAt(start);
43+
}
44+
if (end !== undefined) {
45+
query = query.endAt(end);
46+
}
47+
48+
const snap = await query.limitToFirst(batchSize).get();
4849
if (!snap.exists()) return;
4950

5051
const childKeys: string[] = [];

scripts/src/utils.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,29 @@ import { getDatabase } from "firebase-admin/database";
66
*
77
* @param {string} path The path to the reference to iterate through.
88
* @param {number} batchSize The number of children to fetch in each batch.
9+
* @param {string} [start] The key to start at (inclusive).
10+
* @param {string} [end] The key to end at (inclusive).
911
* @returns {AsyncGenerator<[string, import("firebase-admin/database").DataSnapshot]>}
1012
*/
11-
export async function* databaseIterator(path, batchSize = 1000) {
12-
let lastKey = null;
13+
export async function* databaseIterator(
14+
path,
15+
batchSize = 1000,
16+
start, // inclusive
17+
end, // inclusive
18+
) {
19+
let lastKey = undefined;
1320
while (true) {
14-
const snap = lastKey
15-
? await getDatabase()
16-
.ref(path)
17-
.orderByKey()
18-
.startAfter(lastKey)
19-
.limitToFirst(batchSize)
20-
.get()
21-
: await getDatabase()
22-
.ref(path)
23-
.orderByKey()
24-
.limitToFirst(batchSize)
25-
.get();
21+
let query = getDatabase().ref(path).orderByKey();
22+
if (lastKey !== undefined) {
23+
query = query.startAfter(lastKey);
24+
} else if (start !== undefined) {
25+
query = query.startAt(start);
26+
}
27+
if (end !== undefined) {
28+
query = query.endAt(end);
29+
}
30+
31+
const snap = await query.limitToFirst(batchSize).get();
2632
if (!snap.exists()) return;
2733

2834
const childKeys = [];

0 commit comments

Comments
 (0)