-
Notifications
You must be signed in to change notification settings - Fork 545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: sqlite cache store #3657
base: main
Are you sure you want to change the base?
feat: sqlite cache store #3657
Conversation
c33c78e
to
6c8ea8f
Compare
6c8ea8f
to
a015bf2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some docs?
592ffb3
to
d951470
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
19b0eaf
to
93a306c
Compare
34b34d7
to
60a9679
Compare
60a9679
to
8e0fa89
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Unnecessarily complicated by locking.
- How does it "prune" i.e. free space?
- Why errorCallback?
IMHO I would think you could do this quite similar to the "simplified" memory store. |
index.js
Outdated
const SqliteCacheStore = require('./lib/cache/sqlite-cache-store') | ||
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore | ||
} catch (_) { | ||
// Do nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should check for expect error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wdym?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically verify that this errors with a certain error code, and if not rethrow the error
bf40ed4
to
70fc6d3
Compare
Goes back to the discussion we were having in the original caching pr for how to handle errors, we decided to handle them silently so the request still goes through. Here it's just being used for handling json parse errors, since those shouldn't ever happen I would be in favor of just throwing outright though
It prunes the entire database of expired values when we come across one when looking up a key, we can make this more stricter though (i.e. run the purge query on every write), wdyt? |
70fc6d3
to
dd7ba9a
Compare
When the database is full (we are missing maxSize) we should remove entries whether they are expired or not. I think we should have something like, while beyond maxCount or maxSize drop 10% of the oldest entries. |
I omitted maxCount here since it would break if there were multiple processes using a db for cache If we do want it, I think we might be able to get away with it being stored somewhere in the db and being modified only with a procedure. |
I believe sqlite has meta commands for that, @mcollina ? |
} catch (err) { | ||
if (this.#errorCallback !== undefined) { | ||
this.#errorCallback(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO If the error callback is just for this I would rather skip it. Would even argue we should crash here if this happens (it shouldn't). @mcollina?
lib/cache/sqlite-cache-store.js
Outdated
const existingValue = findValue(key, true) | ||
if (existingValue) { | ||
// Updating an existing response, let's delete it | ||
updateValueQuery.run( | ||
JSON.stringify(stringifyBufferArray(body)), | ||
value.deleteAt, | ||
value.statusCode, | ||
value.statusMessage, | ||
JSON.stringify(stringifyBufferArray(value.rawHeaders)), | ||
value.cachedAt, | ||
value.staleAt, | ||
value.deleteAt, | ||
existingValue.id | ||
) | ||
} else { | ||
// New response, let's insert it | ||
insertValueQuery.run( | ||
url, | ||
key.method, | ||
JSON.stringify(stringifyBufferArray(body)), | ||
value.deleteAt, | ||
value.statusCode, | ||
value.statusMessage, | ||
JSON.stringify(stringifyBufferArray(value.rawHeaders)), | ||
value.vary ? JSON.stringify(value.vary) : null, | ||
value.cachedAt, | ||
value.staleAt, | ||
value.deleteAt | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just always just overwrite? Does it update and insert need to be separate ops?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so but I'm not sure how messy it would be compared to just having two queries
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does update fail if the value doesn't exist? Doesn't sql have an insert or update command?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does update fail if the value doesn't exist
It fails in the sense that it noops.
Doesn't sql have an insert or update command?
I'm not sure how well insert or update would work here based on how we're checking if there's a already cached value though. The only reliable and fast way we have to compare two cached values is with their ids. Otherwise, we need to compare their url, method, and vary headers to see if they were the same. The url and method comparisons are simple and can be done in sql, but comparing the vary headers is more difficult since it's stringified json. We can't compare it in sql since we don't know if the properties are in the same order, meaning we need to parse then compare (like what #findValues
does).
I was experimenting with hashing the vary headers (sort properties alphabetically -> stringify -> crypto.createHash), however, it was a lot more complicated than just having two separate queries. It also would've only benefited writes, for reads we would still need to fetch each response and compare the vary headers like we already do
dd7ba9a
to
89ed094
Compare
Co-authored-by: Robert Nagy <[email protected]> Co-authored-by: Isak Törnros <[email protected]> Signed-off-by: flakey5 <[email protected]>
89ed094
to
afa5d70
Compare
* } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue | ||
*/ | ||
class SqliteCacheStore { | ||
#maxSize = Infinity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs maxEntrySize as well?
const now = nowAbsolute() | ||
for (const value of values) { | ||
if (now >= value.deleteAt && !canBeExpired) { | ||
this.#deleteExpiredValuesQuery.run(now) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a slow query? Should you just do a delete with id?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends, although indexed, as we are doing a range-like query it might hurt on the index lookup.
Have we considered maybe something like an LRU instead?
/** | ||
* Whether or not the cache is full and can not store any more responses | ||
*/ | ||
isFull?: Readonly<boolean> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't see the point of this. The cache is never full. If it's full then the older values should be removed instead of not caching the most recent value.
|
||
### `SqliteCacheStore` | ||
|
||
The `SqliteCacheStore` stores the responses in a SQLite database. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we add a note for the node:sqlite
dependency in case someone attempts to use it within Node.js <= 22?
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} | ||
*/ | ||
get (key) { | ||
if (typeof key !== 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we also support arrays?
* @returns {Writable | undefined} | ||
*/ | ||
createWriteStream (key, value) { | ||
if (typeof key !== 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check that at least the minimum required properties are there; otherwise it can lead to confusing errorrs
const now = nowAbsolute() | ||
for (const value of values) { | ||
if (now >= value.deleteAt && !canBeExpired) { | ||
this.#deleteExpiredValuesQuery.run(now) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends, although indexed, as we are doing a range-like query it might hurt on the index lookup.
Have we considered maybe something like an LRU instead?
Note: this is a draft since #3562 isn't landed. Until it is landed, this will be based off of that pr's branch. For the actual diff see flakey5/undici@flakey5/3231...flakey5:undici:flakey5/20240910/sqlite-cache-storeThis relates to...
Adding client side http caching (#3562)
Rationale
Adds a sqlite cache store option given the
--experimental-sqlite
flag is providedChanges
Features
Bug Fixes
n/a
Breaking Changes and Deprecations
n/a
Status
cc @mcollina @ronag