xref: /dokuwiki/inc/Search/Index/Lock.php (revision 61dea710fc1da6e08c9033ff498556a8a01d3b77)
1<?php
2
3namespace dokuwiki\Search\Index;
4
5use dokuwiki\Search\Exception\IndexLockException;
6
7/**
8 * Static lock registry for index writing
9 *
10 * Manages filesystem locks (directories in the lock dir) with in-process
11 * reference counting. Multiple callers can acquire the same lock name —
12 * the filesystem lock is only created on the first acquire and removed
13 * on the last release.
14 */
15class Lock
16{
17    /** @var array<string, int> Lock names held by this process with reference counts */
18    protected static array $held = [];
19
20    /** @var int Seconds to wait for a lock held by another process before giving up */
21    protected static int $waitTimeout = 3;
22
23    /**
24     * Acquire a filesystem lock and register it
25     *
26     * Idempotent within a process - if already held, increments the reference
27     * count without touching the filesystem. When the lock is held by another
28     * process this waits for it to be released, giving up after a few seconds.
29     * A lock older than five minutes is considered stale and cleared. The lock
30     * directory is created with the configured directory permissions.
31     *
32     * @param string $name The index base name to lock
33     * @throws IndexLockException when the lock cannot be acquired
34     */
35    public static function acquire(string $name): void
36    {
37        global $conf;
38
39        if (isset(self::$held[$name])) {
40            self::$held[$name]++;
41            return;
42        }
43
44        $dir = self::lockDir($name);
45        $timeStart = time();
46        while (!@mkdir($dir)) {
47            // clear and retry immediately if the existing lock has gone stale
48            if (is_dir($dir) && time() - @filemtime($dir) > 60 * 5) {
49                if (!@rmdir($dir)) {
50                    throw new IndexLockException('Could not remove stale lock ' . $name);
51                }
52                continue;
53            }
54            // give up once we have waited long enough for the holder to finish
55            if (time() - $timeStart >= self::$waitTimeout) {
56                throw new IndexLockException('Could not lock ' . $name);
57            }
58            usleep(50);
59        }
60
61        if ($conf['dperm']) {
62            chmod($dir, $conf['dperm']);
63        }
64
65        self::$held[$name] = 1;
66    }
67
68    /**
69     * Release a filesystem lock
70     *
71     * Decrements reference count. Only removes the filesystem lock
72     * when the count reaches zero.
73     *
74     * @param string $name The index base name to unlock
75     */
76    public static function release(string $name): void
77    {
78        if (!isset(self::$held[$name])) return;
79
80        self::$held[$name]--;
81        if (self::$held[$name] <= 0) {
82            unset(self::$held[$name]);
83            @rmdir(self::lockDir($name));
84        }
85    }
86
87    /**
88     * Release all held locks
89     *
90     * Intended for test teardown to ensure a clean state.
91     */
92    public static function releaseAll(): void
93    {
94        foreach (array_keys(self::$held) as $name) {
95            @rmdir(self::lockDir($name));
96        }
97        self::$held = [];
98    }
99
100    /**
101     * Get the lock directory path for a given index name
102     *
103     * @param string $name The index base name
104     * @return string
105     */
106    protected static function lockDir(string $name): string
107    {
108        global $conf;
109        return $conf['lockdir'] . '/' . $name . '.index';
110    }
111}
112