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
 
We'd need to see some actual code to be able to debug this, and since JS can't directly interact with the sessions we need the PHP code. Your link doesn't seem to have anything there but a couple of table tags.

I would start looking at the code that manipulates your sessions, and see if its not creating a new session instead of using the existing one.


Outputting the session contents as you debug may be helpful.

Code:
print_r($_SESSION);

Not much more to say without actual code.



----------------------------------
Phil AKA Vacunita
----------------------------------
Ignorance is not necessarily Bliss, case in point:
Unknown has caused an Unknown Error on Unknown and must be shutdown to prevent damage to Unknown.

Behind the Web, Tips and Tricks for Web Development.
 
My apologies, too unaccustomed to this forum. Here is the script I attached:

Code:
<?php

	include("config.php");
	include("cart.class.php");
	session_start();

	// Make a working copy of the shopping cart from session data //
	$items = $_SESSION["cart"];
	$tix = array('1', '2', '3', '4');
	$rooms = array('6', '7', '8');

	$cart = new Shopping_Cart($items);

	// Retrieve the parameters //
	$item = $_GET['type'];
	$cost = $_GET['cost'];
	$qty = $_GET['qty'];
	$name = $_GET['name'];
	$pack = $_GET['pid'];
	$dat = $_GET['dat'];
	$hotel = $_GET['hid'];
		
	if (in_array($item, $tix)) {
		if (isset($items[$item])) {$cart->deleteFromCart($item, $items[$item]['qty']);}
		$cart->addToCart($item, $qty, $cost, $name);
	}
	elseif ($item == '5' || $item == '21' || $item == '26') {
		if (isset($items[$item])) {$cart->deleteFromCart($item, 1);}
		if ($qty > 0) {$cart->addToCart($item, $qty, $cost, $name);}
	}
	elseif ($item == '13') {
		$cart->addToCart($item, 1, $cost, $name.";".$pack);
		$qry = mysql_query("SELECT hotelnight FROM prepackaged WHERE pid=".$pack);
		$row = mysql_fetch_row($qry);
		if ($row[0]) {
			if (!isset($items[16]) && !isset($items[17])) {$cart->addToCart(16, 1, 0, 'Single King');}
			if (!isset($items[18]) && !isset($items[19])) {$cart->addToCart(18, 1, 0, 'Non-Smoking');}
		}
	}
	// If item is adult or child packages //
	elseif ($item == '14' || $item == '15') {
		// If the item already exists in the cart, remove it //
		if (isset($items[$item])) {$cart->deleteFromCart($item, $items[$item]['qty']);}
		// Add the new quantity of the item to the cart //
		$cart->addToCart($item, $qty, $cost, $name);
		// Update the session cart, and make a new working copy //
		[COLOR=red]$_SESSION["cart"] = $cart->getCart();
		$items = $_SESSION["cart"];[/color]
		// Query the database for hotel quantity pricing //
		$qry = mysql_query("SELECT * FROM prepackaged WHERE pid=".$pack);
		if (!$qry) {die('Query Failed with Error Message: '.mysql_error());}
		$packdat = mysql_fetch_array($qry);
		$ppl = 0;
		// Calculate the hotel cost based on the number of adult occupants //
		switch ($items[14]['qty']) {
			case 1:	
				$ppl = 2;
				$price = $packdat['single'];
				break;
			case 2:
				$ppl = 2;
				if (!empty($packdat['double'])) {$price = $packdat['double'] * 2;}
				else {$price = $packdat['single'] * 2;}
				break;
			case 3:
				$ppl = 3;
				if (!empty($packdat['triple'])) {$price = $packdat['triple'] * 3;}
				elseif (empty($packdat['triple']) && !empty($packdat['double'])) {$price = $packdat['double'] * 3;}
				else {$price = $packdat['single'] * 3;}
				break;
			case 4:
				$ppl = 4;
				if (!empty($packdat['quad'])) {$price = $packdat['quad'] * 4;}
				elseif (empty($packdat['quad']) && !empty($packdat['triple'])) {$price = $packdat['triple'] * 4;}
				elseif (empty($packdat['quad']) && empty($packdat['triple']) && !empty($packdat['double'])) {$price = $packdat['double'] * 4;}
				else {$price = $packdat['single'] * 4;}
				break;
			case 0:
				$ppl = 0;
				$price = 0;
				break;
			default:
				$price = "NO GOOD!";
				break;
		}
		// If there are children set, add the child rates to the hotel costs//
		if (isset($items[15]['qty']) && $items[15]['qty'] > 0){
			if (!empty($packdat['child'])) {$price += ($items[15]['qty'] * $packdat['child']);}
			else {$price += ($items[15]['qty'] * $packdat['single']);}
			$ppl += $items[15]['qty'];
		}
		// Remove the existing package from the cart, and re-add it with the new cost calculation //
		$cart->deleteFromCart(13, 1);
		if (($item == '14' && $qty > 0) || $item == '15') {
			$cart->addToCart(13, 1, $price, $packdat['name'].";".$pack);
		}
		// If there are ticket upgrades selected, recalculate their cost on the new number of packages //
		if (isset($items[20])) {
			$cart ->deleteFromCart(20, $items[20]['qty']);
			$cart ->addToCart(20, $ppl, $items[20]['cost'], $items[20]['name']);
		}
	}
	elseif ($item == '20') {
		if (isset($items[$item])) {$cart ->deleteFromCart($item, $items[$item]['qty']);}
		$ppl = 1;
		if (isset($items[14])) {$ppl = $items[14]['qty'];}
		if (isset($items[15])) {$ppl += $items[15]['qty'];}
		$cart -> addToCart($item, $ppl, $cost, $name);
	}
	elseif (in_array($item, $rooms)) {
		if (isset($items[$item])) {$cart->deleteFromCart($item, $items[$item]['qty']);}
		if ($item == '8') {
			if (isset($items[22])) {$cost*=$qty * $items[22]['qty'];}
			else {$cost*=$qty;}
		}
		$cart->addToCart($item, $qty, $cost, $name);
	}
	elseif ($item == '9' || $item == '11' || $item == '16' || $item == '18') {
		if (isset($items[$item + 1])) {$cart->deleteFromCart($item + 1, 1);}
		$cart->addToCart($item, 1, 0, $name);
	}
	elseif ($item == '10' || $item == '12' || $item == '17' || $item == '19') {
		if (isset($items[$item - 1])) {$cart->deleteFromCart($item - 1, 1);}
		$cart->addToCart($item, 1, 0, $name);
	}
	// If the item being passed is a depart or return date //
	elseif ($item == '23' || $item == '24') {
		// If there is a number of nights stored in the cart, remove them for recalculation //
		if (isset($items[22])) {$cart ->deleteFromCart(22, $items[22]['qty']);}
		// If the item is a departure date, update the departure date //
		if ($item == '23') {$cart->updateDates($dat, $items['DoR']);}
		// If the item is a return date, update the return date //
		elseif ($item == '24') {$cart->updateDates($items['DoD'], $dat);}
		// Update the session cart with changes, and make a new working copy from the session data //
		[COLOR=red]$_SESSION["cart"] = $cart->getCart(); //***************************************************************************
		$items = $_SESSION["cart"];	
		// If only one date is set, assume that the trip is for one night only //
		if (!isset($items['DoD']) || !isset($items['DoR'])) {$cart->addToCart(22, 1, 0, 'Nights');}
		// If both departure and return dates are set, then calculate the number of nights difference //
		else {
			$depart = explode("/", $items['DoD']);
			$return = explode("/", $items['DoR']);
			$date1 = mktime(0, 0, 0, $depart[0], $depart[1], $depart[2]);
			$date2 = mktime(0, 0, 0, $return[0], $return[1], $return[2]);
			$days = floor(($date2-$date1)/(60*60*24));
			$cart ->addToCart(22, $days, 0, 'Nights');
		}
		// Update the session cart with changes, and make a new working copy from the session data //
		$_SESSION["cart"] = $cart->getCart();
		$items = $_SESSION["cart"];[/color] //***************************************************************************
		// If a number of rooms have been selected //
		if (isset($items[8])) {
			// Query the database for the hotel price //
			$qry = mysql_query ("SELECT * FROM hotel WHERE hid=".$hotel);
			$row = mysql_fetch_array($qry);
			$cost = $row['ROOM_TYPE_1_PRICE'];
			// Remove the current rooms from the shopping cart //
			$cart->deleteFromCart(8, $items[8]['qty']);
			// If there is a number of nights set, multiply the cost of the room by the number of rooms, and the number of nights //
			if (isset($items[22]) && $items[22]['qty'] > 0) {$cost*= $items[8]['qty'] * $items[22]['qty'];}
			// If there is no nights set, multiply the cost of the room by the number of rooms //
			else {$cost*= $items[8]['qty'];}
			// Update the cart with the new room cost //
			$cart->addToCart(8, $items[8]['qty'], $cost, $items[8]['name']); 
		}
	}
	elseif ($item == '25') {
		if (isset($items[$item])) {$cart->deleteFromCart(25, $items[$item]['qty']);}
		if ($qty) {
			$cost = $items['subTotal'] * 0.1;
			$cart->addToCart($item, $qty, $cost, $name);
		}		
	}
	$_SESSION["cart"] = $cart->getCart();
	if ($item != '25' && $item != '26') {
		include("viewcart.php");
		//echo "<hr>";
		//print_r($_SESSION["cart"]);
	}
	else {echo "Thanks! Your total has been updated to $", number_format($_SESSION["cart"]['grandTotal'], 2);}
	
?>

I'm particularly suspicious of the lines highlighted in red, where I felt I needed to update the session variable mid-execution.

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

This script is called on pretty much every change of the store form controls.

Here is the shopping cart class, as well, if it helps:

Code:
<?php
	class Shopping_Cart {
		private $cart;

		function __construct($cart="") {$this->cart = $cart;}

		function getCart() {return $this->cart;}
		
		function updateTotals () {
			foreach ($this->cart as $item) {
				if ($item['name'] != 'Rooms') {$total += $item['cost'] * $item['qty'];}
				else {$total += $item['cost'];}
			}
			$this->cart['subTotal'] = $total;
			$this->cart['tax'] = $total * 0.13;
			$this->cart['fee'] = ($this->cart['subTotal'] + $this->cart['tax']) * 0.03;
			$this->cart['grandTotal'] = $this->cart['subTotal'] + $this->cart['tax'] + $this->cart['fee'];
		}
		function updateDates ($depart, $return) {
			$this->cart['DoD'] = $depart;
			$this->cart['DoR'] = $return;
		}
   
		function addToCart($item, $qty, $cost, $name) {
			if(isset($this->cart[$item])) {$this->cart[$item]['qty']+=$qty;}
			else {$this->cart[$item]['qty'] = $qty;}      
			$this->cart[$item]['cost']=$cost;
			$this->cart[$item]['name']=$name;
			$this->updateTotals();
		}

		function deleteFromCart($item, $qty) {
			if(isset($this->cart[$item])) {
				$this->cart[$item]['qty']-= $qty;
				if($this->cart[$item]['qty'] == 0) {unset($this->cart[$item]);}
			}
		}
   
	}
?>

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
if there are, potentially, multiple updates via ajax that can happen quickly then you may get race conditions where the session data is not fully save before the session file is reopened by the next request.

two approaches to this.

1. rearchitect your solution to avoid the race condition.
2. use a database to store your session data rather than a file.

even with item 2, you need to be careful not to create race conditions on read/writes. add some locks and lock checks to be sure.
 
You seem to be updating the session many many times, and then immediately reading from it. Why all the updates?
You just updated the session, you have the same information in your $items, no need to attempt too read the session immediately after.

If the session is large it can take some time to properly write the session. If the session hasn't finished writing before you read from it you will get incomplete or corrupted data.

Since you are also using ajax to run these with all that saving onto the session I'm pretty sure you are falling into a race condition.

Where you are attempting to read a session you haven't finished writing yet. Or one Ajax calls writes to the session and immediately a second one overwrites it. So the first call now lost its data and has data from the second Ajax call instead.




----------------------------------
Phil AKA Vacunita
----------------------------------
Ignorance is not necessarily Bliss, case in point:
Unknown has caused an Unknown Error on Unknown and must be shutdown to prevent damage to Unknown.

Behind the Web, Tips and Tricks for Web Development.
 
Thank you vacunita and jpadie. I was thinking the same thing after finding the above chipmunk ninja link on google. I am going to be taking whatever steps I can to "ease the load" on the session handler, and I will report back if this corrects our problem. (Maybe - it is so intermittent, I am not sure how I will be able to tell without it failing again)

Thanks again for your insight. Users like you make tek-tips a forum worth visiting.

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
Thanks for your thoughts, David. It's a pleasure being able to help.

Do post back if you need more assistance.
 
DLayzell,
On behalf of the guys: It's usual to leave a star (click the thank XXX for this valuable post)
It would be interesting to see how you manage your way out of this apparant race condition. without looking at your code or app in depth I see it as:
1. Generation of a page (which might do some session work) but at the very least has some Ajax functionality in it. By The time you can do the Ajax stuff I would think that the webserver has done its think and closed the session file.
2. As a session is per user if you like no one else will be able to to update the session data
Are we saying that someone is "clicking" the Ajax request really quickly and therfore making multiple simultainous updates to the session file?
(I can't get to the chipmunk link at the moment, it's not complete).
If this is the case have a look at the concept of optimistic locking which might help you out. Optimistic locking is a protcol you can implement tha will detect if someone has updated the data since you read it.
 
Just had a chance to read the article.
It seems to saying that there is a mutex on the session file to prevent two processs trying to access the file. In any case you can only have one writer to a file so that should be ok. I think what it is saying is because the order of "updates" the the session file in unpredictable you have to be able to control it yourself (which seems reasonable). I assume your ajax call is involced by a click in which case I would disable the button until the call come back. If your ajax call happens on its own that would more difficult to handle.
So going back to my last post I would think you might needto look at optimistoc locking to address the data integrity issue. You could move to a DB but the issue is really about the data that a process reads, then potentialy hangs around for a while, then tries to update the data expecting it to look the same as it did when you read it. It's an interesting problem to address is a discontected world. I think using the serialisation aspects of a database would help here because of the disconnected nature.
 
Hi ingresman,

Yes, I am aware of the star system - vacunita has earned a handful or two from me since I joined tek-tips. :)

The annoyance with this issue is it's infrequency. I am unable to duplicate the symptoms consistently to confirm it's resolution. I'm intending to see a period of time where I do not see this issue before I hand out the stars.

Since the site is in production, for the short term, I have added a small delay after the session updates to allow the handler more time to complete it's work. (The speed of execution isn't too much of a concern at this point)

For the long run, I have sandboxed the site, and am working towards reducing (or eliminating) the number of session updates in this script.

To answer your question, "Are we saying that someone is "clicking" the Ajax request really quickly and therfore making multiple simultainous updates to the session file?"[/code], my understanding is that there are two possible causes. This, and the possibility that I am attempting to read the session data before it has completed writing. I hope to polish up the code in both areas. I agree with vacunita that there is some unnecessary bloat in this code.

If you are interested in the chipmunk ninja link, if you copy and paste it (including the "@" sign") into your browser, that should bring up the page we are looking at.

I will see what I can find on the topic of optimistic locking. It certainly can't hurt to further my understanding of what is going on behind the scenes. Thank you for the tip.

MORE

I've just caught your latest post, and this: "I assume your ajax call is involced by a click in which case I would disable the button until the call come back." is something I had been considering, as well. I'd say it's good policy to control the user input as much as possible, so I will work towards implementing this. Thanks again.

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
Yes appreciate the delay on stars !, I must admit I do think the race condition is a red herring as the session handlers in PHP seem to use a mutex, so it looks to be a serialisation issue with the sequende of updates). I haven't looked at the source code aroud this but it seems wise. A mutex is just a switch really to say "I've got it", it it sometimes called a semaphore, kinf of similar to a botton in a race (oops that word again !). The interesting thing is what if two process try to get the mutex at the same time?, that's an issue!
What would be interesting would be to dump the session contents to a trace file to see what is being read.
The optimstic locking (it comes from the db world), works like this:
1. Each row has a version colum (lets say a PHP variable in session called $version)
2. When you read the data you also read the version.
3. you do your stuff with the data
4. You update the data with the new values and set the version up by 1. Then the crucial part the where clause in the SQL the update has to say where versiion=:eek:rignal_version number.
So if the update fails (usualy) it will be because someone has updated the data (and also incremented the version) so the data is stale i.e the data is not what you thought it was now.
This works in a web app as you in effect hold the version number in "state" on the browser. The origianl page generation script would embed the versin umber in the page as a helper or in a static page an extra ajax call to simply get the version would be made. If the ajax fails because the version is incorrect it can then reissue the update (with a new version number) and hopefully update the session correctly. If you see what I mean!
I think to get it to work correctly will requie a little twaeaking of the protocol but it will give you a way of serialising the updates (i.e. doing them in the correct order)
I'll be thinking of this as I'm driving home! I'm off site tommorrow so don't think I've gone quiet on you!
Final question, do the corrupting updates look ok? i.e. would you expect that data if it executed correcty or is it garbage?
 
imo, the best solution for this problem, given that this is a cart, is to store the cart in a database. then you do not need to store stuff in a session file.

but in any case, consider adding a token to each request. no two ajax requests may use the same token else the duplicate is ignored. a new token is returned with each successful response and is then used by the next operation. idem-potent requests do not require a token.

This type of token is often known as a Nonce (a Number used ONCE). I have posted nonce implementations in this forum before.

and to extend this to your precise environment, which seems to be hotel/room booking, beware: i don't think a shopping cart functionality is a perfect fit (but who knows, it may be in your case). whenever I have built room booking systems (hotels and gites etc) I have always rolled my own reservation system. in particular because my clients have wanted to be able to 'reserve' a room for a period of time during which they waited for the deposit to reach them.


 
Hello, and thanks again for your input, everyone.

Am I mistaken, or are the last two posts offering ~close~ to the same suggestion? (Not a problem; it is actually reassuring, if that's the case) Simply put, a mechanism to uniquely identify each request/response, ensuring they are completed in sequence?

@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.

And to clarify, this is not a reservation system; it's ultimate purpose is to collect the user selections so that a 3rd, human party can review the order, and take action on purchasing/scheduling/reservation.

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
very quick question. Is there enough disc space where your session gets held?
 
Good question - I'm assuming so, the server has a huge amount of disc space, but I will confirm that there are no quotas or restrictions (as well as enough free space) with the admin when he returns to the office.

David Layzell, A+, Project+
Computer Network Engineering Grad
5 years SysAdmin, 11 years hobbyist Dev
 
Am I mistaken, or are the last two posts offering ~close~ to the same suggestion? (Not a problem; it is actually reassuring, if that's the case) Simply put, a mechanism to uniquely identify each request/response, ensuring they are completed in sequence?

the same end result, yes. different approaches. the Nonce can be made more granular so that non-idempotent operations can still be allowed asynchronously if they are guaranteed not to affect the previous operation. This can only be achieved through using a db to store the cart data (which is, imo, a no brainer).

race conditions tend to be most prevalent when testing on local machines. remote interactions with the webservers tend to be so slow, and script execution/disk access so quick, that race conditions are less usual in real world scenarios.

If you like I can write a script that will guarantee race condition free session management, even when using file based sessions. It comes at the price of a small speed hit.
 
Hi jpadie,

Thank you for the generous offer, but I would not ask that of you, or deny myself the opportunity to get my hands dirty, and perhaps habitualize some better coding techniques along the way.

Also, (especially) after reading some of your posts to other users, I absolutely trust your instinct on db vs file session management. This would in fact, greatly cut down on the amount of work the code needs to do (in addition to being a better fit for this application), so I think that is inarguably the route to go. Now, to get the time to overhaul and test the system.

Regards,

Dave

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

let me know if you change your mind. it's only a few lines of script to cure.
 
Good Day,

My problem persists, but I have suggested, and been given approval to re-architect the application. This time around, I will be ultra cautious of race conditions, and in ensuring that asynchronous requests are serialized.

Thanks to all for their input.

Regards,

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