strapi-pfapi
Version:
a strapi plugin library uses local and redis caches to achieve single digit milliseconds on average api response time.
141 lines (125 loc) • 5.28 kB
JavaScript
'use strict';
const debug = require('debug')('pfapi:refresh-queue');
const debug_verbose = require('debug')('pfapi-verbose:refresh-queue');
const Cacheable = require('../lib/cacheable');
const get_config = require('../app/get-config');
const get_priority_score = require('../utils/get-priority-score');
const logging = require('../app/logging');
class RefreshQueue {
constructor(redis_cache, local_cache, config) {
if (!redis_cache) {
throw new Error('missing required redis_cache');
}
this.redis_cache = redis_cache;
this.local_cache = local_cache;
this.config = get_config('RefreshQueue');
if (config) Object.assign(this.config, config);
}
async push(keys) {
if (this.stopped || keys.length === 0) return;
if (!Array.isArray(keys)) keys = [ keys ];
const promises = [];
const score_keys = [];
for (const key of keys) {
promises.push(this.get_key_score_argv(score_keys, key));
}
await Promise.all(promises);
const result = await this.push_refresh_queue(score_keys);
if (this.config.max_queue_size) {
const queue_size = await this.get_refresh_queue_size();
if (queue_size > this.config.max_queue_size) {
this.shift_refresh_queue(queue_size - this.config.max_queue_size);
}
}
return result;
}
start() {
this.interval_handle = setInterval(async () => {
if (this.stopped) return;
const queue_size = await this.get_refresh_queue_size();
debug('refresh queue size', queue_size);
if (queue_size > 0) {
const start_ms = Date.now();
// process from top
for (let size = Math.ceil(queue_size * this.config.size_ratio); size > 0;) {
const max_refresh_size = size > this.config.batch_size ? this.config.batch_size : size;
await this.do_refresh(max_refresh_size);
// max time for refresh
if (Date.now() - start_ms > this.config.refresh_interval * this.config.time_ratio) break;
size -= max_refresh_size;
}
// remove from bottom
const remove_count = Math.round(queue_size * this.config.remove_ratio);
if (remove_count > 0) {
await this.shift_refresh_queue(remove_count);
debug('refresh queue removed keys', remove_count);
}
}
}, this.config.refresh_interval);
}
async on_refresh(key) {
try {
const cacheable = new Cacheable({key});
const result = await cacheable.fetch_data(this.redis_cache, this.local_cache);
debug_verbose('refreshed', key, cacheable.module_path, logging.cmsg(cacheable.params));
return result;
} catch(err) {
logging.error(err);
}
}
async do_refresh(max_refresh_size) {
if (!max_refresh_size) max_refresh_size = this.config.batch_size;
const promises = [];
for (let i = 0; i < max_refresh_size; i++) {
const { key } = await this.pop_refresh_queue();
if (!key) break;
promises.push(this.on_refresh(key));
}
if (promises.length > 0) {
await Promise.all(promises);
}
logging.info(`refresh queue refreshed ${max_refresh_size} keys`);
}
stop() {
this.stopped = true;
if (this.interval_handle) {
clearInterval(this.interval_handle);
this.interval_handle.unref();
this.interval_handle = null;
}
}
// support functions
async get_key_score_argv(score_keys, key) {
const cacheable = new Cacheable({key});
if (await this.redis_cache.get_info(cacheable)) {
const { created_time, duration, count } = cacheable;
const slow_duration = cacheable?.config?.slow_duration;
const score = get_priority_score(created_time, duration, count, slow_duration);
score_keys.push(score, key);
}
}
async push_refresh_queue(score_keys) {
const client = await this.redis_cache.get_client();
const result = await client.zadd('REFRESH::priority-queue', ...score_keys);
return result === score_keys.length / 2;
}
async pop_refresh_queue() {
const client = await this.redis_cache.get_client();
const result = await client.zpopmax('REFRESH::priority-queue');
if (!result || result.length !== 2) return {};
const key = result[0];
const score = parseInt(result[1]);
return {key, score};
}
async shift_refresh_queue(count) {
const client = await this.redis_cache.get_client();
const result = await client.zpopmin('REFRESH::priority-queue', count);
if (!result || result.length === 0) return false;
return true;
}
async get_refresh_queue_size() {
const client = await this.redis_cache.get_client();
return await client.zcount('REFRESH::priority-queue', '-inf', '+inf');
}
}
module.exports = RefreshQueue;