Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Mike Lewis on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

PHP Session Data Intermittently Lost 3

Status
Not open for further replies.

DLayzell

IS-IT--Management
Apr 27, 2005
29
CA
Good Day,

I have written a small, PHP shopping cart application, with the customer's selections being stored in a session variable.

Unfortunately, the session data is intermittently lost while proceeding through the checkout flow. I need to resolve this.

There are 3 pages, and a couple of scripts involved in this flow. The first is a storefront/cart summary page, and I think this is the most likely culprit. It has a form that allows the user to select items and quantities, and uses AJAX to update the cart summary. The scripts called by the JS on this page manipulate session data, and I am wondering if perhaps I have created a possibility of conflicting requests that damage the session data. I've attached the script that gets called by the JS.

The cart data is then posted to another page with a very simple form, with no external scripts. The user selects an option here, which does update the session variables, and then posts to the final page, where I can see that the session data is sometimes inaccessible, if not entirely non-existent.

I need to identify the cause. I am not using redirects, which seems to be overwhelmingly the most common cause (judging by google results), so I am uncertain what could do this. Are there methods or tools you can recommend for troubleshooting sessions? Perhaps a way of locking data to prevent multiple requests from clobbering a common variable?

Thanks in advance for any input,

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
David

before re-arch'ing the solution, if you want to make sure that the issue is with race conditions then you could try one of the following:

1. and your scripts with an explicit call to session_write_close(); this will explicitly close the session

2. try this code (the class and instantiation of the class must be before the call to session_start()). this code replaces the standard session handlers with bespoke alternatives that check for the presence of a lock file before allowing the session to be open. I have set the code to apply the test every 100ms up to a maximum of 2secs before allowing execution to continue anyway.

if this code unlocks the problems then at least you know for sure where the rearchitecture is needed.

caveat - I have not tested this but see no reason why it should not work as intended.

Code:
<?php

class antiRace{
	public $sessionPath;
	public $sessionFile = array();
	public $gracePeriod = 2000; //maximum sleeptime in microseconds
	public $sleep = 100;//per cylce sleep time in microseconds
	
	public function __construct(){
		clearstatcache();
		session_set_save_handler(array($this,"open"), array($this,"close"), array($this,"read"), array($this,"write"), array($this,"destroy"), array($this,"gc"));
		$this->grace = intval($this->gracePeriod)/intval($this->sleep); 
	}
	public function open($path, $sessionName){
		$this->sessionPath = $path;
		return true;
	}
	public function close(){
		return true;
	}
	public function read($sessionID){
		$this->setLockFile($sessionID);
		return (string) file_get_contents($this->getSessionFile($sessionID));
	}
	public function write($sessionID, $sessionData){
		$r = @file_put_contents($this->getSessionFile($sessionID), $sessionData);
		$this->closeLockFile($sessionID);
		return $r;
	}
	public function destroy($sessionID){
		return (@unlink($this->getSessionFile($sessionID)));
	}
	public function gc($maxLifeTime){
		foreach (glob($this->sessionSavePath . DIRECTORY_SEPARATOR . "sess_*") as $fileName):
		    if (filemtime($fileName) + $maxLifeTime < time()):
				@unlink($fileName);
		    endif;
  		endforeach;
  		return true;
	}
	private function setLockFile($sessionID){
		$file = $this->getSessionFile($sessionID) . '.jLock';
		$c = 0;
		if (file_exists($file)):
			while (file_exists($file)):
				usleep($this->sleep);
				$c++;
				if ($c > $this->grace) break;
			endwhile;
		else:
			@file_put_contents($file, '');
		endif;
		return true;
	}
	private function closeLockFile($sessionID){
		@unlink($this->getSessionFile($sessionID) . '.jLock');
	}
	private function getSessionFile($sessionID){
		if (empty($this->sessionFile[$sessionID])){
			$this->sessionFile[$sessionID] = $this->sessionPath . DIRECTORY_SEPARATOR . 'sess_'.$sessionID;	
		}
		return $this->sessionFile[$sessionID];
	}
}
$aR = new antiRace();
session_start();
?>
 
I think if the inbuilt session meachnisim is sensitive to race conditions (and I remain unconvinced) we need a test case to report on. I think putting sleeps in the code is recipe to reduce the scaliability of an app.
 
the php session management is fully susceptible to race conditions. no doubt about it.

try this as a test case
Code:
session_start();
echo '<pre>Session Data:';
print_r($_SESSION);
$_SESSION['test'] = 'test';
sleep(10);
exit;

run the code from a browser and press refresh a few times. nothing will be output. then wait ten seconds and press refresh again. the value 'test' will appear.

I do not advocate inserting a sleep into production code just to avoid session management. the code above was for the purposes of determining that it was a race condition issue.

I am not sure that a simple file lock on the session file would work as you need to maintain the lock from file open all the way through to file close due to the way that the session data is serialised and wholly overwritten each time.
 
Must admit I've only used ajax to read data and then on windows. I'll have a play tonight. This looks an interesting article on the issue.
What I don't understand is why *nix would let two process write to the file at the same time, which appears to be happening because of the gargabe in sesssion. The race conditions I've read about with PHP upset the serialisation rather than propduce garbage.
The flock() call is protocol based i.e. is a flag for an application rahter than *nix locking the file for you, so it's a wait thing rather than a real lock.
Maybe I need to briush up my sys prog in *nix ! (which I thought I wouldn't have to do with PHP !)
 
but that is not what happens with sessions. I'm sure that file locking works fine.

what happens with php sessions is the following

1. file is read, if it exists.
2. contents are unserialised and placed in $_SESSIONS super global.

---
3. manipulations are made to the superglobal by the script
---
4. superglobal is serialised
5. session file is opened for writing.
6. serialised data is written
7. session file is closed.

during the session write, the file is locked and any other write process is stalled.

however the session race condition arises due to the time lapse between read and write. more than one request can READ from the session file before the first (or preceding) request has WRITTEN to the session file. thus each intervening read has got the wrong session data and the last to finish will overwrite the session information from each preceding request.

this is not solved by using a database as a session management tool as the data is still read at one point and written at another and not locked in between the two events.

the only solution to session race conditions is to apply a lock between read and write.

the alternative is to work around the problem through careful selection of what data is stored in the session and what is stored elsewhere.
 
Yes, understand that. When I asked my original question:
@ingresman - re: "Final question, do the corrupting updates look ok? i.e. would you expect that data if it executed correcty or is it garbage?"

The data is garbage. I haven't seen it (the data) firsthand, but I do know that the final page in the checkout flow is expecting an array (i.e. a foreach to loop through the items in the cart), and is not getting one. The foreach errors, pointing to a bad parameter, and breaks the application.
I wonder where the array goes ?
The op does say it is garbage (by which I would mean %^%^%&^% kind of stuff) and not what muight have been valid data. This is confusing me about the actual issue.
When you say:
the only solution to session race conditions is to apply a lock between read and write.
How would you do this is in a web farm? i.e. If you used an NFS share flock doesn't work so, so if the AJAX requests got served from different boxes you will get issues so you really you do need to use a DB, but you still get the issue of serialisation so I do think some kind of verison/optimistic lock might still be appropriate.
 
by garbage data I understood the OP to mean dummy data rather than real user information.

you are correct that flock won't work on server farms etc. when expanding to that stage I think that db based sessions are the only way to go (as otherwise you have race conditions on propagations of session files).

my locking solution still works in specie save that in place of using a separate lock file in the fs you would add a value to a db table row.
 
Yes understand your solution, value in a seperate row is like having a version number on the row.
Be interesitng if OP could clarify what the garbage (capturing a trace would be good) is and if he's found out if there is enough filestore.
Over to you David...
 
Hi ingresman,

Yes, I did as I said I would; I've rewrote the cart to rely on the database rather than session files, I've removed any immediate reads after a write operation (this sounds somewhat arbitrary, but the next read happens in a completely different script/page), and I've serialized the async requests. This site has made many sales since then, and the issue has yet to reoccur. :)

Thanks again all!

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top