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