Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8343075
Do some work defining the new artillery load test for notify. Also co…
wildjames Jun 17, 2025
022359a
Add missing newline characters
wildjames Jun 17, 2025
2e4df68
Changed my mind. Use env var config for ramp up and load values
wildjames Jun 17, 2025
07fc662
Forgot to update the runner script
wildjames Jun 17, 2025
4d74d03
Refactor ODS code generation to be more JS, and less python
wildjames Jun 17, 2025
6e8adc9
Merge branch 'main' into aea-5411-load-test-notify
wildjames Jun 18, 2025
b0809f7
loop has to be an array
wildjames Jun 18, 2025
1d7b9a3
Add allowed ODS codes FA501 - FA999
wildjames Jun 18, 2025
9cde765
Fix broken import
wildjames Jun 18, 2025
01b5653
fix context issue
wildjames Jun 18, 2025
b0aa7c1
Running the flow, but getting 401 errors
wildjames Jun 18, 2025
8565140
Get the requests going. Seems to work now
wildjames Jun 18, 2025
3dcbafa
Dont wait after making the last request
wildjames Jun 18, 2025
e56a5ed
fix scripts
wildjames Jun 18, 2025
5b74681
Reduce logging output
wildjames Jun 19, 2025
972e7a1
dotenv flag is deprecated since v2.0.21
wildjames Jun 19, 2025
4f1cbfb
remove mistaken change
wildjames Jun 19, 2025
aaabcdc
Possible path error?
wildjames Jun 19, 2025
768a27f
Revert previous commit
wildjames Jun 19, 2025
c90630d
Merge branch 'main' into aea-5411-load-test-notify
wildjames Jun 20, 2025
a8e6b95
Merge branch 'main' into aea-5411-load-test-notify
wildjames Jun 24, 2025
e928f93
Always check the auth token on each loop of the load test
wildjames Jun 24, 2025
d252aa6
dotenv flag is deprecated since v2.0.21
wildjames Jun 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN apt-get update \
jq apt-transport-https ca-certificates gnupg-agent \
software-properties-common bash-completion python3-pip make libbz2-dev \
libreadline-dev libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev \
xz-utils tk-dev liblzma-dev netcat libyaml-dev
xz-utils tk-dev liblzma-dev netcat-traditional libyaml-dev

# install aws stuff
RUN wget -O /tmp/awscliv2.zip "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" && \
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/run_notify_load.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@ on:
description: 'The duration of the main test'
required: true
default: '900'
rampUpDuration:
description: 'The duration to ramp up to the arrival rate'
required: true
default: '900'
maxVusers:
description: 'Maximum number of vusers to create'
required: true
default: '10000'
default: '1000'

jobs:
run_artillery:
Expand All @@ -38,7 +34,6 @@ jobs:
echo "## environment : ${{ github.event.inputs.environment }}" >> "$GITHUB_STEP_SUMMARY"
echo "## arrivalRate : ${{ github.event.inputs.arrivalRate }}" >> "$GITHUB_STEP_SUMMARY"
echo "## duration : ${{ github.event.inputs.duration }}" >> "$GITHUB_STEP_SUMMARY"
echo "## rampUpDuration : ${{ github.event.inputs.rampUpDuration }}" >> "$GITHUB_STEP_SUMMARY"
echo "## maxVusers : ${{ github.event.inputs.maxVusers }}" >> "$GITHUB_STEP_SUMMARY"

- name: Checkout repo
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ local-cpsu:
./scripts/test_cpsu_load_test.sh

local-psu:
./scripts/test_psu_load_test.sh
./scripts/test_psu_load_test.sh

local-notify:
./scripts/test_notify_load_test.sh
8 changes: 8 additions & 0 deletions artillery/helper/odscodes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// These ODS codes should match the values in AWS parameter store. They're assumed to be enabled in the whitelist
export const allowedOdsCodes = [
"FA565"
]

export const blockedOdsCodes = [
// "B3J1Z"
]
13 changes: 7 additions & 6 deletions artillery/helper/psu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ const logger = pino()
let oauthToken
let tokenExpiryTime

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

const task_identifier = uuidv4()
const prescriptionOrderItemNumber = uuidv4()
const nhsNumber = "9449304130"
const currentTimestamp = new Date().toISOString()
const odsCode = "C9Z1O"
const status = "in-progress"
const businessStatus = "With Pharmacy"
const body = {
resourceType: "Bundle",
type: "transaction",
Expand Down Expand Up @@ -130,7 +132,6 @@ export async function getPSUParams(requestParams, vuContext) {
const isValid = vuContext.scenario.tags.isValid
const body = getBody(isValid)

requestParams.json = body
// This sets the body of the request and some variables so headers are unique
requestParams.json = body
vuContext.vars.x_request_id = uuidv4()
Expand Down
111 changes: 111 additions & 0 deletions artillery/notify_entrypoint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {v4 as uuidv4} from "uuid"
import pino from "pino"
import {getSharedAuthToken, getBody} from "./helper/psu.mjs"
import {allowedOdsCodes, blockedOdsCodes} from "./helper/odscodes.mjs"

export { getSharedAuthToken }

const logger = pino()

const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS = "0123456789";

const randomChar = (chars) => chars[Math.floor(Math.random() * chars.length)];

/** Generate one two-letter, three-digit ODS code, e.g. "AB123" */
const generateOdsCode = () =>
`${randomChar(LETTERS)}${randomChar(LETTERS)}${randomChar(DIGITS)}${randomChar(DIGITS)}${randomChar(DIGITS)}`;

function buildFullOdsCodes(targetCount, seedCodes) {
const codes = new Set(seedCodes);

while (codes.size < targetCount) {
codes.add(generateOdsCode());
}

return Array.from(codes);
}

// The complete list of ODS codes
const fullOdsCodes = allowedOdsCodes.concat(blockedOdsCodes)

function computeCheckDigit(nhsNumber) {
const factors = [10,9,8,7,6,5,4,3,2]
let total = 0

for (let i = 0; i < 9; i++) {
total += parseInt(nhsNumber.charAt(i),10) * factors[i]
}

const rem = total % 11
let d = 11 - rem
if (d === 11) d = 0

return d
}

function generateValidNhsNumber() {
while (true) {
const partial = Array.from({length:9},() => Math.floor(Math.random()*10)).join("")
const cd = computeCheckDigit(partial)
if (cd < 10) return partial + cd
}
}

// Apparently Math.sampleNormal isn't a function? Do a quick Box-Muller transform instead
function sampleNormal(mean = 0, sd = 1) {
let u = 0, v = 0;
// avoid zeros because of log(0)
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
return z * sd + mean;
}

export function initUser(context, events, done) {
// Generate data for a patient
context.vars.odsCode = fullOdsCodes[Math.floor(Math.random()*fullOdsCodes.length)]
context.vars.nhsNumber = generateValidNhsNumber()

let prescriptionCount = Math.round(sampleNormal(3,1))
if (prescriptionCount < 1) prescriptionCount = 1 // just truncate at 1.
context.vars.prescriptionCount = prescriptionCount
context.vars.loopcount = 0

logger.info(`Patient ${context.vars.nhsNumber}, ODS ${context.vars.odsCode} has ${context.vars.prescriptionCount} prescriptions`)

done()
}

export function generatePrescData(requestParams, context, ee, next) {
logger.debug(`Generating a prescription for patient ${context.vars.nhsNumber}`)
const body = getBody(
true, /* isValid */
"completed", /* status */
context.vars.odsCode, /* odsCode */
context.vars.nhsNumber, /* nhsNumber */
"ready to collect" /* Item status */
)
// The body is fine - it works when I put it in postman

requestParams.json = body
context.vars.x_request_id = uuidv4()
context.vars.x_correlation_id = uuidv4()

context.vars.loopcount += 1

// Wait this long between requests
let meanDelay = 10 // seconds
let stdDevDelay = 10 // seconds
let delay = 0
if (context.vars.loopcount < context.vars.prescriptionCount) {
delay = sampleNormal(meanDelay, stdDevDelay)
while (delay < 0) delay = sampleNormal(meanDelay, stdDevDelay)
}

context.vars.nextDelay = delay
logger.debug(`Patient ${context.vars.nhsNumber} (on prescription update ${context.vars.loopcount}/${context.vars.prescriptionCount}) will think for ${context.vars.nextDelay} seconds`)

next()
}

44 changes: 44 additions & 0 deletions artillery/notify_load_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
config:
processor: "./notify_entrypoint.mjs"
plugins:
expect: {}
apdex: {}
phases:
- name: run phase
duration: "{{ $env.duration }}"
arrivalRate: "{{ $env.arrivalRate }}"
maxVusers: "{{ $env.maxVusers }}"
environments:
dev:
target: https://internal-dev.api.service.nhs.uk/
ref:
target: https://ref.api.service.nhs.uk/
int:
target: https://int.api.service.nhs.uk/
pr:
target: https://psu-pr-{{ prNumber }}.dev.eps.national.nhs.uk/

before:
flow:
- function: getSharedAuthToken

scenarios:
- name: dynamic PSU calls for each patient

flow:
- function: initUser

# for each prescription (number of prescriptions is variable)
- loop:
- function: getSharedAuthToken
- post:
url: "/prescription-status-update/"
beforeRequest: "generatePrescData"
headers:
Authorization: "Bearer {{ authToken }}"
x-request-id: "{{ x_request_id }}"
x-correlation-id: "{{ x_correlation_id }}"
expect:
- statusCode: 201
- think: "{{ nextDelay }}seconds"
count: "{{ prescriptionCount }}"
2 changes: 1 addition & 1 deletion scripts/run_cpsu_load_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ npx artillery run-fargate \
--security-group-ids "${security_group}" \
--subnet-ids "${vpc_subnets}" \
--task-role-name "${artillery_worker_role_name}" \
--dotenv runtimeenv.env \
--env-file runtimeenv.env \
--output cpsu_load_test.json \
artillery/cpsu_load_test.yml

Expand Down
64 changes: 64 additions & 0 deletions scripts/run_notify_load_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash


if [ -z "${artillery_key}" ]; then
echo "artillery_key is unset or set to the empty string"
exit 1
fi

if [ -z "${environment}" ]; then
echo "environment is unset or set to the empty string"
exit 1
fi

if [ -z "${maxVusers}" ]; then
echo "maxVusers is unset or set to the empty string"
exit 1
fi

if [ -z "${duration}" ]; then
echo "duration is unset or set to the empty string"
exit 1
fi

if [ -z "${arrivalRate}" ]; then
echo "arrivalRate is unset or set to the empty string"
exit 1
fi

if ! [[ "${environment}" =~ ^(dev|ref)$ ]]
then
echo "environment must be dev or ref"
exit 1
fi

security_group=$(aws cloudformation list-exports --output json | jq -r '.Exports[] | select(.Name == "artillery-resources:ArtillerySecurityGroupId") | .Value' | grep -o '[^:]*$')
export security_group
vpc_subnets=$(aws cloudformation list-exports --output json | jq -r '.Exports[] | select(.Name == "vpc-resources:PrivateSubnets") | .Value' | grep -o '[^:]*$')
export vpc_subnets

artillery_worker_role_name=$(aws cloudformation list-exports --output json | jq -r '.Exports[] | select(.Name == "artillery-resources:ArtilleryWorkerRoleName") | .Value' | grep -o '[^:]*$')
export artillery_worker_role_name

cat <<EOF > runtimeenv.env
maxVusers=$maxVusers
duration=$duration
arrivalRate=$arrivalRate
EOF

echo ${launch_config}

# shellcheck disable=SC2090,SC2086
npx artillery run-fargate \
--environment "${environment}" \
--secret psu_api_key psu_private_key psu_kid \
--region eu-west-2 \
--cluster artilleryio-cluster \
--security-group-ids "${security_group}" \
--subnet-ids "${vpc_subnets}" \
--task-role-name "${artillery_worker_role_name}" \
--env-file runtimeenv.env \
--output notify_load_test.json \
artillery/notify_load_test.yml

npx artillery report notify_load_test.json
2 changes: 1 addition & 1 deletion scripts/run_psu_load_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ npx artillery run-fargate \
--security-group-ids "${security_group}" \
--subnet-ids "${vpc_subnets}" \
--task-role-name "${artillery_worker_role_name}" \
--dotenv runtimeenv.env \
--env-file runtimeenv.env \
--output psu_load_test.json \
artillery/psu_load_test.yml

Expand Down
4 changes: 2 additions & 2 deletions scripts/test_cpsu_load_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ set -e
# Run the Artillery test locally
npx artillery run \
-e "${environment}" \
--dotenv /workspaces/eps-load-test/runtimeenv.env \
--env-file /workspaces/eps-load-test/runtimeenv.env \
--output /workspaces/eps-load-test/cpsu_load_test.json \
/workspaces/eps-load-test/artillery/cpsu_load_test.yml

# Generate a report from the test results
npx /workspaces/eps-load-test/artillery report cpsu_load_test.json
npx artillery report cpsu_load_test.json
Loading