Compare commits

...

3 Commits

Author SHA1 Message Date
Tom Moor c3bcc75e88 Refactor to guard 2024-11-20 17:40:35 -05:00
Tom Moor 92383486dc Handle cannot acquire lock 2024-11-19 22:20:11 -05:00
Tom Moor 37a58ead2e Cache diff generation, closes #7982 2024-11-19 22:11:48 -05:00
5 changed files with 100 additions and 15 deletions
+1
View File
@@ -208,6 +208,7 @@
"react-waypoint": "^10.3.0",
"react-window": "^1.8.10",
"reakit": "^1.3.11",
"redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.2.2",
"refractor": "^3.6.0",
"request-filtering-agent": "^1.1.2",
@@ -7,6 +7,7 @@ import HTMLHelper from "@server/models/helpers/HTMLHelper";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import SubscriptionHelper from "@server/models/helpers/SubscriptionHelper";
import { can } from "@server/policies";
import { CacheHelper } from "@server/utils/CacheHelper";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
@@ -68,21 +69,28 @@ export default class DocumentPublishedOrUpdatedEmail extends BaseEmail<
let body;
if (revisionId && team?.getPreference(TeamPreference.PreviewsInEmails)) {
// generate the diff html for the email
const revision = await Revision.findByPk(revisionId);
body = await CacheHelper.getDataOrSet<string>(
`diff:${revisionId}`,
async () => {
// generate the diff html for the email
const revision = await Revision.findByPk(revisionId);
if (revision) {
const before = await revision.before();
const content = await DocumentHelper.toEmailDiff(before, revision, {
includeTitle: false,
centered: false,
signedUrls: 4 * Day.seconds,
baseUrl: props.teamUrl,
});
if (revision) {
const before = await revision.before();
const content = await DocumentHelper.toEmailDiff(before, revision, {
includeTitle: false,
centered: false,
signedUrls: 4 * Day.seconds,
baseUrl: props.teamUrl,
});
// inline all css so that it works in as many email providers as possible.
body = content ? await HTMLHelper.inlineCSS(content) : undefined;
}
// inline all css so that it works in as many email providers as possible.
return content ? await HTMLHelper.inlineCSS(content) : undefined;
}
return;
},
30
);
}
return {
+49
View File
@@ -1,6 +1,7 @@
import { Day } from "@shared/utils/time";
import Logger from "@server/logging/Logger";
import Redis from "@server/storage/redis";
import { MutexLock } from "./MutexLock";
/**
* A Helper class for server-side cache management
@@ -9,6 +10,54 @@ export class CacheHelper {
// Default expiry time for cache data in seconds
private static defaultDataExpiry = Day.seconds;
/**
* Given a key this method will attempt to get the data from cache store first
* If data is not found, it will call the callback to get the data and save it in cache
* using a distributed lock to prevent multiple writes.
*
* @param key Cache key
* @param callback Callback to get the data if not found in cache
* @param expiry Cache data expiry in seconds
*/
public static async getDataOrSet<T>(
key: string,
callback: () => Promise<T | undefined>,
expiry?: number
): Promise<T | undefined> {
let cache = await this.getData<T>(key);
if (cache) {
return cache;
}
// Nothing in the cache, acquire a lock to prevent multiple writes
let lock;
const lockKey = `lock:${key}`;
try {
try {
lock = await MutexLock.lock.acquire(
[lockKey],
MutexLock.defaultLockTimeout
);
} catch (err) {
Logger.error(`Could not acquire lock for ${key}`, err);
}
cache = await this.getData<T>(key);
if (cache) {
return cache;
}
// Get the data from the callback and save it in cache
const value = await callback();
if (value) {
await this.setData<T>(key, value, expiry);
}
return value;
} finally {
await lock?.release();
}
}
/**
* Given a key, gets the data from cache store
*
+20
View File
@@ -0,0 +1,20 @@
import Redlock from "redlock";
import Redis from "@server/storage/redis";
export class MutexLock {
// Default expiry time for qcuiring lock in milliseconds
public static defaultLockTimeout = 5000;
/**
* Returns the redlock instance
*/
public static get lock(): Redlock {
this.redlock ??= new Redlock([Redis.defaultClient], {
retryJitter: 10,
});
return this.redlock;
}
private static redlock: Redlock;
}
+9 -2
View File
@@ -4075,7 +4075,7 @@
"@smithy/util-stream" "^3.3.1"
tslib "^2.6.2"
"@smithy/types@^3.3.0", "@smithy/types@^3.7.0", "@smithy/types@^3.7.1":
"@smithy/types@^3.7.0", "@smithy/types@^3.7.1":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.7.1.tgz#4af54c4e28351e9101996785a33f2fdbf93debe7"
integrity sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==
@@ -11906,7 +11906,7 @@ node-abort-controller@^1.1.0:
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-1.2.1.tgz#1eddb57eb8fea734198b11b28857596dc6165708"
integrity "sha1-Ht21frj+pzQZixGyiFdZbcYWVwg= sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ=="
node-abort-controller@^3.1.1:
node-abort-controller@^3.0.1, node-abort-controller@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
integrity "sha1-qUN36WSpo3rDl22EjLXHZYM7hUg= sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
@@ -13479,6 +13479,13 @@ redis@^3.0.0:
redis-errors "^1.2.0"
redis-parser "^3.0.0"
redlock@^5.0.0-beta.2:
version "5.0.0-beta.2"
resolved "https://registry.yarnpkg.com/redlock/-/redlock-5.0.0-beta.2.tgz#a629c07e07d001c0fdd9f2efa614144c4416fe44"
integrity sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==
dependencies:
node-abort-controller "^3.0.1"
redux@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"