feat: vacuum storage to reduce its size

This commit is contained in:
teidesu 2021-06-20 18:18:06 +03:00
parent b45cc0df69
commit c8dae335e8
2 changed files with 100 additions and 29 deletions

View file

@ -53,10 +53,13 @@ const USERNAME_TTL = 86400000 // 24 hours
export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ { export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
protected _state: MemorySessionState protected _state: MemorySessionState
private _cachedInputPeers: Record<number, tl.TypeInputPeer> = {} private _cachedInputPeers: LruMap<number, tl.TypeInputPeer> = new LruMap(100)
private _cachedFull: LruMap<number, tl.TypeUser | tl.TypeChat> private _cachedFull: LruMap<number, tl.TypeUser | tl.TypeChat>
private _vacuumTimeout: NodeJS.Timeout
private _vacuumInterval: number
constructor(params?: { constructor(params?: {
/** /**
* Maximum number of cached full entities. * Maximum number of cached full entities.
@ -69,9 +72,31 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
* Defaults to `100`, use `0` to disable * Defaults to `100`, use `0` to disable
*/ */
cacheSize?: number cacheSize?: number
/**
* Interval in milliseconds for vacuuming the storage.
*
* When vacuuming, the storage will remove expired FSM
* states to reduce memory usage.
*
* Defaults to `300_000` (5 minutes)
*/
vacuumInterval?: number
}) { }) {
this.reset() this.reset()
this._cachedFull = new LruMap(params?.cacheSize ?? 100) this._cachedFull = new LruMap(params?.cacheSize ?? 100)
this._vacuumInterval = params?.vacuumInterval ?? 300_000
}
load(): void {
this._vacuumTimeout = setInterval(
this._vacuum.bind(this),
this._vacuumInterval
)
}
destroy(): void {
clearInterval(this._vacuumTimeout)
} }
reset(): void { reset(): void {
@ -122,6 +147,30 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
this._state = obj this._state = obj
} }
private _vacuum(): void {
// remove expired entities from fsm and rate limit storages
const now = Date.now()
// make references in advance to avoid lookups
const state = this._state
const fsm = state.fsm
const rl = state.rl
Object.keys(fsm).forEach((key) => {
const exp = fsm[key].e
if (exp && exp < now) {
delete fsm[key]
}
})
Object.keys(rl).forEach((key) => {
if (rl[key].res < now) {
delete rl[key]
}
})
}
getDefaultDc(): tl.RawDcOption | null { getDefaultDc(): tl.RawDcOption | null {
return this._state.defaultDc return this._state.defaultDc
} }
@ -192,10 +241,10 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
} }
getPeerById(peerId: number): tl.TypeInputPeer | null { getPeerById(peerId: number): tl.TypeInputPeer | null {
if (peerId in this._cachedInputPeers) if (this._cachedInputPeers.has(peerId))
return this._cachedInputPeers[peerId] return this._cachedInputPeers.get(peerId)!
const peer = this._getInputPeer(this._state.entities[peerId]) const peer = this._getInputPeer(this._state.entities[peerId])
if (peer) this._cachedInputPeers[peerId] = peer if (peer) this._cachedInputPeers.set(peerId, peer)
return peer return peer
} }
@ -302,7 +351,7 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
if (!(key in this._state.rl)) { if (!(key in this._state.rl)) {
const state = { const state = {
res: now + window * 1000, res: now + window * 1000,
rem: limit rem: limit,
} }
this._state.rl[key] = state this._state.rl[key] = state
@ -315,7 +364,7 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage */ {
const state = { const state = {
res: now + window * 1000, res: now + window * 1000,
rem: limit rem: limit,
} }
this._state.rl[key] = state this._state.rl[key] = state

View file

@ -124,13 +124,6 @@ interface FsmItem {
expires?: number expires?: number
} }
interface RateLimitItem {
// reset
res: number
// remaining
rem: number
}
const STATEMENTS = { const STATEMENTS = {
getKv: 'select value from kv where key = ?', getKv: 'select value from kv where key = ?',
setKv: 'insert or replace into kv (key, value) values (?, ?)', setKv: 'insert or replace into kv (key, value) values (?, ?)',
@ -156,6 +149,8 @@ const STATEMENTS = {
getEntById: 'select * from entities where id = ?', getEntById: 'select * from entities where id = ?',
getEntByPhone: 'select * from entities where phone = ?', getEntByPhone: 'select * from entities where phone = ?',
getEntByUser: 'select * from entities where username = ?', getEntByUser: 'select * from entities where username = ?',
delStaleState: 'delete from state where expires < ?'
} as const } as const
const EMPTY_BUFFER = Buffer.alloc(0) const EMPTY_BUFFER = Buffer.alloc(0)
@ -175,7 +170,7 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
private _cache?: LruMap<number, CacheItem> private _cache?: LruMap<number, CacheItem>
private _fsmCache?: LruMap<string, FsmItem> private _fsmCache?: LruMap<string, FsmItem>
private _rlCache?: LruMap<string, RateLimitItem> private _rlCache?: LruMap<string, FsmItem>
private _wal?: boolean private _wal?: boolean
@ -183,6 +178,9 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
private _saveUnimportantLater: () => void private _saveUnimportantLater: () => void
private _vacuumTimeout: NodeJS.Timeout
private _vacuumInterval: number
/** /**
* @param filename Database file name, or `:memory:` for in-memory DB * @param filename Database file name, or `:memory:` for in-memory DB
* @param params * @param params
@ -253,6 +251,16 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
* Defaults to `30000` (30 sec) * Defaults to `30000` (30 sec)
*/ */
unimportantSavesDelay?: number unimportantSavesDelay?: number
/**
* Interval in milliseconds for vacuuming the storage.
*
* When vacuuming, the storage will remove expired FSM
* states to reduce disk and memory usage.
*
* Defaults to `300_000` (5 minutes)
*/
vacuumInterval?: number
} }
) { ) {
this._filename = filename this._filename = filename
@ -289,6 +297,9 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
this._pendingUnimportant = {} this._pendingUnimportant = {}
}, params?.unimportantSavesDelay ?? 30000) }, params?.unimportantSavesDelay ?? 30000)
this._vacuumInterval = params?.vacuumInterval ?? 300_000
// todo: add support for workers (idk if really needed, but still) // todo: add support for workers (idk if really needed, but still)
} }
@ -372,6 +383,11 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
} }
} }
private _vacuum(): void {
this._statements.delStaleState.run(Date.now())
// local caches aren't cleared because it would be too expensive
}
load(): void { load(): void {
this._db = sqlite3(this._filename, { this._db = sqlite3(this._filename, {
verbose: debug.enabled ? debug : null, verbose: debug.enabled ? debug : null,
@ -396,6 +412,11 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
this._statements.updateCachedEnt.run(it) this._statements.updateCachedEnt.run(it)
}) })
}) })
this._vacuumTimeout = setInterval(
this._vacuum.bind(this),
this._vacuumInterval
)
} }
save(): void { save(): void {
@ -409,6 +430,7 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
destroy(): void { destroy(): void {
this._db.close() this._db.close()
clearInterval(this._vacuumTimeout)
} }
reset(): void { reset(): void {
@ -653,39 +675,39 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
// leaky bucket // leaky bucket
const now = Date.now() const now = Date.now()
let val: RateLimitItem | undefined = this._rlCache?.get(key) let val: FsmItem | undefined = this._rlCache?.get(key)
const cached = val const cached = val
if (!val) { if (!val) {
const got = this._statements.getState.get(`$rate_limit_${key}`) const got = this._statements.getState.get(`$rate_limit_${key}`)
if (got) { if (got) {
val = JSON.parse(got.value) val = got
} }
} }
if (!val || val.res < now) { if (!val || val.expires! < now) {
// expired or does not exist // expired or does not exist
const state = { const item: FsmItem = {
res: now + window * 1000, expires: now + window * 1000,
rem: limit value: limit
} }
this._statements.setState.run( this._statements.setState.run(
`$rate_limit_${key}`, `$rate_limit_${key}`,
JSON.stringify(state), item.value,
null item.expires
) )
this._rlCache?.set(key, state) this._rlCache?.set(key, item)
return [state.rem, state.res] return [item.value, item.expires!]
} }
if (val.rem > 0) { if (val.value > 0) {
val.rem -= 1 val.value -= 1
this._statements.setState.run( this._statements.setState.run(
`$rate_limit_${key}`, `$rate_limit_${key}`,
JSON.stringify(val), val.value,
null val.expires
) )
if (!cached) { if (!cached) {
// add to cache // add to cache
@ -694,7 +716,7 @@ export class SqliteStorage implements ITelegramStorage /*, IStateStorage */ {
} }
} }
return [val.rem, val.res] return [val.value, val.expires!]
} }
resetRateLimit(key: string): void { resetRateLimit(key: string): void {