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