Reputation: 175
I have php scripts that I have to run on linux as well as on Windows servers. I want to use the same scripts without doing any modifications for those 2 environments.
Theses scripts will be scheduled with cron (on linux) and with the windows scheduler (or other, I don't care for now) for my Windows environment.
However, some of theses scripts might take several minutes to complete. I just want to prevent the same script from being launched by the scheduler (cron or the windows's one) before it has finished the last time it was launched.
I'm not sure how to do that.. I want to be sure that the "lock" is released if something goes wrong during the execution, so it is launched again the next time without human intervention.
Maybe with a flock on a dummy file would do the trick, but i'm not sure how to do that.
I also have a MySQL database on theses servers. I thought maybe using a lock on the database side.
1- Start a transaction
2- Insert script name in a table.
3- execution of the script.
4- If successful then delete the row and commit the transaction or simply rollback;
If the script name is in the table, I then could prevent it from running. If the script execution fails, then Mysql will automatically rollback the transaction so the row does not appear the next time the script is called.
But, in a transaction, is there a way for other connections see the uncommited data? if yes, how?
I also thought using a lock on the row if it is impossible to use the rollback thing..
1- Insert script name in a table if it doesn't already exists.
2- Start a transaction.
2- Select * from Table where script_name FOR UPDATE.
3- execution of the script.
4- If successful then release the lock (rollback or commit).
But my major problem here is with Mysql. Select FOR UPDATE hang until the previous lock is released or if the 50 seconds timeout(innodb_lock_wait_timeout variable) is elapsed. I would like Mysql to tell me right on the spot that my row is locked without affecting the whole Database. That is because the innodb_lock_wait_timeout variable is a global one (not a session one). Is there another variable that mimic the NO_WAIT clause that is available in Oracle?
Or should I let the script hang 50 seconds without any problem?
What is the best approach on that as I am a php newbie and I don't want to cause any problems on the server.
Maybe I have another option that I didn't see..
Upvotes: 2
Views: 3884
Reputation: 4587
Alternatively you could use LOCK files. The idea is simple: if a script S is executed, it will first check the existence of a certain (unique) file, say S.lock
:
If the file exists, S will terminate.
Otherwise, it will create it. If S exits, the file will be deleted.
Upvotes: 0
Reputation:
Modern year 2017 Answer:
There are many ways to implement locks in PHP.
locker.inc.php:
<?php
class Locker
{
private $_filename;
private $_fh = NULL;
public function __construct( string $filename )
{
$this->_filename = $filename;
}
public function __destruct()
{
$this->unlock();
}
/**
* Attempt to acquire an exclusive lock. Always check the return value!
* @param bool $block If TRUE, we'll wait for existing lock release.
* @return bool TRUE if we've acquired the lock, otherwise FALSE.
*/
public function lock( bool $block = TRUE )
{
// Create the lockfile if it doesn't exist.
if( ! is_file( $this->_filename ) ) {
$created = @touch( $this->_filename );
if( ! $created ) {
return FALSE; // no file
}
}
// Open a file handle if we don't have one.
if( $this->_fh === NULL ) {
$fh = @fopen( $this->_filename, 'r' );
if( $fh !== FALSE ) {
$this->_fh = $fh;
} else {
return FALSE; // no handle
}
}
// Try to acquire the lock (blocking or non-blocking).
$lockOpts = ( $block ? LOCK_EX : ( LOCK_EX | LOCK_NB ) );
return flock( $this->_fh, $lockOpts ); // lock
}
/**
* Release the lock. Also happens automatically when the Locker
* object is destroyed, such as when the script ends. Also note
* that all locks are released if the PHP process is force-killed.
* NOTE: We DON'T delete the lockfile afterwards, to prevent
* a race condition by guaranteeing that all PHP instances lock
* on the exact same filesystem inode.
*/
public function unlock()
{
if( $this->_fh !== NULL ) {
flock( $this->_fh, LOCK_UN ); // unlock
fclose( $this->_fh );
$this->_fh = NULL;
}
}
}
testlock.php:
<?php
require_once( 'locker.inc.php' );
$locker = new Locker( 'test.lock' );
echo time() . ": acquiring lock...\n";
$is_locked = $locker->lock( TRUE ); // TRUE = blocking
if( $is_locked ) { // ALWAYS check this return value
echo time() . ": we have a lock...\n";
sleep(10); // hold the lock for 10 seconds
// manually unlock again, but we don't have
// to do this since it also happens when
// the $locker object is destroyed (i.e.
// when the script ends).
$locker->unlock();
} else {
echo time() . ": failed to get lock...\n";
}
You can change the TRUE to FALSE in the test script, if you don't want your other scripts to wait in queue for the lock to be released.
So the choice is yours:
Upvotes: 4
Reputation: 442
Why not using the old fashioned semaphore, it's just made for exactly this. I'm sure there are implementations for Windows available as well, or PHP simply being compatible:
if ($theSemaphore = sem_get("123456",1)) { // this "1" ensures that there is nothing parallel
if (sem_acquire($theSemaphore)) { // this blocks the execution until other processes or threads are finished
<put your code to serialize here>
sem_release($theSemaphore); // This should be called only if sem_acquire() succeeds
}
}
Within the Apache threading environment this is working fine, also within PHP-CLI and mixed. In case a process dies unexpected, the semaphore is invalid and van be acquired again. The semaphores are implemented "atomic" so prevent race conditions during lock.
A nice description based on toilets is here
Upvotes: 2
Reputation: 28723
I solved this problem using... sockets. It you can enable php_sockets
extention then try it. Here is code sample:
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $socket) {
throw new Exception("can't create socket: ".socket_last_error($socket));
}
## set $port to something like 10000
## hide warning, because error will be checked manually
if (false === @socket_bind($socket, '127.0.0.1', $port)) {
## some instanse of the script is running
return false;
} else {
## let's do your job
return $socket;
}
Bind socket on specific $port
is safe operation for concurent execution. Operation system will make sure that there is no other process which bound socket to same port. You just need to check return value.
If script crashes then operation system will unbind the port automatically.
This also can be used in any language. I've tested it wide on perl and php based projects. It stoped parallel execution even when we've added script twice in crontab by mistake.
Upvotes: 10
Reputation: 19990
Check for a lock file (ie. "script_running.lock") pseudocode:
if file exists exit
else
create the file
run the rest of the script
unlink the file when script is done
Upvotes: 1