1<?php 2 3namespace ComboStrap; 4 5 6use dokuwiki\Search\Indexer; 7 8/** 9 * Adapted from the {@link Indexer::lock()} 10 * because the TaskRunner does not run serially 11 * Only the indexer does 12 * https://forum.dokuwiki.org/d/21044-taskrunner-running-multiple-times-eating-the-memory-lock 13 * 14 * Example with debug 15 * ``` 16 * ComboLockTaskRunner(): Trying to get a lock 17 * ComboLockTaskRunner(): Locked 18 * runIndexer(): started 19 * Indexer: index for web:browser:selection up to date 20 * runSitemapper(): started 21 * runSitemapper(): finished 22 * sendDigest(): started 23 * sendDigest(): disabled 24 * runTrimRecentChanges(): started 25 * runTrimRecentChanges(): finished 26 * runTrimRecentChanges(1): started 27 * runTrimRecentChanges(1): finished 28 * ComboDispatchEvent(): Trying to get a lock 29 * ComboDispatchEvent(): Locked 30 * ComboDispatchEvent(): Lock Released 31 * ComboLockTaskRunner(): Lock Released 32 * 33 */ 34class Lock 35{ 36 private string $lockName; 37 private string $lockFile; 38 /** 39 * @var mixed|null 40 */ 41 private $perm; 42 private int $timeOut = 5; 43 /** 44 * @var false|resource 45 */ 46 private $filePointer = null; 47 48 49 /** 50 * @param string $name 51 */ 52 public function __construct(string $name) 53 { 54 $this->lockName = $name; 55 global $conf; 56 $this->lockFile = $conf['lockdir'] . "/_{$this->lockName}.lock"; 57 $this->perm = $conf['dperm'] ?? null; 58 } 59 60 public static function create(string $name): Lock 61 { 62 return new Lock($name); 63 } 64 65 /** 66 * @throws ExceptionTimeOut - with the timeout 67 */ 68 function acquire(): Lock 69 { 70 $run = 0; 71 /** 72 * The flock function follows the semantics of the Unix system call bearing the same name. 73 * Flock utilizes ADVISORY locking only; that is: 74 * * other processes may ignore the lock completely it only affects those that call the flock call. 75 * 76 * * LOCK_SH means SHARED LOCK. Any number of processes MAY HAVE A SHARED LOCK simultaneously. It is commonly called a reader lock. 77 * * LOCK_EX means EXCLUSIVE LOCK. Only a single process may possess an exclusive lock to a given file at a time. 78 * 79 * ie if the file has been LOCKED with LOCK_SH in another process, 80 * * flock with LOCK_SH will SUCCEED. 81 * * flock with LOCK_EX will BLOCK UNTIL ALL READER LOCKS HAVE BEEN RELEASED. 82 * 83 * When the file is closed, the lock is released by the system anyway. 84 */ 85 // LOCK_NB to not block the process 86 while (!$this->getLock()) { 87 sleep(1); 88 /** 89 * Old lock ? More than 10 minutes run 90 */ 91 if (is_file($this->lockFile) && (time() - @filemtime($this->lockFile)) > 60 * 10) { 92 if (!@unlink($this->lockFile)) { 93 throw new ExceptionRuntimeInternal("Removing the lock failed ($this->lockFile)"); 94 } 95 } 96 $run++; 97 if ($run >= $this->timeOut) { 98 throw new ExceptionTimeOut("Unable to get the lock ($this->lockFile) for ($this->timeOut) seconds"); 99 } 100 } 101 if ($this->perm) { 102 chmod($this->lockFile, $this->perm); 103 } 104 register_shutdown_function([Lock::class, 'shutdownHandling'], $this->lockName); 105 return $this; 106 107 } 108 109 /** 110 * 111 * A function that is called when the process shutdown 112 * due to time exceed for instance that cleans the lock created. 113 * 114 * https://www.php.net/manual/en/function.register-shutdown-function.php 115 * 116 * Why ? 117 * The lock are created in the `before` of the the task runner event 118 * and deleted in the `after` of the task runner event 119 * If their is an error somewhere such as as a timeout, the lock 120 * is not deleted and there is no task runner anymore for 5 minutes. 121 * 122 * @param $name - the lock name 123 * @return void 124 */ 125 public static function shutdownHandling($name) 126 { 127 print "Lock::shutdownHandling(): Deleting the lock $name" . NL; 128 Lock::create($name)->release(); 129 } 130 131 /** 132 * Release the lock 133 * and the resources 134 * (Need to be called in all cases) 135 */ 136 function release() 137 { 138 if ($this->filePointer !== null) { 139 fclose($this->filePointer); 140 $this->filePointer = null; 141 } 142 if (file_exists($this->lockFile)) { 143 unlink($this->lockFile); 144 } 145 } 146 147 public function isReleased(): bool 148 { 149 return !file_exists($this->lockFile); 150 } 151 152 public function isLocked(): bool 153 { 154 return file_exists($this->lockFile); 155 } 156 157 public function setTimeout(int $int): Lock 158 { 159 $this->timeOut = $int; 160 return $this; 161 } 162 163 private function getLock(): bool 164 { 165 /** 166 * We test also on the file because 167 * on some operating systems, flock() is implemented at the process level. 168 * 169 * ie when using a multithreaded server API you may not be able to rely on flock() 170 * to protect files against other PHP scripts running in parallel threads of the same server instance 171 */ 172 if (file_exists($this->lockFile)) { 173 return false; 174 } 175 176 if ($this->filePointer === null) { 177 $mode = "c"; // as specified in the doc 178 $this->filePointer = fopen($this->lockFile, $mode); 179 } 180 /** 181 * LOCK_EX: exclusive lock 182 * LOCK_NB: to not wait 183 */ 184 return flock($this->filePointer, LOCK_EX | LOCK_NB); 185 } 186 187} 188