Skip to content
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

perf(seed): run rebuilds in parallel, add perf logs #4334

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5bddaac
perf(seed): run rebuilds in parallel, add perf logs
ThatOneBro Apr 8, 2024
e0f710a
cleanup: remove stray performance.mark
ThatOneBro Apr 8, 2024
f3941d3
feat(server): add both `serial` and `parallel` modes for seed
ThatOneBro Apr 9, 2024
1c424bc
test(seed): split out seed tests, test in parallel to main tests
ThatOneBro Apr 23, 2024
ba9eaa8
cleanup(compose): rm stray service
ThatOneBro Apr 23, 2024
f98f105
cleanup(package.json): rm stray package
ThatOneBro Apr 23, 2024
6ca8cd1
cleanup(tsconfig): add `seed-tests` dir to tsconfig
ThatOneBro Apr 23, 2024
340ee80
fix(test.sh): make parallel tests fail together
ThatOneBro Apr 23, 2024
e5a9888
fix(actions/build): fix `postgres_seed` port
ThatOneBro Apr 23, 2024
fb90a1a
test(actions/build): see if hardcoding port works
ThatOneBro Apr 23, 2024
b4dfc20
cleanup(coverage): `coverage/seed/{parallel,serial}`
ThatOneBro Apr 23, 2024
ec0746b
cleanup(server/test): don't call `docker-compose` in `test` cmd
ThatOneBro Apr 23, 2024
10bc983
test(coverage): add back coverage of `src` to `seed` tests
ThatOneBro Apr 23, 2024
472be29
test(coverage): add exclusions too
ThatOneBro Apr 23, 2024
f99b615
fix(tests/seed): do minimal setup in seed tests to actually test seeding
ThatOneBro Apr 23, 2024
7e18ad8
cleanup(sonar): rm unnecessary `as`s, use `??` operator
ThatOneBro Apr 23, 2024
dfe2307
refactor(seed): always build promise arr first
ThatOneBro Apr 23, 2024
d71b8cd
refactor(seed): move seed tests back into `src`, filter them
ThatOneBro Apr 24, 2024
8369583
docs(server/jest): add comment about weird glob behavior
ThatOneBro Apr 24, 2024
5bdd757
docs(testing): note running `test:seed:parallel` on fresh install
ThatOneBro Apr 24, 2024
c13e908
revert(seed): restore separate rebuild paths
ThatOneBro Apr 24, 2024
743060f
test(seed): make serial seed test run migrations before test
ThatOneBro May 7, 2024
8080800
chore(package-lock): fix needless updates
ThatOneBro May 7, 2024
ed4a256
chore(package-lock): actually fix needless updates
ThatOneBro May 7, 2024
436298b
test(seed): stop needlessly busting cache
ThatOneBro May 8, 2024
8ad3add
test(seed): always bust cache
ThatOneBro May 8, 2024
8ef8b4b
tweak(jest): set the default test timeout lower
ThatOneBro May 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 17 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,20 @@ jobs:
--health-timeout 5s
--health-retries 5
ports:
- 5432/tcp
- 5432:5432/tcp
postgres-seed:
image: postgres:${{ matrix.pg-version }}
env:
POSTGRES_DB: medplum_test
POSTGRES_USER: medplum
POSTGRES_PASSWORD: medplum
options: >-
--health-cmd pg_isready
--health-retries 5
--health-interval 10s
--health-timeout 5s
ports:
- 5433:5432/tcp
redis:
image: redis:${{ matrix.redis-version }}
options: >-
Expand All @@ -186,7 +199,7 @@ jobs:
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
- 6379:6379/tcp

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -224,7 +237,8 @@ jobs:
run: ./scripts/test.sh
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
POSTGRES_PORT: 5432
POSTGRES_SEED_PORT: 5433
REDIS_PASSWORD_DISABLED_IN_TESTS: 1
- name: Upload code coverage
if: ${{ matrix.node-version == 20 && matrix.pg-version == 14 && matrix.redis-version == 7 }}
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.seed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3.7'
services:
postgres-seed:
image: postgres:12
restart: always
environment:
- POSTGRES_USER=medplum
- POSTGRES_PASSWORD=medplum

volumes:
- ./postgres/postgres.conf:/usr/local/etc/postgres/postgres.conf
- ./postgres/:/docker-entrypoint-initdb.d/
command: postgres -c config_file=/usr/local/etc/postgres/postgres.conf
ports:
- '5433:5432'
105 changes: 105 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@types/node": "20.12.8",
"babel-jest": "29.7.0",
"babel-preset-vite": "1.1.3",
"concurrently": "8.2.2",
"cross-env": "7.0.3",
"danger": "12.2.0",
"esbuild": "0.20.2",
Expand Down
12 changes: 12 additions & 0 deletions packages/docs/docs/contributing/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ We use the following tools for testing:

Every pull request is analyzed by [Sonarcloud](https://sonarcloud.io/project/overview?id=medplum_medplum) and [Coveralls](https://coveralls.io/github/medplum/medplum?branch=main) for code coverage and other static analysis.

## Before testing
When you are testing for the first time with a fresh database, you first need to seed and migrate the database.
You can accomplish this by running the following command:

```bash
npx turbo run test:seed:parallel --filter=@medplum/server
```

This process can take a minute or two as it migrates the database and then creates many FHIR resources required for Medplum to operate correctly.

After the test passes and the database is migrated and seeded, you won't need to run it again unless the database schema changes or you destroy the volume attached to your `postgres` Docker container. Now you should be able to run the rest of the tests.

## How to test

To run all tests for all packages, use the build script:
Expand Down
16 changes: 16 additions & 0 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,19 @@ If so, create a new multiarch driver:
```bash
docker buildx create --name multiarch --driver docker-container --use
```

## Testing

### Seeding the database
Before running `npm run test` in `packages/server`, you will want to make sure the database is seeded. You are able to seed the database before running the tests by running:

```bash
npm run test:seed:parallel
```

### Running the tests
To run the tests, simply issue the command:

```bash
npm run test
```
13 changes: 0 additions & 13 deletions packages/server/jest.config.json

This file was deleted.

18 changes: 18 additions & 0 deletions packages/server/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Config } from 'jest';

export default {
testEnvironment: 'node',
testTimeout: 5000,
testSequencer: '<rootDir>/jest.sequencer.js',
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
// Oddly, the glob `<rootDir>/src/seed*.test.ts` correctly matches both seed tests in the positive case in `jest.seed.config.ts`
// But `!<rootDir>/src/seed*.test.ts` doesn't match both in the negative case, and only matches `seed-serial.test.ts`
// That's why we use `!**/src/seed*.test.ts` here
testMatch: ['<rootDir>/src/**/*.test.ts', '!**/src/seed*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
coverageDirectory: 'coverage',
coverageReporters: ['json', 'text'],
collectCoverageFrom: ['**/src/**/*', '!**/src/__mocks__/**/*.ts', '!**/src/migrations/**/*.ts'],
} satisfies Config;
9 changes: 9 additions & 0 deletions packages/server/jest.seed.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Config } from 'jest';
import defaultConfig from './jest.config';

export default {
...defaultConfig,
testTimeout: 300000,
testMatch: ['<rootDir>/src/seed*.test.ts'],
collectCoverageFrom: ['<rootDir>/src/**/*', '!**/src/__mocks__/**/*.ts', '!**/src/migrations/**/*.ts'],
} satisfies Config;
5 changes: 3 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"clean": "rimraf dist",
"dev": "ts-node-dev --poll --respawn --transpile-only --require ./src/otel/instrumentation.ts src/index.ts",
"start": "node --require ./dist/otel/instrumentation.js dist/index.js",
"test": "jest",
"test:seed": "jest seed.test.ts"
"test:seed:serial": "jest seed-serial.test.ts --config jest.seed.config.ts --coverageDirectory \"<rootDir>/coverage/seed/serial\"",
"test:seed:parallel": "jest seed.test.ts --config jest.seed.config.ts --coverageDirectory \"<rootDir>/coverage/seed/parallel\"",
"test": "jest"
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "3.569.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ function addDefaults(config: MedplumServerConfig): MedplumServerConfig {
config.accurateCountThreshold = config.accurateCountThreshold ?? 1000000;
config.defaultBotRuntimeVersion = config.defaultBotRuntimeVersion ?? 'awslambda';
config.defaultProjectFeatures = config.defaultProjectFeatures ?? [];
config.emailProvider = config.emailProvider || (config.smtp ? 'smtp' : 'awsses');
config.emailProvider = config.emailProvider ?? (config.smtp ? 'smtp' : 'awsses');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would || actually be preferable here to guard against config.emailProvider === ''?

return config;
}

Expand Down
56 changes: 56 additions & 0 deletions packages/server/src/seed-serial.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Project } from '@medplum/fhirtypes';
import { shutdownApp } from './app';
import { loadTestConfig } from './config';
import { AuthenticatedRequestContext, requestContextStore } from './context';
import { getDatabasePool, initDatabase } from './database';
import { SelectQuery } from './fhir/sql';
import { loadStructureDefinitions } from './fhir/structure';
import { initRedis } from './redis';
import { seedDatabase } from './seed';
import { withTestContext } from './test.setup';

describe('Seed Serial', () => {
beforeAll(async () => {
console.log = jest.fn();

const config = await loadTestConfig();
config.database.port = process.env['POSTGRES_SEED_PORT']
? Number.parseInt(process.env['POSTGRES_SEED_PORT'], 10)
: 5433;
// Keep Redis separate so caches between main test suite and this are separate
config.redis.db = 8;
config.database.runMigrations = true;

// We load the minimal required to get things running so this actually tests seeding the database
return withTestContext(() =>
requestContextStore.run(AuthenticatedRequestContext.system(), async () => {
loadStructureDefinitions();
initRedis(config.redis);
await initDatabase(config);
})
);
});

afterAll(async () => {
await shutdownApp();
});

test('Seeder completes successfully -- serial version', async () => {
// First time, seeder should run
await seedDatabase({ parallel: false });

// Make sure the first project is a super admin
const rows = await new SelectQuery('Project')
.column('content')
.where('name', '=', 'Super Admin')
.execute(getDatabasePool());
expect(rows.length).toBe(1);

const project = JSON.parse(rows[0].content) as Project;
expect(project.superAdmin).toBe(true);
expect(project.strictMode).toBe(true);

// Second time, seeder should silently ignore
await seedDatabase({ parallel: false });
}, 240000);
});