diff --git a/src/core/Ranks.js b/src/core/Ranks.js index 3f2c679..a1500f8 100644 --- a/src/core/Ranks.js +++ b/src/core/Ranks.js @@ -6,22 +6,27 @@ import { populateRanking } from '../data/sql/RegUser'; import { getRanks, resetDailyRanks, - saveDailyTop, - loadDailyTop, + getPrevTop, + getOnlineUserStats, + storeOnlinUserAmount, + getCountryDailyHistory, + getTopDailyHistory, } from '../data/redis/ranks'; import socketEvents from '../socket/socketEvents'; import logger from './logger'; import { MINUTE } from './constants'; -import { DailyCron } from '../utils/cron'; +import { DailyCron, HourlyCron } from '../utils/cron'; class Ranks { constructor() { - this.resetDailyRanking = this.resetDailyRanking.bind(this); this.ranks = { dailyRanking: [], ranking: [], prevTop: [], + onlineStats: [], + cHistStats: [], + histStats: [], }; /* * we go through socketEvents for sharding @@ -35,18 +40,34 @@ class Ranks { } async initialize() { - this.ranks.prevTop = await loadDailyTop(); - await Ranks.updateRanking(); + try { + let someRanks = await Ranks.dailyUpdateRanking(); + this.ranks = { + ...this.ranks, + ...someRanks, + }; + someRanks = await Ranks.hourlyUpdateRanking(); + this.ranks = { + ...this.ranks, + ...someRanks, + }; + await Ranks.updateRanking(); + } catch (err) { + console.error(`Error initialize ranks: ${err.message}`); + } setInterval(Ranks.updateRanking, 5 * MINUTE); - DailyCron.hook(this.resetDailyRanking); + DailyCron.hook(Ranks.setDailyRanking); + setInterval(Ranks.setHourlyRanking, 5 * MINUTE); + //HourlyCron.hook(Ranks.setHourlyRanking); } + /* + * get daily and total ranking from database + */ static async updateRanking() { - /* - * only main shard updates and sends it to others - */ + // only main shard does it if (!socketEvents.amIImportant()) { - return; + return null; } const ranking = await populateRanking( await getRanks( @@ -60,21 +81,78 @@ class Ranks { 1, 100, )); - socketEvents.rankingListUpdate({ ranking, dailyRanking }); + const ret = { + ranking, + dailyRanking, + }; + socketEvents.rankingListUpdate(ret); + return ret; } - async resetDailyRanking() { - /* - * only main shard updates and sends it to others - */ + /* + * get online counter stats, + * list of users online per hour + */ + static async hourlyUpdateRanking() { + const onlineStats = await getOnlineUserStats(); + const cHistStats = await getCountryDailyHistory(); + const histStats = await getTopDailyHistory(); + const ret = { + onlineStats, + cHistStats, + histStats, + }; + if (socketEvents.amIImportant()) { + // only main shard sends to others + socketEvents.rankingListUpdate(ret); + } + return ret; + } + + /* + * get prevTop from database + */ + static async dailyUpdateRanking() { + const prevTop = await populateRanking( + await getPrevTop(), + ); + const ret = { + prevTop, + }; + if (socketEvents.amIImportant()) { + // only main shard sends to others + socketEvents.rankingListUpdate(ret); + } + return ret; + } + + /* + * get and store amount of online users into stats + */ + static async setHourlyRanking() { + if (!socketEvents.amIImportant()) { + return; + } + let amount; + try { + amount = socketEvents.onlineCounter.total; + await storeOnlinUserAmount(amount); + await Ranks.hourlyUpdateRanking(); + } catch (err) { + console.error(`error on hourly ranking: ${err.message} / ${amount}`); + } + } + + /* + * reset daily rankings, store previous rankings + */ + static async setDailyRanking() { if (!socketEvents.amIImportant()) { return; } - const prevTop = await saveDailyTop(this.ranks.dailyRanking); - socketEvents.rankingListUpdate({ prevTop }); logger.info('Resetting Daily Ranking'); await resetDailyRanks(); - await Ranks.updateRanking(); + await Ranks.dailyUpdateRanking(); } } diff --git a/src/core/draw.js b/src/core/draw.js index 4895e4b..0170a31 100644 --- a/src/core/draw.js +++ b/src/core/draw.js @@ -125,7 +125,9 @@ export default async function drawByOffsets( throw new Error(7); } } - if (canvas.req === 'top' && !rankings.ranks.prevTop.includes(user.id)) { + if (canvas.req === 'top' + && !rankings.ranks.prevTop.find((r) => r.id === user.id) + ) { // not in top ten throw new Error(12); } diff --git a/src/core/utils.js b/src/core/utils.js index 474aaff..4a994fc 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -573,3 +573,16 @@ export function combineObjects(a, b) { } return a + b; } + +/* + * get YYYYMMDD of timestamp + */ +export function getDateKeyOfTs(ts) { + const date = new Date(ts); + let day = date.getUTCDate(); + if (day < 10) day = `0${day}`; + let month = date.getUTCMonth() + 1; + if (month < 10) month = `0${month}`; + const year = date.getUTCFullYear(); + return `${year}${month}${day}`; +} diff --git a/src/data/redis/ranks.js b/src/data/redis/ranks.js index 974aec0..ce79df9 100644 --- a/src/data/redis/ranks.js +++ b/src/data/redis/ranks.js @@ -2,8 +2,7 @@ * counter for daily and total pixels and ranking */ import client from './client'; - -import logger from '../../core/logger'; +import { getDateKeyOfTs } from '../../core/utils'; export const RANKED_KEY = 'rank'; export const DAILY_RANKED_KEY = 'rankd'; @@ -11,7 +10,7 @@ export const DAILY_CRANKED_KEY = 'crankd'; const PREV_DAY_TOP_KEY = 'prankd'; const DAY_STATS_RANKS_KEY = 'ds'; const CDAY_STATS_RANKS_KEY = 'cds'; -const PREV_DAILY_TOP_KEY = 'prevtop'; +const ONLINE_CNTR_KEY = 'tonl'; /* * get pixelcount and ranking @@ -81,6 +80,105 @@ export async function getRanks(daily, start, amount) { return ret; } +/* + * get top 10 from previous day + */ +export async function getPrevTop() { + let prevTop = await client.zRangeWithScores(PREV_DAY_TOP_KEY, 0, 9, { + REV: true, + }); + prevTop = prevTop.map((r) => ({ + id: Number(r.value), + px: Number(r.score), + })); + return prevTop; +} + +/* + * store amount of online Users + */ +export async function storeOnlinUserAmount(amount) { + await client.lPush(ONLINE_CNTR_KEY, String(amount)); + await client.lTrim(ONLINE_CNTR_KEY, 0, 14 * 24); +} + +/* + * get list of online counters + */ +export async function getOnlineUserStats() { + const onlineStats = await client.lRange(ONLINE_CNTR_KEY, 0, -1); + console.log('STAAATTTSS', onlineStats); + return onlineStats; +} + +/* + * get top 10 of daily pixels over the past days + */ +export async function getTopDailyHistory() { + const stats = []; + const users = []; + let ts; + let key; + for (let c = 0; c < 14; c += 1) { + if (!ts) { + ts = Date.now(); + key = DAY_STATS_RANKS_KEY; + } else { + ts -= 1000 * 3600 * 24; + const dateKey = getDateKeyOfTs(ts); + key = `${DAY_STATS_RANKS_KEY}:${dateKey}`; + } + // eslint-disable-next-line no-await-in-loop + let dData = await client.zRangeWithScores(key, 0, 9, { + REV: true, + }); + dData = dData.map((r) => { + const id = Number(r.value); + if (!users.some((q) => q.id === id)) { + users.push({ id }); + } + return { + id, + px: Number(r.score), + }; + }); + stats.push(dData); + } + return { + users, + stats, + }; +} + +/* + * get top 10 countries over the past days + */ +export async function getCountryDailyHistory() { + const ret = []; + let ts; + let key; + for (let c = 0; c < 14; c += 1) { + if (!ts) { + ts = Date.now(); + key = CDAY_STATS_RANKS_KEY; + } else { + ts -= 1000 * 3600 * 24; + const dateKey = getDateKeyOfTs(ts); + key = `${CDAY_STATS_RANKS_KEY}:${dateKey}`; + } + // eslint-disable-next-line no-await-in-loop + let dData = await client.zRangeWithScores(key, 0, 9, { + REV: true, + }); + dData = dData.map((r) => ({ + cc: r.value, + px: Number(r.score), + })); + ret.push(dData); + } + return ret; +} + /* * reset daily ranks * @return boolean for success @@ -91,57 +189,15 @@ export async function resetDailyRanks() { REV: true, }); // store day - const yesterday = new Date(Date.now() - 1000 * 3600 * 24); - let day = yesterday.getUTCDate(); - if (day < 10) day = `0${day}`; - let month = yesterday.getUTCMonth() + 1; - if (month < 10) month = `0${month}`; - const year = yesterday.getUTCFullYear(); - const dateKey = `${year}${month}${day}`; - // TODO check if this works + const dateKey = getDateKeyOfTs( + Date.now() - 1000 * 3600 * 24, + ); await client.rename( DAILY_RANKED_KEY, `${DAY_STATS_RANKS_KEY}:${dateKey}`, ); - /* - await client.zUnionStore( - `${DAY_STATS_RANKS_KEY}:${dateKey}`, - DAILY_RANKED_KEY, - ); - */ await client.rename( DAILY_CRANKED_KEY, `${CDAY_STATS_RANKS_KEY}:${dateKey}`, ); - /* - await client.zUnionStore( - `${CDAY_STATS_RANKS_KEY}:${dateKey}`, - DAILY_CRANKED_KEY, - ); - // reset daily counter - await client.del(DAILY_RANKED_KEY); - await client.del(DAILY_CRANKED_KEY); - */ -} - -/* - * saves the top 10 into redis - * @param dailyRanking Array of dailyRanking - */ -export async function saveDailyTop(dailyRanking) { - const top10 = dailyRanking.slice(0, 10).map((user) => user.id); - const jsonTop = JSON.stringify(top10); - logger.info(`Saving current daily top 10 into redis: ${jsonTop}`); - await client.set(PREV_DAILY_TOP_KEY, jsonTop); - return top10; -} - -/* - * load top10 from redis - * @return Promis Array of user IDs of the top 10 - */ -export async function loadDailyTop() { - const jsonTop = await client.get(PREV_DAILY_TOP_KEY); - logger.info(`Loaded current daily top 10 into redis: ${jsonTop}`); - return (jsonTop) ? JSON.parse(jsonTop) : []; }