From 0cee5e9fb3ce6e6b0943a375f1811bf189df0711 Mon Sep 17 00:00:00 2001 From: Alan Gutierrez Date: Sat, 26 Dec 2020 00:42:36 -0600 Subject: [PATCH] Implement an async cache helper. This is not based on `heft`, just on the count. Could do heft probably. Not even sure if this belongs in this library or if it should be a part of Strata where it will be used. Wanted to unit test it apart from having to simulate errors with file handles, but it would be easy enough to leave it an generic class, but keep it specific to Strata. Then I don't have to document it here, nor to I have to bulk up the Cache class with all these overlays. --- magazine.js | 100 ++++++++++++++++++++++++++++++++++++++++++++++- test/readme.t.js | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 3 deletions(-) diff --git a/magazine.js b/magazine.js index 524e6ab..197a483 100644 --- a/magazine.js +++ b/magazine.js @@ -1,3 +1,4 @@ +// One more assertion and I use Interrupt. (Currently, two.) const assert = require('assert') const Keyify = require('keyify') @@ -104,6 +105,103 @@ class Magazine { } } + static OpenClose = class { + static _MISSING = Symbol('MISSING') + + static _PRESENT = Symbol('PRESENT') + + constructor (magazine, options) { + this.magazine = magazine + this._map = new WeakMap + this._options = options + } + + // If your open function raises an exception no entry will be added to + // the cache and the exception will be propagated to the function that + // called `get`. You should be sure to do any cleanup in your `open` + // function. There will be no way for the `OpenClose` class to call + // close for you. + + // + async get (key, ...vargs) { + for (;;) { + const cartridge = this.magazine.hold(key, Magazine.OpenClose._MISSING) + if (cartridge.value == Magazine.OpenClose._MISSING) { + cartridge.value = Magazine.OpenClose._PRESENT + let capture + const meta = { + promise: new Promise(resolve => capture = { resolve }), + handle: null, + valid: true + } + this._map.set(cartridge, meta) + try { + cartridge.value = await this.open.apply(this, [ key ].concat(vargs)) + } catch (error) { + cartridge.remove() + throw error + } finally { + meta.promise = null + capture.resolve.call(null) + } + return cartridge + } else { + const meta = this._map.get(cartridge) + if (meta.promise != null) { + cartridge.release() + await meta.promise + } else { + assert(meta.valid, 'invalid handle') + return cartridge + } + } + } + } + // + + // If your close function raises an exception we do not remove the entry + // from the cache, we mark it as invalid and we will raise exceptions + // until, hopefully, your evil program is destroyed. + + // An excpetion from `close` will be propagated to the function that + // called `shrink` so it will unwind the stack, but if you handle the + // exception outside of `close` the cache is going to become unusable. + + // This is fine. + + // + async shrink (size) { + while (this.magazine.count > size) { + const cartridge = this.magazine.least() + if (cartridge == null) { + break + } + const meta = this._map.get(cartridge) + meta.valid = false + let capture + meta.promise = new Promise(resolve => capture = { resolve }) + try { + await this.close(cartridge.value) + cartridge.remove() + } catch (error) { + cartridge.release() + throw error + } finally { + meta.promise = null + capture.resolve.call(null) + } + } + } + + async open (...vargs) { + return this._options.open.apply(null, vargs) + } + + async close (handle) { + return this._options.close.call(null, handle) + } + } + constructor (parent = null) { const path = [ this ] let iterator = parent @@ -192,7 +290,7 @@ class Magazine { } least () { - const entry = this._head._links[this._index].previous + const previous = this._head._links[this._index].previous if (previous.cartridge == null || previous.cartridge._references != 0) { return null } diff --git a/test/readme.t.js b/test/readme.t.js index 4e270fa..5882f8c 100644 --- a/test/readme.t.js +++ b/test/readme.t.js @@ -25,7 +25,7 @@ // we make about Magazine. // -require('proof')(66, async okay => { +require('proof')(77, async okay => { // // First we'll talk about the basics of Magazine. Some of the functionality @@ -402,10 +402,11 @@ require('proof')(66, async okay => { // { - const heft = 1024 * 24 + const heft = 1024 * 26 okay(magazine.heft > heft, 'desired heft not met') okay(magazine.count, 2, 'two files in the cache') magazine.purge(heft) + // _Note to self. This test breaks as the `readme.t.js` gets longer. okay(magazine.count, 1, 'one file evicted from the cache') okay(magazine.heft <= heft, 'desired heft achieved') okay(! magazine.hold(files.source), 'source file was removed') @@ -665,5 +666,79 @@ require('proof')(66, async okay => { okay(second.value.data, 1, 'guarded data was set') unlock(second) okay(log, [ 'waiting' ], 'someone had to wait') + magazine.shrink(0) + } + + { + const openings = [{ + expect: { key: 1, vargs: [ 1 ] }, response: 1, message: 'get race' + }, { + expect: { key: 1, vargs: [ 2 ] }, response: 2, message: 'get evict race' + }, new Error('open') ] + const closings = [{ + expect: { handle: 1 }, message: 'close evict race' + }, new Error('close') ] + const openClose = new Magazine.OpenClose(magazine, { + open: async (key, ...vargs) => { + const opening = openings.shift() + if (opening instanceof Error) { + throw opening + } + okay({ key, vargs }, opening.expect, opening.message) + return opening.response + }, + close: async handle => { + const closing = closings.shift() + if (closing instanceof Error) { + throw closing + } + okay({ handle }, closing.expect, closing.message) + } + }) + // Open race. + const gots = [ openClose.get(1, 1), openClose.get(1, 2) ] + { + const got = await gots.shift() + okay(got.value, 1, 'got race winner') + got.release() + } + { + const got = await gots.shift() + okay(got.value, 1, 'got race loser') + + await openClose.shrink(0) + + okay(openClose.magazine.count, 1, 'not shrunk becasue we still hold a reference') + + got.release() + } + // Evict race. + const promises = [ openClose.shrink(0), openClose.get(1, 2) ] + await promises.shift() + { + const got = await promises.shift() + okay(got.value, 2, 'entry recreated') + got.release() + } + + okay(openClose.magazine.count, 1, 'one async entry') + + try { + await openClose.get(2) + } catch (error) { + okay(error.message, 'open', 'open error') + } + + try { + await openClose.shrink(0) + } catch (error) { + okay(error.message, 'close', 'close error') + } + + try { + await openClose.get(1) + } catch (error) { + okay(error.message, 'invalid handle', 'cache is corrupted') + } } })