Skip to content

Commit aaf959e

Browse files
authored
Change membership caching behavior (#189)
- Store membership cache in Redis. - Cache all internal membership statuses for 12 hours. - Cache all external membership statuses for 1 minute. - Refresh cache on provisioning (in SQS handler).
1 parent 636a1a1 commit aaf959e

File tree

3 files changed

+96
-45
lines changed

3 files changed

+96
-45
lines changed

src/api/functions/membership.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { EntraGroupError } from "common/errors/index.js";
1616
import { EntraGroupActions } from "common/types/iam.js";
1717
import { pollUntilNoError } from "./general.js";
1818

19+
export const MEMBER_CACHE_SECONDS = 43200; // 12 hours
20+
1921
export async function checkExternalMembership(
2022
netId: string,
2123
list: string,

src/api/routes/membership.ts

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
checkPaidMembershipFromEntra,
44
checkPaidMembershipFromTable,
55
setPaidMembershipInTable,
6+
MEMBER_CACHE_SECONDS,
67
} from "api/functions/membership.js";
78
import { validateNetId } from "api/functions/validation.js";
89
import { FastifyPluginAsync } from "fastify";
@@ -26,9 +27,7 @@ import rawbody from "fastify-raw-body";
2627
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
2728
import { z } from "zod";
2829
import { withTags } from "api/components/index.js";
29-
30-
const NONMEMBER_CACHE_SECONDS = 60; // 1 minute
31-
const MEMBER_CACHE_SECONDS = 43200; // 12 hours
30+
import { getKey, setKey } from "api/functions/redisCache.js";
3231

3332
const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
3433
await fastify.register(rawbody, {
@@ -89,7 +88,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
8988
},
9089
async (request, reply) => {
9190
const netId = request.params.netId.toLowerCase();
92-
if (fastify.nodeCache.get(`isMember_${netId}`) === true) {
91+
const cacheKey = `membership:${netId}:acmpaid`;
92+
const result = await getKey<{ isMember: boolean }>({
93+
redisClient: fastify.redisClient,
94+
key: cacheKey,
95+
logger: request.log,
96+
});
97+
if (result && result.isMember) {
9398
throw new ValidationError({
9499
message: `${netId} is already a paid member!`,
95100
});
@@ -99,11 +104,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
99104
fastify.dynamoClient,
100105
);
101106
if (isDynamoMember) {
102-
fastify.nodeCache.set(
103-
`isMember_${netId}`,
104-
true,
105-
MEMBER_CACHE_SECONDS,
106-
);
107+
await setKey({
108+
redisClient: fastify.redisClient,
109+
key: cacheKey,
110+
data: JSON.stringify({ isMember: true }),
111+
expiresIn: MEMBER_CACHE_SECONDS,
112+
logger: request.log,
113+
});
107114
throw new ValidationError({
108115
message: `${netId} is already a paid member!`,
109116
});
@@ -121,11 +128,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
121128
paidMemberGroup,
122129
);
123130
if (isAadMember) {
124-
fastify.nodeCache.set(
125-
`isMember_${netId}`,
126-
true,
127-
MEMBER_CACHE_SECONDS,
128-
);
131+
await setKey({
132+
redisClient: fastify.redisClient,
133+
key: cacheKey,
134+
data: JSON.stringify({ isMember: true }),
135+
expiresIn: MEMBER_CACHE_SECONDS,
136+
logger: request.log,
137+
});
129138
reply
130139
.header("X-ACM-Data-Source", "aad")
131140
.send({ netId, isPaidMember: true });
@@ -134,11 +143,14 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
134143
message: `${netId} is already a paid member!`,
135144
});
136145
}
137-
fastify.nodeCache.set(
138-
`isMember_${netId}`,
139-
false,
140-
NONMEMBER_CACHE_SECONDS,
141-
);
146+
// Once the caller becomes a member, the stripe webhook will handle changing this to true
147+
await setKey({
148+
redisClient: fastify.redisClient,
149+
key: cacheKey,
150+
data: JSON.stringify({ isMember: false }),
151+
expiresIn: MEMBER_CACHE_SECONDS,
152+
logger: request.log,
153+
});
142154
const secretApiConfig =
143155
(await getSecretValue(
144156
fastify.secretsManagerClient,
@@ -190,11 +202,19 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
190202
async (request, reply) => {
191203
const netId = request.params.netId.toLowerCase();
192204
const list = request.query.list || "acmpaid";
193-
if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) {
205+
// we don't control external list as its direct upload in Dynamo, cache only for 60 seconds.
206+
const ourCacheSeconds = list === "acmpaid" ? MEMBER_CACHE_SECONDS : 60;
207+
const cacheKey = `membership:${netId}:${list}`;
208+
const result = await getKey<{ isMember: boolean }>({
209+
redisClient: fastify.redisClient,
210+
key: cacheKey,
211+
logger: request.log,
212+
});
213+
if (result) {
194214
return reply.header("X-ACM-Data-Source", "cache").send({
195215
netId,
196216
list: list === "acmpaid" ? undefined : list,
197-
isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`),
217+
isPaidMember: result.isMember,
198218
});
199219
}
200220
if (list !== "acmpaid") {
@@ -203,11 +223,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
203223
list,
204224
fastify.dynamoClient,
205225
);
206-
fastify.nodeCache.set(
207-
`isMember_${netId}_${list}`,
208-
isMember,
209-
MEMBER_CACHE_SECONDS,
210-
);
226+
await setKey({
227+
redisClient: fastify.redisClient,
228+
key: cacheKey,
229+
data: JSON.stringify({ isMember }),
230+
expiresIn: ourCacheSeconds,
231+
logger: request.log,
232+
});
211233
return reply.header("X-ACM-Data-Source", "dynamo").send({
212234
netId,
213235
list,
@@ -219,11 +241,13 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
219241
fastify.dynamoClient,
220242
);
221243
if (isDynamoMember) {
222-
fastify.nodeCache.set(
223-
`isMember_${netId}_${list}`,
224-
true,
225-
MEMBER_CACHE_SECONDS,
226-
);
244+
await setKey({
245+
redisClient: fastify.redisClient,
246+
key: cacheKey,
247+
data: JSON.stringify({ isMember: true }),
248+
expiresIn: ourCacheSeconds,
249+
logger: request.log,
250+
});
227251
return reply
228252
.header("X-ACM-Data-Source", "dynamo")
229253
.send({ netId, isPaidMember: true });
@@ -241,22 +265,26 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
241265
paidMemberGroup,
242266
);
243267
if (isAadMember) {
244-
fastify.nodeCache.set(
245-
`isMember_${netId}_${list}`,
246-
true,
247-
MEMBER_CACHE_SECONDS,
248-
);
268+
await setKey({
269+
redisClient: fastify.redisClient,
270+
key: cacheKey,
271+
data: JSON.stringify({ isMember: true }),
272+
expiresIn: ourCacheSeconds,
273+
logger: request.log,
274+
});
249275
reply
250276
.header("X-ACM-Data-Source", "aad")
251277
.send({ netId, isPaidMember: true });
252278
await setPaidMembershipInTable(netId, fastify.dynamoClient);
253279
return;
254280
}
255-
fastify.nodeCache.set(
256-
`isMember_${netId}_${list}`,
257-
false,
258-
NONMEMBER_CACHE_SECONDS,
259-
);
281+
await setKey({
282+
redisClient: fastify.redisClient,
283+
key: cacheKey,
284+
data: JSON.stringify({ isMember: false }),
285+
expiresIn: ourCacheSeconds,
286+
logger: request.log,
287+
});
260288
return reply
261289
.header("X-ACM-Data-Source", "aad")
262290
.send({ netId, isPaidMember: false });
@@ -315,6 +343,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
315343
) {
316344
const customerEmail = event.data.object.customer_email;
317345
if (!customerEmail) {
346+
request.log.info("No customer email found.");
318347
return reply
319348
.code(200)
320349
.send({ handled: false, requestId: request.id });

src/api/sqs/handlers/provisionNewMember.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
22
import { currentEnvironmentConfig, SQSHandlerFunction } from "../index.js";
33
import { getEntraIdToken } from "../../../api/functions/entraId.js";
4-
import { genericConfig } from "../../../common/config.js";
4+
import { genericConfig, SecretConfig } from "../../../common/config.js";
55

6-
import { setPaidMembership } from "api/functions/membership.js";
6+
import {
7+
MEMBER_CACHE_SECONDS,
8+
setPaidMembership,
9+
} from "api/functions/membership.js";
710
import { createAuditLogEntry } from "api/functions/auditLog.js";
811
import { Modules } from "common/modules.js";
9-
import { getAuthorizedClients } from "../utils.js";
12+
import { getAuthorizedClients, getSecretConfig } from "../utils.js";
1013
import { emailMembershipPassHandler } from "./emailMembershipPassHandler.js";
14+
import RedisModule from "ioredis";
15+
import { setKey } from "api/functions/redisCache.js";
1116

1217
export const provisionNewMemberHandler: SQSHandlerFunction<
1318
AvailableSQSFunctions.ProvisionNewMember
@@ -21,9 +26,16 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
2126
secretName: genericConfig.EntraSecretName,
2227
logger,
2328
});
29+
const secretConfig: SecretConfig = await getSecretConfig({
30+
logger,
31+
commonConfig,
32+
});
33+
const redisClient = new RedisModule.default(secretConfig.redis_url);
34+
const netId = email.replace("@illinois.edu", "");
35+
const cacheKey = `membership:${netId}:acmpaid`;
2436
logger.info("Got authorized clients and Entra ID token.");
2537
const { updated } = await setPaidMembership({
26-
netId: email.replace("@illinois.edu", ""),
38+
netId,
2739
dynamoClient: clients.dynamoClient,
2840
entraToken,
2941
paidMemberGroup: currentEnvironmentConfig.PaidMemberGroupId,
@@ -45,4 +57,12 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
4557
} else {
4658
logger.info(`${email} was already a paid member.`);
4759
}
60+
logger.info("Setting membership in Redis.");
61+
await setKey({
62+
redisClient,
63+
key: cacheKey,
64+
data: JSON.stringify({ isMember: true }),
65+
expiresIn: MEMBER_CACHE_SECONDS,
66+
logger,
67+
});
4868
};

0 commit comments

Comments
 (0)