diff --git a/backend/package-lock.json b/backend/package-lock.json index b4b7479..a1fcbc1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "asty-astq": "^1.14.0", "body-parser": "^1.20.2", "express": "^4.18.3", + "memcached": "^2.2.2", "net": "^1.0.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.1", @@ -1918,6 +1919,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/connection-parse": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz", + "integrity": "sha512-bTTG28diWg7R7/+qE5NZumwPbCiJOT8uPdZYu674brDjBWQctbaQbYlDKhalS+4i5HxIx+G8dZsnBHKzWpp01A==", + "license": "MIT" + }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -3336,6 +3343,16 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hashring": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz", + "integrity": "sha512-xCMovURClsQZ+TR30icCZj+34Fq1hs0y6YCASD6ZqdRfYRybb5Iadws2WS+w09mGM/kf9xyA5FCdJQGcgcraSA==", + "license": "MIT", + "dependencies": { + "connection-parse": "0.0.x", + "simple-lru-cache": "0.0.x" + } + }, "node_modules/hasown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", @@ -3795,6 +3812,22 @@ "node": ">=0.10.0" } }, + "node_modules/jackpot": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/jackpot/-/jackpot-0.0.6.tgz", + "integrity": "sha512-rbWXX+A9ooq03/dfavLg9OXQ8YB57Wa7PY5c4LfU3CgFpwEhhl3WyXTQVurkaT7zBM5I9SSOaiLyJ4I0DQmC0g==", + "dependencies": { + "retry": "0.6.0" + } + }, + "node_modules/jackpot/node_modules/retry": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.6.0.tgz", + "integrity": "sha512-RgncoxLF1GqwAzTZs/K2YpZkWrdIYbXsmesdomi+iPilSzjUyr/wzNIuteoTVaWokzdwZIJ9NHRNQa/RUiOB2g==", + "engines": { + "node": "*" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -4001,6 +4034,16 @@ "node": ">= 0.6" } }, + "node_modules/memcached": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/memcached/-/memcached-2.2.2.tgz", + "integrity": "sha512-lHwUmqkT9WdUUgRsAvquO4xsKXYaBd644Orz31tuth+w/BIfFNuJMWwsG7sa7H3XXytaNfPTZ5R/yOG3d9zJMA==", + "license": "MIT", + "dependencies": { + "hashring": "3.2.x", + "jackpot": ">=0.0.6" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -5632,6 +5675,11 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-lru-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", + "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 66f9420..cc65573 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,6 +32,7 @@ "asty-astq": "^1.14.0", "body-parser": "^1.20.2", "express": "^4.18.3", + "memcached": "^2.2.2", "net": "^1.0.2", "pg-hstore": "^2.3.4", "sequelize": "^6.37.1", diff --git a/backend/src/app.ts b/backend/src/app.ts index 13ebca8..c92442a 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -7,15 +7,20 @@ import { addGameApis } from './gameservice' import { addCharacterApis } from './characterservice' import { addRushStatsApis } from './rushstatsservice' import { addDmApis } from './dmservice' +import { Memcache } from './memcache' + +var Memcached = require('memcached') const app = express() const jsonParser = json() const port = 3001 +// const memcache = new memcached('localhost:11211', {}) +const memcachep = new Memcache('localhost:11211') -addGameApis(app, jsonParser) -addCharacterApis(app, jsonParser) -addRushStatsApis(app, jsonParser) -addDmApis(app, jsonParser) +addGameApis(app, jsonParser, memcachep) +addCharacterApis(app, jsonParser, memcachep) +addRushStatsApis(app, jsonParser, memcachep) +addDmApis(app, jsonParser, memcachep) app.use(express.static(__dirname + '/frontend')) app.use('/', (req, res) => { diff --git a/backend/src/characterservice.ts b/backend/src/characterservice.ts index 1750d46..65eb15f 100644 --- a/backend/src/characterservice.ts +++ b/backend/src/characterservice.ts @@ -1,7 +1,7 @@ import { database, Character, Game, Pick, App } from './db' import { OrderByParser, FilterParser, ParsingError } from './tokenizer' -export function addCharacterApis(app, jsonParser) { +export function addCharacterApis(app, jsonParser, memcace) { app.get('/api/character/:characterId', async (req, res) => { try { const character = await Character.findOne({ diff --git a/backend/src/dmservice.ts b/backend/src/dmservice.ts index 3bb3dad..b305c3b 100644 --- a/backend/src/dmservice.ts +++ b/backend/src/dmservice.ts @@ -2,7 +2,7 @@ import { database, Character, Game, Pick, App } from './db' import { OrderByParser, FilterParser, ParsingError } from './tokenizer' import { fn, col } from 'sequelize' -export function addDmApis(app, jsonParser) { +export function addDmApis(app, jsonParser, memcache) { app.get('/api/dm/:dmName', async (req, res) => { const dmName = req.params.dmName diff --git a/backend/src/gameservice.ts b/backend/src/gameservice.ts index 24d6738..f1f4d6c 100644 --- a/backend/src/gameservice.ts +++ b/backend/src/gameservice.ts @@ -1,7 +1,7 @@ import { database, Character, Game, Pick, App } from './db' import { OrderByParser, FilterParser, ParsingError } from './tokenizer' -export function addGameApis(app, jsonParser) { +export function addGameApis(app, jsonParser, memcache) { app.get('/api/game/:gameId', async (req, res) => { try { const game = await Game.findOne({ where: { id: req.params['gameId'] } }) diff --git a/backend/src/memcache.ts b/backend/src/memcache.ts new file mode 100644 index 0000000..a80984f --- /dev/null +++ b/backend/src/memcache.ts @@ -0,0 +1,27 @@ +import { promisify } from 'util' + +var Memcached = require('memcached') + +export class Memcache { + readonly memcached + + readonly getCallback + readonly setCallback + readonly replaceCallback + + constructor(url: string) { + this.memcached = new Memcached(url, {}) + + this.getCallback = promisify(this.memcached.get).bind(this.memcached) + this.setCallback = promisify(this.memcached.set).bind(this.memcached) + this.replaceCallback = promisify(this.memcached.replace).bind(this.memcached) + } + + async get(key: string) { + return this.getCallback(key) + } + + async set(key: string, value) { + return this.setCallback(key, value, 200) + } +} diff --git a/backend/src/rushstatsservice.ts b/backend/src/rushstatsservice.ts index 16f3f30..ec9063c 100644 --- a/backend/src/rushstatsservice.ts +++ b/backend/src/rushstatsservice.ts @@ -43,20 +43,30 @@ function monthIdToEndSeconds(monthId: number): number { return Number.MAX_SAFE_INTEGER } -export function addRushStatsApis(app, jsonParser) { +export function addRushStatsApis(app, jsonParser, memcache) { app.post('/api/serverstats/gamestats', jsonParser, async (req, res) => { const monthId = req.body.monthId let startSeconds = monthIdToStartSeconds(monthId) let endSeconds = monthIdToEndSeconds(monthId) - const serverGameStats = await database.query(serverGameStatsQuery, { - type: QueryTypes.SELECT, - replacements: { - startSeconds, - endSeconds - } - }) - res.send(serverGameStats[0]) + let cachedResponse = await memcache.get('gamestats' + req.body.monthId) + + if (!cachedResponse) { + console.log('cache miss') + const serverGameStats = await database.query(serverGameStatsQuery, { + type: QueryTypes.SELECT, + replacements: { + startSeconds, + endSeconds + } + }) + + await memcache.set('gamestats' + req.body.monthId, serverGameStats[0]) + res.send(serverGameStats[0]) + } else { + console.log('cache hit') + res.send(cachedResponse) + } }) app.post('/api/serverstats/rolestats', jsonParser, async (req, res) => { @@ -64,52 +74,60 @@ export function addRushStatsApis(app, jsonParser) { let startSeconds = monthIdToStartSeconds(monthId) let endSeconds = monthIdToEndSeconds(monthId) - const games = await Game.findAll({ - include: [ - { model: Character, as: 'characterPickedForGame' }, - { model: Character, as: 'characterAppliedForGame' } - ], - where: { postdate: { [Op.between]: [startSeconds, endSeconds] } } - }) + let cachedResponse = await memcache.get('rolestats' + req.body.monthId) - // count active roles - let activeCharacters = new Map() - let pickedCharacterCount = new Map() - let appedCharacterCount = new Map() - - games.forEach((game, gameNum) => { - const picks = game.dataValues.characterPickedForGame - const appls = game.dataValues.characterAppliedForGame - - picks.forEach((character, characterNum) => { - const role = character.dataValues.role - // Count role application - pickedCharacterCount.set(role, (pickedCharacterCount.get(role) || 0) + 1) + if (!cachedResponse) { + const games = await Game.findAll({ + include: [ + { model: Character, as: 'characterPickedForGame' }, + { model: Character, as: 'characterAppliedForGame' } + ], + where: { postdate: { [Op.between]: [startSeconds, endSeconds] } } }) - appls.forEach((character, characterNum) => { - const role = character.dataValues.role - const charId = character.dataValues.id - // Add apllied characters to active list - if (!activeCharacters.has(role)) { - activeCharacters.set(role, new Set()) + // count active roles + let activeCharacters = new Map() + let pickedCharacterCount = new Map() + let appedCharacterCount = new Map() + + games.forEach((game, gameNum) => { + const picks = game.dataValues.characterPickedForGame + const appls = game.dataValues.characterAppliedForGame + + picks.forEach((character, characterNum) => { + const role = character.dataValues.role + // Count role application + pickedCharacterCount.set(role, (pickedCharacterCount.get(role) || 0) + 1) + }) + + appls.forEach((character, characterNum) => { + const role = character.dataValues.role + const charId = character.dataValues.id + // Add apllied characters to active list + if (!activeCharacters.has(role)) { + activeCharacters.set(role, new Set()) + } + activeCharacters.get(role).add(charId) + // Count role application + appedCharacterCount.set(role, (appedCharacterCount.get(role) || 0) + 1) + }) + }) + + const result = {} + + roleNames.forEach((roleName) => { + result[roleName] = { + apps: appedCharacterCount.get(roleName) || 0, + picks: pickedCharacterCount.get(roleName) || 0, + active: activeCharacters.has(roleName) ? activeCharacters.get(roleName).size : 0 } - activeCharacters.get(role).add(charId) - // Count role application - appedCharacterCount.set(role, (appedCharacterCount.get(role) || 0) + 1) }) - }) - const result = {} - - roleNames.forEach((roleName) => { - result[roleName] = { - apps: appedCharacterCount.get(roleName) || 0, - picks: pickedCharacterCount.get(roleName) || 0, - active: activeCharacters.has(roleName) ? activeCharacters.get(roleName).size : 0 - } - }) - - res.send(result) + await memcache.set('rolestats' + req.body.monthId, result) + res.send(result) + } else { + console.log('cache hit') + res.send(cachedResponse) + } }) }