У меня есть PHP-скрипт, который используется для запроса API и загрузки некоторой информации JSON / вставки этой информации в базу данных MySQL, мы назовем этот скрипт A.php. Мне нужно запускать этот скрипт несколько раз в минуту, желательно столько раз в минуту, сколько я могу, не позволяя двум экземплярам запускаться в одно и то же точное время или с любым перекрытием. Моим решением было создать scriptB.php и выполнить одноминутное задание cron. Вот исходный код scriptB.php ...
function next_run()
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "http://somewebsite.com/scriptA.php");
curl_exec($curl);
curl_close($curl);
unset($curl);
}
$i = 0;
$times_to_run = 7;
$function = array();
while ($i++ < $times_to_run) {
$function = next_run();
sleep(3);
}
Мой вопрос на данном этапе заключается в том, как работает cURL при использовании в цикле, запускает ли этот код scriptA.php и ТОГДА, когда он закончит его загрузку в этот момент, запускает следующий запрос cURL? Имеет ли значение 3-секундный сон вообще, или он будет работать буквально так же быстро, как время, необходимое для выполнения каждого запроса cURL. Моя цель - установить время для этого скрипта и запустить его как можно больше раз за одну минуту без одновременного запуска двух итераций. Я не хочу включать инструкцию сна, если она не нужна. Я считаю, что происходит то, что cURL будет запускать каждый запрос после завершения последнего, если я ошибаюсь, могу ли я каким-то образом проинструктировать его сделать это?
@TimMorton - это не синхронно, когда вы запускаете его в разных заданиях cron. Но у меня есть идеальное решение для этого github.com/ArtisticPhoenix/MISC/blob/master/ProcLock.php






I need to run this script multiple times as minute, preferably as many times in a minute that I can without allowing two instances to run
Вам повезло, поскольку я написал класс, который справляется именно с такими вещами. Вы можете найти это на моем гитхабе здесь
https://github.com/ArtisticPhoenix/MISC/blob/master/ProcLock.php
Я также скопирую полный код в конце этого поста.
Основная идея - создать файл, в этом примере я назову его afile.lock. В этом файле записывается PID или идентификатор текущего процесса, который запускается cron. Затем, когда cron пытается снова запустить процесс, он проверяет этот файл блокировки и видит, запущен ли процесс PHP, использующий этот PID.
В качестве бонуса время изменения файла блокировки может быть использовано сценарием (чей PID мы отслеживаем) как способ завершения работы в случае, если файл не обновляется, например: если cron остановлен, или если файл блокировки удаляется вручную, вы можете настроить его таким образом, чтобы запущенный скрипт обнаружил это и самоуничтожился.
Таким образом, вы можете не только запретить запуск нескольких экземпляров, но и указать текущему экземпляру, что он умер, если cron выключен.
Основное использование выглядит следующим образом. В cron-файле запускается "рабочий"
//define a lock file (this is actually optional)
ProcLock::setLockFile(__DIR__.'/afile.lock');
try{
//if you didn't set a lock file you can pass it in with this method call
ProcLock::lock();
//execute your process
}catch(\Exception $e){
if ($e->getCode() == ProcLock::ALREADY_LOCKED){
//just exit or what have you
}else{
//some other exception happened.
}
}
Это в основном так просто.
Затем в запущенном процессе вы можете время от времени проверять (например, если у вас есть цикл, который что-то запускает)
$expires = 90; //1 1/2 minute (you may need a bit of fudge time)
foreach($something as $a=>$b){
$lastAccess = ProcLock::getLastAccess()
if (false == $lastAccess || $lastAccess + $expires < time()){
//if last access is false (no lock file)
//or last access + expiration, is less then the current time
//log something like killed by lock timeout
exit();
}
}
В основном это говорит о том, что либо файл блокировки был удален во время выполнения процесса, либо cron не удалось обновить его до истечения срока действия. Итак, мы даем ему 90 секунд, и cron должен обновлять файл блокировки каждые 60 секунд. Как я уже сказал, файл блокировки обновляется автоматически, если он обнаруживается при вызове lock(), который вызывает canLock(), который, если он возвращает true, что означает, что мы можем заблокировать процесс, потому что он в настоящее время не заблокирован, затем он запускает touch($lockfile), который обновляет mtime (измененное время).
Очевидно, вы можете самостоятельно убить процесс таким образом, только если он активно проверяет время доступа и истечения срока действия.
Этот скрипт предназначен для работы как в Windows, так и в Linux. В Windows при определенных обстоятельствах файл блокировки не удаляется должным образом (иногда при нажатии ctrl + c в окне CMD), однако я приложил большие усилия, чтобы этого не произошло, поэтому файл класса содержит собственный register_shutdown_function, который запускается при завершении сценария PHP.
При запуске чего-либо с использованием ProcLoc в браузере обратите внимание, что идентификатор процесса всегда будет одним и тем же, независимо от вкладки, на которой он запущен. Поэтому, если вы откроете одну вкладку, которая заблокирована, а затем откроете другую вкладку, шкафчик процессов увидит ее. как тот же процесс, и позвольте ему снова заблокироваться. Чтобы правильно запустить его в браузере и проверить блокировку, это необходимо сделать с помощью двух отдельных браузеров, таких как crome и firefox. На самом деле он не предназначен для запуска в браузере, но я заметил одну особенность.
И последнее замечание: этот класс полностью статичен, так как у вас может быть только один идентификатор процесса для каждого запущенного процесса, что должно быть очевидно.
Сложные части
К счастью для вас, я уже потратил на это достаточно времени, чтобы сделать все эти вещи, это более общая версия оригинального сценария блокировки, который я сделал для своей работы, который мы успешно использовали таким образом в течение 3 лет для поддержания контроля над различными синхронными cron, все, от сканирования загрузки sFTP, очистки просроченных файлов до рабочих сообщений RabbitMq, которые выполняются в течение неопределенного периода времени.
В любом случае вот полный код, наслаждайтесь.
<?php
/*
(c) 2017 ArtisticPhoenix
For license information please view the LICENSE file included with this source code GPL3.0.
Proccess Locker
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses files to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
4 - when pid of lock does not match self::$_pid
==================================================================
Only one Lock per Process!
-note- when running in a browser typically all tabs will have the same PID
so the locking will not be able to tell if it's the same process, to get
around this run in CLI, or use 2 diffrent browsers, so the PID numbers are diffrent.
This class is static for the simple fact that locking is done per-proces, so there is no need
to ever have duplate ProcLocks within the same process
---------------------------------------------------------------
*/
final class {
/**
* exception code numbers
* @var int
*/
const DIRECTORY_NOT_FOUND = 2000;
const LOCK_FIRST = 2001;
const FAILED_TO_UNLOCK = 2002;
const FAILED_TO_LOCK = 2003;
const ALREADY_LOCKED = 2004;
const UNKNOWN_PID = 2005;
const PROC_UNKNOWN_PID = 2006;
/**
* process _key
* @var string
*/
protected static $_lockFile;
/**
*
* @var int
*/
protected static $_pid;
/**
* No construction allowed
*/
private function __construct(){}
/**
* No clones allowed
*/
private function __clone(){}
/**
* globaly sets the lock file
* @param string $lockFile
*/
public static function setLockFile( $lockFile ){
$dir = dirname( $lockFile );
if ( !is_dir( dirname( $lockFile ))){
throw new Exception("Directory {$dir} not found", self::DIRECTORY_NOT_FOUND); //pid directroy invalid
}
self::$_lockFile = $lockFile;
}
/**
* return global lockfile
*/
public static function getLockFile() {
return ( self::$_lockFile ) ? self::$_lockFile : false;
}
/**
* safe check for local or global lock file
*/
protected static function _chk_lock_file( $lockFile = null ){
if ( !$lockFile && !self::$_lockFile ){
throw new Exception("Lock first", self::LOCK_FIRST); //
}elseif ( $lockFile ){
return $lockFile;
}else{
return self::$_lockFile;
}
}
/**
*
* @param string $lockFile
*/
public static function unlock( $lockFile = null ){
if ( !self::$_pid ){
//no pid stored - not locked for this process
return;
}
$lockFile = self::_chk_lock_file($lockFile);
if (!file_exists($lockFile) || unlink($lockFile)){
return true;
}else{
throw new Exception("Failed to unlock {$lockFile}", self::FAILED_TO_UNLOCK ); //no lock file exists to unlock or no permissions to delete file
}
}
/**
*
* @param string $lockFile
*/
public static function lock( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
if ( self::canLock( $lockFile )){
self::$_pid = getmypid();
if (!file_put_contents($lockFile, self::$_pid ) ){
throw new Exception("Failed to lock {$lockFile}", self::FAILED_TO_LOCK ); //no permission to create pid file
}
}else{
throw new Exception('Process is already running[ '.$lockFile.' ]', self::ALREADY_LOCKED );//there is a process running with this pid
}
}
/**
*
* @param string $lockFile
*/
public static function getPidFromLockFile( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
if (!file_exists($lockFile) || !is_file($lockFile)){
return false;
}
$pid = file_get_contents($lockFile);
return intval(trim($pid));
}
/**
*
* @return number
*/
public static function getMyPid(){
return ( self::$_pid ) ? self::$_pid : false;
}
/**
*
* @param string $lockFile
* @param string $myPid
* @throws Exception
*/
public static function validatePid($lockFile = null, $myPid = false ){
$lockFile = self::_chk_lock_file($lockFile);
if ( !self::$_pid && !$myPid ){
throw new Exception('no pid supplied', self::UNKNOWN_PID ); //no stored or injected pid number
}elseif ( !$myPid ){
$myPid = self::$_pid;
}
return ( $myPid == self::getPidFromLockFile( $lockFile ));
}
/**
* update the mtime of lock file
* @param string $lockFile
*/
public static function canLock( $lockFile = null){
if ( self::$_pid ){
throw new Exception("Process was already locked", self::ALREADY_LOCKED ); //process was already locked - call this only before locking
}
$lockFile = self::_chk_lock_file($lockFile);
$pid = self::getPidFromLockFile( $lockFile );
if ( !$pid ){
//if there is a not a pid then there is no lock file and it's ok to lock it
return true;
}
//validate the pid in the existing file
$valid = self::_validateProcess($pid);
if ( !$valid ){
//if it's not valid - delete the lock file
if (unlink($lockFile)){
return true;
}else{
throw new Exception("Failed to unlock {$lockFile}", self::FAILED_TO_UNLOCK ); //no lock file exists to unlock or no permissions to delete file
}
}
//if there was a valid process running return false, we cannot lock it.
//update the lock files mTime - this is usefull for a heartbeat, a periodic keepalive script.
touch($lockFile);
return false;
}
/**
*
* @param string $lockFile
*/
public static function getLastAccess( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
clearstatcache( $lockFile );
if ( file_exists( $lockFile )){
return filemtime( $lockFile );
}
return false;
}
/**
*
* @param int $pid
*/
protected static function _validateProcess( $pid ){
$task = false;
$pid = intval($pid);
if (stripos(php_uname('s'), 'win') > -1){
$task = shell_exec("tasklist /fi \"PID eq {$pid}\"");
/*
'INFO: No tasks are running which match the specified criteria.
'
*/
/*
'
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
php.exe 5064 Console 1 64,516 K
'
*/
}else{
$cmd = "ps ".intval($pid);
$task = shell_exec($cmd);
/*
' PID TTY STAT TIME COMMAND
'
*/
}
//print_rr( $task );
if ($task){
return ( preg_match('/php|httpd/', $task) ) ? true : false;
}
throw new Exception("pid detection failed {$pid}", self::PROC_UNKNOWN_PID); //failed to parse the pid look up results
//this has been tested on CentOs 5,6,7 and windows 7 and 10
}
/**
* destroy a lock ( safe unlock )
*/
public static function destroy($lockFile = null){
try{
$lockFile = self::_chk_lock_file($lockFile);
self::unlock( $lockFile );
}catch( Exception $e ){
//ignore errors here - this called from distruction so we dont care if it fails or succeeds
//generally a new process will be able to tell if the pid is still in use so
//this is just a cleanup process
}
}
}
/*
* register our shutdown handler - if the script dies unlock the lock
* this is superior to __destruct(), because the shutdown handler runs even in situation where PHP exhausts all memory
*/
register_shutdown_function(array('\\Lib\\Queue\\ProcLock',"destroy"));
что-нибудь не так с $lock=fopen(__FILE__,"rb");flock($lock,LOCK_EX); register_shutdown_function(function()use(&$lock){flock($lock,LOCK_UN);});?
да, flock Exclusive помешает вам сопоставить PID, и вы не узнаете, умер ли процесс и оставил ли файл блокировки. В некоторых случаях это может произойти, например, при перезагрузке сервера. Активно проверяя PID на соответствие текущим запущенным процессам, мы можем исправить это автоматически. Кроме того, мы теряем возможность сообщить фоновой задаче, что она корректно завершается, когда мы закрываем задание cron. Например, если вам нужно перезагрузить воркера, вы просто остановите cron, подождите около 2 минут, проверьте список процессов с чем-то вроде ps aux | grep php, а затем, когда все рабочие самоуничтожатся
вы можете перезагрузиться без проблем, это дает вам средства связи с ними, которых обычно не хватает PHP. Еще немного при перезагрузке сервера. PHP не может запустить функцию выключения, в результате чего ваши файлы блокировки останутся на диске. Скорее всего, они все еще заблокированы EX, я не знаю после перезагрузки. Но, проверив PID, вы можете определить, использует ли процесс PHP сохраненный в файлах номер PID. Другими словами, все еще возможно оставить сервер в заблокированном состоянии после жесткой перезагрузки или других «пограничных» случаев.
preferably as many times in a minute that I can without allowing two instances to run at the same exact time or with any overlap. - тогда вам вообще не следует использовать cronjob, вы должны использовать демон. но если вам по какой-то причине необходимо использовать cronjob (например, если вы используете общую платформу веб-хостинга, которая не допускает демонов), угадайте, вы могли бы использовать хакерский режим сна для запуска одного и того же кода несколько раз в минуту?
* * * * * /usr/bin/php /path/to/scriptA.php
* * * * * sleep 10; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 20; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 30; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 40; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 50; /usr/bin/php /path/to/scriptA.php
должен запускать его каждые 10 секунд.
чтобы убедиться, что он не запускается в параллельном режиме, если предыдущее выполнение еще не завершено, добавьте это в начало scriptA
call_user_func ( function () {
static $lock;
$lock = fopen ( __FILE__, "rb" );
if (! flock ( $lock, LOCK_EX | LOCK_NB )) {
// failed to get a lock, probably means another instance is already running
die ();
}
register_shutdown_function ( function () use (&$lock) {
flock ( $lock, LOCK_UN );
} );
} );
и он просто умрет (), если другой экземпляр scriptA уже запущен. однако, если вы хотите, чтобы он дождался завершения предыдущего выполнения, вместо того, чтобы просто выйти, удалите LOCK_NB ... но это может быть опасно, если каждое или даже большинство выполнений используют более 10 секунд, вы ' у вас будет все больше и больше процессов, ожидающих завершения предыдущего выполнения, пока у вас не закончится оперативная память.
Что касается ваших вопросов о завитках,
My question at this point is to how cURL performs when used in a loop, does this code trigger scriptA.php and THEN once it has finished loading it at that point start the next cURL request, это правильно, curl ждет, пока страница полностью загрузится, обычно это означает, что весь scriptA завершен. (вы можете указать scriptA преждевременно завершить загрузку страницы с помощью функции fastcgi_finish_request (), если вы действительно хотите, но это необычно)
Does the 3 second sleep even make a difference or will this literally run as fast as the time it takes each cURL request to complete - да, сон сделает цикл медленнее на 3 секунды за итерацию.
My objective is to time this script and run it as many times as possible in a one minute window without two iterations of it being run at the same time - тогда сделайте его демоном, который никогда не завершает работу, а не cronjob.
I don't want to include the sleep statement if it is not needed. - не нужен.
I believe what happens is cURL will run each request upon finishing the last - это правильно.
локон синхронный. Он ждет ответа.