Skip to content

Commit 9b24990

Browse files
Merge pull request #1107 from NHSDigital/feature/axkr1-NRL-1666-k6-load-profile-5
NRL-1666 Performance test implementation
2 parents b51fa2e + ef157f2 commit 9b24990

20 files changed

+1759
-22
lines changed

Makefile

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
.EXPORT_ALL_VARIABLES:
32
.NOTPARALLEL:
43
.PHONY: *
@@ -16,6 +15,8 @@ ACCOUNT ?= dev
1615
APP_ALIAS ?= default
1716
HOST ?= $(TF_WORKSPACE_NAME).api.record-locator.$(ENV).national.nhs.uk
1817
ENV_TYPE ?= $(ENV)
18+
PERFTEST_TABLE_NAME ?= perftest
19+
PERFTEST_HOST ?= perftest-1.perftest.record-locator.national.nhs.uk
1920

2021
export PATH := $(PATH):$(PWD)/.venv/bin
2122
export USE_SHARED_RESOURCES := $(shell poetry run python scripts/are_resources_shared_for_stack.py $(TF_WORKSPACE_NAME))
@@ -246,3 +247,33 @@ generate-models: check-warn ## Generate Pydantic Models
246247
--output ./layer/nrlf/consumer/fhir/r4/model.py \
247248
--base-class nrlf.core.parent_model.Parent \
248249
--output-model-type "pydantic_v2.BaseModel"
250+
251+
252+
generate-perftest-permissions: ## Generate perftest permissions and add to nrlf_permissions
253+
poetry run python tests/performance/producer/generate_permissions.py --output_dir="$(DIST_PATH)/nrlf_permissions/K6PerformanceTest"
254+
255+
perftest-producer:
256+
@echo "Running producer performance tests with HOST=$(PERFTEST_HOST) and ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)"
257+
k6 run tests/performance/producer/perftest.js -e HOST=$(PERFTEST_HOST) -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH)
258+
259+
perftest-consumer:
260+
@echo "Running consumer performance tests with HOST=$(PERFTEST_HOST) and ENV_TYPE=$(ENV_TYPE) and DIST_PATH=$(DIST_PATH)"
261+
k6 run tests/performance/consumer/perftest.js -e HOST=$(PERFTEST_HOST) -e ENV_TYPE=$(ENV_TYPE) -e DIST_PATH=$(DIST_PATH)
262+
263+
perftest-prep-generate-producer-data:
264+
@echo "Generating producer reference with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)"
265+
mkdir -p $(DIST_PATH)
266+
PYTHONPATH=. poetry run python tests/performance/perftest_environment.py generate_producer_data --output_dir="$(DIST_PATH)"
267+
268+
perftest-prep-extract-consumer-data:
269+
@echo "Generating consumer reference with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)"
270+
mkdir -p $(DIST_PATH)
271+
PYTHONPATH=. poetry run python tests/performance/perftest_environment.py extract_consumer_data --output_dir="$(DIST_PATH)"
272+
273+
perftest-prep-generate-pointer-table-extract:
274+
@echo "Generating pointer table extract with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)"
275+
mkdir -p $(DIST_PATH)
276+
PYTHONPATH=. poetry run python tests/performance/perftest_environment.py generate_pointer_table_extract --output_dir="$(DIST_PATH)"
277+
278+
perftest-prepare: perftest-prep-generate-producer-data perftest-prep-extract-consumer-data perftest-prep-generate-pointer-table-extract
279+
@echo "Prepared performance tests with PERFTEST_TABLE_NAME=$(PERFTEST_TABLE_NAME) and DIST_PATH=$(DIST_PATH)"

tests/performance/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Performance Testing
2+
3+
some high level context short
4+
5+
## Run perf tests
6+
7+
### Prep the environment
8+
9+
Perf tests are generally conducted in the perftest env. There's a selection of tables in the perftest env representing different pointer volume scenarios e.g. perftest-baseline vs perftest-1million (todo: update with real names!).
10+
11+
To reset this table to the expected state for perftests, restore the table from a backup.
12+
13+
In the steps below, make sure the table name is the table your environment is pointing at. You might need to redeploy NRLF lambdas to point at the desired table.
14+
15+
### Prepare to run tests
16+
17+
#### Pull certs for env
18+
19+
```sh
20+
assume management
21+
make truststore-pull-all ENV=perftest
22+
```
23+
24+
#### Generate permissions
25+
26+
You will need to generate pointer permissions the first time performance tests are run in an environment e.g. if the perftest environment is destroyed & recreated.
27+
28+
```sh
29+
make generate permissions # makes a bunch of json permission files
30+
make build # will take all permissions & create nrlf_permissions.zip file
31+
32+
# apply this new permissions zip file to your environment
33+
cd ./terraform/infrastructure
34+
assume test # needed?
35+
make init TF_WORKSPACE_NAME=perftest-1 ENV=perftest
36+
tf apply
37+
```
38+
39+
#### Generate input files
40+
41+
```sh
42+
# creates 2 csv files and a json file
43+
make perftest-prepare PERFTEST_TABLE_NAME=perftest-baseline
44+
```
45+
46+
### Run tests
47+
48+
```sh
49+
make perftest-consumer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk
50+
make perftest-producer ENV_TYPE=perftest PERFTEST_HOST=perftest-1.perftest.record-locator.national.nhs.uk
51+
```

tests/performance/constants.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CATEGORY_TYPE_GROUPS } from "./type-category-mappings.js";
2+
13
export const DEFAULT_TEST_RECORD = open(
24
"../data/DocumentReference/Y05868-736253002-Valid.json"
35
);
@@ -12,23 +14,34 @@ export const ALL_POINTER_IDS =
1214
export const POINTERS_TO_DELETE = ALL_POINTER_IDS.slice(0, 3500);
1315
export const POINTER_IDS = ALL_POINTER_IDS.slice(3500);
1416
export const NHS_NUMBERS = REFERENCE_DATA["nhs_numbers"];
15-
export const POINTER_TYPES = [
17+
18+
// filter only 736253001, 736253002, 1363501000000100, 861421000000109, 749001000000101 for now
19+
export const FILTERED_POINTER_TYPES = [
20+
"736253001",
1621
"736253002",
1722
"1363501000000100",
18-
"1382601000000107",
19-
"325691000000100",
20-
"736373009",
2123
"861421000000109",
22-
"887701000000100",
23-
"736366004",
24-
"735324008",
25-
"824321000000109",
26-
"2181441000000107",
27-
];
28-
export const CATEGORIES = [
29-
"734163000",
30-
"1102421000000108",
31-
"823651000000106",
32-
"721981007",
33-
"103693007",
24+
"749001000000101",
3425
];
26+
27+
export const POINTER_TYPES = FILTERED_POINTER_TYPES;
28+
29+
export const CATEGORIES = CATEGORY_TYPE_GROUPS.map(
30+
(group) => group.category.code
31+
);
32+
export const POINTER_TYPE_DISPLAY = Object.fromEntries(
33+
CATEGORY_TYPE_GROUPS.flatMap((group) =>
34+
group.types.map((t) => [t.code, t.display])
35+
)
36+
);
37+
export const TYPE_CATEGORY_MAP = Object.fromEntries(
38+
CATEGORY_TYPE_GROUPS.flatMap((group) =>
39+
group.types.map((t) => [t.code, group.category.code])
40+
)
41+
);
42+
export const CATEGORY_DISPLAY = Object.fromEntries(
43+
CATEGORY_TYPE_GROUPS.map((group) => [
44+
group.category.code,
45+
group.category.display,
46+
])
47+
);
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import http from "k6/http";
2+
import { check } from "k6";
3+
import exec from "k6/execution";
4+
import { CATEGORY_TYPE_GROUPS } from "../type-category-mappings.js";
5+
6+
const csvPath = __ENV.DIST_PATH
7+
? `../../../${__ENV.DIST_PATH}/producer_reference_data.csv`
8+
: "../producer_reference_data.csv";
9+
const csv = open(csvPath);
10+
const lines = csv.trim().split("\n");
11+
// Skip header
12+
const dataLines = lines.slice(1);
13+
14+
function getNextPointer() {
15+
// pick the next line according to iteration in scenario
16+
const iter = exec.vu.iterationInScenario;
17+
const index = iter % dataLines.length;
18+
const line = dataLines[index];
19+
const [count, pointer_id, pointer_type, custodian, nhs_number] = line
20+
.split(",")
21+
.map((field) => field.trim());
22+
return { pointer_id, pointer_type, nhs_number };
23+
}
24+
25+
function getHeaders(odsCode) {
26+
return {
27+
"Content-Type": "application/fhir+json",
28+
"X-Request-Id": `K6perftest-consumer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`,
29+
"NHSD-Correlation-Id": `K6perftest-consumer-${exec.scenario.name}-${exec.vu.idInTest}-${exec.vu.iterationInScenario}`,
30+
"NHSD-Connection-Metadata": JSON.stringify({
31+
"nrl.ods-code": odsCode,
32+
"nrl.app-id": "K6PerformanceTest",
33+
}),
34+
"NHSD-Client-RP-Details": JSON.stringify({
35+
"developer.app.name": "K6PerformanceTest",
36+
"developer.app.id": "K6PerformanceTest",
37+
}),
38+
};
39+
}
40+
41+
function getCustodianFromPointerId(pointer_id) {
42+
// pointer_id format is "CUSTODIAN-XXXX"
43+
return pointer_id.split("-")[0];
44+
}
45+
46+
function checkResponse(res) {
47+
const is_success = check(res, { "status is 200": (r) => r.status === 200 });
48+
if (!is_success) {
49+
console.warn(res.json());
50+
}
51+
}
52+
53+
const pointerTypeToCategoryMap = new Map();
54+
for (const group of CATEGORY_TYPE_GROUPS) {
55+
for (const type of group.types) {
56+
pointerTypeToCategoryMap.set(type.code, group.category.code);
57+
}
58+
}
59+
60+
export function countDocumentReference() {
61+
const { pointer_id, nhs_number } = getNextPointer();
62+
const custodian = getCustodianFromPointerId(pointer_id);
63+
const identifier = encodeURIComponent(
64+
`https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`
65+
);
66+
67+
const res = http.get(
68+
`https://${__ENV.HOST}/consumer/DocumentReference?_summary=count&subject:identifier=${identifier}`,
69+
{
70+
headers: getHeaders(custodian),
71+
}
72+
);
73+
checkResponse(res);
74+
}
75+
76+
export function readDocumentReference() {
77+
const { pointer_id } = getNextPointer();
78+
const custodian = getCustodianFromPointerId(pointer_id);
79+
80+
const res = http.get(
81+
`https://${__ENV.HOST}/consumer/DocumentReference/${pointer_id}`,
82+
{
83+
headers: getHeaders(custodian),
84+
}
85+
);
86+
87+
checkResponse(res);
88+
}
89+
90+
export function searchDocumentReference() {
91+
const { pointer_id, pointer_type, nhs_number } = getNextPointer();
92+
const custodian = getCustodianFromPointerId(pointer_id);
93+
94+
const identifier = encodeURIComponent(
95+
`https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`
96+
);
97+
const type = encodeURIComponent(`http://snomed.info/sct|${pointer_type}`);
98+
99+
const res = http.get(
100+
`https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&type=${type}`,
101+
{
102+
headers: getHeaders(custodian),
103+
}
104+
);
105+
checkResponse(res);
106+
}
107+
108+
export function searchDocumentReferenceByCategory() {
109+
const { pointer_id, pointer_type, nhs_number } = getNextPointer();
110+
const custodian = getCustodianFromPointerId(pointer_id);
111+
const category_code = pointerTypeToCategoryMap.get(pointer_type);
112+
113+
const identifier = encodeURIComponent(
114+
`https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`
115+
);
116+
const category = encodeURIComponent(
117+
`http://snomed.info/sct|${category_code}`
118+
);
119+
120+
const res = http.get(
121+
`https://${__ENV.HOST}/consumer/DocumentReference?subject:identifier=${identifier}&category=${category}`,
122+
{
123+
headers: getHeaders(custodian),
124+
}
125+
);
126+
checkResponse(res);
127+
}
128+
129+
export function searchPostDocumentReference() {
130+
const { pointer_id, pointer_type, nhs_number } = getNextPointer();
131+
const custodian = getCustodianFromPointerId(pointer_id);
132+
133+
const body = JSON.stringify({
134+
"subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`,
135+
type: `http://snomed.info/sct|${pointer_type}`,
136+
});
137+
138+
const res = http.post(
139+
`https://${__ENV.HOST}/consumer/DocumentReference/_search`,
140+
body,
141+
{
142+
headers: getHeaders(custodian),
143+
}
144+
);
145+
checkResponse(res);
146+
}
147+
148+
export function searchPostDocumentReferenceByCategory() {
149+
const { pointer_id, pointer_type, nhs_number } = getNextPointer();
150+
const custodian = getCustodianFromPointerId(pointer_id);
151+
const category_code = pointerTypeToCategoryMap.get(pointer_type);
152+
153+
const body = JSON.stringify({
154+
"subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`,
155+
category: `http://snomed.info/sct|${category_code}`,
156+
});
157+
158+
const res = http.post(
159+
`https://${__ENV.HOST}/consumer/DocumentReference/_search`,
160+
body,
161+
{
162+
headers: getHeaders(custodian),
163+
}
164+
);
165+
checkResponse(res);
166+
}
167+
168+
export function countPostDocumentReference() {
169+
const { pointer_id, nhs_number } = getNextPointer();
170+
const custodian = getCustodianFromPointerId(pointer_id);
171+
172+
const body = JSON.stringify({
173+
"subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`,
174+
});
175+
176+
const res = http.post(
177+
`https://${__ENV.HOST}/consumer/DocumentReference/_search?_summary=count`,
178+
body,
179+
{
180+
headers: getHeaders(custodian),
181+
}
182+
);
183+
checkResponse(res);
184+
}
185+
186+
export function searchPostDocumentReferenceAccessDenied() {
187+
const { nhs_number, pointer_type } = getNextPointer();
188+
189+
const body = JSON.stringify({
190+
"subject:identifier": `https://fhir.nhs.uk/Id/nhs-number|${nhs_number}`,
191+
type: `http://snomed.info/sct|${pointer_type}`,
192+
});
193+
194+
// Use a custodian that should not have access (simulate denied)
195+
const deniedCustodian = "DENIED_ODS_CODE";
196+
let headers = getHeaders(deniedCustodian);
197+
headers["NHSD-Connection-Metadata"] = JSON.stringify({
198+
"nrl.ods-code": deniedCustodian,
199+
"nrl.app-id": "K6PerformanceTest",
200+
});
201+
202+
const res = http.post(
203+
`https://${__ENV.HOST}/consumer/DocumentReference/_search`,
204+
body,
205+
{
206+
headers: headers,
207+
}
208+
);
209+
210+
const is_denied = check(res, { "status is 403": (r) => r.status === 403 });
211+
if (!is_denied) {
212+
console.warn(`Expected access denied but got: ${res.status}`);
213+
}
214+
}
215+
216+
export function readDocumentReferenceNotFound() {
217+
const { custodian } = getNextPointer();
218+
219+
const res = http.get(
220+
`https://${__ENV.HOST}/consumer/DocumentReference/NonExistentID`,
221+
{
222+
headers: getHeaders(custodian),
223+
}
224+
);
225+
226+
// we expect a 404 here
227+
check(res, { "status is 404": (r) => r.status === 404 });
228+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"nhs_numbers": ["9694202043"],
3+
"pointer_ids": [
4+
"RQI-9347490b-6087-4be6-8c95-82ad9fb0c83f",
5+
"RQI-123",
6+
"RQI-7fba4cfb-acfe-4b62-ac85-916197a24868"
7+
],
8+
"custodians": ["RQI"]
9+
}

0 commit comments

Comments
 (0)