Skip to content

Commit

Permalink
chore(refactor): extract cache to middleware (#156)
Browse files Browse the repository at this point in the history
* chore(refactor): extract the cache logic to middleware

* chore: rename middleware to reflect route

* chore: remove bluebird, in profit of built-in promise class

* chore: add tests for useProxyCache middleware

* chore: instrument the ipfs gateway cache layer

* chore: instrument the hit/miss ratio and cache size

* chore: fix remaining merge conflict
  • Loading branch information
wa0x6e authored Sep 21, 2023
1 parent b0a09a7 commit 2039c31
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 30 deletions.
11 changes: 11 additions & 0 deletions src/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,14 @@ export async function get(key) {
return false;
}
}

export async function remove(key) {
try {
return await client.deleteObject({
Bucket: process.env.AWS_BUCKET_NAME,
Key: `public/${dir}/${key}`
});
} catch (e: any) {
return false;
}
}
12 changes: 12 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ export const ipfsGatewaysReturnCount = new client.Counter({
labelNames: ['name']
});

export const ipfsGatewaysCacheHitCount = new client.Counter({
name: 'ipfs_gateways_cache_hit_count',
help: 'Number of hit/miss of the IPFS gateways cache layer',
labelNames: ['status']
});

export const ipfsGatewaysCacheSize = new client.Counter({
name: 'ipfs_gateways_cache_size',
help: 'Total size going through the IPFS gateways cache layer',
labelNames: ['status']
});

export const countOpenProvidersRequest = new client.Gauge({
name: 'providers_open_connections_count',
help: 'Number of open connections to providers.',
Expand Down
42 changes: 42 additions & 0 deletions src/middlewares/useProxyCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import { MAX } from '../utils';
import { get, set } from '../aws';
import { ipfsGatewaysCacheHitCount, ipfsGatewaysCacheSize } from '../metrics';

/**
* This middleware serves a cache if it exists, else it will process the controller
* and caches its results if it's less than 1MB
*/
export default async function useProxyCache(req, res, next) {
const { cid } = req.params;

const cache = await get(cid);
if (cache) {
const cachedSize = Buffer.from(JSON.stringify(cache)).length;
ipfsGatewaysCacheHitCount.inc({ status: 'HIT' });
ipfsGatewaysCacheSize.inc({ status: 'HIT' }, cachedSize);
return res.json(cache);
}

const oldJson = res.json;
res.json = async body => {
res.locals.body = body;

if (res.statusCode === 200 && body) {
try {
const size = Buffer.from(JSON.stringify(body)).length;
if (size <= MAX) {
ipfsGatewaysCacheHitCount.inc({ status: 'MISS' });
ipfsGatewaysCacheSize.inc({ status: 'MISS' }, size);
await set(cid, body);
}
} catch (e) {
capture(e);
}
}

return oldJson.call(res, body);
};

next();
}
25 changes: 7 additions & 18 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@ import express from 'express';
import fetch from 'node-fetch';
import { capture } from '@snapshot-labs/snapshot-sentry';
import gateways from './gateways.json';
import { set, get } from './aws';
import { MAX } from './utils';
import {
ipfsGatewaysReturnCount,
timeIpfsGatewaysResponse,
countOpenGatewaysRequest
} from './metrics';
import useProxyCache from './middlewares/useProxyCache';

const router = express.Router();
const UNSUPPORTED_FILE_TYPE = 'unsupported file type';

router.get('^/ipfs/:cid([0-9a-zA-Z]+)$', async (req, res) => {
const { cid } = req.params;
router.get('^/ipfs/:cid([0-9a-zA-Z]+)$', useProxyCache, async (req, res) => {
try {
const cache = await get(cid);
if (cache) return res.json(cache);

const result = await Promise.any(
gateways.map(async gateway => {
const end = timeIpfsGatewaysResponse.startTimer({ name: gateway });
Expand All @@ -34,14 +30,14 @@ router.get('^/ipfs/:cid([0-9a-zA-Z]+)$', async (req, res) => {
}

if (!['text/plain', 'application/json'].includes(response.headers.get('content-type'))) {
return Promise.reject('');
return Promise.reject(UNSUPPORTED_FILE_TYPE);
}

let json;
try {
json = await response.json();
} catch {
return Promise.reject('');
} catch (e: any) {
return Promise.reject(e);
}

status = 1;
Expand All @@ -54,17 +50,10 @@ router.get('^/ipfs/:cid([0-9a-zA-Z]+)$', async (req, res) => {
);
ipfsGatewaysReturnCount.inc({ name: result.gateway });

try {
const size = Buffer.from(JSON.stringify(result.json)).length;
if (size <= MAX) await set(cid, result.json);
} catch (e) {
capture(e);
}

return res.json(result.json);
} catch (e) {
if (e instanceof AggregateError) {
return res.status(400).json();
return res.status(e.errors.includes(UNSUPPORTED_FILE_TYPE) ? 415 : 400).json();
}

capture(e);
Expand Down
47 changes: 36 additions & 11 deletions test/e2e/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
import request from 'supertest';
import { set, get, remove } from '../../src/aws';

const HOST = `http://localhost:${process.env.PORT || 3003}`;

describe('GET /ipfs/*', () => {
describe('GET /ipfs/:cid', () => {
describe('when the IPFS cid exists', () => {
it('returns a JSON file', async () => {
const response = await request(HOST).get(
'/ipfs/bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi'
);
const cid = 'bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi';
const path = `/ipfs/${cid}`;
const content = { status: 'OK' };

afterEach(async () => {
await remove(cid);
});

describe('when the file is cached', () => {
const cachedContent = { status: 'CACHED' };

it('returns the cache file', async () => {
await set(cid, cachedContent);
const response = await request(HOST).get(path);

expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ status: 'OK' });
expect(response.body).toEqual(cachedContent);
expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toBe('application/json; charset=utf-8');
expect(await get(cid)).toEqual(cachedContent);
});
});

it('returns a 400 error when not a JSON file', async () => {
describe('when the file is not cached', () => {
it('returns the file and caches it', async () => {
const response = await request(HOST).get(path);

expect(response.body).toEqual(content);
expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toBe('application/json; charset=utf-8');
expect(await get(cid)).toEqual(response.body);
});
});

it('returns a 415 error when not a JSON file', async () => {
const response = await request(HOST).get(
'/ipfs/bafybeie2x4ptheqskiauhfz4w4pbq7o6742oupitganczhjanvffp2spti'
);

expect(response.statusCode).toBe(400);
}, 15e3);
expect(response.statusCode).toBe(415);
}, 30e3);
});

describe('when the IPFS cid does not exist', () => {
it('returns a 400 error', async () => {
const response = await request(HOST).get('/ipfs/test');

expect(response.statusCode).toBe(400);
});
}, 30e3);
});
});
2 changes: 1 addition & 1 deletion test/e2e/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('POST /upload', () => {
});

describe('when the file is correct', () => {
it('uploads the file and returns a JSO-RPC response with the CID and its provider', async () => {
it('uploads the file and returns a JSON-RPC response with the CID and its provider', async () => {
const response = await request(HOST)
.post('/upload')
.attach('file', path.join(__dirname, './fixtures/valid.png'));
Expand Down

0 comments on commit 2039c31

Please sign in to comment.