-
-
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. |
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 enhances ColdBox event caching by allowing developers to specify which request collection (RC) keys to include, exclude, or filter when generating cache keys.
- Adds
cacheInclude
,cacheExclude
, andcacheFilter
metadata to handlers and tests - Updates
EventURLFacade
to apply filtering, whitelisting, and blacklisting of RC keys - Modifies
RequestService
andHandlerService
to pass the full event dictionary tobuildEventKey
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
tests/specs/integration/EventCachingSpec.cfc | Expanded integration tests for include/exclude/filter metadata |
tests/specs/cache/util/EventsURLFacadeTest.cfc | Updated facade tests to pass simulated eventDictionary |
test-harness/handlers/eventcaching.cfc | Added handler actions with new cache metadata for testing |
system/web/services/RequestService.cfc | Changed buildEventKey call to include eventDictionary |
system/web/services/HandlerService.cfc | Adjusted key-building logic and default metadata for cache annotations |
system/cache/util/EventURLFacade.cfc | Implemented filter/include/exclude logic and updated method signatures |
Comments suppressed due to low confidence (5)
tests/specs/cache/util/EventsURLFacadeTest.cfc:53
- There’s a typo in the assertion: the variable is named
testHash
, so it should beexpect(testHash)
instead oftesthash
.
expect( testhash ).notToBeEmpty();
system/web/services/HandlerService.cfc:211
- The call no longer passes
targetEvent
orkeySuffix
, which are required for the correct cache key. Update the call to match the updated signature.
eventCachingData.cacheKey = oEventURLFacade.buildEventKey(
targetContext = oRequestContext,
eventDictionary = eventDictionaryEntry
);
system/web/services/RequestService.cfc:160
- The updated
buildEventKey
signature expects(targetContext, eventDictionary)
but the invocation still includestargetEvent
. Align the parameters or adjust the signature.
eventCache[ "cacheKey" ] = oEventURLFacade.buildEventKey(
targetEvent = currentEvent,
targetContext = arguments.context,
eventDictionary = eventDictionary
);
system/cache/util/EventURLFacade.cfc:98
- ColdFusion structs don’t have an
append
method; usestructAppend(targetMixer, arguments.event.getRoutedStruct(), true)
instead.
targetMixer.append( arguments.event.getRoutedStruct(), true );
system/cache/util/EventURLFacade.cfc:150
buildEventKey
no longer declarestargetEvent
, soarguments.targetEvent
is undefined. Addrequired targetEvent
to the signature or remove its usage.
keySuffix = arguments.eventDictionary.suffix,
targetEvent = arguments.targetEvent
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…' 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. |
@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: