Session variables being overwritten when using Redis for session storage
Copy link to clipboard
Copied
Would greatly appreciate if someone could comment on this snippet from the external session storage doc:
Once a session object is received from Redis, there can be changes to the object within the timespan of a given request. The session is persisted (if modified) back to external storage on request end. The changes made by the current request on one node are available to all other nodes.
https://helpx.adobe.com/coldfusion/using/external-session-storage.html
As part of my organization's move to a cluster of cf server instances, we're also moving to storing sessions in a redis cache. Some places in the app make remote invocations via fetch request to first modify the session in some way, then before that request has finished, re-fetch some data which may or may not use the newly modified state. This was fine while we were storing session in memory, but we're finding that the modified state is getting overwritten by the second request (perhaps because that second request pulls from the cache before it was modified by the first request). This is all happening on 1 server using the cache, as we haven't moved to the cluster yet.
Ideally, we'd make 1 single request that does both, but going through our huge legacy app to find where this might happen would be a nightmare. I'm wondering if our situation is something other people have seen--because the documentation makes it seem like the modified session should be available to both requests?
Copy link to clipboard
Copied
One potentially solution I am imagining would be to queue requests of the same session, so that coldfusion executes them sequentially so that the session is always the most recent.
On the frontend, our application has various event listeners that will shoot requests to the server--ideally, we could queue/batch these, But I wonder if a queuing the requests on recieving from the server might be another way
Copy link to clipboard
Copied
@zachary_3780 , Queueing is known to be, in general, very inefficient. That's not new. In Shakespeare's Macbeth (written in 1607), the Lady says "Stand not upon the order of your going, But go at once.."
So I am a bit wary to say whether or not queueing requests is a solution for you. Not without knowing what your application does.
Here then are some questions on that:
- What is the purpose of the application?
- Who or what is the user of your application?
- How does the user initially interact with the Frontend?
- How are the event-listeners triggered at the Frontend?
- How do requests arrive at the ColdFusion server?
- What does your ColdFusion application do and what is the end-result?
Copy link to clipboard
Copied
Thanks for your thoughts there, Zachary, and I don't think you're wrong. 🙂 I feel you've proven what I've been trying to explain to bkbk for several replies. And he seems to regard this as a bug, while I will contend (as we did originally) that the behavior is documented, as you'd quoted.
It seems you've just hit a use case that doesn't suit the design limitations. And the conclusion in my first reply is still what I'd say now: that I can't see any feature that will solve this. But you proposed a workaround in your next reply, and I'll offer a response there (again given the threaded nature of this forum).
/Charlie (troubleshooter, carehart. org)
Copy link to clipboard
Copied
Here are my thoughts on your workaround, Zachary.
First, I'll note that Bkbk and I were responding (with our last replies) at the exact same second. 🙂 He'd not seen what I'd written, and vice-versa.
As for that workaround you're considering, Zachary, I suspect forcing such single-threading may prove undesirable. But you may feel the pain of that is less than the pain of the data inconsistency.
And if you really want to pursue it, here's a solution that could work--though only for a single server. More on that in a moment.
You could do a cflock scope="session" type="exclusive" around each of the entire requests that need to be single-threaded. That way, while one request (for a given session) is running, any other request (in that same session, using such a lock) would await its conclusion. This lock could be done either literally within the requests or perhaps on your application.cfc if it's invoking them.
How does this differ from bkbk's proposal, of locking the setting of variables? It differs because we're now locking THE ENTIRE request--queuing it, as you say. That WILL be held up. That WILL prevent a race condition regarding settings session vars in these requests.
Again, it will NOT (NOT NOT NOT) "lock" the session scope. If any code ran in another request that read or wrote to the session scope, that request would NOT be "protected". You'd have to queue the entire request, as you're proposing, this way such as I'm proposing.
And to be clear, we're ONLY considering this potentially painful "single lane bridge" because of the quirk of how redis sessions are not read after the request start and not written until the end.
Finally, this won't work when you try to use multiple instances instead. CF has no feature to do locks across instances. There could be ways to cobble together such queueing across instances: it's just not built-in.
Let us know what you think.
/Charlie (troubleshooter, carehart. org)
Copy link to clipboard
Copied
@zachary_3780 , the code you have provided needs to be refactored. Only after refactoring would it make sense to think about requests, sessions and locks.
Reason why refactoring is necessary:
- The implementation of onRequestStart is incorrect. It currently returns void, but it should return a boolean. In fact, onRequestStart should return True for us to be 100% certain that the request will start.
- In any case, onRequestStart is not the appropriate Application.cfc event-handler for CFC requests. The appropriate event-handler is onCFCRequest. So, use something like the following instead
/* cfcName is the name of the CFC as a dotted path from the root: for example, on my system, this is 'workspace.CF_Project.forum20.test2'; methods: request1, request2; args: struct representing the arguments passed to the method */ public void function onCFCRequest(string cfcname, string method, struct args) { if (arguments.cfcname == "workspace.CF_Project.forum20.test2" || arguments.cfcname == "workspace.CF_Project.forum20.test3") { session[arguments.cfcname & '_start_time'] = now(); } }​
Copy link to clipboard
Copied
@zachary_3780 , Let us know how you're getting along with refactoring the code. Also give us an idea of what your application does.
When you get that far, there is a neat "queueing" solution waiting. It is a ColdFusion solution. It has the added advantage that it is based on our discussions so far: a combination of locks and sessions!
<!---
The lock's name user_#session.sessionID# uniquely identifies the session.
A name-lock is application-wide.
Hence it synchronizes the update of session.userVar cluster-wide, across all instances.
--->
<cflock name="user_#session.sessionID#" type="exclusive" timeout="10">
<cfset session.userVar = 1>
</cflock>
Copy link to clipboard
Copied
Here is a recap which, I hope, will help as you design your application.
Let's assume that the application is distributed among 2 or more ColdFusion instances in a cluster. The application uses sessions, which are stored on Redis. Suppose the value of session.userVar stored on Redis is 0.
Now, suppose a request starts on instance 1. The request updates session.userVar from 0 to 1.
- Note: When a request starts, and needs to use session.userVar, it retrieves the latest value stored in Redis. That is 0, in this case.
Next, suppose that, for the same user and same session, a new request starts on instance 2. That is, while the request on instance 1 is still in progress.
- Note: As the request on instance 1 has not ended, ColdFusion does not yet save the updated session variable in Redis. Such updates are saved only when the request ends.
When the request on instance 2 started, it too read the value session.userVar=0 from Redis. Suppose this request updates session.userVar from 0 to 4.
- Question: How does ColdFusion know which of the updated sessions to store on Redis?
- Answer: The final value in Redis will be determined by whichever request finishes last:
If instance_1_request finishes last, session.userVar = 1 in Redis.
If instance_2_request finishes last, session.userVar = 4 in Redis.
Copy link to clipboard
Copied
Hi @zachary_3780 , I have had a closer look at your code., and have bundled all my suggestions into a test-suite. The test-suite consists of files named similarly to yours.
The code contains ample documentation, justifying the choices made. At every step, I ensured that the contents are based on your code.
Application.cfc
component {
this.name = "sessionTestApp";
this.applicationTimeout = "#createTimespan(1,0,0,0)#";
this.sessionManagement = "true";
this.sessionTimeout = "#createTimeSpan(0,0,20,0)#";
this.setClientCookies = "true";
this.scriptProtect = "all";
public void function onSessionStart() {
// Ideal place to initialize the session variable
session.myVariable = 0;
}
/*
Assumed: requests are directly to the URL of the CFC, for example, http...test2.cfc?method=request1.
In that case, onCFCRequest is the more appropriate event-handler than onRequestStart.
*/
public any function onCFCRequest(string cfcname, string method, struct args) {
var response = 0;
/* 'workspace.CF_Project.forum20' is based on my setup. Use the CFC path in your setup. */
if (arguments.cfcname is "workspace.CF_Project.forum20.test2" || arguments.cfcname is "workspace.CF_Project.forum20.test3") {
session[arguments.cfcname & '_start_time'] = now();
// Process the request by invoking the CFC
response = invoke(arguments.cfcName, arguments.method, {});
}
return response;
}
}
test.cfm
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
<script>
async function sendRequests() {
var message1 = await fetch('http://localhost:8500/workspace/CF_Project/forum20/test2.cfc?method=request1')
.then(function(response) { return response.text(); });
var responseText1 = document.getElementById('response1');
responseText1.innerHTML = message1;
await setTimeout(250)
var message2 = await fetch('http://localhost:8500/workspace/CF_Project/forum20/test2.cfc?method=request2')
.then(function(response) { return response.text(); })
var responseText2 = document.getElementById('response2');
responseText2.innerHTML = message2;
}
</script>
</head>
<body>
<p id="response1"></p>
<p id="response2"></p>
<button type="button" onclick="sendRequests()">Click Me</button>
<button onClick="window.location.reload();">Refresh Page</button>
<cfdump var="#session#" label="Session scope dump when page first opened.">
</body>
</html>
test2.cfc
component {
remote numeric function request1() returnformat="JSON" {
/* A named lock is effective application-wide, hence across a cluster of nodes in the same application */
lock name="userLock_#session.sessionId#" type="exclusive" timeout="3" {
session.myVariable = session.myVariable + 1;
}
/* Dump as HTML to logs directory: to assist in debugging */
/* writedump(var="Value of session.myVariable in request1(): " & session.myVariable, format="html", output="#server.coldfusion.rootDir#\logs\dumpInRequest1methodInTest2CFC.html");*/
return session.myVariable;
}
remote numeric function request2() returnformat="JSON" {
/* A named lock is effective application-wide, hence across a cluster of nodes in the same application */
lock name="userLock_#session.sessionId#" type="readonly" timeout="3" {
/* Dump as HTML to logs directory: to assist in debugging */
/* writedump(var="Value of session.myVariable in request2(): " & session.myVariable, format="html", output="#server.coldfusion.rootDir#\logs\dumpInRequest2methodInTest2CFC.html");*/
return session.myVariable;
}
}
}
What I have provided is just a skeleton. You can flesh it out to suit your needs. The test can be run on a cluster consisting of one, two or more ColdFusion instances.
Copy link to clipboard
Copied
My test-suite copies yours. So, the requests in test.cfm are as they are in your code. Namely, requests that are directly to the CFC, by means of an asynchronous call in Javascript.
However, there is another way to make the requests: by instantiating the component in test.cfm. You could then, if necessary, make use of ColdFusion's own asynchronous calls.
The corresponding code for Application.cfc, test.cfm and test2.cfc are as follows:
Application.cfc
component {
this.name = "sessionTestApp";
this.applicationTimeout = "#createTimespan(1,0,0,0)#";
this.sessionManagement = "true";
this.sessionTimeout = "#createTimeSpan(0,0,20,0)#";
this.setClientCookies = "true";
this.scriptProtect = "all";
public void function onSessionStart() {
// Ideal place to initialize the session variable
session.myVariable = 0;
}
public boolean function onRequestStart(string targetPage) {
return true;
}
}
test.cfm
<!DOCTYPE html>
<html>
<head>
<title>Test Page 2</title>
</head>
<body>
<cfscript>
test2Object = new test2();
future = runAsync(test2Object.request1)
.then(function(result1){tmpResult=result1;})
.then(test2Object.request2)
.then(function(result2){return tmpResult & result2;});
writeoutput(future.get());
</cfscript>
<button onClick="window.location.reload();">Refresh Page</button>
<cfdump var="#session#" label="Session scope dump when page first opened.">
</body>
</html>
test2.cfc
component {
remote string function request1() returnformat="JSON" {
/* A named lock is effective application-wide, hence across a cluster of nodes in the same application */
lock name="userLock_#session.sessionId#" type="exclusive" timeout="3" {
session.myVariable = session.myVariable + 1;
}
/* Dump as HTML to logs directory: to assist in debugging */
/* writedump(var="Value of session.myVariable in request1(): " & session.myVariable, format="html", output="#server.coldfusion.rootDir#\logs\dumpInRequest1methodInTest2CFC.html");*/
return "<p>" & "Session.myVariable has been updated in request1() to: " & session.myVariable & "</p>";
}
remote string function request2() returnformat="JSON" {
/* A named lock is effective application-wide, hence across a cluster of nodes in the same application */
lock name="userLock_#session.sessionId#" type="readonly" timeout="3" {
/* Dump as HTML to logs directory: to assist in debugging */
/* writedump(var="Value of session.myVariable in request2(): " & session.myVariable, format="html", output="#server.coldfusion.rootDir#\logs\dumpInRequest2methodInTest2CFC.html");*/
return "<p>" & "Session.myVariable is read in request2() as: " & session.myVariable & "</p>";
}
}
}


-
- 1
- 2