CFML 101 revisited... We're doing a side-channel r...
# cfml-general
d
CFML 101 revisited... We're doing a side-channel re-evaluation of application, session, and request variable handling, with an eye towards what can be "cached" in each of those scopes, and how to manage that. We want locks where they're needed, only, minimal performance impact, data integrity, and as little complexity as possible within those constraints. Can anyone articulate a clean and ideally simple strategy on that level?
d
A lot is going to depend on how you've got things distributed, and how you do roll-outs, and what exactly you are doing. The only rules that are hard and fast that I have found so far, are the "two hard things", and "it depends" Probably something that uses redis or another cache for sure, but as to what and how, it is all relative to what you need to do and what constraints you have
a
Def a "it depends" question. And can't be answered any more helpfully than that on such a nebulous question (legit starting point, but we need more than a starting point). I would start by wrapping all shared scope access into some manner of adapter to the scopes. Only the adapter ever references the scope. Other code would be given a - eg - SessionContext object which it interacts. No code outside of Application.cfc (and the apprioriate context initialisation handlers (onApplicationStart, onSessionStart, onRequest start etc) should ever reference those scopes. https://en.wikipedia.org/wiki/Adapter_pattern As far as caching goes, I'd maybe consider the decorator pattern (https://en.wikipedia.org/wiki/Decorator_pattern) for wrapping "cacheable" objects transparently. And one of the caching strategies could be "ScopeCache" which in tern uses application / session / request scope as its persistence tier.
👍 1
d
Thanks Adam. Not to be a complete zero, but what does interacting with for instance this SessionContext object look like? Where does code find it? Request scope? Say lower level code talks to request.SessionContext, which I figure would be set in onRequest() or onRequestStart(). So request.SessionContext = session.SessionContext, which in turn gets created in onSessionStart(). That seem about right? None of that needs any locking, yes?
Further, suppose a user interaction modifies db data that's cached in application scope. I wouldn't think code in that interaction itself should reinit that cache, so it must set an application scope flag telling the next request to refresh the appropriate cache from the db. That refresh would need locking, since it's outside onApplicationStart(). That seem right?
a
"it depends" "cache management" is one of those things in dev - along with naming things - which is hard. And it's impossible to state what needs scope locking without have an example. But in general in 2024 the answer is "when there's a race condition"
Session context shiz (pseudocode, but close to CFML)
Copy code
// ScopeAdapter.cfc
abstract component {

    public function init(required scope) {
        variables.scope = arguments.scope
    }
    
    public any function get(required string key) {
        return variables.scope[key]
    }
    
    public void function set(required string key, required value) {
        variables.scope[key] = value
    }
    
    abstract public void function runSynchronised(required function callback, struct lockParams={})
}


// SessionScopeAdapter.cfc
component extends=ScopeAdapter {

    public void function runSynchronised(required function callback, struct lockParams={}) {
        lock type="session" attributeCollection=arguments.lockParams {
            arguments.callback()
        }
    }
}


// SomethingNeedingSessionStuff.cfc
component {

    public function init(required SessionScopeAdapter sessionContext) {
        variables.sessionContext = arguments.sessionContext
    }
    
    public doTheThing() {
        variables.sessionContext.runSynchronised(() => {
            local.theUser = variables.sessionContext.get("user")
            
            // do stuff with other services
            local.theUser.newThing = whatevs
            
            
            variables.sessionContext.set("user", local.theUser)
        }, {throwOnTimeout=true, timeout=60, type="exclusive"})
    }
}
Short version: your adapter has a get and set method to stuff out and in to the scope. And there's an example of a method to synchronise some code to prevent a race condition.
(very naive implementation)
d
Thanks for that Adam, appreciated. Unsurprisingly, since these sorts of considerations are very much not unique to our apps, this feels a whole lot like reinventing several wheels, which ColdBox for instance has already solved. Am I right that out of the box, it provides patterns and infrastructure for application/session/request flow, locking where needed, etc? If so, next question would be, how hard would it be to retrofit ColdBox into a big semi-legacy app, which is probably a question for the ColdBox channel.
a
I would presume it does, but I've never used it. But having mentioned "box" you will have conjured @bdw429s. he's probs just putting his Clippy outfit on before making an appearance
😆 1
b
lol
I'm actually presenting on Charlie's online CF meetup at the moment
The zipper is stuck on the clippy outfit so I can't get it on....
🤣 3
d
There's also CFWheels and FW/1, as far as popular frameworks go — to some extent it's 6 of one, as it were, because CF itself is a framework — regardless, you'll be learning whatever paradigms each framework uses, so there's no escape from learning new things. You can sort of push the complexities around but you still pay the piper in the end. The heavier the framework, the more it is like its own language, and the more the framework does for you, the more you need to learn the framework. At least when things start actually getting complex. So you won't really be able to evaluate a framework until things are complex— and each framework has different ways of dealing with complexity. A big semi-legacy app might be a good fit for ColdBox, it might be a good fit for FW/1 or CFWheels, or maybe it just needs a little love. What is going to be "best" or "least reinventing of wheels" is highly context dependent ("big simi-legacy app" isn't enough context to tell)
👍 1
a
OK, I dunno what most of that was all about, but I dispute that "there is also CFWheels". There might have been in 2010, but it has no place in any conversation in 2024. It is not any sort of example of a project anyone should interact with.
d
Mostly it was about trade-offs, but tell me more about CFWheels, and why nobody should interact with it?
Does it have something to do with your transition from it to something written in Kotlin instead? How'd that go?
1
d
@denny, is your post actually supposed to be in this thread? My question and the other replies are all cf.
d
Ah, sorry, I can see how it's a little confusing! I was adding to my reply to @Adam Cameron, about CFWheels, and the "maybe in 2010" comment, since he had been using wheels up to until just a couple years ago (but said he was transitioning to something in Kotlin — which was more the rage then, tho that fervor seems to have died down from what I can see). I was thinking maybe his transition didn't go well, since from what I can tell CFWheels is still kicking— but it might also be that he never liked that it was modeled after Ruby on Rails (which is why I mentioned it, as many people love Rails, and it's a different approach from ColdBox or FW/1 (which each have their own approaches)) or that it wasn't modeled after Ruby enough, or something else. Whatever the critique is, I will not be surprised if it is salty. 😃 (I love that things are the same as always, even years later (tho it seems more like months than years!))
d
Ah, understood. I didn't know about Adam's Kotlin adventure, now you've got me curious how it went too.
b
Articulation of a simple caching strategy. Your application could benefit (in performance) by caching: 1. application.someVar if every user of the application may have access to it and if its value remains unchanged throughout the lifetime of the application. Configure the cache's idletime and timespan to be at least the value of the applicationTimeout. Depending on your requirements, it may be beneficial for both to exceed the applicationTimeout. 2. session.someVar if its value remains unchanged throughout the user's session. Configure the cache's idletime and timespan to be at least the sessionTimeout value. Depending on your requirements, it may be beneficial for both to exceed the sessionTimeout. 3. request.someVar if its value remains unchanged throughout the request. Configure the cache's idletime and timespan to be the same as the requestTimeout value. Overall consideration: The idletime should generally be less than the timespan.
It goes without saying: • prime candidates for caching are variables that require a lot of effort or resources to assemble or to evaluate. For example, queries, tables, multidimensional arrays, trees, instances, complex objects, and so on. • locks may usually be unnecessary, as we presume the cached variable to be unchanging within the scope. However, you might need to do something like the following to avoid race condition at the point where the cached variable is initialized:
if (!structKeyExists(application, "someVar")) {
lock type="exclusive" timeout="10" scope="application"
{
// application.someVar cache code goes here
}
}