Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/lib/features/project/project-read-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ProjectReadModel implements IProjectReadModel {
this.db = db;
this.timer = (action) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'project',
store: 'project-read-model',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to differentiate this from the project store that also has a similar query

action,
});
this.flagResolver = flagResolver;
Expand Down
118 changes: 108 additions & 10 deletions src/lib/features/project/project-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { subDays } from 'date-fns';
import { subDays, secondsToMilliseconds } from 'date-fns';
import joi from 'joi';
const { ValidationError } = joi;
import createSlug from 'slug';
Expand Down Expand Up @@ -86,7 +86,31 @@
import { batchExecute } from '../../util/index.js';
import metricsHelper from '../../util/metrics-helper.js';
import { FUNCTION_TIME } from '../../metric-events.js';
import memoizee from 'memoizee';
import {
PROJECT_ACCESS_ADDED,
PROJECT_ACCESS_GROUP_ROLES_DELETED,
PROJECT_ACCESS_GROUP_ROLES_UPDATED,
PROJECT_ACCESS_UPDATED,
PROJECT_ACCESS_USER_ROLES_DELETED,
PROJECT_ACCESS_USER_ROLES_UPDATED,
PROJECT_ARCHIVED,
PROJECT_CREATED,
PROJECT_DELETED,
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
PROJECT_FAVORITED,
PROJECT_GROUP_ADDED,
PROJECT_IMPORT,
PROJECT_REVIVED,
PROJECT_UNFAVORITED,
PROJECT_UPDATED,
PROJECT_USER_ADDED,
PROJECT_USER_REMOVED,
PROJECT_USER_ROLE_CHANGED,
} from '../../events/index.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
import type { Logger } from '../../logger.js';

type Days = number;
type Count = number;
Expand Down Expand Up @@ -126,7 +150,7 @@

private groupService: GroupService;

private logger: any;
private logger: Logger;

private featureToggleService: FeatureToggleService;

Expand Down Expand Up @@ -156,6 +180,40 @@

private timer: Function;

private getProjectsForAdminUiCached: ((
query?: IProjectQuery & IProjectsQuery,
userId?: number,
) => Promise<ProjectForUi[]>) &
memoizee.Memoized<
(
query?: IProjectQuery & IProjectsQuery,
userId?: number,
) => Promise<ProjectForUi[]>
>;

private readonly projectCacheInvalidationEvents = [
PROJECT_CREATED,
PROJECT_UPDATED,
PROJECT_DELETED,
PROJECT_ARCHIVED,
PROJECT_REVIVED,
PROJECT_IMPORT,
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
PROJECT_USER_ADDED,
PROJECT_USER_REMOVED,
PROJECT_USER_ROLE_CHANGED,
PROJECT_GROUP_ADDED,
PROJECT_FAVORITED,
PROJECT_UNFAVORITED,
PROJECT_ACCESS_ADDED,
PROJECT_ACCESS_UPDATED,
PROJECT_ACCESS_USER_ROLES_UPDATED,
PROJECT_ACCESS_USER_ROLES_DELETED,
PROJECT_ACCESS_GROUP_ROLES_UPDATED,
PROJECT_ACCESS_GROUP_ROLES_DELETED,
];

constructor(
{
projectStore,
Expand Down Expand Up @@ -221,16 +279,39 @@
className: 'ProjectService',
functionName,
});
const cacheTtl = secondsToMilliseconds(60);
this.getProjectsForAdminUiCached = memoizee(
(
projectsQuery?: IProjectQuery & IProjectsQuery,
projectsUserId?: number,
) =>
this.projectReadModel.getProjectsForAdminUi(
projectsQuery,
projectsUserId,
),
{
promise: true,
maxAge: cacheTtl,
normalizer: ([projectsQuery, projectsUserId]) =>
this.buildProjectsCacheKey(
projectsQuery as
| (IProjectQuery & IProjectsQuery)
| undefined,
projectsUserId as number | undefined,
),
},
);
this.registerProjectCacheInvalidationListeners();
}

async getProjects(
query?: IProjectQuery & IProjectsQuery,
userId?: number,
): Promise<ProjectForUi[]> {
const projects = await this.projectReadModel.getProjectsForAdminUi(
query,
userId,
);
const useCache = this.flagResolver.isEnabled('project-admin-cache');
const projects = useCache
? await this.getProjectsForAdminUiCached(query, userId)
: await this.projectReadModel.getProjectsForAdminUi(query, userId);

if (userId) {
const projectAccess =
Expand All @@ -249,6 +330,27 @@
return projects;
}

private buildProjectsCacheKey(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this will work but it feels a bit dirty to be using big ass JSON blobs as hash keys, we can't just do this?

private buildProjectsCacheKey(
    query?: IProjectQuery & IProjectsQuery,
    userId?: number,
): string {
    const hash = createHash('sha256');
    if (query?.ids) {
        const sorted = [...query.ids].sort();
        hash.update(sorted.join(','));
    }
    hash.update(String(userId ?? ''));
    hash.update(String(query?.id ?? ''));
    hash.update(String(query?.archived ?? ''));
    return hash.digest('base64url');
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we could just concatenate strings... hsould be better, will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I like your approach

query?: IProjectQuery & IProjectsQuery,
userId?: number,
): string {
const ids = query?.ids === undefined ? null : [...query.ids].sort();
return JSON.stringify({
userId: userId ?? null,
id: query?.id ?? null,
archived:
typeof query?.archived === 'undefined' ? null : query.archived,
ids,
});
}

private registerProjectCacheInvalidationListeners(): void {
const invalidate = () => this.getProjectsForAdminUiCached.clear();
this.projectCacheInvalidationEvents.forEach((eventName) => {
this.eventBus.on(eventName, invalidate);

Check failure on line 350 in src/lib/features/project/project-service.ts

View workflow job for this annotation

GitHub Actions / build

src/lib/features/project/project-service.limit.test.ts > Should not allow to exceed project limit on revive

TypeError: this.eventBus.on is not a function ❯ src/lib/features/project/project-service.ts:350:27 ❯ ProjectService.registerProjectCacheInvalidationListeners src/lib/features/project/project-service.ts:349:45 ❯ new ProjectService src/lib/features/project/project-service.ts:304:14 ❯ createFakeProjectService src/lib/features/project/createProjectService.ts:210:28 ❯ src/lib/features/project/project-service.limit.test.ts:39:32

Check failure on line 350 in src/lib/features/project/project-service.ts

View workflow job for this annotation

GitHub Actions / build

src/lib/features/project/project-service.limit.test.ts > Should not allow to exceed project limit on create

TypeError: this.eventBus.on is not a function ❯ src/lib/features/project/project-service.ts:350:27 ❯ ProjectService.registerProjectCacheInvalidationListeners src/lib/features/project/project-service.ts:349:45 ❯ new ProjectService src/lib/features/project/project-service.ts:304:14 ❯ createFakeProjectService src/lib/features/project/createProjectService.ts:210:28 ❯ src/lib/features/project/project-service.limit.test.ts:18:32
});
}

async addOwnersToProjects(
projects: ProjectForUi[],
): Promise<ProjectForUi[]> {
Expand Down Expand Up @@ -1096,10 +1198,6 @@
);
}

async getMembers(projectId: string): Promise<number> {
return this.projectStore.getMembersCountByProject(projectId);
}

async getProjectUsers(
projectId: string,
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export type IFlagKey =
| 'milestoneProgression'
| 'featureReleasePlans'
| 'plausibleMetrics'
| 'safeguards';
| 'safeguards'
| 'project-admin-cache';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;

Expand Down
Loading