|
|
|
|
|
by bheadmaster
823 days ago
|
|
> I'm honestly having a difficult time creating a steel man js sample that exhibits data races I can give you a real-life example I've encountered: const CACHE_EXPIRY = 1000; // Cache expiry time in milliseconds
let cache = {}; // Shared cache object
function getFromCache(key) {
const cachedData = cache[key];
if (cachedData && Date.now() - cachedData.timestamp < CACHE_EXPIRY) {
return cachedData.data;
}
return null; // Cache entry expired or not found
}
function updateCache(key, data) {
cache[key] = {
data,
timestamp: Date.now(),
};
}
var mockFetchCount = 0;
// simulate web request shorter than cache time
async function mockFetch(url) {
await new Promise(resolve => setTimeout(resolve, 100));
mockFetchCount += 1;
return `result from ${url}`;
}
async function fetchDataAndUpdateCache(key) {
const cachedData = getFromCache(key);
if (cachedData) {
return cachedData;
}
// Simulate fetching data from an external source
const newData = await mockFetch(`https://example.com/data/${key}`); // Placeholder fetch
updateCache(key, newData);
return newData;
}
// Race condition:
(async () => {
const key = 'myData';
// Fetch data twice in a sequence - OK
await fetchDataAndUpdateCache(key);
await fetchDataAndUpdateCache(key);
console.log('mockFetchCount should be 1:', mockFetchCount);
// Reset counter and wait cache expiry
mockFetchCount = 0;
await new Promise(resolve => setTimeout(resolve, CACHE_EXPIRY));
// Fetch data twice concurrently - we executed fetch twice!
await Promise.all([fetchDataAndUpdateCache(key), fetchDataAndUpdateCache(key)]);
console.log('mockFetchCount should be 1:', mockFetchCount);
})();
This is what happens when you convince programmers that concurrency is not a problem in JavaScript. Even though this cache works for sequential fetching and will pass trivial testing, as soon as you have concurrent fetching, the program will execute multiple fetches in parallel. If server implements some rate-limiting, or is simply not capable of handling too many parallel connections, you're going to have a really bad time.Now, out of curiosity, how would you implement this kind of cache in idiomatic, lock-free javascript? |
|
The simplest way is to cache the Promise<data> instead of waiting until you have the data:
From this the correct behavior flows naturally; the API of fetchDataAndUpdateCache() is exactly the same (it still returns a Promise<result>), but it’s not itself async so you can tell at a glance that its internal operation is atomic. (This does mildly change the behavior in that the expiry is now from the start of the request instead of the end; if this is critical to you you can put some code in `updateCache()` like `data.then(() => cache[key].timestamp = Date.now()).catch(() => delete cache[key])` or whatever the exact behavior you want is.)I‘m not even sure what it would mean to “add a lock” to this code; I guess you could add another map of promises that you’ll resolve when the data is fetched and await on those before updating the cache, but unless you’re really exposing the guts of the cache to your callers that’d achieve exactly the same effect but with a lot more code.