Skip to content

Commit bf87243

Browse files
authored
Fix: [AEA-0000] - Reduce the number of calls to the parameter store (#1844)
## Summary - Routine Change ### Details The parameter store was getting hit once per call for the PSU and notify processor. Update the logic to cache values, and only fetch them once per minute.
1 parent c901f4e commit bf87243

10 files changed

Lines changed: 193 additions & 82 deletions

File tree

SAMtemplates/functions/lambda_resources.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Resources:
116116
- Effect: Allow
117117
Action:
118118
- ssm:GetParameter
119+
- ssm:GetParameters
119120
Resource:
120121
- "*"
121122

packages/nhsNotifyLambda/src/utils.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "@aws-sdk/client-sqs"
99
import {DynamoDBClient} from "@aws-sdk/client-dynamodb"
1010
import {DynamoDBDocumentClient, GetCommand, PutCommand} from "@aws-sdk/lib-dynamodb"
11-
import {getParameter} from "@aws-lambda-powertools/parameters/ssm"
11+
import {SSMProvider} from "@aws-lambda-powertools/parameters/ssm"
1212
import {getSecret} from "@aws-lambda-powertools/parameters/secrets"
1313

1414
import {NotifyDataItem} from "@PrescriptionStatusUpdate_common/commonTypes"
@@ -30,7 +30,6 @@ const DUMMY_NOTIFY_DELAY_MS = 150
3030
// these are only ever changed by a deployment
3131
const dynamoTable = process.env.TABLE_NAME
3232
const sqsUrl = process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL
33-
const MAKE_REAL_NOTIFY_REQUESTS_PARAM = process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM
3433

3534
// AWS clients
3635
const sqs = new SQSClient({region: process.env.AWS_REGION})
@@ -44,6 +43,25 @@ const marshallOptions = {
4443
const dynamo = new DynamoDBClient({region: process.env.AWS_REGION})
4544
const docClient = DynamoDBDocumentClient.from(dynamo, {marshallOptions})
4645

46+
const ssm = new SSMProvider()
47+
const paramNames = {
48+
[process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM!]: {maxAge: 60},
49+
[process.env.NOTIFY_API_BASE_URL_PARAM!]: {maxAge: 60}
50+
}
51+
const configPromise = ssm.getParametersByName(paramNames)
52+
53+
async function loadConfig(): Promise<{
54+
makeRealNotifyRequests: boolean,
55+
notifyApiBaseUrlRaw: string
56+
}> {
57+
const all = await configPromise
58+
59+
return {
60+
makeRealNotifyRequests: all[process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM!] === "true",
61+
notifyApiBaseUrlRaw: all[process.env.NOTIFY_API_BASE_URL_PARAM!] as string
62+
}
63+
}
64+
4765
/**
4866
* Returns the original array, chunked in batches of up to <size>
4967
*
@@ -94,7 +112,7 @@ export async function reportQueueStatus(logger: Logger): Promise<void> {
94112
// and helps track the nhs notify results
95113
export interface NotifyDataItemMessage extends Message {
96114
PSUDataItem: NotifyDataItem
97-
success?: boolean
115+
deliveryStatus?: string
98116
messageBatchReference?: string,
99117
// message reference is our internal UUID for the message
100118
messageReference: string
@@ -309,7 +327,7 @@ export async function addPrescriptionMessagesToNotificationStateStore(
309327
RequestId: data.PSUDataItem.RequestID,
310328
SQSMessageID: data.MessageId,
311329
LastNotifiedPrescriptionStatus: data.PSUDataItem.Status,
312-
DeliveryStatus: data.success ? "requested" : "notify request failed",
330+
DeliveryStatus: data.deliveryStatus ?? "unknown", // Fall back to unknown if not set
313331
NotifyMessageID: data.notifyMessageId, // This is a GSI, but leaving it blank is fine
314332
NotifyMessageReference: data.messageReference,
315333
NotifyMessageBatchReference: data.messageBatchReference, // Will be undefined when request fails
@@ -413,12 +431,11 @@ export async function makeBatchNotifyRequest(
413431
routingPlanId: string,
414432
data: Array<NotifyDataItemMessage>
415433
): Promise<Array<NotifyDataItemMessage>> {
416-
// Fetch secrets and parameters
417-
if (!process.env.NOTIFY_API_BASE_URL_PARAM || !process.env.API_KEY_SECRET) {
434+
if (!process.env.API_KEY_SECRET) {
418435
throw new Error("Environment configuration error")
419436
}
420437

421-
const notifyApiBaseUrlRaw = await getParameter(process.env.NOTIFY_API_BASE_URL_PARAM)
438+
const {makeRealNotifyRequests, notifyApiBaseUrlRaw} = await loadConfig()
422439
const apiKeyRaw = await getSecret(process.env.API_KEY_SECRET)
423440

424441
if (!notifyApiBaseUrlRaw) throw new Error("NOTIFY_API_BASE_URL is not defined in the environment variables!")
@@ -479,6 +496,21 @@ export async function makeBatchNotifyRequest(
479496
return [...res1, ...res2]
480497
}
481498

499+
if (!makeRealNotifyRequests) {
500+
logger.info("Not doing real Notify requests. Simply waiting for some time and returning success on all messages")
501+
await new Promise(f => setTimeout(f, DUMMY_NOTIFY_DELAY_MS))
502+
503+
// Map each input item to a "successful" NotifyDataItemMessage
504+
return data.map(item => {
505+
return {
506+
...item,
507+
messageBatchReference,
508+
deliveryStatus: "silent running",
509+
notifyMessageId: v4() // Create a dummy UUID
510+
}
511+
})
512+
}
513+
482514
logger.info("Making a request for notifications to NHS notify", {count: data.length, routingPlanId})
483515

484516
// Create an axios instance configured for Notify
@@ -502,24 +534,6 @@ export async function makeBatchNotifyRequest(
502534
onRetry: onAxiosRetry
503535
})
504536

505-
let doRealRequest: boolean = false
506-
if (MAKE_REAL_NOTIFY_REQUESTS_PARAM) doRealRequest = await getParameter(MAKE_REAL_NOTIFY_REQUESTS_PARAM) === "true"
507-
508-
if (!doRealRequest) {
509-
logger.info("Not doing real Notify requests. Simply waiting for some time and returning success on all messages")
510-
await new Promise(f => setTimeout(f, DUMMY_NOTIFY_DELAY_MS))
511-
512-
// Map each input item to a "successful" NotifyDataItemMessage
513-
return data.map(item => {
514-
return {
515-
...item,
516-
messageBatchReference,
517-
success: true,
518-
notifyMessageId: v4() // Create a dummy UUID
519-
}
520-
})
521-
}
522-
523537
try {
524538
const resp = await axiosInstance.post<CreateMessageBatchResponse>("/v1/message-batches", body)
525539

@@ -528,7 +542,7 @@ export async function makeBatchNotifyRequest(
528542
logger.info("Requested notifications OK!", {
529543
messageBatchReference,
530544
messageReferences: messages.map(e => e.messageReference),
531-
success: "Requested Success"
545+
deliveryStatus: "requested"
532546
})
533547

534548
// Map each input item to a NotifyDataItemMessage, marking success and attaching the notify ID
@@ -541,7 +555,7 @@ export async function makeBatchNotifyRequest(
541555
return {
542556
...item,
543557
messageBatchReference,
544-
success: !!match,
558+
deliveryStatus: match ? "requested" : "notify request failed",
545559
notifyMessageId: match?.id
546560
}
547561
})
@@ -552,7 +566,7 @@ export async function makeBatchNotifyRequest(
552566
statusText: resp.statusText,
553567
messageBatchReference,
554568
messageReferences: messages.map(e => e.messageReference),
555-
success: "Requested Failed"
569+
deliveryStatus: "notify request failed"
556570
})
557571
throw new Error("Notify batch request failed")
558572
}
@@ -561,7 +575,7 @@ export async function makeBatchNotifyRequest(
561575
logger.error("Notify batch request failed", {error})
562576
return data.map(item => ({
563577
...item,
564-
success: false,
578+
deliveryStatus: "notify request failed",
565579
messageBatchReference,
566580
notifyMessageId: undefined
567581
}))

packages/nhsNotifyLambda/tests/testUtils.test.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ import {constructMessage, constructPSUDataItemMessage, mockSQSClient} from "./te
1111
const {mockSend: sqsMockSend} = mockSQSClient()
1212

1313
const TEST_URL = "https://example.com"
14-
const mockGetParameter = jest.fn().mockImplementation((name) => {
15-
if (name === "NOTIFY_API_BASE_URL_PARAM") {
16-
return TEST_URL
14+
const mockGetParametersByName = jest.fn(async () => Promise.resolve(
15+
{
16+
[process.env.NOTIFY_API_BASE_URL_PARAM!]: TEST_URL,
17+
[process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM!]: "true"
1718
}
18-
return name
19-
})
19+
))
20+
2021
jest.unstable_mockModule(
2122
"@aws-lambda-powertools/parameters/ssm",
2223
async () => ({
2324
__esModule: true,
24-
getParameter: mockGetParameter
25+
SSMProvider: jest.fn().mockImplementation(() => ({
26+
getParametersByName: mockGetParametersByName
27+
}))
2528
})
2629
)
2730

@@ -515,14 +518,14 @@ describe("NHS notify lambda helper functions", () => {
515518
expect(result).toHaveLength(2)
516519
expect(result[0]).toMatchObject({
517520
PSUDataItem: data[0].PSUDataItem,
518-
success: true,
521+
deliveryStatus: "requested",
519522
notifyMessageId: "msg-id-1",
520523
messageBatchReference: expect.any(String),
521524
messageReference: expect.any(String)
522525
})
523526
expect(result[1]).toMatchObject({
524527
PSUDataItem: data[1].PSUDataItem,
525-
success: false,
528+
deliveryStatus: "notify request failed",
526529
notifyMessageId: undefined,
527530
messageBatchReference: expect.any(String),
528531
messageReference: expect.any(String)
@@ -555,7 +558,7 @@ describe("NHS notify lambda helper functions", () => {
555558
expect(result).toMatchObject([
556559
{
557560
PSUDataItem: data[0].PSUDataItem,
558-
success: false,
561+
deliveryStatus: "notify request failed",
559562
notifyMessageId: undefined,
560563
messageBatchReference: expect.any(String),
561564
messageReference: expect.any(String)
@@ -603,7 +606,10 @@ describe("NHS notify lambda helper functions", () => {
603606
expect(result).toHaveLength(2)
604607
result.forEach((r) =>
605608
expect(r).toEqual(
606-
expect.objectContaining({success: false, notifyMessageId: undefined})
609+
expect.objectContaining({
610+
deliveryStatus: "notify request failed",
611+
notifyMessageId: undefined
612+
})
607613
)
608614
)
609615
expect(errorSpy).toHaveBeenCalledWith(
@@ -704,7 +710,12 @@ describe("NHS notify lambda helper functions", () => {
704710
})
705711

706712
it("uses a dummy call when the MAKE_REAL_NOTIFY_REQUESTS_PARAM is false", async () => {
707-
process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM = "false"
713+
mockGetParametersByName.mockImplementation(async () => Promise.resolve(
714+
{
715+
[process.env.NOTIFY_API_BASE_URL_PARAM!]: TEST_URL,
716+
[process.env.MAKE_REAL_NOTIFY_REQUESTS_PARAM!]: "false"
717+
}
718+
))
708719
const {makeBatchNotifyRequest: fn} = await import("../src/utils")
709720

710721
const data = [
@@ -743,14 +754,14 @@ describe("NHS notify lambda helper functions", () => {
743754
expect(result).toHaveLength(2)
744755
expect(result[0]).toMatchObject({
745756
PSUDataItem: data[0].PSUDataItem,
746-
success: true,
757+
deliveryStatus: "silent running",
747758
notifyMessageId: expect.any(String), // it will be assigned a dummy ID
748759
messageBatchReference: expect.any(String),
749760
messageReference: expect.any(String)
750761
})
751762
expect(result[1]).toMatchObject({
752763
PSUDataItem: data[1].PSUDataItem,
753-
success: true,
764+
deliveryStatus: "silent running",
754765
notifyMessageId: expect.any(String),
755766
messageBatchReference: expect.any(String),
756767
messageReference: expect.any(String)

packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {APIGatewayProxyEvent, APIGatewayProxyResult} from "aws-lambda"
33
import {Logger} from "@aws-lambda-powertools/logger"
44
import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware"
55
import {TransactionCanceledException} from "@aws-sdk/client-dynamodb"
6-
import {getParameter} from "@aws-lambda-powertools/parameters/ssm"
6+
import {SSMProvider} from "@aws-lambda-powertools/parameters/ssm"
77

88
import middy from "@middy/core"
99
import inputOutputLogger from "@middy/input-output-logger"
@@ -45,7 +45,22 @@ export const TEST_PRESCRIPTIONS_1 = (process.env.TEST_PRESCRIPTIONS_1 ?? "")
4545
export const TEST_PRESCRIPTIONS_2 = (process.env.TEST_PRESCRIPTIONS_2 ?? "")
4646
.split(",").map(item => item.trim()) || []
4747

48-
const ENABLE_NOTIFICATIONS_PARAM = process.env.ENABLE_NOTIFICATIONS_PARAM
48+
// Fetching the parameters from SSM using a dedicated provider, so that the values can be cached
49+
// and reused across invocations, reducing the number of calls to SSM.
50+
// (it was failing load tests using getParameter directly)
51+
const ssm = new SSMProvider()
52+
const paramNames = {
53+
[process.env.ENABLE_NOTIFICATIONS_PARAM!]: {maxAge: 60}
54+
}
55+
const configPromise = ssm.getParametersByName(paramNames)
56+
57+
async function loadConfig() {
58+
const all = await configPromise
59+
console.log(all)
60+
return {
61+
enableNotifications: all[process.env.ENABLE_NOTIFICATIONS_PARAM!] === "true"
62+
}
63+
}
4964

5065
const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
5166
logger.appendKeys({
@@ -66,6 +81,8 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPro
6681
"x-request-id": xRequestID
6782
})
6883

84+
const paramPromise = loadConfig()
85+
6986
const requestBody = event.body
7087
const requestBundle = castEventBody(requestBody, responseEntries)
7188
if (!requestBundle) {
@@ -115,16 +132,17 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPro
115132

116133
await logTransitions(dataItems)
117134

118-
// This prescription update is valid and all seems alright, so request for a notification
119-
let enableNotifications: boolean = false
135+
// Await the parameter promise before we continue
136+
let enableNotificationsFlag = false
120137
try {
121-
if (ENABLE_NOTIFICATIONS_PARAM) enableNotifications = await getParameter(ENABLE_NOTIFICATIONS_PARAM) === "true"
122-
} catch {
123-
logger.error("Failed to fetch ENABLE_NOTIFICATIONS_PARAM. Defaulting to false.", {ENABLE_NOTIFICATIONS_PARAM})
138+
const {enableNotifications} = await paramPromise
139+
enableNotificationsFlag = enableNotifications
140+
} catch (err) {
141+
logger.error("Failed to load parameters from SSM", {err})
124142
}
125143

126144
let created_messageIds: Array<string> = []
127-
if (enableNotifications) {
145+
if (enableNotificationsFlag) {
128146
try {
129147
const requestId = event.headers["x-request-id"] ?? "x-request-id-not-found"
130148
created_messageIds = await pushPrescriptionToNotificationSQS(requestId, dataItems, logger)
@@ -135,7 +153,7 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayPro
135153
} else {
136154
logger.info(
137155
"enableNotifications is not true, skipping the notification request.",
138-
{enableNotifications}
156+
{enableNotificationsFlag}
139157
)
140158
}
141159

packages/updatePrescriptionStatus/src/utils/sqsClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,15 @@ export async function pushPrescriptionToNotificationSQS(
176176
Entries: batch
177177
})
178178
const result = await sqs.send(command)
179-
if (result.Successful && result.Successful.length) {
179+
if (result.Successful?.length) {
180180
logger.info("Successfully sent a batch of prescriptions to the notifications SQS", {result})
181181

182182
// For each successful message, get its message ID. I don't think there will ever be undefined
183183
// actually in here, but the typing suggests that there could be so filter those out
184184
out.push(...result.Successful.map(e => e.MessageId).filter(msg_id => msg_id !== undefined))
185185
}
186186
// Some may succeed, and some may fail. So check for both
187-
if (result.Failed && result.Failed.length) {
187+
if (result.Failed?.length) {
188188
throw new Error("Failed to send a batch of prescriptions to the notifications SQS")
189189
}
190190
} catch (error) {

0 commit comments

Comments
 (0)