New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgrade sequelize and improve tests #2723
Conversation
…stomScript tests to run first
…encies as it is no longer used in the project
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], | ||
globalSetup: '<rootDir>/stubstub/global/setup.ts', | ||
globalTeardown: '<rootDir>/stubstub/global/teardown.ts', | ||
runtime: '@side/jest-runtime', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to lower memory consumption in node 18+
package.json
Outdated
"test-dev": "jest --forceExit", | ||
"test-no-lint": "NODE_OPTIONS=--max-old-space-size=8192 firebase emulators:exec --only database --import .firebase/default-contents \"jest --forceExit --runInBand --logHeapUsage --silent\"", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is to improve the speed of running tests, so you don't need to wait for lints every time
@@ -99,7 +100,7 @@ | |||
"classnames": "^2.2.6", | |||
"color": "^3.1.1", | |||
"compression": "^1.7.4", | |||
"connect-session-sequelize": "^6.0.0", | |||
"connect-session-sequelize": "^7.1.7", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for compat with sequelize v6
@@ -151,7 +152,7 @@ | |||
"newrelic": "^5.7.0", | |||
"no-slash": "^1.2.15", | |||
"node-fetch": "^2.6.7", | |||
"passport": "^0.4.0", | |||
"passport": "^0.5.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to fix memory leak
@@ -207,7 +208,6 @@ | |||
"styled-components": "^5.3.0", | |||
"tmp-promise": "^2.0.1", | |||
"ts-node": "^9.0.0", | |||
"typescript": "^4.4.4", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should go in dev deps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in general, all the sequelize redefs look like this.
Most of the changes are indentation, which is annoying
); | ||
}; | ||
}, | ||
) as any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i have added "as any" for now to ensure compatibility with the rest of the codebase, as before the models were also interpreted as any
organizationId: { type: dataTypes.UUID }, | ||
}, | ||
{ | ||
// @ts-expect-error ts(2345): Argument of type '{ classMethods: { associate: (models: any) => void; }; }' is not assignable to parameter of type 'ModelOptions<Model<any, any>>'. Object literal may only specify known properties, and 'classMethods' does not exist in type 'ModelOptions<Model<any, any>>'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since the sequelize define's now have proper typechecking, this raises an error, so we have to ts-expect-error
it
@@ -26,6 +26,8 @@ setup(beforeAll, async () => { | |||
await models.resolve(); | |||
}); | |||
|
|||
teardown(afterAll); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ive added teardown
everywhere setup
is used to close the connection with sequelize
and prevent a memory leak.
I've confirmed that this seems to get rid of a part of the memory leaks by running jest --detectLeaks
: without the teardown
it fails, with it it passes
TD:DR if you get "Model is not defined" errors, make sure you import the | ||
`sequelize` object from this file, not form `server/sequelize.ts`. | ||
*/ | ||
export { sequelize }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this re-export is here to make sure that if you import sequelize
all the models have been defined already
}, | ||
{ | ||
indexes: [ | ||
{ fields: ['communityId'], method: 'BTREE' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the one change to the define
s. method
is not a valid property, i don't think it ever was checking the sequelize v4 and v5 docs. But I think BTREE
is the default index method anywan
windowMs: 60000, // 1 minute for requests to be kept in memory. value of 60000ms is default but expressed here for clarity | ||
delayAfter: 60, // allow 60 requests per minute, then... | ||
delayMs: 100, // 60th request has a 100ms delay, 7th has a 200ms delay, 8th gets 300ms, etc. | ||
maxDelayMs: 20000, // max time of request delay will be 20secs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important: I changed maxDelay to maxDelayMs: maxDelay
is not a property you can set, so did not do anything. This might cause unexpected behavior now it is actually set, although i've not noticed anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for pointing this out. We'll keep an eye on it!
* While testing, this is not very useful anyway. | ||
*/ | ||
const speedLimiter: RequestHandler = | ||
process.env.NODE_ENV === 'test' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disable this on test, bc it (monkey-)patches node http
and therefore causes a memory leak during tests. Since we don't test for this (maybe we should) i thought it could be okay to just ignore it
// carefully limit what we require from here. Luckily, all we need is server/models.ts, and | ||
// the handful of utilities that it must import. | ||
// | ||
// This is absurd, but it works. If you know how to fix this, please do! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the hack is still needd, but i just set it manually below. could reinstate the function wrapper if desired, probably should just remove it
stubstub/global/setup.ts
Outdated
@@ -0,0 +1,59 @@ | |||
/* eslint-disable no-console, global-require */ | |||
// require('ts-node').register({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not required, ts-jest
already loads ts-node
should probably remove it
|
||
import { ChildProcessWithoutNullStreams } from 'child_process'; | ||
|
||
declare namespace global { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just add lil typesafety.
* ``` | ||
* | ||
*/ | ||
type WithOptional< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small typehelper to make typescript happy
: WithOptionalBase<T, K>[PossiblyNullKey]; | ||
}; | ||
|
||
export const builders = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
main change here is that this is from
const builders = {}
builders.User = ...
to
const builders = {
User: //
}
for better type inference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
const definitionsByBoundName = {}; | ||
const definitions = []; | ||
const resolveIdentifiersCallbacks = []; | ||
export type ModelDefinition = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these types are inferred from the parser, which i did not change.
I think they are accurate!
tools/5to6/v5/models.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should maybe not be changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. I left a few questions and small nits. I'm going to go ahead and do some smoke testing while those are resolved. Then we can hopefully get this merged by EOD.
custom-sequencer.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we create the feature flag in the setupFiles instead of using a custom sequencer?
'^.+\\.[tj]sx?$': [ | ||
'ts-jest', | ||
{ | ||
diagnostics: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooh, does this speed up the tests at all?
windowMs: 60000, // 1 minute for requests to be kept in memory. value of 60000ms is default but expressed here for clarity | ||
delayAfter: 60, // allow 60 requests per minute, then... | ||
delayMs: 100, // 60th request has a 100ms delay, 7th has a 200ms delay, 8th gets 300ms, etc. | ||
maxDelayMs: 20000, // max time of request delay will be 20secs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for pointing this out. We'll keep an eye on it!
: WithOptionalBase<T, K>[PossiblyNullKey]; | ||
}; | ||
|
||
export const builders = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
Issue(s) Resolved
#2575
Test Plan
Make all tests pass, check if
pgdump
produces same results.Notes
The main change of this PR is to upgrade
sequelize
from v5 to v6. The motivation for this being both a routine upgrade and the (possible) typesafety improvements that come with it (more on that later).This PR seems really big (it's probably too big, see below for suggestions on making it smaller) but most of the changed lines are from
package-lock.json
and indentation in themodel.ts
files!.As a secondary change, this PR also adds (some) typesafety to the testing tools in
stubstub
, addresses some memory leaks in the tests which caused the tests to fail on my machine, and adds some minor typesafety improvements in other places.Future work
Since this is the most important I'll mention this first.
Typesafety
This PR does not add any type inference to the sequelize model definitions, as that is not something sequelize can do (at the moment). In a separate PR i will propose a way to do this.
Incorporating #2707
I first want to make sure nothing breaks when upgrading to sequelize v6, and then I will incorporate #2707. I will do this in a separate PR, as this PR is already quite large, and doing this at the same time will obfuscate the changes.
Sequelize upgrade
The main change the upgrade to
sequelize
v6 induced is the deprecation ofsequelize.import
.This means that rather than defining the sequelize instance in
server/models.ts
and then callingsequelize.import
a bunch of times, we now define thesequelize
instance inserver/sequelize.ts
, import it in every model file, and then callsequelize.define
using that instance.We then export the models from
server/models.ts
and thesequelize
instance fromserver/models.ts
, as before.Result: everything still works!
Why move the sequelize instance to a different file?
I chose to instantiate the model in a different file instead of keeping it in
server/models.ts
, because otherwise we would be dealing with a circular dependency betweenserver/models.ts
andserver/<model>/model.ts
. I am not sure if this would actually lead to problems, but it seemed like a good idea to avoid it.Why the
as any
after every model definiton?sequelize.define
does not do any type inference on its own, so without theas any
every model would be defined asModelCtor<Model<any,any>>
, which would cause a bunch of type-errors. I chose to include theas any
for now as that is what the models were defined as before, but I want to get rid of this as soon as possible.Why not include #2707 here?
This PR is already wayy to big, I wanted to keep it as minimal as possible. I will include #2707 in a separate PR.
Testing tools
While doing this upgrade I ran into a lot of nasty errors when testing, and I found it hard to tell whether they were due to the upgrade or due to something else I did, or just general errors.
To gain a better understanding of how the tests were run, I added types to most of the files in
stubstub
. I decided not to remove them for now, because I thought it might be useful to have them in the future. If you do think this is too much to include (in this PR or in general) let me know and I can remove it!The bugs
Database not being initialized properly
At first, because I separated the
sequelize
instance fromserver/models.ts
, the database was not initialized properly when running the tests. This was because the models were not imported in the global setup, and thus whensequelize.sync()
was called there it did not create any models! But, because in thesetup
function instubstub/prepare.ts
also doessequelize.sync()
, the database was eventually populated, but only after the first test that did that setup was run, which wasn't always the first test.As a result, depending on the order jest decides to run the tests in, the first few tests would sometimes fail because the database was not initialized properly.
To fix this, I re-export the
sequelize
instance fromserver/models.ts
(where all the models are also imported/exported from), and then import thesequelize
instance from there instubstub/global/setup.ts
. This way all the models are properly loaded.I added a note that the sequelize instance should only be imported from
server/models.ts
to avoid this problem in the future.Missing Feature Flags
The tests for the routes would sometimes randomly fail because the
customScript
feature flag wasn't set properly.In short: normally the
server/customScript/__test__/api.ts
always runs pretty early in the testing suite. This test creates thecustomScript
feature flag in the db, and so the routes (which check for this feature flag) will operate fine.However, because of the large amount of changes in this PR, jest decided that there is a better way to run the tests sometimes (or something, no idea how jest does this), having the routes tests run before the
customScript
test, causing them to fail.To fix this, I created a custom test sequencer that always runs thecustomScript
test first. This is not ideal, but it works for now. Please let me know if you have a better idea!Now this feature flag will be created during global setup, that way we don't need a sequencer.
Memory leaks
TL;DR: Some things were leaking memory, causing tests to fail on my machine. The main culprits are packages that monkey-patch core Node functionality such as
Request
andfs
, and not closing connections to the database. The other seems to be an issue with Jest, and is solved by using a different runtime.This was by far the most time consuming part of this PR: I could not run the tests without Node running out of memory. What's even weirder is that this also happened/happens on the master branch! But the tests run fine elsewhere, so I thought it was a problem with my machine. For ref, I was running the tests on an Macbook Pro M1 Max with 64GB of RAM, so I don't think it was per se a physical memory issue as CI runs on way less memory (presumably, I don't know how much memory CI has).
I've tried a lot of things to try and fix this, but here is what works:
@side/jest-runtime
as the runtime. This basically halved the leaked memory from ~100mb per test to ~40mb per test. Still not ideal, but otherwise still breaks on my machine.passport@0.5.0
from0.4.0
. Known issue that it leaks in Jest (Jest detects memory leak when requiring passport jaredhanson/passport#711)express-slow-down
in tests. It leaks, but there's no fix. Since we don't test it, and it only affects/pubDocument
routes, I thought it was fine to just stub it out.setup(beforeAll)
,sequelize.sync()
is called, which (not sure) opens up a new connection to the db, one which isn't closed. This is also an issue on master. To combat this, I added a simplesequelize.close()
toteardown
. This does yield some errors, but the tests still pass so 🤷supertest
, create a connection to theapp
and pass that. This is a knownsupertest
issue (see Jest --detectleaks shows memory leak while using SuperTest with Express ladjs/supertest#634), although this is also fine on Node 16.10.By passing just a connection, we can then close that connection after the tests are done.
I also slightly changed how the
supertest
agent is created, upgraded to latest version ofsupertest
, removedsupertest-as-promised
, assupertest
now provides its own promises.All this combined allows for somewhat stable memory allocation.
Some packages, such as
firebase
, still leak, but upgrading that is quite the undertaking, and only a few tests use it anyway.Other changes to testing
eslint
and when compiling the appnpm run test-no-lint
to test without linting, as that is quite slow.Package upgrades & justifications
Dependencies
Updated
sequelize
: v5.22 to v6.31. The main point of this PR.express-slow-down
: v1.4 to v1.6. Originally to debug the memory leak.passport
: v0.4.0 to v0.5.0. To solve a memory leak issueDev dependencies
Updated
jest
: v26.6 to v29.5supertest
: v4.0.2 to v.6.3.3ts-jest
: v26.4.4 to v29.1.0typescript
: v4.4.4 to v4.5.5. Fixes an issue withjest-environment-jsdom
Added
jest-environment-jsdom
: No longer included injest
by default, but needed forsupertest
to work properly.@side/jest-runtime
: to fix the memory leaking of jest in Node 18+Types packages added
I also took the (completely self-indulgent) liberty to add some types packages for some untyped libraries that were used.
I took care to match the version of the types package to the version of the library, and to only add types packages for libraries that were used in the codebase.
I discovered these missing types by temporarily setting
noImplicitAny
totrue
intsconfig.json
, which caused the compiler to complain about missing types.If undesired these can be removed!
@types/request-promise
: ^4.1.48,@types/rss
: ^0.0.30,@types/sinon
: ^7.5.2,@types/supertest
: ^2.0.12,@types/unidecode
: ^0.1.1,@types/uuid
: ^3.4.10,@types/lodash.mergewith
: ^4.6.7,@types/lodash.throttle
: ^4.1.7,@types/lodash.unescape
: ^4.0.7,@types/mudder
: ^1.0.0,@types/passport
: ^0.4.7,@types/pegjs
: ^0.10.3,@types/express-session
: ^1.17.7,@types/express-slow-down
: ^1.3.2,@types/express-sslify
: ^1.2.2,@types/file-saver
: ^2.0.5,@types/filesize
: ^4.2.0,@types/fs-extra
: ^8.1.2,@types/fuzzysearch
: ^1.0.0,@types/graphlib
: ^2.1.8,@types/hasbin
: 1.2,@types/jest
: ^29.5.2,@types/jsonpath
: ^0.2.0,@types/jsonwebtoken
: ^9.0.2,@types/amqplib
: ^0.5.3,@types/color
: ^3.0.3,@types/compression
: ^1.7.2,@types/cookie-parser
: ^1.4.3,@types/cors
: ^2.8.13,@types/crypto-js
: ^4.1.1,@types/dateformat
: ^3.0.1,