Closed xiedacon closed 2 weeks ago
Is there a specific usage scenario that requires clone ctx?
In our case, it happens when we use async_hooks and sequelize at the same time.
At the async_hooks part, we need to use it to pass the ctx, so there is no need to add the ctx parameter to every function.
const { AsyncLocalStorage } = require('async_hooks')
const Koa = require('koa')
const localStorage = new AsyncLocalStorage();
const app = new Koa();
app.use((ctx, next) => {
localStorage.run({ ctx }, next)
});
app.use((ctx) => {
ctx.body = fn1()
});
const fn1 = () => fn2()
const fn2 = () => fn3()
const fn3 = () => {
const { ctx } = localStorage.getStore()
// do something
return ctx.query
}
app.listen(3000);
At the sequelize part, we need to use the dialectModule
options to pass our own mysql driver to sequelize.
There are two important things:
_.cloneDeep
to copy options before querying sql. https://github.com/sequelize/sequelize/blob/main/lib/dialects/abstract/connection-manager.js#L21const { Sequelize } = require('sequelize')
const dialectModule = {
intervalId: setInterval(() => {
console.log('some thing')
}, 1000)
}
const sequelize = new Sequelize({
dialectModule
})
// It will call _.cloneDeep() to dialectModule.
sequelize.query('SELECT 1 = 1')
In the end, our code looks like the following:
const { AsyncLocalStorage } = require('async_hooks')
const Koa = require('koa')
const { Sequelize } = require('sequelize')
const localStorage = new AsyncLocalStorage();
const app = new Koa();
app.use((ctx, next) => {
localStorage.run({ ctx }, next)
});
app.use(async (ctx) => {
ctx.body = await someController()
});
const someController = async () => someService()
const someService = async () => someModel()
const someModel = async () => {
const { ctx } = localStorage.getStore()
const sequelize = new Sequelize({
dialectModule: {
intervalId: setInterval(() => {
console.log('some thing')
}, 1000)
}
})
console.log(ctx.query)
// TypeError: Cannot read property 'length' of undefined
return await sequelize.query('SELECT 1 = 1')
}
app.listen(3000);
It will be thrown when querying sql, even if the async_hooks has no business with it.
The reason is
AsyncLocalStorage
, it will store the ctx in the Symbol(kResourceStore)
property of each timer._.cloneDeep
to copy options._.cloneDeep(ctx)
.An example without sequelize.
const { AsyncLocalStorage } = require('async_hooks')
const _ = require('lodash')
const Koa = require('koa')
const localStorage = new AsyncLocalStorage();
const app = new Koa();
app.use((ctx, next) => {
localStorage.run({ ctx }, next)
});
app.use(async (ctx) => {
ctx.body = await someController()
});
const someController = async () => someService()
const someService = async () => someModel()
const someModel = async () => {
// TypeError: Cannot read property 'length' of undefined
_.cloneDeep(setTimeout(() => {}, 1000))
return { some: 'thing' }
}
app.listen(3000);
The reason for this error is that length is a delegated property of ctx. So when lodash tries to read it to see if the object has a length, delegate getter is actually called and I guess as the properties are not fully cloned it fails.
I guess this is fixed in newer version of lodash as it checks to see if the property is not a function first.
why do you need to clone the ctx? can't you just pass the ctx itself?
Test codes:
I found it was caused by the code here https://github.com/koajs/koa/blob/19f3353e5da6a0d52fb968a9429523585e42a288/lib/context.js
The simple code is like this:
When execute
_.cloneDeep(ctx)
,app
will also be cloned. Butapp.context
can not be clone, becauselodash
needs thelength
proproty to decide whether obj is array-like https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/isArrayLike.jsI think maybe we should do some special operations for
length
property, just like this: