diff --git a/src/backend/src/filesystem/ECMAP.js b/src/backend/src/filesystem/ECMAP.js new file mode 100644 index 000000000..249d33501 --- /dev/null +++ b/src/backend/src/filesystem/ECMAP.js @@ -0,0 +1,95 @@ +const { Context } = require("../util/context"); +const { NodeUIDSelector, NodePathSelector, NodeInternalIDSelector } = require("./node/selectors"); + +const LOG_PREFIX = '\x1B[31;1m[[\x1B[33;1mEC\x1B[32;1mMAP\x1B[31;1m]]\x1B[0m'; + +/** + * The ECMAP class is a memoization structure used by FSNodeContext + * whenever it is present in the execution context (AsyncLocalStorage). + * It is assumed that this object is transient and invalidation of stale + * entries is not necessary. + * + * The name ECMAP simple means Execution Context Map, because the map + * exists in memory at a particular frame of the execution context. + */ +class ECMAP { + static SYMBOL = Symbol('ECMAP'); + + constructor () { + this.identifier = require('uuid').v4(); + + // entry caches + this.uuid_to_fsNodeContext = {}; + this.path_to_fsNodeContext = {}; + this.id_to_fsNodeContext = {}; + + // identifier association caches + this.path_to_uuid = {}; + this.uuid_to_path = {}; + } + + get logPrefix () { + return `${LOG_PREFIX} \x1B[36[1m${this.identifier}\x1B[0m`; + } + + log (...a) { + if ( ! process.env.LOG_ECMAP ) return; + console.log(this.logPrefix, ...a); + } + + get_fsNodeContext_from_selector (selector) { + this.log('GET', selector.describe()); + const retvalue = (() => { + let value; + if ( selector instanceof NodeUIDSelector ) { + value = this.uuid_to_fsNodeContext[selector.value]; + if ( value ) return value; + + let maybe_path = this.uuid_to_path[value]; + if ( ! maybe_path ) return; + value = this.path_to_fsNodeContext[maybe_path]; + if ( value ) return value; + } + else + if ( selector instanceof NodePathSelector ) { + value = this.path_to_fsNodeContext[maybe_path]; + if ( value ) return value; + + let maybe_uid = this.path_to_uuid[value]; + value = this.uuid_to_fsNodeContext[maybe_uid]; + if ( value ) return value; + } + })(); + if ( retvalue ) { + this.log('\x1B[32;1m <<<<< ECMAP HIT >>>>> \x1B[0m'); + } else { + this.log('\x1B[31;1m <<<<< ECMAP MISS >>>>> \x1B[0m'); + } + return retvalue; + } + + store_fsNodeContext_to_selector (selector, node) { + this.log('STORE', selector.describe()); + if ( selector instanceof NodeUIDSelector ) { + this.uuid_to_fsNodeContext[selector.value] = node; + } + if ( selector instanceof NodePathSelector ) { + this.path_to_fsNodeContext[selector.value] = node; + } + if ( selector instanceof NodeInternalIDSelector ) { + this.id_to_fsNodeContext[selector.service+':'+selector.id] = node; + } + } + + static async arun (cb) { + let context = Context.get(); + if ( ! context.get(this.SYMBOL) ) { + context = context.sub({ + [this.SYMBOL]: new this(), + }); + } + return await context.arun(cb); + } +} + +module.exports = { ECMAP }; diff --git a/src/backend/src/filesystem/FSNodeContext.js b/src/backend/src/filesystem/FSNodeContext.js index cc062c94c..ccbf4ca43 100644 --- a/src/backend/src/filesystem/FSNodeContext.js +++ b/src/backend/src/filesystem/FSNodeContext.js @@ -27,6 +27,7 @@ const { NodeRawEntrySelector } = require("./node/selectors"); const { DB_READ } = require("../services/database/consts"); const { UserActorType, AppUnderUserActorType, Actor } = require("../services/auth/Actor"); const { PermissionUtil } = require("../services/auth/PermissionService"); +const { ECMAP } = require("./ECMAP"); /** * Container for information collected about a node @@ -77,6 +78,19 @@ module.exports = class FSNodeContext { provider, fs }) { + const ecmap = Context.get(ECMAP.SYMBOL); + + if ( ecmap ) { + // We might return an existing FSNodeContext + const maybe_node = ecmap?. + get_fsNodeContext_from_selector?.(selector); + if ( maybe_node ) return maybe_node; + } else { + if ( process.env.LOG_ECMAP ) { + console.log('\x1B[31;1m !!! NO ECMAP !!! \x1B[0m'); + } + } + this.log = services.get('log-service').create('fsnode-context', { concern: this.constructor.CONCERN, }); @@ -128,6 +142,12 @@ module.exports = class FSNodeContext { for ( const selector of this.selectors_ ) { if ( selector instanceof new_selector.constructor ) return; } + + const ecmap = Context.get(ECMAP.SYMBOL); + if ( ecmap ) { + ecmap.store_fsNodeContext_to_selector(new_selector, this); + } + this.selectors_.push(new_selector); this.selector_ = new_selector; } diff --git a/src/backend/src/filesystem/hl_operations/hl_readdir.js b/src/backend/src/filesystem/hl_operations/hl_readdir.js index ebf0be06a..88cf23a06 100644 --- a/src/backend/src/filesystem/hl_operations/hl_readdir.js +++ b/src/backend/src/filesystem/hl_operations/hl_readdir.js @@ -18,6 +18,7 @@ */ const APIError = require("../../api/APIError"); const { stream_to_buffer } = require("../../util/streamutil"); +const { ECMAP } = require("../ECMAP"); const { TYPE_DIRECTORY, TYPE_SYMLINK } = require("../FSNodeContext"); const { LLListUsers } = require("../ll_operations/ll_listusers"); const { LLReadDir } = require("../ll_operations/ll_readdir"); @@ -26,7 +27,12 @@ const { HLFilesystemOperation } = require("./definitions"); class HLReadDir extends HLFilesystemOperation { static CONCERN = 'filesystem'; - async _run () { + async _run() { + return ECMAP.arun(async () => { + return await this.__run(); + }); + } + async __run () { const { subject: subject_let, user, no_thumbs, no_assocs, actor } = this.values; let subject = subject_let; diff --git a/src/backend/src/filesystem/ll_operations/ll_readdir.js b/src/backend/src/filesystem/ll_operations/ll_readdir.js index 1c2439723..ea0d68992 100644 --- a/src/backend/src/filesystem/ll_operations/ll_readdir.js +++ b/src/backend/src/filesystem/ll_operations/ll_readdir.js @@ -18,6 +18,7 @@ */ const APIError = require("../../api/APIError"); const fsCapabilities = require("../definitions/capabilities"); +const { ECMAP } = require("../ECMAP"); const { TYPE_SYMLINK } = require("../FSNodeContext"); const { RootNodeSelector } = require("../node/selectors"); const { NodeUIDSelector, NodeChildSelector } = require("../node/selectors"); @@ -25,7 +26,12 @@ const { LLFilesystemOperation } = require("./definitions"); class LLReadDir extends LLFilesystemOperation { static CONCERN = 'filesystem'; - async _run () { + async _run() { + return ECMAP.arun(async () => { + return await this.__run(); + }); + } + async __run () { const { context } = this; const { subject: subject_let, actor, no_acl } = this.values; let subject = subject_let; diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index 5d87b1667..4a45fbf92 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -19,7 +19,9 @@ */ const APIError = require("../../api/APIError"); +const { ECMAP } = require("../../filesystem/ECMAP"); const { get_user, get_app } = require("../../helpers"); +const { Context } = require("../../util/context"); const BaseService = require("../BaseService"); const { DB_WRITE } = require("../database/consts"); const { UserActorType, Actor, AppUnderUserActorType } = require("./Actor"); @@ -354,7 +356,9 @@ class PermissionService extends BaseService { async scan (actor, permission_options, _reserved, state) { const svc_trace = this.services.get('traceService'); return await svc_trace.spanify(`permission:scan`, async () => { - return await this.scan_(actor, permission_options, _reserved, state); + return await ECMAP.arun(async () => { + return await this.scan_(actor, permission_options, _reserved, state); + }); }, { attributes: { permission_options }, actor: actor.uid }); } async scan_ (actor, permission_options, _reserved, state) {