Skip to content

Commit

Permalink
Implement an async cache helper.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
flatheadmill committed Dec 26, 2020
1 parent 74a0afe commit 0cee5e9
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 3 deletions.
100 changes: 99 additions & 1 deletion magazine.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// One more assertion and I use Interrupt. (Currently, two.)
const assert = require('assert')

const Keyify = require('keyify')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
79 changes: 77 additions & 2 deletions test/readme.t.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
}
}
})

0 comments on commit 0cee5e9

Please sign in to comment.