Skip to content

Commit

Permalink
Merge pull request #837 from digirati-co-uk/feature/cache-project
Browse files Browse the repository at this point in the history
Cache project
  • Loading branch information
stephenwf authored Nov 27, 2024
2 parents 22068fd + 1bf3ce0 commit 879746e
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 15 deletions.
12 changes: 8 additions & 4 deletions services/madoc-ts/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import cookieParser from 'cookie-parser';
import schedule from 'node-schedule';
import passport from 'koa-passport';
import { WebhookServerExtension } from './webhooks/webhook-server-extension';
import { slowRequests } from './middleware/slow-requests';

const { readFile, readdir } = promises;

Expand Down Expand Up @@ -124,17 +125,20 @@ export async function createApp(config: ExternalConfig, env: EnvConfig) {
}

if (enabled) {
passport.serializeUser(function(user: any, done) {
passport.serializeUser(function (user: any, done) {
done(null, user);
});

passport.deserializeUser(function(user: any, done) {
passport.deserializeUser(function (user: any, done) {
done(null, user);
});

app.use(passport.initialize());
}

if (!env.flags.disable_slow_request_tracking) {
app.use(slowRequests);
}
app.use(errorHandler);
app.use(staticPage);
app.use(setJwt);
Expand All @@ -147,7 +151,7 @@ export async function createApp(config: ExternalConfig, env: EnvConfig) {
(app.context.cron as CronJobs).addJob(
'check-expired-manifests',
'Check expired manifests',
schedule.scheduleJob('*/15 * * * *', async function(fireDate) {
schedule.scheduleJob('*/15 * * * *', async function (fireDate) {
await pool.connect(async connection => {
await checkExpiredManifests({ ...app.context, connection } as any, fireDate);
});
Expand All @@ -157,7 +161,7 @@ export async function createApp(config: ExternalConfig, env: EnvConfig) {
(app.context.cron as CronJobs).addJob(
'restart-queue',
'Restart queue 3am once a day',
schedule.scheduleJob('0 3 * * *', async function() {
schedule.scheduleJob('0 3 * * *', async function () {
await bounceQueue();
})
);
Expand Down
1 change: 1 addition & 0 deletions services/madoc-ts/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const config: EnvConfig = {
},
flags: {
capture_model_api_migrated: castBool(process.env.CAPTURE_MODEL_API_MIGRATED, false),
disable_slow_request_tracking: castBool(process.env.DISABLE_SLOW_REQUEST_TRACKING, false),
},
postgres: {
host: process.env.DATABASE_HOST as string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { createUniversalComponent } from '../../../shared/utility/create-univers
import { HrefLink } from '../../../shared/utility/href-link';
import { UniversalComponent } from '../../../types';
import { AdminHeader } from '../../molecules/AdminHeader';
import { SlowRequests } from '../../../../middleware/slow-requests';
import { TableContainer, TableRow, TableRowLabel } from '../../../shared/layout/Table';

type SystemStatusType = {
data: { list: Pm2Status[]; build: EnvConfig['build'] };
data: { list: Pm2Status[]; build: EnvConfig['build']; slowRequests: SlowRequests };
query: unknown;
params: unknown;
variables: unknown;
Expand Down Expand Up @@ -70,6 +72,8 @@ export const SystemStatus: UniversalComponent<SystemStatusType> = createUniversa
)
: { memory: 0, cpu: 0 };

const slowestRequests = Object.values(data?.slowRequests || {}).sort((a, b) => b.slowest - a.slowest);

return (
<>
<AdminHeader title={t('System status')} breadcrumbs={[{ label: 'Site admin', link: '/' }]} />
Expand Down Expand Up @@ -250,6 +254,28 @@ export const SystemStatus: UniversalComponent<SystemStatusType> = createUniversa
})
: null}
</div>

<div>
<h3>Slow requests</h3>
<table className="p-2 w-full">
<thead>
<tr>
<th>Route</th>
<th>Count</th>
<th>Average time</th>
<th>Max time</th>
</tr>
</thead>
{slowestRequests.map(slow => (
<tr key={slow.key}>
<td className="p-2">{slow.key}</td>
<td className="p-2">{slow.count}</td>
<td className="p-2">{(slow.avg / 1000).toFixed(2)}s</td>
<td className="p-2">{(slow.slowest / 1000).toFixed(2)}s</td>
</tr>
))}
</table>
</div>
</WidePage>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export function serverRendererFor<TVariables = any, Data = any>(
getData?: (key: string, vars: TVariables, api: ApiClient, pathname: string) => Promise<Data>;
hooks?: AdditionalHooks[];
theme?: { name: string } & Partial<MadocTheme>;
noSsr?: boolean;
}
) {
(component as any).noSsr = config.noSsr;
(component as any).getKey = config.getKey;
(component as any).getData = config.getData;
(component as any).hooks = config.hooks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function createServerRenderer(
routeContext.project = match.params.slug ? match.params.slug : undefined;
}

if (route.component && route.component.getKey && route.component.getData) {
if (route.component && route.component.getKey && route.component.getData && route.component.noSsr !== true) {
requests.push(
prefetchCache.prefetchQuery(
route.component.getKey(match.params, queryString, path),
Expand Down Expand Up @@ -362,7 +362,7 @@ export function createServerRenderer(
: `
<script>document.body.classList.add('dev-loading');</script>
<style>
body > * {
body > * {
transition: opacity 200ms;
}
.dev-loading > * {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function createUniversalComponent<
Component: React.FC,
options: {
getKey?: GetKey;
noSsr?: boolean;
getData?: GetData;
hooks?: AdditionalHooks[];
}
Expand All @@ -26,5 +27,6 @@ export function createUniversalComponent<
ReturnComponent.getKey = options.getKey;
ReturnComponent.getData = options.getData;
ReturnComponent.hooks = options.hooks;
ReturnComponent.noSsr = options.noSsr;
return ReturnComponent;
}
8 changes: 7 additions & 1 deletion services/madoc-ts/src/frontend/site/pages/all-tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export const AllTasks: UniversalComponent<AllTasksType> = createUniversalCompone
const user = useUser();
const isAdmin = user && user.scope && user.scope.indexOf('site.admin') !== -1;
const isReviewer = isAdmin || (user && user.scope && user.scope.indexOf('tasks.create') !== -1);
const { data: pages, fetchMore, canFetchMore, isFetchingMore } = useInfiniteData(AllTasks, undefined, {
const {
data: pages,
fetchMore,
canFetchMore,
isFetchingMore,
} = useInfiniteData(AllTasks, undefined, {
getFetchMore: lastPage => {
if (lastPage.pagination.totalPages === 0 || lastPage.pagination.totalPages === lastPage.pagination.page) {
return undefined;
Expand Down Expand Up @@ -172,6 +177,7 @@ export const AllTasks: UniversalComponent<AllTasksType> = createUniversalCompone
);
},
{
noSsr: true,
getKey: (params, { preview, ...query }) => {
return ['all-tasks', { query, projectSlug: params.slug }];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ function SingleReviewTableRow({
}

serverRendererFor(ReviewListingPage, {
noSsr: true,
getKey: (params, { preview, ...query }) => {
return ['all-review-tasks', { query, projectSlug: params.slug, page: query.page || 1 }];
},
Expand Down
2 changes: 1 addition & 1 deletion services/madoc-ts/src/gateway/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export class ApiClient {
}

async getPm2Status() {
return this.request<{ list: Pm2Status[]; build: any }>(`/api/madoc/pm2/list`);
return this.request<{ list: Pm2Status[]; build: any; slowRequests: any }>(`/api/madoc/pm2/list`);
}

async pm2Restart(service: 'auth' | 'queue' | 'madoc' | 'scheduler') {
Expand Down
45 changes: 45 additions & 0 deletions services/madoc-ts/src/middleware/slow-requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RouteMiddleware } from '../types/route-middleware';

export type SlowRequests = Record<
string,
{
key: string;
count: number;
avg: number;
slowest: number;
}
>;

const slowRequestStore: SlowRequests = {};

export const slowRequests: RouteMiddleware<{ slug: string }> = async (context, next) => {
const requestStart = Date.now();
await next();

const routeKey = `${context.method} ${context._matchedRoute}`;

const requestEnd = Date.now();
const requestTime = requestEnd - requestStart;
if (!slowRequestStore[routeKey]) {
slowRequestStore[routeKey] = { key: routeKey, count: 0, avg: 0, slowest: 0 };
}

const existinRoute = slowRequestStore[routeKey];

existinRoute.count++;
existinRoute.avg = (existinRoute.avg * (existinRoute.count - 1) + requestTime) / existinRoute.count;
existinRoute.slowest = Math.max(existinRoute.slowest, requestTime);

if (Object.keys(slowRequestStore).length > 50) {
// Sort by slowest.
const sorted = Object.values(slowRequestStore).sort((a, b) => b.slowest - a.slowest);
const slowest = sorted.pop();
if (slowest && slowest.key) {
delete slowRequestStore[slowest.key];
}
}
};

export function getSlowRequests() {
return slowRequestStore;
}
2 changes: 2 additions & 0 deletions services/madoc-ts/src/routes/admin/pm2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import pm2, { ProcessDescription } from 'pm2';
import { config } from '../../config';
import { RouteMiddleware } from '../../types/route-middleware';
import { onlyGlobalAdmin } from '../../utility/user-with-scope';
import { getSlowRequests } from '../../middleware/slow-requests';

async function pm2Connect() {
await new Promise<void>((resolve, reject) =>
Expand Down Expand Up @@ -66,6 +67,7 @@ export const pm2Status: RouteMiddleware = async context => {
uptime: (item as any)?.pm2_env?.pm_uptime,
};
}),
slowRequests: getSlowRequests(),
};

pm2.disconnect();
Expand Down
22 changes: 16 additions & 6 deletions services/madoc-ts/src/routes/projects/get-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { api } from '../../gateway/api.server';
import { RouteMiddleware } from '../../types/route-middleware';
import { ProjectConfiguration } from '../../types/schemas/project-configuration';
import { cachePromise, cachePromiseSWR } from '../../utility/cache-helper';
import { castBool } from '../../utility/cast-bool';
import { NotFound } from '../../utility/errors/not-found';
import { parseProjectId } from '../../utility/parse-project-id';
Expand Down Expand Up @@ -33,13 +34,22 @@ export const getProject: RouteMiddleware<{ id: string }> = async context => {

const [content, taskStats, projectConfiguration, annotationStyles] = await Promise.all([
userApi.getCollectionStatistics(project.collection_id),
userApi.getTaskStats(project.task_id, {
type: 'crowdsourcing-task',
root: true,
distinct_subjects: true,
}),
cachePromiseSWR(
`task-stats:${project.task_id}`,
() =>
userApi.getTaskStats(project.task_id, {
type: 'crowdsourcing-task',
root: true,
distinct_subjects: true,
}),
1000 * 60 * 15 // 15 minutes.
),
userApi.getConfiguration<ProjectConfiguration>('madoc', [siteUrn, `urn:madoc:project:${project.id}`]),
context.annotationStyles.getProjectAnnotationStyle(project.id, siteId),
cachePromiseSWR(
`annotation-style:${project.id}`,
() => context.annotationStyles.getProjectAnnotationStyle(project.id, siteId),
1000 * 60 * 15 // 15 minutes.
),
]);

const taskStatuses = taskStats.statuses || {};
Expand Down
52 changes: 52 additions & 0 deletions services/madoc-ts/src/utility/cache-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import cache from 'memory-cache';

export async function cachePromise<T extends object>(
key: string,
getter: () => Promise<T>,
timeInMs: number
): Promise<T> {
const resource = cache.get(key);
if (resource) {
return resource as T;
}

const result = await getter();
cache.put(key, result, timeInMs);
return result;
}

const swcPromises: Record<string, Promise<any> | null> = {};
const DEFAULT_STALE_TIME = 1000 * 60 * 60 * 8; // 8 hours

export async function cachePromiseSWR<T extends object | null>(
key: string,
getter: () => Promise<T>,
timeInMs: number,
staleTimeInMs: number = DEFAULT_STALE_TIME
): Promise<T> {
if (swcPromises[key]) {
await swcPromises[key];
}
const resource = cache.get(key);
const staleResource = cache.get(`@stale/${key}`);
if (!resource) {
const promise = getter();
swcPromises[key] = promise;
promise.then(result => {
cache.put(key, result, timeInMs);
cache.put(`@stale/${key}`, result, staleTimeInMs);

delete swcPromises[key];

return result;
});

if (staleResource) {
return staleResource;
}

return promise;
}

return resource;
}

0 comments on commit 879746e

Please sign in to comment.