Ver Fonte

dev: apply TTL cache for stat entries

Adds CachedFilesystem layer to client filesystem chain. Currently only
stat is implemented. The stat implementation will hold onto an entry for
3 seconds as per hardcoded configuration. Eventually, once invalidation
via websockets is working, this TTL should be extended.
KernelDeimos há 7 meses atrás
pai
commit
6ee7caaee1

+ 124 - 0
src/puter-js/src/modules/FileSystem/CacheFS.js

@@ -0,0 +1,124 @@
+import putility from "@heyputer/putility";
+import { RWLock } from "@heyputer/putility/src/libs/promise";
+import { ProxyFilesystem, TFilesystem } from "./definitions";
+
+export const ROOT_UUID = '00000000-0000-0000-0000-000000000000';
+
+export class CacheFS extends putility.AdvancedBase {
+    static PROPERTIES = {
+        // 'internal_uuid' maps a path or external UUID to
+        // the respective cache entry UUID
+        // (which for now is the same as the public UUID)
+        internal_uuid: () => ({}),
+        entries: () => ({}),
+    };
+
+    get_entry_ei (external_identifier) {
+        const internal_identifier = this.internal_uuid[external_identifier];
+        if ( ! internal_identifier ) {
+            return;
+        }
+        return this.entries[internal_identifier];
+    }
+
+    add_entry ({
+        external_identifiers,
+        internal_identifier,
+    }) {
+        const entry = {
+            stat_has: {},
+            stat_exp: 0,
+            locks: {
+                stat: new RWLock(),
+            },
+        };
+        for ( const ident of external_identifiers ) {
+            this.internal_uuid[ident] = internal_identifier;
+        }
+        this.entries[internal_identifier] = entry;
+        console.log('cREATED ENTRY', this.internal_uuid, this.entries);
+        return entry;
+    }
+}
+
+export class CachedFilesystem extends ProxyFilesystem {
+    constructor (o) {
+        super(o);
+        // this.cacheFS = cacheFS;
+        this.cacheFS = new CacheFS();
+    }
+    static IMPLEMENTS = {
+        [TFilesystem]: {
+            stat: async function (o) {
+                let cent = this.cacheFS.get_entry_ei(o.path ?? o.uid);
+
+                const modifiers = [
+                    'subdomains',
+                    'permissions',
+                    'versions',
+                    'size',
+                ];
+
+                let values_requested = {};
+                for ( const mod of modifiers ) {
+                    const optionsKey = 'return' +
+                        mod.charAt(0).toUpperCase() +
+                        mod.slice(1);
+                    if ( ! o[optionsKey] ) continue;
+                    values_requested[mod] = true;
+                }
+
+                const satisfactory_cache = cent => {
+                    for ( const mod of modifiers ) {
+                        if ( ! values_requested[mod] ) continue;
+                        if ( ! cent.stat_has[mod] ) {
+                            return false;
+                        }
+                    }
+                    return true;
+                }
+                
+                let cached_stat;
+                if ( cent && cent.stat && cent.stat_exp > Date.now() ) {
+                    const l = await cent.locks.stat.rlock();
+                    if ( satisfactory_cache(cent) ) {
+                        cached_stat = cent.stat;
+                    }
+                    l.unlock();
+                }
+
+                if ( cached_stat ) {
+                    console.log('CACHE HIT');
+                    return cached_stat;
+                }
+                console.log('CACHE MISS');
+
+                let l;
+                if ( cent ) {
+                    l = await cent.locks.stat.wlock();
+                }
+
+                console.log('DOING THE STAT', o);
+                const entry = await this.delegate.stat(o);
+
+                if ( ! cent ) {
+                    cent = this.cacheFS.add_entry({
+                        external_identifiers: [entry.path, entry.uid],
+                        internal_identifier: entry.uid,
+                    });
+                    l = await cent.locks.stat.wlock();
+                }
+
+                cent.stat = entry;
+                cent.stat_has = { ...values_requested };
+                // TODO: increase cache TTL once invalidation works
+                cent.stat_exp = Date.now() + 1000*3;
+
+                l.unlock();
+
+                console.log('RETRUNING THE ENTRY', entry);
+                return entry;
+            }
+        }
+    }
+}

+ 5 - 1
src/puter-js/src/modules/FileSystem/index.js

@@ -15,6 +15,7 @@ import sign from "./operations/sign.js";
 import deleteFSEntry from "./operations/deleteFSEntry.js";
 import { ProxyFilesystem, PuterAPIFilesystem, TFilesystem } from './definitions.js';
 import { AdvancedBase } from '../../../../putility/index.js';
+import { CachedFilesystem } from './CacheFS.js';
 
 export class PuterJSFileSystemModule extends AdvancedBase {
 
@@ -34,12 +35,14 @@ export class PuterJSFileSystemModule extends AdvancedBase {
     static NARI_METHODS = {
         stat: {
             positional: ['path'],
-            fn (parameters) {
+            firstarg_options: true,
+            async fn (parameters) {
                 return this.filesystem.stat(parameters);
             }
         },
         readdir: {
             positional: ['path'],
+            firstarg_options: true,
             fn (parameters) {
                 return this.filesystem.readdir(parameters);
             }
@@ -75,6 +78,7 @@ export class PuterJSFileSystemModule extends AdvancedBase {
 
         // Construct the decorator chain for the client-side filesystem.
         let fs = new PuterAPIFilesystem({ api_info }).as(TFilesystem);
+        fs = new CachedFilesystem({ delegate: fs }).as(TFilesystem);
         fs = new ProxyFilesystem({ delegate: fs }).as(TFilesystem);
         this.filesystem = fs;
     }