/** * Copyright 2013-2019 GenieACS Inc. * * This file is part of GenieACS. * * GenieACS is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * GenieACS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ import { Collection } from "mongodb"; import { onConnect } from "./db"; import * as config from "./config"; const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL"); let mongoCollection: Collection; let mongoTimeOffset = 0; onConnect(async (db) => { mongoCollection = db.collection("cache"); await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); const now = Date.now(); const res = await db.command({ hostInfo: 1 }); mongoTimeOffset = res.system.currentTime.getTime() - now; }); export async function get(key: string | string[]): Promise { const expire = new Date(Date.now() - mongoTimeOffset); if (Array.isArray(key)) { const res = await mongoCollection.find({ _id: { $in: key } }).toArray(); const indices = {}; key.forEach((v, i) => { indices[v] = i; }); const values = []; res.forEach((r) => { if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; }); return values; } else { const res = await mongoCollection.findOne({ _id: { $in: [key] } }); if (res && res["expire"] > expire) return res["value"]; return null; } } export async function del(key: string | string[]): Promise { if (Array.isArray(key)) await mongoCollection.deleteMany({ _id: { $in: key } }); else await mongoCollection.deleteOne({ _id: key }); } export async function set( key: string, value: string | number, ttl: number = MAX_CACHE_TTL ): Promise { const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000); await mongoCollection.replaceOne( { _id: key }, { _id: key, value: value, expire: expire }, { upsert: true } ); } export async function pop(key: string): Promise { const res = await mongoCollection.findOneAndDelete({ _id: key }); if ( res && res["value"] && +res["value"]["expire"] - (Date.now() - mongoTimeOffset) ) return res["value"]["value"]; return null; } export async function lock( lockName: string, ttl: number ): Promise<(extendTtl?: number) => Promise> { const token = Math.random().toString(36).slice(2); async function unlockOrExtend(extendTtl?: number): Promise { if (!extendTtl) { const res = await mongoCollection.deleteOne({ _id: lockName, value: token, }); if (res["result"]["n"] !== 1) throw new Error("Lock expired"); } else { const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000); const res = await mongoCollection.updateOne( { _id: lockName, value: token }, { expire: expire } ); if (res["result"]["n"] !== 1) throw new Error("Lock expired"); } } const expireTest = new Date(Date.now() - mongoTimeOffset); const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000); try { await mongoCollection.updateOne( { _id: lockName, expire: { $lte: expireTest } }, { $set: { value: token, expire: expireSet } }, { upsert: true } ); } catch (err) { if (err && err.code === 11000) { await new Promise((resolve) => setTimeout(resolve, 200)); return lock(lockName, ttl); } } return unlockOrExtend; }