Skip to content

Commit

Permalink
perf: cache stfc user service requests instead of responses (#831)
Browse files Browse the repository at this point in the history
  • Loading branch information
ACLay authored Oct 24, 2024
1 parent b2ebc6c commit 7bf3f37
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 86 deletions.
24 changes: 17 additions & 7 deletions apps/backend/src/auth/StfcUserAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class StfcUserAuthorization extends UserAuthorization {
private static readonly tokenCacheMaxElements = 1000;
private static readonly tokenCacheSecondsToLive = 600; // 10 minutes

private uowsTokenCache = new Cache<boolean>(
private uowsTokenCache = new Cache<Promise<boolean>>(
StfcUserAuthorization.tokenCacheMaxElements,
StfcUserAuthorization.tokenCacheSecondsToLive
).enableStatsLogging('uowsTokenCache');
Expand Down Expand Up @@ -281,15 +281,25 @@ export class StfcUserAuthorization extends UserAuthorization {
}

async isExternalTokenValid(token: string): Promise<boolean> {
const cachedValidity = this.uowsTokenCache.get(token);
if (cachedValidity) {
const cachedValidity = await this.uowsTokenCache.get(token);
if (cachedValidity !== undefined) {
if (!cachedValidity) {
this.uowsTokenCache.remove(token);
}

return cachedValidity;
}

const isValid: boolean = (await client.isTokenValid(token))?.return;
// Only cache valid tokens to avoid locking out users for a long time
if (isValid) {
this.uowsTokenCache.put(token, true);
const tokenRequest: Promise<boolean> = client
.isTokenValid(token)
.then((response) => response?.return);

this.uowsTokenCache.put(token, tokenRequest);

const isValid: boolean = await tokenRequest;
// Only keep valid tokens cached to avoid locking out users for a long time
if (!isValid) {
this.uowsTokenCache.remove(token);
}

return isValid;
Expand Down
2 changes: 0 additions & 2 deletions apps/backend/src/datasources/stfc/StfcProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import { ProposalsFilter } from './../../resolvers/queries/ProposalsQuery';
import PostgresProposalDataSource from './../postgres/ProposalDataSource';
import { StfcUserDataSource } from './StfcUserDataSource';

const stfcUserDataSource = new StfcUserDataSource();

const fieldMap: { [key: string]: string } = {
finalStatus: 'final_status',
callShortCode: 'call_short_code',
Expand Down
187 changes: 110 additions & 77 deletions apps/backend/src/datasources/stfc/StfcUserDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,21 @@ export class StfcUserDataSource implements UserDataSource {
private static readonly rolesCacheMaxElements = 1000;
private static readonly rolesCacheSecondsToLive = 600; //10 minutes

private uowsBasicUserDetailsCache = new Cache<StfcBasicPersonDetails>(
private uowsBasicUserDetailsCache = new Cache<
Promise<StfcBasicPersonDetails | undefined>
>(
StfcUserDataSource.userDetailsCacheMaxElements,
StfcUserDataSource.userDetailsCacheSecondsToLive
).enableStatsLogging('uowsBasicUserDetailsCache');

private uowsSearchableBasicUserDetailsCache =
new Cache<StfcBasicPersonDetails>(
StfcUserDataSource.userDetailsCacheMaxElements,
StfcUserDataSource.userDetailsCacheSecondsToLive
).enableStatsLogging('uowsSearchableBasicUserDetailsCache');
private uowsSearchableBasicUserDetailsCache = new Cache<
Promise<StfcBasicPersonDetails | undefined>
>(
StfcUserDataSource.userDetailsCacheMaxElements,
StfcUserDataSource.userDetailsCacheSecondsToLive
).enableStatsLogging('uowsSearchableBasicUserDetailsCache');

private uowsRolesCache = new Cache<Role[]>(
private uowsRolesCache = new Cache<Promise<Role[]>>(
StfcUserDataSource.rolesCacheMaxElements,
StfcUserDataSource.rolesCacheSecondsToLive
).enableStatsLogging('uowsRolesCache');
Expand All @@ -143,66 +146,92 @@ export class StfcUserDataSource implements UserDataSource {
? this.uowsSearchableBasicUserDetailsCache
: this.uowsBasicUserDetailsCache;

const stfcUsers: StfcBasicPersonDetails[] = [];
const stfcUserRequests: Promise<StfcBasicPersonDetails | undefined>[] = [];
const cacheMisses: string[] = [];

for (const userNumber of userNumbers) {
const cachedUser = cache.get(userNumber);
if (cachedUser) {
stfcUsers.push(cachedUser);
stfcUserRequests.push(cachedUser);
} else {
cacheMisses.push(userNumber);
}
}

if (cacheMisses.length > 0) {
const uowsRequest = searchableOnly
? client.getSearchableBasicPeopleDetailsFromUserNumbers(
token,
cacheMisses
)
: client.getBasicPeopleDetailsFromUserNumbers(token, cacheMisses);
const usersFromUows: StfcBasicPersonDetails[] | null = (await uowsRequest)
?.return;
const uowsRequest: Promise<StfcBasicPersonDetails[] | null> = (
searchableOnly
? client.getSearchableBasicPeopleDetailsFromUserNumbers(
token,
cacheMisses
)
: client.getBasicPeopleDetailsFromUserNumbers(token, cacheMisses)
).then((result) => result?.return);

for (const userNumber of cacheMisses) {
const userRequest = uowsRequest.then((users) =>
users?.find((user) => user.userNumber == userNumber)
);
cache.put(userNumber, userRequest);
stfcUserRequests.push(userRequest);
}

const usersFromUows = await uowsRequest;

if (usersFromUows) {
await this.ensureDummyUsersExist(
usersFromUows.map((stfcUser) => parseInt(stfcUser.userNumber))
);
usersFromUows.map((user) => cache.put(user.userNumber, user));
stfcUsers.push(...usersFromUows);
}
}

const stfcUsers: StfcBasicPersonDetails[] = await Promise.all(
stfcUserRequests
).then((users) =>
users.filter((user): user is StfcBasicPersonDetails => user !== undefined)
);
// Uncache any failed lookups
userNumbers
.filter(
(un) => stfcUsers.find((user) => user.userNumber === un) === undefined
)
.forEach((un) => cache.remove(un));

return stfcUsers;
}

private async getStfcBasicPersonByEmail(
email: string,
searchableOnly?: boolean
): Promise<StfcBasicPersonDetails | null> {
): Promise<StfcBasicPersonDetails | undefined> {
const cache = searchableOnly
? this.uowsSearchableBasicUserDetailsCache
: this.uowsBasicUserDetailsCache;

const cachedUser = cache.get(email);
const cachedUser = await cache.get(email);
if (cachedUser) {
return cachedUser;
}

const uowsRequest = searchableOnly
? client.getSearchableBasicPersonDetailsFromEmail(token, email)
: client.getBasicPersonDetailsFromEmail(token, email);
const stfcUser: StfcBasicPersonDetails | null = (await uowsRequest)?.return;

if (!stfcUser) {
return null;
}
const uowsRequest = (
searchableOnly
? client.getSearchableBasicPersonDetailsFromEmail(token, email)
: client.getBasicPersonDetailsFromEmail(token, email)
)
.then((response) => response?.return)
.then((stfcUser: StfcBasicPersonDetails | null) => {
if (!stfcUser) {
return undefined;
}

return this.ensureDummyUserExists(parseInt(stfcUser.userNumber)).then(
() => stfcUser
);
});

await this.ensureDummyUserExists(parseInt(stfcUser.userNumber));
cache.put(email, stfcUser);
cache.put(email, uowsRequest);

return stfcUser;
return uowsRequest;
}

async delete(id: number): Promise<User | null> {
Expand Down Expand Up @@ -281,53 +310,57 @@ export class StfcUserDataSource implements UserDataSource {
return cachedRoles;
}

const stfcRoles: stfcRole[] | null = (
await client.getRolesForUser(token, id)
)?.return;

const roleDefinitions: Role[] = await this.getRoles();
const userRole: Role | undefined = roleDefinitions.find(
(role) => role.shortCode == Roles.USER
);
if (!userRole) {
return Promise.resolve([]);
}
const stfcRolesRequest = client
.getRolesForUser(token, id)
.then((response): stfcRole[] | null => response?.return)
.then((stfcRoles) =>
this.getRoles().then((roleDefinitions) => {
const userRole: Role | undefined = roleDefinitions.find(
(role) => role.shortCode == Roles.USER
);
if (!userRole) {
return [];
}

if (!stfcRoles || stfcRoles.length == 0) {
return [userRole];
}

/*
* Convert the STFC roles to the Roles enums which refers to roles
* by short code. We will use the short code to filter relevant
* roles.
*/
const userRolesAsEnum: Roles[] = stfcRoles
.flatMap((stfcRole) => stfcRolesToEssRoleDefinitions[stfcRole.name])
.filter((r) => r !== undefined) as Roles[];

/*
* Filter relevant roles by short code.
*/
const userRolesAsRole: Role[] = userRolesAsEnum
.map((r) => roleDefinitions.find((d) => d.shortCode === r))
.filter((r) => r !== undefined) as Role[];

/*
* We can't return non-unique roles.
*/
const uniqueRoles: Role[] = [...new Set(userRolesAsRole)];

uniqueRoles.sort((a, b) => a.id - b.id);

/*
* Prepend the user role as it must be first.
*/
const userRoles = [userRole, ...uniqueRoles];

return userRoles;
})
);

if (!stfcRoles || stfcRoles.length == 0) {
return [userRole];
}
this.uowsRolesCache.put(String(id), stfcRolesRequest);

/*
* Convert the STFC roles to the Roles enums which refers to roles
* by short code. We will use the short code to filter relevant
* roles.
*/
const userRolesAsEnum: Roles[] = stfcRoles
.flatMap((stfcRole) => stfcRolesToEssRoleDefinitions[stfcRole.name])
.filter((r) => r !== undefined) as Roles[];

/*
* Filter relevant roles by short code.
*/
const userRolesAsRole: Role[] = userRolesAsEnum
.map((r) => roleDefinitions.find((d) => d.shortCode === r))
.filter((r) => r !== undefined) as Role[];

/*
* We can't return non-unique roles.
*/
const uniqueRoles: Role[] = [...new Set(userRolesAsRole)];

uniqueRoles.sort((a, b) => a.id - b.id);

/*
* Prepend the user role as it must be first.
*/
const userRoles = [userRole, ...uniqueRoles];

this.uowsRolesCache.put(String(id), userRoles);

return userRoles;
return stfcRolesRequest;
}

async getRoles(): Promise<Role[]> {
Expand Down

0 comments on commit 7bf3f37

Please sign in to comment.