xref: /template/strap/ComboStrap/Lock.php (revision ea72c62952242fb769e2d6140561a54b2280afd1)
1a6d63b89Sgerardnico<?php
2a6d63b89Sgerardnico
3a6d63b89Sgerardniconamespace ComboStrap;
4a6d63b89Sgerardnico
5a6d63b89Sgerardnico
6a6d63b89Sgerardnicouse dokuwiki\Search\Indexer;
7a6d63b89Sgerardnico
8a6d63b89Sgerardnico/**
9a6d63b89Sgerardnico * Adapted from the {@link Indexer::lock()}
10a6d63b89Sgerardnico * because the TaskRunner does not run serially
11a6d63b89Sgerardnico * Only the indexer does
12a6d63b89Sgerardnico * https://forum.dokuwiki.org/d/21044-taskrunner-running-multiple-times-eating-the-memory-lock
13*ea72c629Sgerardnico *
14*ea72c629Sgerardnico * Example with debug
15*ea72c629Sgerardnico * ```
16*ea72c629Sgerardnico * ComboLockTaskRunner(): Trying to get a lock
17*ea72c629Sgerardnico * ComboLockTaskRunner(): Locked
18*ea72c629Sgerardnico * runIndexer(): started
19*ea72c629Sgerardnico * Indexer: index for web:browser:selection up to date
20*ea72c629Sgerardnico * runSitemapper(): started
21*ea72c629Sgerardnico * runSitemapper(): finished
22*ea72c629Sgerardnico * sendDigest(): started
23*ea72c629Sgerardnico * sendDigest(): disabled
24*ea72c629Sgerardnico * runTrimRecentChanges(): started
25*ea72c629Sgerardnico * runTrimRecentChanges(): finished
26*ea72c629Sgerardnico * runTrimRecentChanges(1): started
27*ea72c629Sgerardnico * runTrimRecentChanges(1): finished
28*ea72c629Sgerardnico * ComboDispatchEvent(): Trying to get a lock
29*ea72c629Sgerardnico * ComboDispatchEvent(): Locked
30*ea72c629Sgerardnico * ComboDispatchEvent(): Lock Released
31*ea72c629Sgerardnico * ComboLockTaskRunner(): Lock Released
32*ea72c629Sgerardnico *
33a6d63b89Sgerardnico */
34a6d63b89Sgerardnicoclass Lock
35a6d63b89Sgerardnico{
36a6d63b89Sgerardnico    private string $lockName;
37a6d63b89Sgerardnico    private string $lockFile;
38a6d63b89Sgerardnico    /**
39a6d63b89Sgerardnico     * @var mixed|null
40a6d63b89Sgerardnico     */
41a6d63b89Sgerardnico    private $perm;
429fc13f00Sgerardnico    private int $timeOut = 5;
432becc3acSgerardnico    /**
442becc3acSgerardnico     * @var false|resource
452becc3acSgerardnico     */
462becc3acSgerardnico    private $filePointer = null;
47a6d63b89Sgerardnico
48a6d63b89Sgerardnico
49a6d63b89Sgerardnico    /**
50a6d63b89Sgerardnico     * @param string $name
51a6d63b89Sgerardnico     */
52a6d63b89Sgerardnico    public function __construct(string $name)
53a6d63b89Sgerardnico    {
54a6d63b89Sgerardnico        $this->lockName = $name;
55a6d63b89Sgerardnico        global $conf;
56a6d63b89Sgerardnico        $this->lockFile = $conf['lockdir'] . "/_{$this->lockName}.lock";
57a6d63b89Sgerardnico        $this->perm = $conf['dperm'] ?? null;
58a6d63b89Sgerardnico    }
59a6d63b89Sgerardnico
60a6d63b89Sgerardnico    public static function create(string $name): Lock
61a6d63b89Sgerardnico    {
62a6d63b89Sgerardnico        return new Lock($name);
63a6d63b89Sgerardnico    }
64a6d63b89Sgerardnico
65a6d63b89Sgerardnico    /**
66a6d63b89Sgerardnico     * @throws ExceptionTimeOut - with the timeout
67a6d63b89Sgerardnico     */
689fc13f00Sgerardnico    function acquire(): Lock
69a6d63b89Sgerardnico    {
70a6d63b89Sgerardnico        $run = 0;
712becc3acSgerardnico        /**
722becc3acSgerardnico         * The flock function follows the semantics of the Unix system call bearing the same name.
732becc3acSgerardnico         * Flock utilizes ADVISORY locking only; that is:
742becc3acSgerardnico         * * other processes may ignore the lock completely it only affects those that call the flock call.
752becc3acSgerardnico         *
762becc3acSgerardnico         * * LOCK_SH means SHARED LOCK. Any number of processes MAY HAVE A SHARED LOCK simultaneously. It is commonly called a reader lock.
772becc3acSgerardnico         * * LOCK_EX means EXCLUSIVE LOCK. Only a single process may possess an exclusive lock to a given file at a time.
782becc3acSgerardnico         *
792becc3acSgerardnico         * ie if the file has been LOCKED with LOCK_SH in another process,
802becc3acSgerardnico         * * flock with LOCK_SH will SUCCEED.
812becc3acSgerardnico         * * flock with LOCK_EX will BLOCK UNTIL ALL READER LOCKS HAVE BEEN RELEASED.
822becc3acSgerardnico         *
832becc3acSgerardnico         * When the file is closed, the lock is released by the system anyway.
842becc3acSgerardnico         */
852becc3acSgerardnico        // LOCK_NB to not block the process
862becc3acSgerardnico        while (!$this->getLock()) {
87*ea72c629Sgerardnico            sleep(1);
88a6d63b89Sgerardnico            /**
892becc3acSgerardnico             * Old lock ? More than 10 minutes run
90a6d63b89Sgerardnico             */
912becc3acSgerardnico            if (is_file($this->lockFile) && (time() - @filemtime($this->lockFile)) > 60 * 10) {
922becc3acSgerardnico                if (!@unlink($this->lockFile)) {
93a6d63b89Sgerardnico                    throw new ExceptionRuntimeInternal("Removing the lock failed ($this->lockFile)");
94a6d63b89Sgerardnico                }
95a6d63b89Sgerardnico            }
969fc13f00Sgerardnico            $run++;
979fc13f00Sgerardnico            if ($run >= $this->timeOut) {
989fc13f00Sgerardnico                throw new ExceptionTimeOut("Unable to get the lock ($this->lockFile) for ($this->timeOut) seconds");
99a6d63b89Sgerardnico            }
100a6d63b89Sgerardnico        }
101a6d63b89Sgerardnico        if ($this->perm) {
102a6d63b89Sgerardnico            chmod($this->lockFile, $this->perm);
103a6d63b89Sgerardnico        }
104*ea72c629Sgerardnico        register_shutdown_function([Lock::class, 'shutdownHandling'], $this->lockName);
1059fc13f00Sgerardnico        return $this;
106a6d63b89Sgerardnico
107a6d63b89Sgerardnico    }
108a6d63b89Sgerardnico
109a6d63b89Sgerardnico    /**
110*ea72c629Sgerardnico     *
111*ea72c629Sgerardnico     * A function that is called when the process shutdown
112*ea72c629Sgerardnico     * due to time exceed for instance that cleans the lock created.
113*ea72c629Sgerardnico     *
114*ea72c629Sgerardnico     * https://www.php.net/manual/en/function.register-shutdown-function.php
115*ea72c629Sgerardnico     *
116*ea72c629Sgerardnico     * Why ?
117*ea72c629Sgerardnico     * The lock are created in the `before` of the the task runner event
118*ea72c629Sgerardnico     * and deleted in the `after` of the task runner event
119*ea72c629Sgerardnico     * If their is an error somewhere such as as a timeout, the lock
120*ea72c629Sgerardnico     * is not deleted and there is no task runner anymore for 5 minutes.
121*ea72c629Sgerardnico     *
122*ea72c629Sgerardnico     * @param $name - the lock name
123*ea72c629Sgerardnico     * @return void
124*ea72c629Sgerardnico     */
125*ea72c629Sgerardnico    public static function shutdownHandling($name)
126*ea72c629Sgerardnico    {
127*ea72c629Sgerardnico        print "Lock::shutdownHandling(): Deleting the lock $name" . NL;
128*ea72c629Sgerardnico        Lock::create($name)->release();
129*ea72c629Sgerardnico    }
130*ea72c629Sgerardnico
131*ea72c629Sgerardnico    /**
1322becc3acSgerardnico     * Release the lock
1332becc3acSgerardnico     * and the resources
1342becc3acSgerardnico     * (Need to be called in all cases)
135a6d63b89Sgerardnico     */
136a6d63b89Sgerardnico    function release()
137a6d63b89Sgerardnico    {
1382becc3acSgerardnico        if ($this->filePointer !== null) {
1392becc3acSgerardnico            fclose($this->filePointer);
1402becc3acSgerardnico            $this->filePointer = null;
1412becc3acSgerardnico        }
1422becc3acSgerardnico        if (file_exists($this->lockFile)) {
1432becc3acSgerardnico            unlink($this->lockFile);
1442becc3acSgerardnico        }
145a6d63b89Sgerardnico    }
146a6d63b89Sgerardnico
1470360a848Sgerardnico    public function isReleased(): bool
1480360a848Sgerardnico    {
1492becc3acSgerardnico        return !file_exists($this->lockFile);
1500360a848Sgerardnico    }
1510360a848Sgerardnico
1522becc3acSgerardnico    public function isLocked(): bool
1532becc3acSgerardnico    {
1542becc3acSgerardnico        return file_exists($this->lockFile);
1552becc3acSgerardnico    }
1562becc3acSgerardnico
1572becc3acSgerardnico    public function setTimeout(int $int): Lock
1589fc13f00Sgerardnico    {
1599fc13f00Sgerardnico        $this->timeOut = $int;
1609fc13f00Sgerardnico        return $this;
1619fc13f00Sgerardnico    }
1629fc13f00Sgerardnico
1632becc3acSgerardnico    private function getLock(): bool
1642becc3acSgerardnico    {
1652becc3acSgerardnico        /**
1662becc3acSgerardnico         * We test also on the file because
1672becc3acSgerardnico         * on some operating systems, flock() is implemented at the process level.
1682becc3acSgerardnico         *
1692becc3acSgerardnico         * ie when using a multithreaded server API you may not be able to rely on flock()
1702becc3acSgerardnico         * to protect files against other PHP scripts running in parallel threads of the same server instance
1712becc3acSgerardnico         */
1722becc3acSgerardnico        if (file_exists($this->lockFile)) {
1732becc3acSgerardnico            return false;
1742becc3acSgerardnico        }
1752becc3acSgerardnico
1762becc3acSgerardnico        if ($this->filePointer === null) {
1772becc3acSgerardnico            $mode = "c"; // as specified in the doc
1782becc3acSgerardnico            $this->filePointer = fopen($this->lockFile, $mode);
1792becc3acSgerardnico        }
1802becc3acSgerardnico        /**
1812becc3acSgerardnico         * LOCK_EX: exclusive lock
1822becc3acSgerardnico         * LOCK_NB: to not wait
1832becc3acSgerardnico         */
1842becc3acSgerardnico        return flock($this->filePointer, LOCK_EX | LOCK_NB);
1852becc3acSgerardnico    }
1862becc3acSgerardnico
187a6d63b89Sgerardnico}
188