1

I have a script that does an update function live. I would move it to a cron job, but due to some limitations I'd much rather have it live and called when the page loads.

The issue is that when there is a lot of traffic, it doesn't quite work, because it's using some random and weighted numbers, so if it's hit a bunch of times, the results aren't what we want.

So, question is. Is there a way to tell how many times a particular script is being accessed? And limit it to only once at a time?

Thank you!

dzm
  • 22,844
  • 47
  • 146
  • 226

3 Answers3

6

The technique you are looking for is called locking.

The simplest way to do this is to create a temporary file, and remove it when the operation has completed. Other processes will look for that temporary file, see that it already exists and go away.

However, you also need to take care of the possibility of the lock's owner process crashing, and failing to remove the lock. This is where this simple task seems to become complicated.

File based locking solutions

PHP has a built-in flock() function that promises a OS-independent file-based locking feature. This question has some practical hints on how to use it. However, the manual page warns that under some circumstances, flock() has problems with multiple instances of PHP scripts trying to get a lock simultaneously. This question seems to have more advanced answers on the issue, but they are all not trivial to implement.

Database based locking

The author of this question - probably scared away by the complications surrounding flock() - asks for other, not file-based locking techniques and comes up with MySQL's GET_LOCK(). I have never worked with it, but it looks pretty straightforward - if you use mySQL anyway, it may be worth a shot.

Damn, this issue is complicated if you want to do it right! Interested to see whether anything more elegant comes up.

Community
  • 1
  • 1
Pekka
  • 442,112
  • 142
  • 972
  • 1,088
  • 2
    You can register a function with register_shutdown_function() to clean up your locks. It gets called even if the script crashes from a fatal error. – mellowsoon Oct 21 '10 at 22:08
3

You could do something like this (requires PHP 5):

if(file_get_contents("lock.txt") == "unlocked"){
  // no lock present, so place one
  file_put_contents("lock.txt", "locked");

  // do your processing
  ...

  // remove the lock
  file_put_contents("lock.txt", "unlocked", LOCK_EX);
}

file_put_contents() overwrites the file (as opposed to appending) by default, so the contents of the file should only ever be "locked" or nothing. You'll want to specify the LOCK_EX flag to ensure that the file isn't currently open by another instance of the script when you're trying to write to it.

Obviously, as @Pekka mentioned in his answer, this can cause problems if a fatal error occurs (or PHP crashes, or the server crashes, etc, etc) in between placing the lock and removing it, as the file will simply remain locked.

Brad Westness
  • 1,532
  • 13
  • 18
  • Brad, thank you. What if we wanted a lock for multiple products? Would you create a file for each product? like product_id-lock.txt or would you maybe move it to a db table 'product_locks' with records for each product? – dzm Oct 21 '10 at 18:07
  • 2
    I would even go further than this, and only create / delete an empty lock file. That eliminates the possibility of race conditions – Pekka Oct 21 '10 at 22:12
1

Start the script with a sql query that tests if a timestamp field from database is over 1 day ago. If yes - write current timestamp and execute script.

pseudo-sql to show the idea:

UPDATE runs SET lastrun=NOW() WHERE lastrun<NOW()-1DAY

(different sql servers will require different changes in the above)

Check how many rows were updated to see if this script run got the lock. Do not make it with two queries - SELECT and UPDATE because it won't be atomic anymore.

naugtur
  • 16,827
  • 5
  • 70
  • 113
  • Using update seems very smart. Thank You. In SQL SERVER, query can look like this (added IDrun to possibility of choosing more scripts): `UPDATE [runs] SET [lastrun] = GETDATE() WHERE [IDrun]= 1 AND [lastrun] < DATEADD(dd, -1, GETDATE()) ` – jir Jun 14 '17 at 13:49
  • At the end of the script, You can run `UPDATE [runs] SET [lastrun] = DATEADD(dd, -1, GETDATE()) WHERE [IDrun]= 1 ` to release "lock" so script can run again. – jir Jun 14 '17 at 14:06