Skip to content
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: http caching #3562

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open

feat: http caching #3562

wants to merge 15 commits into from

Conversation

flakey5
Copy link
Member

@flakey5 flakey5 commented Sep 7, 2024

Implements bare-bones opt-in http caching as per rfc9111. Bare-bones in this case means what's required by the spec and a few extra bits, with more coming in future prs.

Opening as a draft since there's still some more work to be done (mostly tests, but a bit more functionality-wise)

No request cache directives are supported at this time, this will come later.

Response caching directives supported:

  • public
  • private
  • s-maxage
  • max-age
  • Expires header
  • no-cache
  • no-store
  • stale-while-revalidate

This relates to...

Closes #3231
Closes #2760
Closes #2256
Closes #1146

Changes

Features

  • Opt-in http caching
  • In-memory default cache store

Bug Fixes

n/a

Breaking Changes and Deprecations

n/a

Status

lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
@ronag
Copy link
Member

ronag commented Sep 7, 2024

Would it maybe be better to use jsdoc types rather than separate type definition files? @mcollina

index.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Show resolved Hide resolved
lib/interceptor/cache.js Outdated Show resolved Hide resolved
// Dump body
opts.body?.on('error', () => {}).resume()

const result = globalOpts.store.get(opts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't add support just yet for Promises, maybe keep it sync for now and see if needed later

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @mcollina wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really should support promises from the get-go, we should consider adding a fs based one using promises too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was having double-thoughts on this. Most of the APIs are callback based, shall we do something similar?

e.g. retry allows async ops by passing a callback that will trigger (or cancel) the next retry

@mcollina
Copy link
Member

mcollina commented Sep 9, 2024

Would it maybe be better to use jsdoc types rather than separate type definition files? @mcollina

I think using type definitions are in line with the rest of the codebase, and therefore the correct implementation.

If we want to migrate to a jsdoc-generated world, let's discuss in another issue!

lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
return this.#handler.onData(chunk)
}

onComplete (rawTrailers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also invalidate cache upon successful status code for non-safe methods (e.g. PUT, POST, PATCH)

https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to think on a good way to be able to tell the cache store to purge all of the values for a certain host, wdyt of just something like store.deleteAllFromHost('localhost')?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that could work; I imagine will require to change the way we store the data?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this also means we need to always run this interceptor for unsafe methods, and the methods property in the options can just detail what safe methods the user wants to cache. Wdyt?

lib/handler/cache-handler.js Outdated Show resolved Hide resolved
@ronag
Copy link
Member

ronag commented Sep 11, 2024

@flakey5 please make sure to add me and @IsakT as co-authors (assuming you took some of our code in nxt-undici).

lib/cache/lru-cache-store.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
@flakey5 flakey5 force-pushed the flakey5/3231 branch 2 times, most recently from e07e3ec to 938fa7a Compare September 12, 2024 03:58
Implements bare-bones http caching as per rfc9111

Closes nodejs#3231
Closes nodejs#2760
Closes nodejs#2256
Closes nodejs#1146

Co-authored-by: Carlos Fuentes <[email protected]>

Co-authored-by: Robert Nagy <[email protected]>

Co-authored-by: Isak Törnros <[email protected]>

Signed-off-by: flakey5 <[email protected]>
@flakey5 flakey5 marked this pull request as ready for review September 14, 2024 05:29
types/interceptors.d.ts Show resolved Hide resolved
test/cache-interceptor/cache-stores.js Outdated Show resolved Hide resolved
test/cache-interceptor/interceptor.js Outdated Show resolved Hide resolved
lib/cache/memory-cache-store.js Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/cache/memory-cache-store.js Outdated Show resolved Hide resolved
lib/cache/memory-cache-store.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-handler.js Outdated Show resolved Hide resolved
lib/handler/cache-revalidation-handler.js Outdated Show resolved Hide resolved
lib/interceptor/cache.js Show resolved Hide resolved
test/cache-interceptor/interceptor.js Show resolved Hide resolved
lib/cache/memory-cache-store.js Outdated Show resolved Hide resolved
lib/cache/memory-cache-store.js Outdated Show resolved Hide resolved
Copy link
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers could be null or undefined

lib/cache/memory-cache-store.js Show resolved Hide resolved
Copy link
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronag
Should we use fastTimers?

lib/cache/memory-cache-store.js Show resolved Hide resolved
lib/interceptor/cache.js Outdated Show resolved Hide resolved
lib/interceptor/cache.js Outdated Show resolved Hide resolved
lib/interceptor/cache.js Outdated Show resolved Hide resolved
lib/interceptor/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
const nextPart = directives[j]
if (nextPart.endsWith('"')) {
foundEndingQuote = true
headers.push(...directives.splice(i + 1, j - 1).map(header => header.trim()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

horrible! why??

lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
lib/util/cache.js Outdated Show resolved Hide resolved
@Uzlopak
Copy link
Contributor

Uzlopak commented Sep 17, 2024

I cheated by using vscode to make the review of the files in lib by adding // @ts-code at the top of the js file.

flakey5 and others added 3 commits September 16, 2024 18:19
Signed-off-by: flakey5 <[email protected]>
@Uzlopak
Copy link
Contributor

Uzlopak commented Sep 17, 2024

@flakey5

Please revert the fastTimer changes for now.

@Uzlopak
Copy link
Contributor

Uzlopak commented Sep 17, 2024

AHA! In case of the interceptor, you could instead of calling this.#handler.onError you could call super.onError ?!

Signed-off-by: flakey5 <[email protected]>
Comment on lines +195 to +197
if (typeof this.#handler.onError === 'function') {
this.#handler.onError(err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would always be set

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types allow it to be undefined however?

onError?(err: Error): void;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not sure why's that but I haven't find that scenario at all

return true
}

return this.#handler.onHeaders(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function would always be there as marks the kick-off for processing the response; shouldn't be possible to find a non onHeaders (the whole process will be wrong if not provided). Same to handler.onError

Copy link
Member

@ronag ronag left a 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 change the cache store interface to:

interace CacheStore {
   createWriteStream(key, props)
   createReadStrream(key)
}

and move the max entry size into the cache store. That way we can store any size of request without necessarily buffering through memory.

@mcollina wdyt?

Something like:

class CacheHandler extends DecoratorHandler {
  onHeaders(statusCode, rawHeaders, resume, statusMessage, headers) {
    this.#value = this.#store
      .createWriteStream({ statusCode, rawHeaders, statusMessage, key: this.#key, headers })
      .on('drain', resume)
      .on('error', () => {
        // TODO
      })
    return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
  }

  onData(chunk) {
    return this.#handler.onData(chunk) && this.#value.write(chunk)
  }

  onComplete(rawTrailers) {
    this.#value.rawTrailers = rawTrailers
    this.#value.end()
    return this.#handler.onComplete(rawTrailers)
  }

  onError(err) {
    this.#value.destroy(err)
    return this.#handler.onError(err
  }
}

@flakey5
Copy link
Member Author

flakey5 commented Sep 19, 2024

I think we should change the cache store interface to:

I'd be in favor of this - I think this would also work a lot better than what I was thinking for reusing responses too (https://www.rfc-editor.org/rfc/rfc9111.html#section-4-7).

If a cache store was writing to some async non-memory store this should also work great still

@flakey5
Copy link
Member Author

flakey5 commented Sep 19, 2024

and move the max entry size into the cache store

The max entry size is already in the cache store though? Or unless you were talking about where we check the response size in the CacheHandler

this.#entryCount++

if (!values) {
// We've never cached anything at this origin before
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is kinda wrong will fix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement HTTP caching fetch: caching Caching Question: Does undici.fetch support RFC 7234?
5 participants