UNPKG

strapi-pfapi

Version:

a strapi plugin library uses local and redis caches to achieve single digit milliseconds on average api response time.

293 lines (263 loc) 10.8 kB
'use strict'; const debug = require('debug')('pfapi:servers'); const debug_verbose = require('debug')('pfapi-verbose:servers'); const logging = require('./logging'); const RedisPubsub = require('../lib/redis-pubsub'); const Cacheable = require('../lib/cacheable'); const RefreshQueue = require('../lib/refresh-queue'); const ExpiresWatch = require('../lib/expires-watch'); const get_checksum = require('../utils/get-checksum'); const get_dependency_key = require('../utils/get-dependency-key'); const lifecycles = require('./lifecycles'); const uids_config = require('./uids-config'); const { delete_attrs_file, replace_attrs_file} = require('./handle-config'); class Servers extends RedisPubsub { constructor(app) { super(app.redis_cache); this.local_cache = app.local_cache; this.app = app; this.strapi = app.strapi; this.instances = []; this.subscribed_uids = []; } is_primary() { if (this.instances.length === 0) return false; return this.uuid === this.instances[0].uuid; } async on_receive(message, from) { debug_verbose(from, logging.cmsg(message)); switch(message.action) { case 'keep-alive': this.update_instances(message, from); break; case 'shutdown': this.remove_instance(from); break; case 'subscribe-db-event': this.subscribe_lifecycle_events(message.uid, false); break; case 'evict-local-cache': this.evict_local_cache(message, from) break; case 'upsert': await this.on_db_upsert(message); break; case 'delete': await this.on_db_delete(message); break; default: logging.error(`unknown action ${message.action}`); } } async start(started_at, maintenance_interval) { await RedisPubsub.prototype.start.call(this); await this.publish({action: 'keep-alive', started_at, now_ms: started_at}); this.timer_handle = setInterval(async () => { const now_ms = Date.now(); await this.publish({action: 'keep-alive', started_at, now_ms}); for (let i = this.instances.length - 1; i >= 0; i--) { const instance = this.instances[i]; if (now_ms - instance.now_ms > 3 * maintenance_interval) { this.instances.splice(i, 1); } } if (now_ms - started_at > maintenance_interval * 3) { if (this.is_primary()) { if (!this.refresh_queue) await this.start_refresh_queue() } else { if (this.refresh_queue) await this.stop_refresh_queue(); } } }, maintenance_interval) } async stop() { if (this.timer_handle) { clearInterval(this.timer_handle); } if (this.expires_watch) { await this.expires_watch.stop(); } if (this.refresh_queue) { await this.refresh_queue.stop(); } await RedisPubsub.prototype.stop(); } async start_refresh_queue() { if (this.refresh_queue) { await this.refresh_queue.stop(); } this.refresh_queue = new RefreshQueue(this.redis_cache, this.local_cache); await this.refresh_queue.start(); if (this.expires_watch) { await this.expires_watch.stop(); } this.expires_watch = new ExpiresWatch(this.redis_cache, this.refresh_queue); await this.expires_watch.start(); logging.info(`expires watch/refresh started - ${this.uuid}`); } async stop_refresh_queue() { if (this.expires_watch) { await this.expires_watch.stop(); } this.expires_watch = null; if (this.refresh_queue) { await this.refresh_queue.stop(); } this.refresh_queue = null; logging.info(`expires watch/refresh stopped - ${this.uuid}`); } evict_local_cache({keys}, from) { if (from === this.uuid) return; if (!keys || keys.length === 0) return; for (const key of keys) this.local_cache.delete(key); } async on_db_upsert(message) { const {uid, data} = message; if (uid && data) { debug('on_db_upsert', uid, data.id); const uids = Object.values(uids_config); if (uids.includes(uid)) { this.app.update_config(uid, data); if (uids_config.handle_uid === uid) { await this.evict_dependent(uid, data.handle); } else if (uids_config.files_uid === uid) { await this.on_file_change(data, replace_attrs_file); } } else if (data.id) { await this.evict_dependent(uid, data.id); } } else { logging.error('unknown on_db_upsert message', message); } } async on_db_delete(message) { const {uid, data} = message; if (uid && data) { debug('on_db_delete', uid, data.id); const uids = Object.values(uids_config); if (uids.includes(uid)) { if (uids_config.files_uid === uid) { await this.on_file_change(data, delete_attrs_file); } else { this.app.del_config(uid, data); if (uids_config.handle_uid === uid) { await this.evict_dependent(uid, data.handle); } } } else { await this.evict_dependent(uid, data.id); } } else { logging.error('unknown on_db_delete message', message); } } async on_file_change(data, do_the_change) { const file_key = get_checksum({file_id: data.id}); const config_keys = this.local_cache.get(file_key); if (!config_keys) return; for (const config_key of config_keys) { const config = this.local_cache.get(config_key); if (!config) continue; // this modify the data in the cache too do_the_change(config, data); const {checksum, modified_time, timestamp, ...rest } = config; config.checksum = get_checksum(rest); if (config.checksum !== checksum) { config.modified_time = Date.now(); config.timestamp = timestamp; await this.evict_dependent_by_key(config_key); } } } update_instances(message, from) { if (!message.started_at) return; let instance = {uuid: from, started_at: message.started_at}; if (this.instances.length > 0) { if (this.instances.find(x => x.uuid === from)) { instance = null; } else { for (let i = 0; i < this.instances.length; i++) { const { uuid, started_at } = this.instances[i]; if (instance.started_at < started_at) { this.instances.splice(i, 0, instance); instance = null; break; } else if (instance.started_at === started_at && from > uuid) { this.instances.splice(i, 0, instance); instance = null; break; } } } } if (instance) this.instances.push(instance); } remove_instance(uuid) { const index = this.instances.findIndex(x => x.uuid === uuid); if (index !== -1) this.instances.splice(index, 1); } async evict_dependent(uid, id) { const key = get_dependency_key({uid, id}); debug('evict_dependent', key, uid, id); await this.evict_dependent_by_key(key); } async evict_dependent_by_key(key) { const keys = await this.redis_cache.get_dependencies(key); debug('evict_dependent_by_key', key, logging.cmsg(keys)); if (keys.length === 0) return; for (const key of keys) { const cacheable = new Cacheable({key}); await cacheable.del(this.local_cache, this.redis_cache); } this.publish({action: 'evict-local-cache', keys}) } after_upsert(event) { const uid = event.model.uid; if (!event.result.hasOwnProperty('publishedAt') || event.result.publishedAt) { this.publish({uid, action: 'upsert', data: event.result}); } else if (event.params.data.publishedAt === null) { this.publish({uid, action: 'delete', data: event.result}); } } after_delete(event) { const uid = event.model.uid; if (event.params.deleted_items) { for (const item of event.params.deleted_items) { if (!item.hasOwnProperty('publishedAt') || item.publishedAt) { this.publish({uid, action: 'delete', data: item}); } } } else if (!event.result.hasOwnProperty('publishedAt') || event.result.publishedAt) { this.publish({uid, action: 'delete', data: event.result}); } } subscribe_lifecycle_events(uid, publish = true) { if (!uid) { logging.error('failed to subscribe_lifecycle_events, invalid uid'); return; } if (this.subscribed_uids.includes(uid)) return; if (this.is_primary()) { // save the new the uid for other servers and restart this.save_lifecycle_uid(uid); } this.subscribed_uids.push(uid); if (publish) this.publish({uid, action: 'subscribe-db-event'}); this.strapi.db.lifecycles.subscribe(lifecycles(this, uid)); logging.debug(`subscribe_lifecycle_events ${uid}`); } save_lifecycle_uid(uid) { this.strapi.entityService.findMany(uids_config.state_uid, {filters: {key: 'lifecycle_uids'}, limit: 1}).then(result => { if (result.length === 0) { this.strapi.entityService.create(uids_config.state_uid, {data: {key: 'lifecycle_uids', value: [uid]}}); } else { const { id, value } = result[0]; const lifecycle_uids = value || []; if (lifecycle_uids.includes(uid)) return; lifecycle_uids.push(uid); this.strapi.entityService.update(uids_config.state_uid, id, {data: {value: lifecycle_uids}}); } }) } } module.exports = Servers;