-
-
Notifications
You must be signed in to change notification settings - Fork 172
[COLDBOX-1331] Add ability to ignore or include RC keys when caching events #607
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
base: development
Are you sure you want to change the base?
[COLDBOX-1331] Add ability to ignore or include RC keys when caching events #607
Conversation
The metadata option `cacheIncludeRcKeys` gives developers more control over which RC scope variables are considered when generating the cachekey
@lmajano I see some tests failed, but I do not think they are related to my PR. |
Hi @lmajano, I just wanted to ensure you saw this PR you requested me to create again. This would be a really powerful addition to Coldbox and help many users who use event caching, but also rely on marketing parameters like UTM variables, which can pollute the RC scope and bypass event caching. |
I think this is a really good addition. The name The reason for the abstraction is that, authentication info might be retained in the In the function arguments, though, I think you can use shorter argument names, because the context is already clear. I would also suggest adding handling for this at the function annotation level:
|
@jclausen Great idea! I like the idea of 1) simplifying the meta annotation and 2) being able to use the As for naming, I think we could call the annotation
Is that in line with what you were thinking? If my assumptions are correct, it sounds like we need to make some changes to the
Edit: I might want to check |
I would be curious to hear @lmajano's thoughts on this item as well. I'm happy to make the edits to the PR once I get feedback. |
@jclausen can you think of a faster way to isolate the
|
Ok, I have reviewed it all. Here are my comments and suggestions so we can incorporate.
Examples // Default, it includes *
function show( event, rc, prc ) cache="true" cacheTimeout="10"{
}
// Simple include - only use these RC keys for caching
function show( event, rc, prc ) cache="true" cacheTimeout="10" cacheInclude="id,category,status" {
}
// Simple exclude - use all RC keys except these
function list( event, rc, prc ) cache="true" cacheTimeout="10" cacheExclude="timestamp,csrf,debug" {
}
// Custom filter function for complex logic
function search( event, rc, prc ) cache="true" cacheTimeout="10" cacheFilter="getSearchCacheKeys" {
}
private function getProductCacheKeys() {
// Can capture handler state or configuration in the closure
var validKeys = this.getCacheableKeys(); // some handler method
return ( rc ) => {
return rc.filter( ( key, value ) => arrayFindNoCase( validKeys, key ) > 0 );
};
} Implementation DetailsI think at this time, it's easier to pass in the /**
* Build an event key according to passed in params
*
* @targetEvent The targeted ColdBox event executed
* @targetContext The targeted request context object
* @eventDictionary The event metadata containing cache annotations
*/
string function buildEventKey(
required targetEvent,
required targetContext,
required struct eventDictionary
){
return buildBasicCacheKey(
keySuffix = arguments.eventDictionary.suffix,
targetEvent = arguments.targetEvent
) & getUniqueHash( arguments.targetContext, arguments.eventDictionary );
} This way, the dictionary has all the metadata needed for the cache request. // Build the event cache key according to incoming request
eventCache[ "cacheKey" ] = oEventURLFacade.buildEventKey(
targetEvent = currentEvent,
targetContext = arguments.context,
eventDictionary = eventDictionary
); The suffix is inside the dictionary so we can remove that as well and just encapsulate it into the dictionary. So we can have ultimately something like this /**
* Build a unique hash from an incoming request context
*
* @event A request context object
* @eventDictionary The event metadata containing cache annotations
*/
string function getUniqueHash( required event, required struct eventDictionary ){
var rcTarget = arguments.event.getCollection();
// Remove "event" key if it exists
rcTarget.delete( "event", false );
// Apply cache key filtering based on annotations
// Use custom filter closure stored in dictionary
if ( isClosure( arguments.eventDictionary?.cacheFilter ) ) {
rcTarget = arguments.eventDictionary.cacheFilter( rcTarget );
}
// Cache Includes
else if ( len( arguments.eventDictionary?.cacheInclude ) ) {
// Whitelist specific keys
var includeKeys = arguments.eventDictionary.cacheInclude.listToArray();
rcTarget = rcTarget.filter( ( key, value ) => {
return includeKeys.findNoCase( key ) > 0;
});
}
// Cache Excludes
else if ( len( arguments.eventDictionary?.cacheExclude ) ) {
// Blacklist specific keys
var excludeKeys = arguments.eventDictionary.cacheExclude.listToArray();
rcTarget = rcTarget.filter( ( key, value ) => {
return excludeKeys.findNoCase( key ) == 0;
});
}
var targetMixer = {
// Get the original incoming context hash
"incomingHash" : hash( variables.jTreeMap.init( rcTarget ).toString() ),
// Multi-Host support
"cgihost" : buildAppLink()
};
// Incorporate Routed Structs
targetMixer.append( arguments.event.getRoutedStruct(), true );
// Return unique identifier
return hash( targetmixer.toString() );
} |
@lmajano This update adds support for more granular control over cache key generation via event metadata. Based on your feedback, the following enhancements are now supported in the
These filters are not mutually exclusive, giving developers flexibility to use one or more depending on their use case. However, the order is important. We execute them in this order: Test coverage is included to validate different combinations and behaviors. Please let me know if you have any feedback or run into any issues. |
Woot! Looks like most of the tests are passing. The ACF 2023 fail has something to do with PDFs, so I am unsure if it is related. |
Co-authored-by: Copilot <[email protected]>
…' into feature/cache-include-rc-keys
system/cache/util/EventURLFacade.cfc
Outdated
// 2. Include specific keys (if `cacheInclude` is not "*") | ||
// 3. Exclude specific keys (if `cacheExclude` is provided and not empty) | ||
|
||
// Use custom filter closure stored in dictionary |
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.
Sorry but this is not what was discussed for a cache filter. This goes through tons of machinery to make it work. The idea for the cacheFilter
is that it's a function that returns a closure, which is stored in the metadata.
This way, it's just executed and used. This is going to create more issues than assist. See the code samples sent
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.
Hi @lmajano
I'm not sure I understand your original intent. I don't think you can include a closure in the metadata, can you?
This is the code sample you sent:
// Custom filter function for complex logic
function search( event, rc, prc ) cache="true" cacheTimeout="10" cacheFilter="getSearchCacheKeys" {
}
private function getProductCacheKeys() {
// Can capture handler state or configuration in the closure
var validKeys = this.getCacheableKeys(); // some handler method
return ( rc ) => {
return rc.filter( ( key, value ) => arrayFindNoCase( validKeys, key ) > 0 );
};
}
My interpretation was that cacheFilter
points to a private method in the handler. I assumed you made a typo with the getProductCacheKeys()
method and meant getSearchCacheKeys()
.
When I tried to insert a closure into the cacheFilter
metadata directly, it triggers an exception "Value of attribute [cachefilter] must have a literal/constant value"
// non-working example:
function withFilterClosure( event, rc, prc )
cache ="true"
cacheTimeout ="10"
cacheFilter = function( rcTarget ) {
// Example filter that removes UTM parameters
return filterUtmParams( rcTarget );
}
If you're not looking to execute a handler method here, can you please clarify?
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 had an idea to accomplish what I think you're looking for... what do you think of this theoretical solution?
// HandlerService.cfc
private struct function getEventCachingMetadata( required ehBean, required oEventHandler ){
// ...
mdEntry.cacheFilter = arguments.ehBean.getActionMetadata( "cacheFilter", "" );
// if the cacheFilter is a closure, then we store it in the mdEntry
if (
len( mdEntry.cacheFilter ) &&
(
isClosure( arguments.oEventHandler[ mdEntry.cacheFilter ] ) ||
isCustomFunction( arguments.oEventHandler[ mdEntry.cacheFilter ] )
)
) {
mdEntry.cacheFilter = arguments.oEventHandler[ mdEntry.cacheFilter ];
}
// ...
Then...
// EventUrlFacade.cfc
string function getUniqueHash(
required event,
required struct eventDictionary
){
// ...
// Use custom filter closure stored in dictionary
if ( isClosure( arguments.eventDictionary.cacheFilter ) ) {
rcTarget = arguments.eventDictionary.cacheFilter( rcTarget );
}
// ...
The only catch is that the custom function/closure can't be private. Otherwise, it's not accessible in oEventHandler
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.
@lmajano, please take a look at my changes and let me know if they better match what you originally intended.
Instead of executing a private handler method, we now look for and store a UDF in the event caching metadata.
You are almost there. What you are missing is the following.
The handler service does the following:
EventURLFacade
|
Validates that the `cacheFilter` defined in event handlers exists as a private method that returns a closure.
@lmajano, when you have a minute, could you please check again to see if it meets your expectations? I am currently working on updating the documentation. |
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.
Pull Request Overview
This PR extends ColdBox event caching to allow developers to include, exclude, or filter specific request collection (RC) keys when generating cache keys. It introduces three new metadata options (cacheInclude
, cacheExclude
, and cacheFilter
), updates the core services and URL facade to support these options, and adds integration and unit tests covering the new behavior.
- Adds
cacheInclude
,cacheExclude
, andcacheFilter
metadata to handler definitions and propagates them throughHandlerService
andRequestService
. - Revamps
EventURLFacade
to apply custom filter closures, key whitelists, and key blacklists when building cache hashes. - Introduces new tests and example handlers in the test harness to validate include/exclude/filter behaviors.
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
tests/specs/integration/EventCachingSpec.cfc | New integration specs for cacheInclude, cacheExclude, cacheFilter cases |
tests/specs/cache/util/EventsURLFacadeTest.cfc | Updated unit tests to simulate event metadata in URL facade methods |
test-harness/handlers/eventcaching.cfc | Added handler actions covering all combinations of include/exclude/filter options |
system/web/services/RequestService.cfc | Passes new eventDictionary into URL facade for request-level caching |
system/web/services/HandlerService.cfc | Reads and validates cacheInclude , cacheExclude , cacheFilter metadata and invokes closures |
system/cache/util/EventURLFacade.cfc | Implements include/exclude/filter logic in getUniqueHash and updates buildEventKey signature |
Comments suppressed due to low confidence (4)
system/web/services/RequestService.cfc:158
- The new buildEventKey signature requires both a targetEvent (used by buildBasicCacheKey) and eventDictionary, but this call only passes targetContext and eventDictionary. Add targetEvent (or keySuffix) to the argument list so the cache key is constructed correctly.
eventCache[ "cacheKey" ] = oEventURLFacade.buildEventKey(
system/web/services/HandlerService.cfc:211
- Similar to RequestService, this call to buildEventKey omits the targetEvent parameter. The method relies on arguments.targetEvent for the suffix, so include the event name or adjust the signature accordingly.
eventCachingData.cacheKey = oEventURLFacade.buildEventKey(
system/cache/util/EventURLFacade.cfc:92
- CFML structs do not have an
append
method; the originalstructAppend
built-in must be used to merge routedStruct into targetMixer. Replace this call withstructAppend(targetMixer, arguments.event.getRoutedStruct(), true)
.
targetMixer.append( arguments.event.getRoutedStruct(), true );
tests/specs/cache/util/EventsURLFacadeTest.cfc:53
- The variable is declared as
testHash
but referenced astesthash
. Fix the casing to match (expect(testHash).notToBeEmpty()
).
expect( testhash ).notToBeEmpty();
} | ||
|
||
// Cache Excludes | ||
if ( len( arguments.eventDictionary?.cacheExclude ) ) { |
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.
Get rid of the ?.
-> .
it will always exist.
Fantastic job. Few moer changes and this is solid gold! |
@lmajano
This pull request is in response to this discussion in the Coldbox community and is a revival of the original PR here: #503
This new feature for Coldbox gives developers more flexibility with event caching and different RC scopes. The end result should be more efficient caching and improved page load speed for many users.
The Problem:
Currently, event caching considers the entire RC scope when generating a cache key. This means that changing any URL or FORM variable on a request will force Coldbox to generate a new cache entry. This behavior is problematic for a few reasons:
A malicious attacker could take advantage and create an unlimited number of cache entries for the same page simply by adding a random URL variable.
Marketing efforts often require appending a unique tracking variable to a URL (e.g. utm_source). In this type of scenario, there is no need to create a new cached event for every unique URL.
The Solution
I was inspired by Wirebox's populator include/exclude arguments which lets developers specify a list of keys to consider when populating an entity. My idea was to create a new metadata property called cacheIncludeRcKeys which would allow specifying a list of RC keys to include when generating a cache key. All keys not in the list are ignored. To maintain backward compatibility, the default value for cacheIncludeRcKeys is * which considers all keys.
How Does it Work?
Simply include a new metadata attribute cacheIncludeRcKeys to a handler and specify a list of RC keys you want to be included in the cache key generation process. Here are some examples:
The above code will ignore all RC keys except for slug when generating a cache key. So, if you had a route pages/:slug resolve to pages.show, the following URLs would all utilize the same cache key:
https://mysite.com/pages/foo
https://mysite.com/pages/foo?utm_source=google
https://mysite.com/pages/foo?utm_source=bing
You can also include multiple RC keys by specifying a list in the metadata attribute like this: