-
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. |
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 ? |
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
89ed094
to
afa5d70
Compare
lib/cache/sqlite-cache-store.js
Outdated
* @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?
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.
Of cache keys? Currently no
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.
Why not make the database primary key a hash of url + method + vary headers?
afa5d70
to
005f51b
Compare
That would still lead to the same state that I talked about here #3657 (comment)
|
005f51b
to
f178cd1
Compare
CI is very red :( |
I think this would do it: PRAGMA page_count;
PRAGMA page_size; Then do |
@ronag I think this is possible, but I would likely investigate it after this lands. I think we would need to bring in https://www.npmjs.com/package/safe-stable-stringify to be able to have "stable" JSONs. |
What's wrong with e.g: function makeKey({ method, origin, path, vary }) {
const hasher = crypto.createHash('sha256')
hasher.update(origin.toLowerCase())
hasher.update(method.toLowerCase())
hasher.update(path ?? '')
hasher.update(vary?.map(x => x.toLowerCase()).sort().join(',') ?? '')
return hasher.digest('hex')
} |
That would be very slow unfortunately :(. Unless we really need it. |
Co-authored-by: Robert Nagy <[email protected]> Co-authored-by: Isak Törnros <[email protected]> Co-authored-by: Matteo Collina <[email protected]> Signed-off-by: flakey5 <[email protected]>
27f1d63
to
c40b97a
Compare
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