Skip to content

Commit

Permalink
First attempt at some new caching; including in-memory, on-disk, and …
Browse files Browse the repository at this point in the history
…on S3
  • Loading branch information
mattgodbolt committed May 18, 2018
1 parent 5b60a1d commit 36b7208
Show file tree
Hide file tree
Showing 8 changed files with 587 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .idea/jsLibraryMappings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions lib/cache/base.js
@@ -0,0 +1,93 @@
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

const crypto = require('crypto'),
logger = require('../logger').logger;

const HashVersion = 'Compiler Explorer Cache Version 1';
const ReportEveryMs = 5 * 60 * 1000;

class BaseCache {
constructor(name) {
this.name = name;
this.gets = 0;
this.hits = 0;
this.puts = 0;
this.reporter = setInterval(() => this.report(), ReportEveryMs);
}

dispose() {
clearInterval(this.reporter);
}

stats() {
return {hits: this.hits, puts: this.puts, gets: this.gets};
}

statString() {
const pc = this.gets ? (100 * this.hits) / this.gets : 0;
const misses = this.gets - this.hits;
return `${this.puts} puts; ${this.gets} gets, ${this.hits} hits, ${misses} misses (${pc.toFixed(2)}%)`;
}

report() {
logger.info(`${this.name}: cache stats: ${this.statString()}`);
}

static hash(object) {
return crypto.createHmac('sha256', HashVersion)
.update(JSON.stringify(object))
.digest('hex');
}

get(key) {
this.gets++;
return this.getInternal(key)
.then(result => {
if (result.hit) {
this.hits++;
}
return result;
});
}

put(key, value, creator) {
if (!(value instanceof Buffer))
value = new Buffer(value);
this.puts++;
return this.putInternal(key, value, creator);
}

// eslint-disable-next-line no-unused-vars
getInternal(key) {
return Promise.reject("should be implemented in subclass");
}

// eslint-disable-next-line no-unused-vars
putInternal(key, value, creator) {
return Promise.reject("should be implemented in subclass");
}
}

module.exports = BaseCache;
59 changes: 59 additions & 0 deletions lib/cache/in-memory.js
@@ -0,0 +1,59 @@
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

const LRU = require('lru-cache'),
BaseCache = require('./base.js');

class InMemoryCache extends BaseCache {
constructor(cacheMb) {
super(`InMemoryCache(${cacheMb}Mb)`);
this.cache = LRU({
max: cacheMb * 1024 * 1024,
length: n => {
if (n instanceof Buffer)
return n.length;
return JSON.stringify(n).length;
}
});
}

statString() {
return `${super.statString()}, LRU has ${this.cache.itemCount} item(s) totalling ${this.cache.length} bytes`;
}

getInternal(key) {
const cached = this.cache.get(key);
return Promise.resolve({
hit: !!cached,
data: cached
});
}

putInternal(key, value/*, creator*/) {
this.cache.set(key, value);
return Promise.resolve();
}
}

module.exports = InMemoryCache;
58 changes: 58 additions & 0 deletions lib/cache/multi.js
@@ -0,0 +1,58 @@
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

const BaseCache = require('./base.js');

// A write-through multiple cache.
// Writes get pushed to all caches, but reads are serviced from the first cache that returns
// a hit.
class MultiCache extends BaseCache {
constructor(...upstream) {
super("Multi");
this.upstream = upstream;
}

statString() {
return `${super.statString()}. ${this.upstream.map(c => `${c.name}: ${c.statString()}`).join(". ")}`;
}

getInternal(key) {
let promiseChain = Promise.resolve({hit: false});
for (let cache of this.upstream) {
promiseChain = promiseChain.then(upstream => {
if (upstream.hit) return upstream;
return cache.get(key);
});
}
return promiseChain;
}

putInternal(object, value, creator) {
return Promise.all(this.upstream.map(cache => {
return cache.put(object, value, creator);
}));
}
}

module.exports = MultiCache;
104 changes: 104 additions & 0 deletions lib/cache/on-disk.js
@@ -0,0 +1,104 @@
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

const LRU = require('lru-cache'),
fs = require('fs'),
path = require('path'),
mkdirp = require('mkdirp'),
BaseCache = require('./base.js'),
denodeify = require('denodeify');

// With thanks to https://gist.github.com/kethinov/6658166
function getAllFiles(root, dir) {
dir = dir || root;
return fs.readdirSync(dir).reduce((files, file) => {
const fullPath = path.join(dir, file);
const name = path.relative(root, fullPath);
const isDirectory = fs.statSync(fullPath).isDirectory();
return isDirectory ? [...files, ...getAllFiles(root, fullPath)] : [...files, {name, fullPath}];
}, []);
}

const readFile = denodeify(fs.readFile);
const writeFile = denodeify(fs.writeFile);

class OnDiskCache extends BaseCache {
constructor(path, cacheMb) {
super(`OnDiskCache(${path}, ${cacheMb}mb)`);
this.path = path;
this.cache = LRU({
max: cacheMb * 1024 * 1024,
length: n => n.size,
dispose: (key, n) => {
fs.unlink(n.path, () => {});
}
});
mkdirp.sync(path);
const info = getAllFiles(path).map(({name, fullPath}) => {
const stat = fs.statSync(fullPath);
return {
key: name,
sort: stat.ctimeMs,
data: {
path: fullPath,
size: stat.size
}
};
});
// Sort oldest first
info.sort((x, y) => {
return x.sort - y.sort;
});
for (let i of info) {
this.cache.set(i.key, i.data);
}
}

statString() {
return `${super.statString()}, LRU has ${this.cache.itemCount} item(s) ` +
`totalling ${this.cache.length} bytes on disk`;
}

getInternal(key) {
const cached = this.cache.get(key);
if (!cached) return Promise.resolve({hit: false});
return readFile(cached.path)
.then((data) => {
return {hit: true, data: data};
});
}

putInternal(key, value) {
const info = {
path: path.join(this.path, key),
size: value.length
};
return writeFile(info.path, value)
.then(() => {
this.cache.set(key, info);
});
}
}

module.exports = OnDiskCache;
63 changes: 63 additions & 0 deletions lib/cache/s3.js
@@ -0,0 +1,63 @@
// Copyright (c) 2018, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

const BaseCache = require('./base.js');
const AWS = require('aws-sdk');

class S3Cache extends BaseCache {
constructor(bucket, path, region) {
super(`S3Cache(s3://${bucket}/${path} in ${region})`);
this.bucket = bucket;
this.path = path;
this.s3 = new AWS.S3({region});
}

statString() {
return `${super.statString()}, LRU has ${this.cache.itemCount} item(s) ` +
`totalling ${this.cache.length} bytes on disk`;
}

getInternal(key) {
return this.s3.getObject({Bucket: this.bucket, Key: `${this.path}/${key}`})
.promise()
.then((result) => {
return {hit: true, data: result.Body};
})
.catch((x) => {
if (x.code === 'NoSuchKey') return {hit: false};
throw x;
});
}

putInternal(key, value, creator) {
return this.s3.putObject({
Bucket: this.bucket, Key: `${this.path}/${key}`, Body: value,
StorageClass: "REDUCED_REDUNDANCY",
Metadata: {CreatedBy: creator}
})
.promise();
}
}

module.exports = S3Cache;
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -36,6 +36,7 @@
"jquery": "^3.3.1",
"lru-cache": "^4.1.2",
"lz-string": "^1.4.4",
"mkdirp": "^0.5.1",
"monaco-editor": "0.10.1",
"morgan": "^1.9.0",
"nopt": "3.0.x",
Expand Down

0 comments on commit 36b7208

Please sign in to comment.