Skip to content

Commit

Permalink
feat(functions-load-balancing): update functions to new connections l…
Browse files Browse the repository at this point in the history
…ogic
  • Loading branch information
albertodigioacchino committed Jan 28, 2021
1 parent d7e425e commit efc295e
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 124 deletions.

This file was deleted.

18 changes: 6 additions & 12 deletions packages/functions/src/load-balancing/onOnlineGameStatusCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,30 @@ import * as admin from "firebase-admin";
import {FirebaseCollection, RTDBInstance, RTDBPaths} from "@pipeline/common";
import {PROJECT_ID} from "../utils/rtdb";
import FieldValue = admin.firestore.FieldValue;
import {Status} from "../models/Status";

const db = admin.firestore();
const logger = functions.logger;

const INSTANCE_ID = `${PROJECT_ID}-default-rtdb`

/**
* It triggers when the path /statuses/{userId} of that RTDB instance is created.
*
* If the new one is 'online', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by +1
* If the new one is 'offline', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by -1
* It triggers when the path /connections/{gameId}/{userId} of that RTDB instance is created.
*
* The proper document of Firestore, representing that RTDB instance, is updated incrementing by 1
*
*/

export const onOnlineGameStatusCreate = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Statuses}/{userId}`)
export const onOnlineGameStatusCreate = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onCreate(async (snapshot, context) => {

const instanceId = INSTANCE_ID;
const userId = context.params.userId;
const gameId = context.params.gameId;

const status = snapshot.val() as Status;

logger.log(`User ${userId} just created status as ${status.state}`);
logger.log(`User ${userId} just created connection for game ${gameId}`);
const docInstanceId = instanceId.split(`${PROJECT_ID}-`)[1];
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
onlineOnGameCount: status.state === 'online' ?
FieldValue.increment(1) as any :
FieldValue.increment(-1) as any,
connectionsCount: FieldValue.increment(1) as any
} as Partial<RTDBInstance>);
});
28 changes: 16 additions & 12 deletions packages/functions/src/load-balancing/onOnlineGameStatusDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,40 @@ import * as admin from "firebase-admin";
import {FirebaseCollection, RTDBInstance, RTDBPaths} from "@pipeline/common";
import {PROJECT_ID} from "../utils/rtdb";
import FieldValue = admin.firestore.FieldValue;
import {Status} from "../models/Status";
import {handleLockedCards, handleMoveGame, INSTANCE_NAME} from "./utils";

const db = admin.firestore();
const logger = functions.logger;

const INSTANCE_ID = `${PROJECT_ID}-default-rtdb`

/**
* It triggers when the path /statuses/{userId} of that RTDB instance is deleted.
*
* If the new one is 'online', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by +1
* If the new one is 'offline', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by -1
* It triggers when the path /connections/{gameId}/{userId} of that RTDB instance is deleted.
*
* The proper document of Firestore, representing that RTDB instance, is updated incrementing by -1
*
* Next, an RTDB query is performed to look for the online users for those games.
* If more than one were found (because one is the one we were updating), it means there is still someone in the game
* Otherwise, we can move the game from RTDB back to Firestore, for each game.
*/

export const onOnlineGameStatusDelete = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Statuses}/{userId}`)
export const onOnlineGameStatusDelete = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onDelete(async (snapshot, context) => {

const instanceId = INSTANCE_ID;
const userId = context.params.userId;
const gameId = context.params.gameId;

const status = snapshot.val() as Status;

logger.log(`User ${userId} just deleted status as ${status.state}`);
logger.log(`User ${userId} just closed all connections for game ${gameId}`);
const docInstanceId = instanceId.split(`${PROJECT_ID}-`)[1];
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
onlineOnGameCount: status.state === 'online' ?
FieldValue.increment(1) as any :
FieldValue.increment(-1) as any,
connectionsCount: FieldValue.increment(-1) as any,
} as Partial<RTDBInstance>);

const rtdb = admin.app().database(`https://${INSTANCE_NAME}.firebasedatabase.app`);
await handleLockedCards(gameId, rtdb, userId);
logger.log('Locked cards handled');
await handleMoveGame(gameId, rtdb);
logger.log('Locked cards handled');
});
82 changes: 21 additions & 61 deletions packages/functions/src/load-balancing/onOnlineGameStatusUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,19 @@ import * as functions from 'firebase-functions';
import * as admin from "firebase-admin";
import {FirebaseCollection, RTDBInstance, RTDBPaths} from "@pipeline/common";
import {PROJECT_ID} from "../utils/rtdb";
import {moveGameFromRTDBToFirestore} from "./moveGameFromRTDBToFirestore";
import FieldValue = admin.firestore.FieldValue;
import {Status} from "../models/Status";
import {INSTANCE_ID} from "./utils";

const db = admin.firestore();
const logger = functions.logger;

const INSTANCE_ID = `${PROJECT_ID}-default-rtdb`
const INSTANCE_NAME = `${INSTANCE_ID}.europe-west1`

async function handleMoveGame(nextStatus: Status, rtdb: admin.database.Database) {
for (const gameId of Object.keys(nextStatus.gameIds!)) {
if (nextStatus.gameIds![gameId]) {
const snap = await rtdb.ref(`/${RTDBPaths.Statuses}`).orderByChild(`gameIds/${gameId}`).equalTo(true).get();
const statuses: Status[] = [];
snap.forEach(s => {
statuses.push(s.val());
});
const onlineCount = statuses.filter((s: Status) => s.state === 'online').length;
logger.log(`Online user for game ${gameId}: ${onlineCount}`);
if (onlineCount <= 1) {
await moveGameFromRTDBToFirestore(gameId, db, rtdb);
logger.log(`Game ${gameId} moved from RTDB to Firestore`);
}
}
}
}

async function handleLockedCards(nextStatus: Status, rtdb: admin.database.Database, userId: string) {
for (const gameId of Object.keys(nextStatus.gameIds!)) {
if (nextStatus.gameIds![gameId]) {
const snap = await rtdb.ref(`/${RTDBPaths.Cards}/${gameId}`).orderByChild(`lockedBy`).equalTo(userId).get();
const cardRefs = [] as admin.database.Reference[];
snap.forEach(s => {
cardRefs.push(s.ref);
});
logger.log(`${cardRefs.length} cards locked by ${userId}`);
for (const cardRef of cardRefs) {
await cardRef.update({lockedBy: null});
}
}
}
}

/**
* It triggers when the path /statuses/{userId} of that RTDB instance is updated.
* It triggers when the path /connections/{gameId}/{userId} of that RTDB instance is updated.
*
* If the status state is different from the previous one, then:
* - if the new one is 'online', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by +1
* - if the new one is 'offline', the correct document of Firestore, representing the RTDB instance, should be updated incrementing by -1
* If the number of connections is different from the previous one, then:
* - if the new ones are more, the proper document of Firestore, representing the RTDB instance, is updated incrementing by +1
* - if the new ones are less, the proper document of Firestore, representing the RTDB instance, is updated incrementing by -1
*
* Next, if the new status is 'offline', an RTDB query is performed to look for the online users for those games.
* If more than one were found (because one is the one we were updating), it means there is still someone in the game
Expand All @@ -60,35 +23,32 @@ async function handleLockedCards(nextStatus: Status, rtdb: admin.database.Databa
*
*/

export const onOnlineGameStatusUpdate = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Statuses}/{userId}`)
export const onOnlineGameStatusUpdate = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onUpdate(async (snapshot, context) => {

const instanceId = INSTANCE_ID;
const userId = context.params.userId;
const gameId = context.params.gameId;

const previousConnections = snapshot.before.numChildren();
const afterConnections = snapshot.after.numChildren();

const originalStatus = snapshot.before.val() as Status;
const nextStatus = snapshot.after.val() as Status;


if (originalStatus.state !== nextStatus.state) {
logger.log(`User ${userId} is going from ${originalStatus.state} to ${nextStatus.state}`);
if (previousConnections > afterConnections) {
logger.log(`User ${userId} for game ${gameId} has closed one connection`);
const docInstanceId = instanceId.split(`${PROJECT_ID}-`)[1];
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
connectionsCount: FieldValue.increment(-1) as any,
} as Partial<RTDBInstance>);
}
if (afterConnections > previousConnections) {
logger.log(`User ${userId} for game ${gameId} has opened one connection`);
const docInstanceId = instanceId.split(`${PROJECT_ID}-`)[1];
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
onlineOnGameCount: nextStatus.state === 'online' ?
FieldValue.increment(1) as any :
FieldValue.increment(-1) as any,
connectionsCount: FieldValue.increment(1) as any,
} as Partial<RTDBInstance>);
if (nextStatus.state === 'offline') {
const rtdb = admin.app().database(`https://${INSTANCE_NAME}.firebasedatabase.app`);
if (nextStatus.gameIds) {
await handleLockedCards(nextStatus, rtdb, userId);
logger.log('Locked cards handled');
await handleMoveGame(nextStatus, rtdb);
logger.log('Locked cards handled');
} else {
logger.log('No games');
}
}
}
});
10 changes: 5 additions & 5 deletions packages/functions/src/load-balancing/selectBestRTDBInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const createNewRTDBInstance = async () => {
await createRTDBInstance(newRTDBInstanceName);
await db.collection(FirebaseCollection.RTDBInstances).doc(newRTDBInstanceName).set({
createdAt: FieldValue.serverTimestamp(),
onlineOnGameCount: 0,
connectionsCount: 0,
} as RTDBInstance)
admin.app().database(`https://secondary_db_url.firebaseio.com`)
Expand All @@ -42,7 +42,7 @@ const createNewRTDBInstance = async () => {
*/

/**
* API to effectively balance the game load by selected the best RTDB instance, in terms of minimum number of online users.
* API to effectively balance the game load by selected the best RTDB instance, in terms of minimum number connections (tabs, browsers, devices, ...).
* It needs a "gameId" as url parameter and can be called only by authenticated users.
*
* This will be called, and then effective, only by the first user entering a game without any other users.
Expand Down Expand Up @@ -70,15 +70,15 @@ export const selectBestRTDBInstance = functions.region(


const bestRTDBInstanceQuery = await db.collection(FirebaseCollection.RTDBInstances)
.orderBy('onlineOnGameCount', "asc").limit(1).get();
.orderBy('connectionsCount', "asc").limit(1).get();
const bestRTDBInstanceDoc = bestRTDBInstanceQuery.docs[0];
const bestRTDBInstance = bestRTDBInstanceDoc.data();
const bestRTDBInstanceName = `${PROJECT_ID}-${bestRTDBInstanceDoc.id}.${bestRTDBInstance.region}`;

logger.log(`Selected instance ${bestRTDBInstanceName} with ${bestRTDBInstance.onlineOnGameCount} online on game users`);
logger.log(`Selected instance ${bestRTDBInstanceName} with ${bestRTDBInstance.connectionsCount} game connections`);

/*
if (bestRTDBInstance.onlineOnGameCount >= RTDB_THRESHOLD) {
if (bestRTDBInstance.connectionsCount >= RTDB_THRESHOLD) {
//create new instance
axi
admin.database()
Expand Down
61 changes: 61 additions & 0 deletions packages/functions/src/load-balancing/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as admin from "firebase-admin";
import {FirebaseCollection, CardState, RTDBPaths} from "@pipeline/common";
import {Game} from "../models/Game";
import {RTDBGame} from "../models/RTDBGame";
import * as functions from "firebase-functions";
import {PROJECT_ID} from "../utils/rtdb";

const db = admin.firestore();
const logger = functions.logger;

/**
* It moves a game from a particular RTDB instance to Firestore.
* Before writing to Firestore, the 'rtdbInstance' field is set to null and all the cards are unlocked
*
* @param gameId
* @param db
* @param rtdb
*/
const moveGameFromRTDBToFirestore = async (gameId: string, db: FirebaseFirestore.Firestore, rtdb: admin.database.Database) => {
const gameSnap = await rtdb.ref(`/${RTDBPaths.Games}/${gameId}`).get();
const game = gameSnap.val() as RTDBGame;
const cardsSnap = await rtdb.ref(`/${RTDBPaths.Cards}/${gameId}`).get();
let newCards = null;
if (cardsSnap.exists()) {
const cards = cardsSnap.val() as {[key: string]: CardState};
newCards = Object.keys(cards).reduce((acc, cardId) => {
acc[cardId] = {...cards[cardId], lockedBy: null};
return acc;
}, {} as {[key: string]: CardState});
}
await db.collection(FirebaseCollection.Games).doc(gameId).update({...game, rtdbInstance: null, cards: newCards} as Game);
}

async function handleMoveGame(gameId: string, rtdb: admin.database.Database) {
const snap = await rtdb.ref(`/${RTDBPaths.Connections}/${gameId}`).orderByChild(`updatedAt`).get();
console.log('handleMoveGame snap', snap);
const onlineCount = snap.numChildren();
logger.log(`Online user for game ${gameId}: ${onlineCount}`);
if (onlineCount <= 1) {
await moveGameFromRTDBToFirestore(gameId, db, rtdb);
logger.log(`Game ${gameId} moved from RTDB to Firestore`);
}

}

async function handleLockedCards(gameId: string, rtdb: admin.database.Database, userId: string) {
const snap = await rtdb.ref(`/${RTDBPaths.Cards}/${gameId}`).orderByChild(`lockedBy`).equalTo(userId).get();
const cardRefs = [] as admin.database.Reference[];
snap.forEach(s => {
cardRefs.push(s.ref);
});
logger.log(`${cardRefs.length} cards locked by ${userId} in game ${gameId}`);
for (const cardRef of cardRefs) {
await cardRef.update({lockedBy: null});
}
}

const INSTANCE_ID = `${PROJECT_ID}-default-rtdb`
const INSTANCE_NAME = `${INSTANCE_ID}.europe-west1`

export {moveGameFromRTDBToFirestore, handleMoveGame, handleLockedCards, INSTANCE_ID, INSTANCE_NAME};
5 changes: 0 additions & 5 deletions packages/functions/src/models/Status.ts

This file was deleted.

0 comments on commit efc295e

Please sign in to comment.