Skip to content

Commit 8343075

Browse files
committed
Do some work defining the new artillery load test for notify. Also copy and edit the psu load test definition. Needs some more config.
1 parent 9be2d3a commit 8343075

6 files changed

Lines changed: 280 additions & 6 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN apt-get update \
99
jq apt-transport-https ca-certificates gnupg-agent \
1010
software-properties-common bash-completion python3-pip make libbz2-dev \
1111
libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev \
12-
xz-utils tk-dev liblzma-dev netcat libyaml-dev
12+
xz-utils tk-dev liblzma-dev netcat-traditional libyaml-dev
1313

1414
# install aws stuff
1515
RUN wget -O /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" && \
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: Run psu notify load test
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: 'Environment to run tests against. Allowed values are dev or ref'
8+
required: true
9+
default: 'ref'
10+
arrivalRate:
11+
description: 'The number of new users to add every second'
12+
required: true
13+
default: '275'
14+
duration:
15+
description: 'The duration of the main test'
16+
required: true
17+
default: '900'
18+
rampUpDuration:
19+
description: 'The duration to ramp up to the arrival rate'
20+
required: true
21+
default: '900'
22+
maxVusers:
23+
description: 'Maximum number of vusers to create'
24+
required: true
25+
default: '500'
26+
27+
jobs:
28+
run_artillery:
29+
name: Run Artillery
30+
runs-on: ubuntu-latest
31+
permissions:
32+
id-token: write
33+
contents: read
34+
steps:
35+
- name: Show input params
36+
shell: bash
37+
run: |
38+
echo "## environment : ${{ github.event.inputs.environment }}" >> "$GITHUB_STEP_SUMMARY"
39+
echo "## arrivalRate : ${{ github.event.inputs.arrivalRate }}" >> "$GITHUB_STEP_SUMMARY"
40+
echo "## duration : ${{ github.event.inputs.duration }}" >> "$GITHUB_STEP_SUMMARY"
41+
echo "## rampUpDuration : ${{ github.event.inputs.rampUpDuration }}" >> "$GITHUB_STEP_SUMMARY"
42+
echo "## maxVusers : ${{ github.event.inputs.maxVusers }}" >> "$GITHUB_STEP_SUMMARY"
43+
44+
- name: Checkout repo
45+
uses: actions/checkout@v4
46+
with:
47+
ref: ${{ env.BRANCH_NAME }}
48+
49+
- name: Install asdf
50+
uses: asdf-vm/actions/setup@05e0d2ed97b598bfce82fd30daf324ae0c4570e6
51+
with:
52+
asdf_branch: v0.14.0
53+
54+
- name: Cache asdf
55+
uses: actions/cache@v4
56+
with:
57+
path: |
58+
~/.asdf
59+
key: ${{ runner.os }}-asdf-${{ hashFiles('**/.tool-versions') }}
60+
restore-keys: |
61+
${{ runner.os }}-asdf-
62+
63+
- name: Install asdf dependencies in .tool-versions
64+
uses: asdf-vm/actions/install@05e0d2ed97b598bfce82fd30daf324ae0c4570e6
65+
with:
66+
asdf_branch: v0.14.0
67+
env:
68+
PYTHON_CONFIGURE_OPTS: --enable-shared
69+
70+
- name: Install Dependencies
71+
run: make install
72+
73+
- name: Assume dev artillery runner role
74+
if: github.event.inputs.environment == 'dev'
75+
uses: aws-actions/configure-aws-credentials@v4
76+
with:
77+
aws-region: eu-west-2
78+
role-to-assume: ${{ secrets.DEV_ARTILLERY_RUNNER_ROLE }}
79+
role-session-name: github-actions-artillery
80+
81+
- name: Assume ref artillery runner role
82+
if: github.event.inputs.environment == 'ref'
83+
uses: aws-actions/configure-aws-credentials@v4
84+
with:
85+
aws-region: eu-west-2
86+
role-to-assume: ${{ secrets.REF_ARTILLERY_RUNNER_ROLE }}
87+
role-session-name: github-actions-artillery
88+
89+
- name: Run load tests
90+
shell: bash
91+
env:
92+
environment: ${{ github.event.inputs.environment }}
93+
arrivalRate: ${{ github.event.inputs.arrivalRate }}
94+
duration: ${{ github.event.inputs.duration }}
95+
rampUpDuration: ${{ github.event.inputs.rampUpDuration }}
96+
maxVusers: ${{ github.event.inputs.maxVusers }}
97+
run: |
98+
./scripts/run_psu_load_test.sh
99+
100+
- uses: actions/upload-artifact@v4
101+
if: always()
102+
name: Upload test_report
103+
with:
104+
name: test_report
105+
path: |
106+
psu_load_test.json
107+
psu_load_test.json.html
108+
109+
- name: Upload artifacts to S3, if we are using REF environment
110+
if: github.event.inputs.environment == 'ref'
111+
continue-on-error: true
112+
env:
113+
AWS_REGION: eu-west-2
114+
BUCKET_NAME: artilleryio-test-data-${{ secrets.AWS_ACCOUNT_ID }}
115+
RUN_ID: ${{ github.run_id }}
116+
run: |
117+
aws s3 cp psu_load_test.json s3://$BUCKET_NAME/reports/$RUN_ID/psu_load_test.json --acl public-read
118+
aws s3 cp psu_load_test.json.html s3://$BUCKET_NAME/reports/$RUN_ID/psu_load_test.json.html --acl public-read
119+
120+
- name: Output link to HTML report
121+
if: github.event.inputs.environment == 'ref'
122+
env:
123+
AWS_REGION: eu-west-2
124+
BUCKET_NAME: artilleryio-test-data-${{ secrets.AWS_ACCOUNT_ID }}
125+
RUN_ID: ${{ github.run_id }}
126+
run: |
127+
REPORT_URL="https://$BUCKET_NAME.s3.${AWS_REGION}.amazonaws.com/reports/$RUN_ID/psu_load_test.json.html"
128+
echo "Test report is hosted at: $REPORT_URL" >> "$GITHUB_STEP_SUMMARY"
129+
echo "::set-output name=report_url::$REPORT_URL"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// These ODS codes should match the values in AWS parameter store. They're assumed to be enabled in the whitelist
2+
export const allowedOdsCodes = [
3+
// TODO: I need to define these
4+
]

artillery/helper/psu.mjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ const logger = pino()
66
let oauthToken
77
let tokenExpiryTime
88

9-
export function getBody(isValid = true) {
9+
export function getBody(
10+
isValid = true,
11+
status = "in-progress",
12+
odsCode = "C9Z10",
13+
nhsNumber = "9449304130",
14+
businessStatus = "With Pharmacy",
15+
) {
1016
// If this is intended to be a failed request, mangle the prescription ID.
1117
const prescriptionID = isValid ? shortPrescId() : invalidShortPrescId();
1218

1319
const task_identifier = uuidv4()
1420
const prescriptionOrderItemNumber = uuidv4()
15-
const nhsNumber = "9449304130"
1621
const currentTimestamp = new Date().toISOString()
17-
const odsCode = "C9Z1O"
18-
const status = "in-progress"
19-
const businessStatus = "With Pharmacy"
2022
const body = {
2123
resourceType: "Bundle",
2224
type: "transaction",

artillery/notify_entrypoint.mjs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {v4 as uuidv4} from "uuid"
2+
import pino from "pino"
3+
import {getSharedAuthToken, getBody} from "./psu_entrypoint.mjs"
4+
import {allowedOdsCodes} from "./allowed_odsCodes.js"
5+
6+
const logger = pino()
7+
8+
const NUM_ODS_CODES = 10000
9+
10+
// make a list of 10k ODS codes, using the known-valid ones as a seed
11+
const fullOdsCodes = (() => {
12+
const set = new Set(allowedOdsCodes)
13+
const codes = [...allowedOdsCodes]
14+
15+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
16+
const numbers = "0123456789"
17+
18+
while (codes.length < NUM_ODS_CODES) {
19+
let c = ""
20+
for (let i = 0; i < 2; i++) {
21+
c += letters.charAt(Math.floor(Math.random() * letters.length))
22+
}
23+
for (let i = 0; i < 3; i++) {
24+
c += numbers.charAt(Math.floor(Math.random() * numbers.length))
25+
}
26+
27+
if (!set.has(c)) {
28+
set.add(c)
29+
codes.push(c)
30+
}
31+
}
32+
33+
return codes
34+
})()
35+
36+
function computeCheckDigit(nhsNumber) {
37+
const factors = [10,9,8,7,6,5,4,3,2]
38+
let total = 0
39+
40+
for (let i = 0; i < 9; i++) {
41+
total += parseInt(nhsNumber.charAt(i),10) * factors[i]
42+
}
43+
44+
const rem = total % 11
45+
let d = 11 - rem
46+
if (d === 11) d = 0
47+
48+
return d
49+
}
50+
51+
function generateValidNhsNumber() {
52+
while (true) {
53+
const partial = Array.from({length:9},() => Math.floor(Math.random()*10)).join("")
54+
const cd = computeCheckDigit(partial)
55+
if (cd < 10) return partial + cd
56+
}
57+
}
58+
59+
export function initUser(_, vuContext) {
60+
// Generate data for a patient
61+
vuContext.vars.odsCode = fullOdsCodes[Math.floor(Math.random()*fullOdsCodes.length)]
62+
vuContext.vars.nhsNumber = generateValidNhsNumber()
63+
64+
let prescriptionCount = Math.round(Math.sampleNormal(3,1))
65+
if (prescriptionCount < 1) prescriptionCount = 1 // just truncate at 1.
66+
vuContext.vars.prescriptionCount = prescriptionCount
67+
}
68+
69+
// beforeEach request
70+
export function generatePrescData(requestParams, vuContext) {
71+
const body = getBody(
72+
true, /* isValid */
73+
"ready to collect", /* status */
74+
vuContext.vars.odsCode, /* odsCode */
75+
vuContext.vars.nhsNumber /* nhsNumber */
76+
)
77+
78+
requestParams.json = body
79+
vuContext.vars.x_request_id = uuidv4()
80+
vuContext.vars.x_correlation_id = uuidv4()
81+
82+
// Wait this long between requests
83+
let delay = Math.sampleNormal(150, 60)
84+
if (delay < 0) delay = 0
85+
vuContext.vars.nextDelay = delay
86+
}
87+
88+
export { getSharedAuthToken }

artillery/notify_load_test.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
config:
2+
processor: "./notify_entrypoint.mjs"
3+
phases:
4+
- name: ramp-up
5+
duration: "10m"
6+
arrivalRate: 20
7+
rampTo: 100
8+
maxVusers: 1000
9+
- name: steady-state
10+
duration: "30m"
11+
arrivalRate: 100
12+
maxVusers: 1000
13+
environments:
14+
dev:
15+
target: https://internal-dev.api.service.nhs.uk
16+
ref:
17+
target: https://ref.api.service.nhs.uk
18+
int:
19+
target: https://int.api.service.nhs.uk
20+
pr:
21+
target: https://psu-pr-{{ prNumber }}.dev.eps.national.nhs.uk
22+
defaults:
23+
headers:
24+
Authorization: "Bearer {{ authToken }}"
25+
26+
scenarios:
27+
- name: dynamic PSU calls for each patient
28+
29+
# grab token once per VU
30+
before:
31+
- function: getSharedAuthToken
32+
33+
flow:
34+
- function: initUser
35+
36+
# for each prescription (number of prescriptions is variable)
37+
- loop:
38+
count: "{{ prescriptionCount }}"
39+
steps:
40+
- beforeRequest: generatePrescData
41+
42+
- post:
43+
url: "/prescription-status-update/"
44+
headers:
45+
x-request-id: "{{ x_request_id }}"
46+
x-correlation-id: "{{ x_correlation_id }}"
47+
expect:
48+
- statusCode: 201
49+
50+
# wait before next prescription
51+
- think: "{{ nextDelay }}"

0 commit comments

Comments
 (0)