From dbd371fe21b28a48f0178ae4d65b1cf372fc251b Mon Sep 17 00:00:00 2001 From: tomoyane Date: Tue, 26 Mar 2024 11:58:45 +0900 Subject: [PATCH] support multi thread cache --- src/proxy/contentCache.js | 18 +++++++++++++++++- src/proxy/contentCache.test.js | 34 ++++++++++++++++++++++++++++++---- src/proxy/notionProxy.js | 4 ++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/proxy/contentCache.js b/src/proxy/contentCache.js index 1714a4e..cea42c3 100644 --- a/src/proxy/contentCache.js +++ b/src/proxy/contentCache.js @@ -1,6 +1,7 @@ class ContentCache { constructor(expiresSec) { this.cache = {}; + this.locks = {}; this.expiresSec = parseInt(expiresSec); } @@ -13,20 +14,31 @@ class ContentCache { this.cache[originUrl] = new CacheData(now + this.expiresSec, content); } - getData(originUrl) { + async getData(originUrl) { if (this.expiresSec === 0) { return null; } + if (!this.locks[originUrl]) { + this.locks[originUrl] = new Promise(resolve => resolve()); + } + + await this.locks[originUrl]; + const data = this.cache[originUrl]; if (data === undefined) { + this.releaseLock(originUrl); return null; } + const now = this.toSec(Date.now()); if (now > data.timestamp) { this.delData(originUrl); + this.releaseLock(originUrl); return null; } + + this.releaseLock(originUrl); return data.contentData; } @@ -34,6 +46,10 @@ class ContentCache { delete this.cache[originUrl]; } + releaseLock(originUrl) { + delete this.locks[originUrl]; + } + toSec(unixTimeMilliseconds) { return Math.floor(unixTimeMilliseconds / 1000) } diff --git a/src/proxy/contentCache.test.js b/src/proxy/contentCache.test.js index a4d9937..e6c2403 100644 --- a/src/proxy/contentCache.test.js +++ b/src/proxy/contentCache.test.js @@ -4,16 +4,16 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } -test('Cache set and get', () => { +test('Cache set and get', async () => { const cache = new ContentCache('2'); const targetUrl = '/test'; cache.setData(targetUrl, 'xxxxx'); - let fetchedData = cache.getData(targetUrl) + let fetchedData = await cache.getData(targetUrl) expect(fetchedData).toBe('xxxxx'); - fetchedData = cache.getData('NOT_FOUND') + fetchedData = await cache.getData('NOT_FOUND') expect(fetchedData).toBe(null); }); @@ -27,6 +27,32 @@ test('Cache already expired data', async () => { await sleep(3000); // Data is expired so data is removed - const fetchedData = cache.getData(targetUrl) + const fetchedData = await cache.getData(targetUrl) expect(fetchedData).toBe(null); }); + +test('Access multiple thread', async () => { + const cache = new ContentCache(60); + const getDataSpy = jest.spyOn(cache, 'getData'); + const releaseLockSpy = jest.spyOn(cache, 'releaseLock'); + + const targetUrl = '/test'; + const valueData = "XXXXXXXXXXXXXXX" + cache.setData(targetUrl, valueData); + + // Define getData tasks + const asyncTaskCnt = 100000; + const tasks = []; + for (let i = 0; i < asyncTaskCnt; i++) { + tasks.push(cache.getData(targetUrl)); + } + + // Execute getData tasks concurrently + const results = await Promise.all(tasks); + results.forEach(result => { + expect(result).toBe(valueData); + }); + + expect(getDataSpy).toHaveBeenCalledTimes(asyncTaskCnt); + expect(releaseLockSpy).toHaveBeenCalledTimes(asyncTaskCnt); +}); diff --git a/src/proxy/notionProxy.js b/src/proxy/notionProxy.js index 68cceac..aa72957 100644 --- a/src/proxy/notionProxy.js +++ b/src/proxy/notionProxy.js @@ -59,7 +59,7 @@ class NotionProxy { * @param res Response of express * @returns {*|void} */ - get(req, res) { + async get(req, res) { let url; try { url = utility.generateNotionUrl(req, this.SLUG_TO_PAGE); @@ -87,7 +87,7 @@ class NotionProxy { res.removeHeader('Content-Security-Policy') res.removeHeader('X-Content-Security-Policy') - const cachedData = this.CACHE_STORE.getData(req.originalUrl); + const cachedData = await this.CACHE_STORE.getData(req.originalUrl); if (cachedData !== null) { return res.send(cachedData); }