Skip to content

Commit

Permalink
feat(functions-general): add deploy of functions to all rtdb instances
Browse files Browse the repository at this point in the history
  • Loading branch information
rams23 committed Jan 31, 2021
1 parent fbce281 commit d447cac
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 101 deletions.
2 changes: 1 addition & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"npx lerna run update-package-lock"
],
"postdeploy": [
"node scripts/pre-deploy.js"
"node scripts/post-deploy.js"
],
"source": "packages/functions"
},
Expand Down
4 changes: 4 additions & 0 deletions fixtures/firestore-data/rtdbInstances.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
"default-rtdb": {
"connectionsCount": 0,
"region": "europe-west1"
},
"instance-1": {
"connectionsCount": 0,
"region": "europe-west1"
}
}
15 changes: 9 additions & 6 deletions packages/functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import * as admin from "firebase-admin";
import {exportEverythingFrom} from "./utils/exportFunctionsOnAllRTDBInstances";

admin.initializeApp();

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'onOnlineGameStatusCreate') {
exports.onOnlineGameStatusCreate = require('./load-balancing/onOnlineGameStatusCreate').onOnlineGameStatusCreate;
const functionName = process.env.FUNCTION_NAME;

if (!functionName || functionName.startsWith('onOnlineGameStatusCreate')) {
exportEverythingFrom(exports, require('./load-balancing/onOnlineGameStatusCreate'))
}

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'onOnlineGameStatusDelete') {
exports.onOnlineGameStatusDelete = require('./load-balancing/onOnlineGameStatusDelete').onOnlineGameStatusDelete;
if (!functionName || functionName.startsWith('onOnlineGameStatusDelete')) {
exportEverythingFrom(exports, require('./load-balancing/onOnlineGameStatusDelete'))
}

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'onOnlineGameStatusUpdate') {
exports.onOnlineGameStatusUpdate = require('./load-balancing/onOnlineGameStatusUpdate').onOnlineGameStatusUpdate;
if (!functionName || functionName.startsWith('onOnlineGameStatusUpdate')) {
exportEverythingFrom(exports, require('./load-balancing/onOnlineGameStatusUpdate'))
}

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'selectBestRTDBInstance') {
Expand Down
33 changes: 18 additions & 15 deletions packages/functions/src/load-balancing/onOnlineGameStatusCreate.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
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 exportFunctionsOnAllRTDBInstances from "../utils/exportFunctionsOnAllRTDBInstances";
import FieldValue = admin.firestore.FieldValue;

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

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

/**
* 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
*
*/
async function handler(snapshot: functions.database.DataSnapshot, context: functions.EventContext, rtdbId: string) {
const userId = context.params.userId;
const gameId = context.params.gameId;
// TODO snapshot.instance in emulator is always the default one
// const docInstanceId = parseRTDBInstanceId(snapshot.instance);

export const onOnlineGameStatusCreate = functions.database.instance(INSTANCE_ID).ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onCreate(async (snapshot, context) => {
logger.log(`User ${userId} just created connection for game ${gameId} in ${rtdbId} snapshotInstance ${snapshot.instance}`);
await db.collection(FirebaseCollection.RTDBInstances).doc(rtdbId)
.update({
connectionsCount: FieldValue.increment(1) as any,
} as Partial<RTDBInstance>);
}

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

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({
connectionsCount: FieldValue.increment(1) as any,
} as Partial<RTDBInstance>);
});
exportFunctionsOnAllRTDBInstances(
'onOnlineGameStatusCreate',
(builder, dbId) => builder.ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onCreate((snapshot, context) => handler(snapshot, context, dbId)),
exports
);
58 changes: 34 additions & 24 deletions packages/functions/src/load-balancing/onOnlineGameStatusDelete.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
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 FieldValue = admin.firestore.FieldValue;
import {handleLockedCards, handleMoveGame, INSTANCE_NAME} from "./utils";
import {handleLockedCards, handleMoveGame} from "./utils";
import exportFunctionsOnAllRTDBInstances from "../utils/exportFunctionsOnAllRTDBInstances";
import {getAppForDB} from "../utils/rtdb";

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

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

/**
* It triggers when the path /connections/{gameId}/{userId} of that RTDB instance is deleted.
*
Expand All @@ -20,23 +19,34 @@ const INSTANCE_ID = `${PROJECT_ID}-default-rtdb`
* Otherwise, we can move the game from RTDB back to Firestore, for each game.
*/

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;

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({
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, db, rtdb);
logger.log('Locked cards handled');
});
async function handler(snapshot: functions.database.DataSnapshot, context: functions.EventContext, rtdbId:string, rtdbUrl:string) {

const userId = context.params.userId;
const gameId = context.params.gameId;
// TODO snapshot.instance in emulator is always the default one
// const docInstanceId = parseRTDBInstanceId(snapshot.instance);

logger.log(`User ${userId} just closed all connections for game ${gameId} in instance ${rtdbId}`);
await db.collection(FirebaseCollection.RTDBInstances).doc(rtdbId)
.update({
connectionsCount: FieldValue.increment(-1) as any,
} as Partial<RTDBInstance>);

const rtdb = getAppForDB(
rtdbId,
rtdbUrl
).database();

await handleLockedCards(gameId, rtdb, userId);
logger.log('Locked cards handled');
await handleMoveGame(gameId, db, rtdb);
logger.log('Locked cards handled');
}


exportFunctionsOnAllRTDBInstances(
'onOnlineGameStatusDelete',
(builder, rtdbId, rtdbUrl) => builder.ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onDelete((snapshot, context) => handler(snapshot, context, rtdbId, rtdbUrl)),
exports
);
68 changes: 37 additions & 31 deletions packages/functions/src/load-balancing/onOnlineGameStatusUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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 exportFunctionsOnAllRTDBInstances from "../utils/exportFunctionsOnAllRTDBInstances";
import FieldValue = admin.firestore.FieldValue;
import {INSTANCE_ID} from "./utils";

const db = admin.firestore();
const logger = functions.logger;
Expand All @@ -23,32 +22,39 @@ const logger = functions.logger;
*
*/

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 docInstanceId = instanceId.split(`${PROJECT_ID}-`)[1];

const connectionsDiff = afterConnections - previousConnections;

if (connectionsDiff < 0) {
logger.log(`User ${userId} for game ${gameId} has closed one connection`);
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
connectionsCount: FieldValue.increment(connectionsDiff) as any,
} as Partial<RTDBInstance>);
}
if (connectionsDiff > 0) {
logger.log(`User ${userId} for game ${gameId} has opened one connection`);
await db.collection(FirebaseCollection.RTDBInstances).doc(docInstanceId)
.update({
connectionsCount: FieldValue.increment(connectionsDiff) as any,
} as Partial<RTDBInstance>);
}
});
async function handler(snapshot: functions.Change<functions.database.DataSnapshot>, context: functions.EventContext, rtdbId: string) {
logger.log('test', context.resource, context.params);

const userId = context.params.userId;
const gameId = context.params.gameId;
// TODO snapshot.instance in emulator is always the default one
//const docInstanceId = parseRTDBInstanceId(snapshot.after.instance);

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

const connectionsDiff = afterConnections - previousConnections;

if (connectionsDiff < 0) {
logger.log(`User ${userId} for game ${gameId} has closed one connection instance ${rtdbId}`);
await db.collection(FirebaseCollection.RTDBInstances).doc(rtdbId)
.update({
connectionsCount: FieldValue.increment(connectionsDiff) as any,
} as Partial<RTDBInstance>);
}
if (connectionsDiff > 0) {
logger.log(`User ${userId} for game ${gameId} has opened one connection`);
await db.collection(FirebaseCollection.RTDBInstances).doc(rtdbId)
.update({
connectionsCount: FieldValue.increment(connectionsDiff) as any,
} as Partial<RTDBInstance>);
}
}


exportFunctionsOnAllRTDBInstances(
'onOnlineGameStatusUpdate',
(builder, rtdbId) => builder.ref(`/${RTDBPaths.Connections}/{gameId}/{userId}`)
.onUpdate((change, context) => handler(change, context, rtdbId)),
exports
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions';
import * as admin from "firebase-admin";
import {runTransactionWithRetry} from "../utils/db";
import {FirebaseCollection, RTDBPaths} from '@pipeline/common';
import {PROJECT_ID} from "../utils/rtdb";
import {getAppForDB, PROJECT_ID} from "../utils/rtdb";
import {Game} from "../models/Game";

const db = admin.firestore();
Expand Down Expand Up @@ -85,7 +85,10 @@ export const selectBestRTDBInstance = functions.region(
}
*/

const rtdb = admin.app().database(`https://${bestRTDBInstanceName}.firebasedatabase.app`);
const rtdb = getAppForDB(
bestRTDBInstanceDoc.id,
`https://${bestRTDBInstanceName}.firebasedatabase.app`
).database();

const gameRef = db.collection(FirebaseCollection.Games).doc(gameId);

Expand Down
14 changes: 14 additions & 0 deletions packages/functions/src/rtdbInstances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const rtdbInstancesUrl = [
{
url: 'https://pipeline-game-dev-default-rtdb.firebasedatabase.app',
name: 'pipeline-game-dev-default-rtdb',
id: 'default-rtdb',
},
{
url: 'https://pipeline-game-dev-instance-1.firebasedatabase.app',
name: 'pipeline-game-dev-instance-1',
id: 'instance-1',
},
];

export default rtdbInstancesUrl;
19 changes: 0 additions & 19 deletions packages/functions/src/utils/auth.ts

This file was deleted.

39 changes: 39 additions & 0 deletions packages/functions/src/utils/exportFunctionsOnAllRTDBInstances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import rtdbInstancesUrl from "../rtdbInstances";
import * as functions from "firebase-functions";

/**
* Exports a function with a trigger on all rtdbInstances defined.
*
* the functions will be exported using the functionName and the suffix `[dbId]`
*
* @param functionName the name of the function to export
* @param functionBuilder receive a database instance and returns a function instance
* @param exportObj the exports object to be filled with the functions to export
*/
export default function exportFunctionsOnAllRTDBInstances<T>(
functionName: string,
functionBuilder: (build: functions.database.InstanceBuilder, rtdbId:string, rtdbUrl:string) => functions.CloudFunction<T>,
exportObj: any,
) {
for (const db of rtdbInstancesUrl) {
exportObj[`${functionName}[${db.id.replace('-', '_')}]`] = functionBuilder(
functions.database.instance(db.name),
db.id,
db.url
);
}

}

/**
*
* Re-export all the elements inside object inside exportsObj
*
* @param exportsObj the export object to fill
* @param object the object containing elements to re-export
*/
export function exportEverythingFrom(exportsObj: any, object: any) {
for (const key in object) {
exportsObj[key] = object[key];
}
}
23 changes: 23 additions & 0 deletions packages/functions/src/utils/parseRTDBInstanceId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {PROJECT_ID} from "./rtdb";
import * as functions from "firebase-functions";

const logger = functions.logger;

/**
* Returns the instance id from the complete db url
*
* @param instanceUrl
*/
export default function parseRTDBInstanceId(instanceUrl: string) {
let completeName;
if (instanceUrl.includes('localhost')) {
completeName = instanceUrl.split('ns=')[1];
} else {
completeName = instanceUrl.replace('https://', '').split('.')[0];
}
logger.info(
`[parseRTDBInstanceId] instanceUrl ${instanceUrl}
completeName ${completeName} id ${completeName.replace(`${PROJECT_ID}-`, '')}`
);
return completeName.replace(`${PROJECT_ID}-`, '');
}
18 changes: 15 additions & 3 deletions packages/functions/src/utils/rtdb.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import * as admin from "firebase-admin";

export const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG!).projectId;

export const getRTDBInstanceName = (num: number) => {
return `${PROJECT_ID}-${num}-rtdb`
};

export function getAppForDB(dbId: string, url: string) {
const app = admin.apps.find(a => a?.name === dbId);

if (app) {
return app;
} else {
return admin.initializeApp({
databaseURL: url,
}, dbId);
}

}

0 comments on commit d447cac

Please sign in to comment.