Skip to content

Commit 3400f78

Browse files
authored
Merge pull request #166 from wchaws/dev
Support memory cache
2 parents 7828ddc + a592ce4 commit 3400f78

13 files changed

+167
-56
lines changed

source/constructs/cdk.context.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"config_json_parameter_name": "<parameter-name>",
1616
"ecs_desired_count": 10,
1717
"env": {
18-
"SHARP_QUEUE_LIMIT": "1"
18+
"SHARP_QUEUE_LIMIT": "1",
19+
"CACHE_TTL_SEC": "300",
20+
"CACHE_MAX_ITEMS": "10000",
21+
"CACHE_MAX_SIZE_MB": "1024"
1922
}
2023
}

source/constructs/lib/ecs-image-handler.ts

+5-23
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import * as ecs from '@aws-cdk/aws-ecs';
77
import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns';
88
import * as iam from '@aws-cdk/aws-iam';
99
import * as s3 from '@aws-cdk/aws-s3';
10-
import * as ssm from '@aws-cdk/aws-ssm';
1110
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
11+
import * as ssm from '@aws-cdk/aws-ssm';
1212
import { Aws, CfnOutput, Construct, Duration, Stack } from '@aws-cdk/core';
1313

1414
const GB = 1024;
@@ -178,33 +178,15 @@ function getOrCreateVpc(scope: Construct): ec2.IVpc {
178178
if (scope.node.tryGetContext('use_default_vpc') === '1' || process.env.CDK_USE_DEFAULT_VPC === '1') {
179179
return ec2.Vpc.fromLookup(scope, 'Vpc', { isDefault: true });
180180
} else if (scope.node.tryGetContext('use_vpc_id')) {
181-
const vpcFromLookup = ec2.Vpc.fromLookup(scope, 'Vpc', { vpcId: scope.node.tryGetContext('use_vpc_id') });
182-
const privateSubnetIds: string[] = scope.node.tryGetContext('subnet_ids');
183-
let publicSubnetIds: string[] = [];
184-
vpcFromLookup.publicSubnets.forEach((subnet) => {
185-
publicSubnetIds.push(subnet.subnetId);
186-
});
187-
// TODO: Try to use vpcFromLookup instead
188-
const vpc = ec2.Vpc.fromVpcAttributes(scope, 'VpcFromAttributes', {
189-
availabilityZones: vpcFromLookup.availabilityZones,
190-
vpcId: vpcFromLookup.vpcId,
191-
publicSubnetIds: publicSubnetIds,
192-
privateSubnetIds: privateSubnetIds,
193-
});
194-
return vpc;
181+
return ec2.Vpc.fromLookup(scope, 'Vpc', { vpcId: scope.node.tryGetContext('use_vpc_id') });
195182
}
196183
return new ec2.Vpc(scope, 'Vpc', { maxAzs: 3, natGateways: 1 });
197184
}
198185

199186
function getTaskSubnets(scope: Construct, vpc: ec2.IVpc): ec2.ISubnet[] {
200-
const subnetIds: string[] = scope.node.tryGetContext('subnet_ids');
201-
// TODO: use filter subnets from vpc
202-
let subnets: ec2.ISubnet[] = [];
203-
if (subnetIds) {
204-
subnetIds.forEach((subnetId, index) => {
205-
subnets.push(ec2.Subnet.fromSubnetId(scope, 'subnet' + index, subnetId));
206-
});
207-
return subnets;
187+
const subnetIds: string[] = scope.node.tryGetContext('subnet_ids') || [];
188+
if (subnetIds.length) {
189+
return subnetIds.map((subnetId, index) => ec2.Subnet.fromSubnetId(scope, 'subnet' + index, subnetId));
208190
} else {
209191
return vpc.privateSubnets;
210192
}

source/new-image-handler/Dockerfile

+1-3
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,12 @@ RUN apk update && \
5454

5555
ENV NODE_ENV=production
5656

57-
# https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/
5857
RUN yarn --production && \
59-
yarn add --dev pm2 && \
6058
yarn cache clean --all
6159

6260
COPY --from=builder /app/lib /app/lib
6361
# COPY test/fixtures /app/lib/test/fixtures
6462

6563
EXPOSE 8080
6664

67-
CMD ["node_modules/.bin/pm2-runtime", "/app/lib/src/index.js"]
65+
CMD ["node", "/app/lib/src/index.js"]

source/new-image-handler/Dockerfile.dev

-5
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,11 @@ RUN apk update && \
5555

5656
# ENV NODE_ENV=production
5757

58-
# https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/
5958
RUN yarn && \
60-
yarn add --dev pm2 && \
6159
yarn cache clean --all
6260

6361
COPY . .
64-
# COPY test/fixtures /app/lib/test/fixtures
6562

6663
EXPOSE 8080
6764

68-
# CMD ["node_modules/.bin/pm2-runtime", "/app/lib/src/index.js"]
69-
7065
CMD yarn watch-server

source/new-image-handler/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "new-image-handler",
33
"scripts": {
4-
"watch-server": "nodemon --ignore test/ --watch src -e ts,tsx --exec ts-node -P tsconfig.json src/index.ts",
4+
"watch-server": "nodemon --ignore test/ --watch src -e ts,tsx --exec ts-node -P tsconfig.json --files src/index.ts",
55
"serve": "node src/lib/index.js",
66
"compile": "tsc --project tsconfig.json",
77
"build": "yarn test && yarn compile",
@@ -24,6 +24,7 @@
2424
"@types/jest": "^26.0.24",
2525
"@types/koa": "^2.13.4",
2626
"@types/koa-bodyparser": "^4.3.7",
27+
"@types/koa-cash": "^4.1.0",
2728
"@types/koa-logger": "^3.1.2",
2829
"@types/koa-router": "^7.4.4",
2930
"@types/node": "^14.18.16",
@@ -51,8 +52,10 @@
5152
"http-errors": "^1.8.1",
5253
"koa": "^2.13.4",
5354
"koa-bodyparser": "^4.3.0",
55+
"koa-cash": "^4.1.1",
5456
"koa-logger": "^3.2.1",
5557
"koa-router": "^10.1.1",
58+
"lru-cache": "^10.0.0",
5659
"sharp": "^0.31.1"
5760
},
5861
"bundledDependencies": [],
+34-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
const {
2+
REGION,
3+
AWS_REGION,
4+
NODE_ENV,
5+
BUCKET,
6+
SRC_BUCKET,
7+
STYLE_TABLE_NAME,
8+
AUTO_WEBP,
9+
SECRET_NAME,
10+
SHARP_QUEUE_LIMIT,
11+
CONFIG_JSON_PARAMETER_NAME,
12+
CACHE_TTL_SEC,
13+
CACHE_MAX_ITEMS,
14+
CACHE_MAX_SIZE_MB,
15+
} = process.env;
16+
117
export interface IConfig {
218
port: number;
319
region: string;
@@ -8,18 +24,28 @@ export interface IConfig {
824
secretName: string;
925
sharpQueueLimit: number;
1026
configJsonParameterName: string;
27+
CACHE_TTL_SEC: number;
28+
CACHE_MAX_ITEMS: number;
29+
CACHE_MAX_SIZE_MB: number;
30+
}
31+
32+
function parseInt(s: string) {
33+
return Number.parseInt(s, 10);
1134
}
1235

1336
const conf: IConfig = {
1437
port: 8080,
15-
region: process.env.REGION ?? process.env.AWS_REGION ?? 'us-west-2',
16-
isProd: process.env.NODE_ENV === 'production',
17-
srcBucket: process.env.BUCKET || process.env.SRC_BUCKET || 'sih-input',
18-
styleTableName: process.env.STYLE_TABLE_NAME || 'style-table-name',
19-
autoWebp: ['yes', '1', 'true'].includes((process.env.AUTO_WEBP ?? '').toLowerCase()),
20-
secretName: process.env.SECRET_NAME ?? 'X-Client-Authorization',
21-
sharpQueueLimit: Number.parseInt(process.env.SHARP_QUEUE_LIMIT ?? '1', 10),
22-
configJsonParameterName: process.env.CONFIG_JSON_PARAMETER_NAME ?? '',
38+
region: REGION ?? AWS_REGION ?? 'us-west-2',
39+
isProd: NODE_ENV === 'production',
40+
srcBucket: BUCKET || SRC_BUCKET || 'sih-input',
41+
styleTableName: STYLE_TABLE_NAME || 'style-table-name',
42+
autoWebp: ['yes', '1', 'true'].includes((AUTO_WEBP ?? '').toLowerCase()),
43+
secretName: SECRET_NAME ?? 'X-Client-Authorization',
44+
sharpQueueLimit: parseInt(SHARP_QUEUE_LIMIT ?? '1'),
45+
configJsonParameterName: CONFIG_JSON_PARAMETER_NAME ?? '',
46+
CACHE_TTL_SEC: parseInt(CACHE_TTL_SEC ?? '300'),
47+
CACHE_MAX_ITEMS: parseInt(CACHE_MAX_ITEMS ?? '10000'),
48+
CACHE_MAX_SIZE_MB: parseInt(CACHE_MAX_SIZE_MB ?? '1024'),
2349
};
2450

2551
export default conf;

source/new-image-handler/src/debug.ts

+44-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as os from 'os';
2+
import { LRUCache } from 'lru-cache';
23
import * as sharp from 'sharp';
34

45
export interface ISharpInfo {
@@ -42,22 +43,39 @@ export interface IDebugInfo {
4243
cpus: number;
4344
loadavg: number[];
4445
};
45-
memoryStats: string;
46-
memoryUsage: NodeJS.MemoryUsage;
47-
resourceUsage: NodeJS.ResourceUsage;
46+
memory: {
47+
stats: string;
48+
free: number;
49+
total: number;
50+
usage: NodeJS.MemoryUsage;
51+
};
52+
resource: {
53+
usage: NodeJS.ResourceUsage;
54+
};
55+
lruCache?: {
56+
keys: number;
57+
sizeMB: number;
58+
ttlSec: number;
59+
};
4860
sharp: ISharpInfo;
4961
}
5062

51-
export default function debug(): IDebugInfo {
52-
return {
63+
export default function debug(lruCache?: LRUCache<string, CacheObject>): IDebugInfo {
64+
const ret: IDebugInfo = {
5365
os: {
5466
arch: os.arch(),
5567
cpus: os.cpus().length,
5668
loadavg: os.loadavg(),
5769
},
58-
memoryStats: `free: ${fmtmb(os.freemem())}, total: ${fmtmb(os.totalmem())}, usage ${Math.round(100 * (os.totalmem() - os.freemem()) / os.totalmem()) / 100} %`,
59-
memoryUsage: process.memoryUsage(),
60-
resourceUsage: process.resourceUsage(),
70+
memory: {
71+
stats: `free: ${formatBytes(os.freemem())}, total: ${formatBytes(os.totalmem())}, usage ${((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(2)} %`,
72+
free: os.freemem(),
73+
total: os.totalmem(),
74+
usage: process.memoryUsage(),
75+
},
76+
resource: {
77+
usage: process.resourceUsage(),
78+
},
6179
sharp: {
6280
cache: sharp.cache(),
6381
simd: sharp.simd(),
@@ -66,9 +84,25 @@ export default function debug(): IDebugInfo {
6684
versions: sharp.versions,
6785
},
6886
};
87+
if (lruCache) {
88+
ret.lruCache = {
89+
keys: lruCache.size,
90+
sizeMB: Math.round(b2mb(lruCache.calculatedSize) * 100) / 100,
91+
ttlSec: Math.round(lruCache.ttl / 1000),
92+
};
93+
}
94+
return ret;
6995
}
7096

71-
function fmtmb (v: number) {
72-
return `${Math.round(v / 1024 / 1024 * 100) / 100} MB`;
97+
function b2mb(v: number) {
98+
return v / 1048576;
7399
}
74100

101+
function formatBytes(bytes: number) {
102+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
103+
let i = 0;
104+
for (; bytes >= 1024 && i < units.length - 1; i++) {
105+
bytes /= 1024;
106+
}
107+
return `${bytes.toFixed(2)} ${units[i]}`;
108+
};

source/new-image-handler/src/index-lambda.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const handler = WrapError(async (event: APIGatewayProxyEventV2): Promise<
1414
if (event.rawPath === '/' || event.rawPath === '/ping') {
1515
return resp(200, 'ok');
1616
} else if (event.rawPath === '/_debug') {
17-
console.log(debug());
17+
console.log(JSON.stringify(debug()));
1818
return resp(400, 'Please check your server logs for more details!');
1919
}
2020

source/new-image-handler/src/index.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,54 @@ import * as SSM from 'aws-sdk/clients/ssm';
44
import * as HttpErrors from 'http-errors';
55
import * as Koa from 'koa'; // http://koajs.cn
66
import * as bodyParser from 'koa-bodyparser';
7+
import * as koaCash from 'koa-cash';
78
import * as logger from 'koa-logger';
89
import * as Router from 'koa-router';
10+
import { LRUCache } from 'lru-cache';
911
import * as sharp from 'sharp';
1012
import config from './config';
1113
import debug from './debug';
1214
import { bufferStore, getProcessor, parseRequest, setMaxGifSizeMB, setMaxGifPages } from './default';
1315
import * as is from './is';
1416
import { IHttpHeaders, InvalidArgument } from './processor';
1517

18+
const MB = 1048576;
19+
1620
const ssm = new SSM({ region: config.region });
1721
const smclient = new SecretsManager({ region: config.region });
1822

1923
const DefaultBufferStore = bufferStore();
2024
const app = new Koa();
2125
const router = new Router();
26+
const lruCache = new LRUCache<string, CacheObject>({
27+
max: config.CACHE_MAX_ITEMS,
28+
maxSize: config.CACHE_MAX_SIZE_MB * MB,
29+
ttl: config.CACHE_TTL_SEC * 1000,
30+
sizeCalculation: (value) => {
31+
return value.body.length;
32+
},
33+
});
2234

2335
sharp.cache({ items: 1000, files: 200, memory: 2000 });
2436

2537
app.use(logger());
2638
app.use(errorHandler());
2739
app.use(bodyParser());
40+
app.use(koaCash({
41+
setCachedHeader: true,
42+
get: (key) => {
43+
return Promise.resolve(lruCache.get(key));
44+
},
45+
set: (key, value) => {
46+
lruCache.set(key, value as CacheObject);
47+
return Promise.resolve();
48+
},
49+
}));
2850

2951
router.post('/images', async (ctx) => {
3052
console.log('post request body=', ctx.request.body);
3153

3254
const opt = await validatePostRequest(ctx);
33-
console.log(opt);
3455
ctx.path = opt.sourceObject;
3556
ctx.query['x-oss-process'] = opt.params;
3657
ctx.headers['x-bucket'] = opt.sourceBucket;
@@ -61,12 +82,14 @@ router.get(['/', '/ping'], async (ctx) => {
6182
});
6283

6384
router.get(['/debug', '/_debug'], async (ctx) => {
64-
console.log(debug());
85+
console.log(JSON.stringify(debug(lruCache)));
6586
ctx.status = 400;
6687
ctx.body = 'Please check your server logs for more details!';
6788
});
6889

6990
router.get('/(.*)', async (ctx) => {
91+
if (await ctx.cashed()) return;
92+
7093
const queue = sharp.counters().queue;
7194
if (queue > config.sharpQueueLimit) {
7295
ctx.body = { message: 'Too many requests, please try again later.' };
@@ -89,7 +112,7 @@ app.on('error', (err: Error) => {
89112

90113
app.listen(config.port, () => {
91114
console.log(`Server running on port ${config.port}`);
92-
console.log('Config:', config);
115+
console.log('Config:', JSON.stringify(config));
93116
});
94117

95118
function errorHandler(): Koa.Middleware<Koa.DefaultState, Koa.DefaultContext, any> {
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
declare module "style.json" {
22
const value: { [key: string]: { [k: string]: string } };
33
export default value;
4+
}
5+
6+
interface CacheObject {
7+
body: Buffer;
48
}

source/new-image-handler/tsconfig.jest.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"include": [
2828
"src/**/*.js",
2929
"src/**/*.ts",
30+
"src/**/*.d.ts",
3031
"test/**/*.ts"
3132
],
3233
"exclude": [

source/new-image-handler/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"include": [
2929
"src/**/*.js",
3030
"src/**/*.ts",
31+
"src/**/*.d.ts",
3132
"test/**/*.ts"
3233
],
3334
"exclude": [],

0 commit comments

Comments
 (0)