pref(fn): cache context

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2023-02-12 15:03:16 +08:00
parent a1b2668ca1
commit 73eb4ed155
6 changed files with 150 additions and 98 deletions

View File

@@ -6,8 +6,6 @@ import { load as yamlLoad } from 'js-yaml'
import { machineIdSync } from 'node-machine-id'
import path from 'path'
import { isDev } from './global/env.global'
const commander = program
.option('-p, --port <number>', 'server port')
.option('--demo', 'enable demo mode')
@@ -112,7 +110,7 @@ export const REDIS = {
ttl: null,
httpCacheTTL: 5,
max: 5,
disableApiCache: (isDev || argv.disable_cache) && !ENABLE_CACHE_DEBUG,
disableApiCache: false,
}
export const AXIOS_CONFIG: AxiosRequestConfig = {

View File

@@ -1,5 +1,6 @@
import { DynamicModule, Module, NestModule, Type } from '@nestjs/common'
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
import { ThrottlerGuard } from '@nestjs/throttler'
import { DEMO_MODE } from './app.config'
import { AppController } from './app.controller'
@@ -141,6 +142,10 @@ import { RedisModule } from './processors/redis/redis.module'
provide: APP_GUARD,
useClass: RolesGuard,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {

View File

@@ -58,11 +58,11 @@ export class HttpCacheInterceptor implements NestInterceptor {
const handler = context.getHandler()
const isDisableCache = this.reflector.get(META.HTTP_CACHE_DISABLE, handler)
const key = this.trackBy(context) || `mx-api-cache:${request.url}`
if (isDisableCache) {
return call$
}
const key = this.trackBy(context) || `mx-api-cache:${request.url}`
const metaTTL = this.reflector.get(META.HTTP_CACHE_TTL_METADATA, handler)
const ttl = metaTTL || REDIS.httpCacheTTL

View File

@@ -130,7 +130,7 @@ export class LinkService {
if (!model.email) {
return
}
const enable = (await this.configs.get('mailOptions')).enable
const { enable } = await this.configs.get('mailOptions')
if (!enable || isDev) {
console.log(`
To: ${model.email}

View File

@@ -13,6 +13,7 @@ import {
Request,
Response,
} from '@nestjs/common'
import { Throttle } from '@nestjs/throttler'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Auth } from '~/common/decorators/auth.decorator'
@@ -49,7 +50,8 @@ export class ServerlessController {
throw new InternalServerErrorException('code defined file not found')
}
}
@Get('/:reference/:name/*')
@All('/:reference/:name/*')
@Throttle(100, 5)
@HTTPDecorators.Bypass
async runServerlessFunctionWildcard(
@Param() param: ServerlessReferenceDto,
@@ -62,6 +64,7 @@ export class ServerlessController {
}
@All('/:reference/:name')
@Throttle(100, 5)
@HTTPDecorators.Bypass
async runServerlessFunction(
@Param() param: ServerlessReferenceDto,

View File

@@ -1,6 +1,7 @@
import { isURL } from 'class-validator'
import fs, { mkdir, stat } from 'fs/promises'
import { isPlainObject } from 'lodash'
import LRUCache from 'lru-cache'
import { createRequire } from 'module'
import { mongo } from 'mongoose'
import path, { resolve } from 'path'
@@ -44,9 +45,21 @@ import { builtInSnippets } from './pack/built-in'
import { ServerlessStorageCollectionName } from './serverless.model'
import { complieTypeScriptBabelOptions, hashStable } from './serverless.util'
class CleanableScope {
public requireModuleIdSet = new Set<string>()
public scopeContextLRU = new LRUCache<string, any>({
max: 100,
ttl: 1000 * 60 * 5,
})
public scopeModuleLRU = new LRUCache<string, any>({
max: 20,
ttl: 1000 * 60 * 5,
})
}
@Injectable()
export class ServerlessService implements OnModuleInit {
private readonly logger: Logger
private readonly cleanableScope: CleanableScope
constructor(
@InjectModel(SnippetModel)
private readonly snippetModel: MongooseModel<SnippetModel>,
@@ -58,6 +71,7 @@ export class ServerlessService implements OnModuleInit {
private readonly configService: ConfigsService,
) {
this.logger = new Logger(ServerlessService.name)
this.cleanableScope = new CleanableScope()
}
async onModuleInit() {
@@ -265,92 +279,15 @@ export class ServerlessService implements OnModuleInit {
context: { req: FunctionContextRequest; res: FunctionContextResponse },
) {
const { raw: functionString } = model
const logger = new Logger(`fx:${model.reference}/${model.name}`)
const document = await this.model.findById(model.id)
const secretObj = model.secret
? qs.parse(EncryptUtil.decrypt(model.secret))
: {}
const scope = `${model.reference}/${model.name}`
if (!isPlainObject(secretObj)) {
throw new InternalServerErrorException(
`secret parsing error, must be object, got ${typeof secretObj}`,
)
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this
const globalContext = {
context: {
// inject app req, res
...context,
...context.res,
query: context.req.query,
headers: context.req.headers,
// TODO wildcard params
params: Object.assign({}, context.req.params),
storage: {
cache: this.mockStorageCache(),
db: this.mockDb(
`${model.reference || '#########debug######'}@${model.name}`,
),
dangerousAccessDbInstance: () => {
return [this.databaseService.db, mongo]
},
},
secret: secretObj,
model,
document,
name: model.name,
reference: model.reference,
getMaster: this.mockGetMaster.bind(this),
getService: this.getService.bind(this),
writeAsset: async (
path: string,
data: any,
options: Parameters<typeof fs.writeFile>[2],
) => {
return await this.assetService.writeUserCustomAsset(
safePathJoin(path),
data,
options,
)
},
readAsset: async (
path: string,
options: Parameters<typeof fs.readFile>[1],
) => {
return await this.assetService.getAsset(safePathJoin(path), options)
},
},
// inject global
__dirname: DATA_DIR,
__filename: '',
// inject some zx utils
fetch,
// inject Global API
Buffer,
// inject logger
console: logger,
const logger = new Logger(`fx:${scope}`)
const globalContext = await this.createScopeContext(
scope,
context,
model,
logger,
require: this.inNewContextRequire(),
import(module: string) {
return Promise.resolve(self.require(module))
},
process: {
env: Object.freeze({ ...process.env }),
nextTick: process.nextTick.bind(null),
},
}
)
const cacheKey = model.updated
? getRedisKey(
@@ -412,15 +349,17 @@ export class ServerlessService implements OnModuleInit {
return res.code
}
private requireModuleIdSet = new Set<string>()
@Interval(5 * 60 * 1000)
private cleanRequireCache() {
Array.from(this.requireModuleIdSet.values()).forEach((id) => {
private cleanup() {
const { requireModuleIdSet, scopeContextLRU, scopeModuleLRU } =
this.cleanableScope
Array.from(requireModuleIdSet.values()).forEach((id) => {
delete this.require.cache[id]
})
this.requireModuleIdSet.clear()
requireModuleIdSet.clear()
scopeContextLRU.clear()
scopeModuleLRU.clear()
}
private resolvePath(id: string) {
@@ -446,8 +385,13 @@ export class ServerlessService implements OnModuleInit {
? createRequire(resolve(process.cwd(), './node_modules'))
: createRequire(NODE_REQUIRE_PATH)
private inNewContextRequire() {
private createNewContextRequire(scope: string) {
const __require = (id: string) => {
const cacheKey = `${scope}_${id}`
if (this.cleanableScope.scopeModuleLRU.has(cacheKey)) {
return this.cleanableScope.scopeModuleLRU.get(cacheKey)
}
const isBuiltin = isBuiltinModule(id)
const resolvePath = this.resolvePath(id)
@@ -456,9 +400,11 @@ export class ServerlessService implements OnModuleInit {
// eslint-disable-next-line no-empty
if (Object.keys(PKG.dependencies).includes(id) || isBuiltin) {
} else {
this.requireModuleIdSet.add(resolvePath)
this.cleanableScope.requireModuleIdSet.add(resolvePath)
}
const clonedModule = deepCloneWithFunction(module)
this.cleanableScope.scopeModuleLRU.set(cacheKey, clonedModule)
return clonedModule
}
@@ -577,6 +523,107 @@ export class ServerlessService implements OnModuleInit {
return $require.bind(this)
}
private async createScopeContext(
scope: string,
context: any,
model: SnippetModel,
logger: Logger,
) {
if (this.cleanableScope.scopeContextLRU.has(scope)) {
return this.cleanableScope.scopeContextLRU.get(scope)
}
const document = await this.model.findById(model.id)
const secretObj = model.secret
? qs.parse(EncryptUtil.decrypt(model.secret))
: {}
if (!isPlainObject(secretObj)) {
throw new InternalServerErrorException(
`secret parsing error, must be object, got ${typeof secretObj}`,
)
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this
const createdContext = {
context: {
// inject app req, res
...context,
...context.res,
query: context.req.query,
headers: context.req.headers,
// TODO wildcard params
params: Object.assign({}, context.req.params),
storage: {
cache: this.mockStorageCache(),
db: this.mockDb(
`${model.reference || '#########debug######'}@${model.name}`,
),
dangerousAccessDbInstance: () => {
return [this.databaseService.db, mongo]
},
},
secret: secretObj,
model,
document,
name: model.name,
reference: model.reference,
getMaster: this.mockGetMaster.bind(this),
getService: this.getService.bind(this),
writeAsset: async (
path: string,
data: any,
options: Parameters<typeof fs.writeFile>[2],
) => {
return await this.assetService.writeUserCustomAsset(
safePathJoin(path),
data,
options,
)
},
readAsset: async (
path: string,
options: Parameters<typeof fs.readFile>[1],
) => {
return await this.assetService.getAsset(safePathJoin(path), options)
},
},
// inject global
__dirname: DATA_DIR,
__filename: '',
// inject some zx utils
fetch,
// inject Global API
Buffer,
// inject logger
console: logger,
logger,
require: this.createNewContextRequire(scope),
import(module: string) {
return Promise.resolve(self.require(module))
},
process: {
env: Object.freeze({ ...process.env }),
nextTick: process.nextTick.bind(null),
},
}
this.cleanableScope.scopeContextLRU.set(scope, createdContext)
return createdContext
}
async isValidServerlessFunction(raw: string) {
try {
// 验证 handler 是否存在并且是函数
@@ -673,7 +720,6 @@ export class ServerlessService implements OnModuleInit {
if (!builtIn) {
throw new InternalServerErrorException('built-in function not found')
}
console.log('reset built-in function: ', name, builtIn.code)
await this.model.updateOne(
{