Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Most reliable & safe method of preventing race conditions in PHP

Tags:

I need to use mutexes or semaphores in PHP, and it scares me. To clarify, I'm not scared of writing deadlock-free code that synchronizes properly or afraid of the perils of concurrent programming, but of how well PHP handles fringe cases.

Quick background: writing a credit card handler interface that sits between the users and the 3rd party credit card gateway. Need to prevent duplicate requests, and already have a system in place that works, but if the user hits submit (w/out JS enabled so I can't disable the button for them) milliseconds apart, a race condition ensues where my PHP script does not realize that a duplicate request has been made. Need a semaphore/mutex so I can ensure only one successful request goes through for each unique transaction.

I'm running PHP behind nginx via PHP-FPM with multiple processes on a multi-core Linux machine. I want to be sure that

  1. semaphores are shared between all php-fpm processes and across all cores (i686 kernel).
  2. php-fpm handles a PHP process crash while holding a mutex/semaphore and releases it accordingly.
  3. php-fpm handles a session abort while holding a mutex/semaphore and releases it accordingly.

Yes, I know. Very basic questions, and it would be foolish to think that a proper solution doesn't exist for any other piece of software. But this is PHP, and it was most certainly not built with concurrency in mind, it crashes often (depending on which extensions you have loaded), and is in a volatile environment (PHP-FPM and on the web).

With regards to (1), I'm assuming if PHP is using the POSIX functions that both these conditions hold true on a SMP i686 machine. As for (2), I see from briefly skimming the docs that there is a parameter that decides this behavior (though why would one ever want PHP to NOT release a mutex is the session is killed I don't understand). But (3) is my main concern and I don't know if it's safe to assume that php-fpm properly handles all fringe cases for me. I (obviously) don't ever want a deadlock, but I'm not sure I can trust PHP to never leave my code in a state where it cannot obtain a mutex because the session that grabbed it was either gracefully or ungracefully terminated.

I have considered using a MySQL LOCK TABLES approach, but there's even more doubt there because while I trust the MySQL lock more than the PHP lock, I fear if PHP aborts a request (with*out* crashing) while holding the MySQL session lock, MySQL might keep the table locked (esp. because I can easily envision the code that would cause this to take place).

Honestly, I'd be most comfortable with a very basic C extension where I can see exactly what POSIX calls are being made and with what params to ensure the exact behavior I want.. but I don't look forward to writing that code.

Anyone have any concurrency-related best practices regarding PHP they'd like to share?

like image 687
Mahmoud Al-Qudsi Avatar asked Feb 15 '12 07:02

Mahmoud Al-Qudsi


2 Answers

In fact, i think there is no need for a complex mutex / semaphore whatever solution.

Form keys stored in a PHP $_SESSION are all you need. As a nice side effect, this method also protects your form against CSRF attacks.

In PHP, sessions are locked by aquiring a POSIX flock() and PHP's session_start() waits until the user session is released. You just have to unset() the form key on the first valid request. The second request has to wait until the first one releases the session.

However, when running in a (not session or source ip based) load balancing scenario involving multiple hosts things are getting more complicated. For such a scenario, i'm sure you will find a valuable solution in this great paper: http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

I reproduced your use case with the following demonstration. just throw this file onto your webserver and test it:

<?php session_start(); if (isset($_REQUEST['do_stuff'])) {   // do stuff   if ($_REQUEST['uniquehash'] == $_SESSION['uniquehash']) {     echo "valid, doing stuff now ... "; flush();     // delete formkey from session     unset($_SESSION['uniquehash']);     // release session early - after committing the session data is read-only     session_write_close();     sleep(20);       echo "stuff done!";   }   else {     echo "nope, {$_REQUEST['uniquehash']} is invalid.";   }      } else {   // show form with formkey   $_SESSION['uniquehash'] = md5("foo".microtime().rand(1,999999)); ?> <html> <head><title>session race condition example</title></head> <body>   <form method="POST">     <input type="hidden" name="PHPSESSID" value="<?=session_id()?>">     <input type="text" name="uniquehash"        value="<?= $_SESSION['uniquehash'] ?>">     <input type="submit" name="do_stuff" value="Do stuff!">   </form> </body> </html> <?php } ?> 
like image 161
Kaii Avatar answered Oct 12 '22 09:10

Kaii


An interesting question you have but you don't have any data or code to show.

For 80% of cases the chances of anything nasty happening because of PHP itself are virtually zero if you follow the standard procedures and practices regarding stopping users from submitting forms multiple times, which applies to nearly every other setup, not just PHP.

If you're the 20% and your environment demands it, then one option is using message queues which I'm sure you are familiar with. Again, this idea is language agnostic. Nothing to do with languages. Its all about how data moves around.

like image 38
zaf Avatar answered Oct 12 '22 09:10

zaf