Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cache.class.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <?
  2. /*************************************************************************|
  3. |--------------- Caching class -------------------------------------------|
  4. |*************************************************************************|
  5. This class is a wrapper for the Memcache class, and it's been written in
  6. order to better handle the caching of full pages with bits of dynamic
  7. content that are different for every user.
  8. As this inherits memcache, all of the default memcache methods work -
  9. however, this class has page caching functions superior to those of
  10. memcache.
  11. Also, Memcache::get and Memcache::set have been wrapped by
  12. Cache::get_value and Cache::cache_value. get_value uses the same argument
  13. as get, but cache_value only takes the key, the value, and the duration
  14. (no zlib).
  15. // Unix sockets
  16. memcached -d -m 5120 -s /var/run/memcached.sock -a 0777 -t16 -C -u root
  17. // TCP bind
  18. memcached -d -m 8192 -l 10.10.0.1 -t8 -C
  19. |*************************************************************************/
  20. if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
  21. die('Memcache Extension not loaded.');
  22. }
  23. if (class_exists('Memcached')) {
  24. class MemcacheCompat extends Memcached {}
  25. } else {
  26. class MemcacheCompat extends Memcache {}
  27. }
  28. class Cache extends MemcacheCompat {
  29. // Torrent Group cache version
  30. const GROUP_VERSION = 5;
  31. public $CacheHits = [];
  32. public $MemcacheDBArray = [];
  33. public $MemcacheDBKey = '';
  34. protected $InTransaction = false;
  35. public $Time = 0;
  36. private $Servers = [];
  37. private $PersistentKeys = [
  38. 'ajax_requests_*',
  39. 'query_lock_*',
  40. 'stats_*',
  41. 'top10tor_*',
  42. 'users_snatched_*',
  43. // Cache-based features
  44. 'global_notification',
  45. 'notifications_one_reads_*',
  46. ];
  47. private $ClearedKeys = [];
  48. public $CanClear = false;
  49. public $InternalCache = true;
  50. function __construct($Servers) {
  51. if (is_subclass_of($this, 'Memcached')) parent::__construct();
  52. $this->Servers = $Servers;
  53. foreach ($Servers as $Server) {
  54. if (is_subclass_of($this, 'Memcache')) {
  55. $this->addServer($Server['host'], $Server['port'], true, $Server['buckets']);
  56. } else {
  57. $this->addServer(str_replace('unix://', '', $Server['host']), $Server['port'], $Server['buckets']);
  58. }
  59. }
  60. }
  61. //---------- Caching functions ----------//
  62. // Allows us to set an expiration on otherwise perminantly cache'd values
  63. // Useful for disabled users, locked threads, basically reducing ram usage
  64. public function expire_value($Key, $Duration = 2592000) {
  65. $StartTime = microtime(true);
  66. $this->set($Key, $this->get($Key), $Duration);
  67. $this->Time += (microtime(true) - $StartTime) * 1000;
  68. }
  69. // Wrapper for Memcache::set, with the zlib option removed and default duration of 30 days
  70. public function cache_value($Key, $Value, $Duration = 2592000) {
  71. $StartTime = microtime(true);
  72. if (empty($Key)) {
  73. trigger_error("Cache insert failed for empty key");
  74. }
  75. $SetParams = [$Key, $Value, 0, $Duration];
  76. if (is_subclass_of($this, 'Memcached')) unset($SetParams[2]);
  77. if (!$this->set(...$SetParams)) {
  78. trigger_error("Cache insert failed for key $Key");
  79. }
  80. if ($this->InternalCache && array_key_exists($Key, $this->CacheHits)) {
  81. $this->CacheHits[$Key] = $Value;
  82. }
  83. $this->Time += (microtime(true) - $StartTime) * 1000;
  84. }
  85. // Wrapper for Memcache::add, with the zlib option removed and default duration of 30 days
  86. public function add_value($Key, $Value, $Duration = 2592000) {
  87. $StartTime = microtime(true);
  88. $Added = $this->add($Key, $Value, 0, $Duration);
  89. $this->Time += (microtime(true) - $StartTime) * 1000;
  90. return $Added;
  91. }
  92. public function replace_value($Key, $Value, $Duration = 2592000) {
  93. $StartTime = microtime(true);
  94. $ReplaceParams = [$Key, $Value, false, $Duration];
  95. if (is_subclass_of($this, 'Memcached')) unset($ReplaceParams[2]);
  96. $this->replace(...$ReplaceParams);
  97. if ($this->InternalCache && array_key_exists($Key, $this->CacheHits)) {
  98. $this->CacheHits[$Key] = $Value;
  99. }
  100. $this->Time += (microtime(true) - $StartTime) * 1000;
  101. }
  102. public function get_value($Key, $NoCache = false) {
  103. if (!$this->InternalCache) {
  104. $NoCache = true;
  105. }
  106. $StartTime = microtime(true);
  107. if (empty($Key)) {
  108. trigger_error('Cache retrieval failed for empty key');
  109. }
  110. if (!empty($_GET['clearcache']) && $this->CanClear && !isset($this->ClearedKeys[$Key]) && !Misc::in_array_partial($Key, $this->PersistentKeys)) {
  111. if ($_GET['clearcache'] === '1') {
  112. // Because check_perms() isn't true until LoggedUser is pulled from the cache, we have to remove the entries loaded before the LoggedUser data
  113. // Because of this, not user cache data will require a secondary pageload following the clearcache to update
  114. if (count($this->CacheHits) > 0) {
  115. foreach (array_keys($this->CacheHits) as $HitKey) {
  116. if (!isset($this->ClearedKeys[$HitKey]) && !Misc::in_array_partial($HitKey, $this->PersistentKeys)) {
  117. $this->delete($HitKey);
  118. unset($this->CacheHits[$HitKey]);
  119. $this->ClearedKeys[$HitKey] = true;
  120. }
  121. }
  122. }
  123. $this->delete($Key);
  124. $this->Time += (microtime(true) - $StartTime) * 1000;
  125. return false;
  126. } elseif ($_GET['clearcache'] == $Key) {
  127. $this->delete($Key);
  128. $this->Time += (microtime(true) - $StartTime) * 1000;
  129. return false;
  130. } elseif (substr($_GET['clearcache'], -1) === '*') {
  131. $Prefix = substr($_GET['clearcache'], 0, -1);
  132. if ($Prefix === '' || $Prefix === substr($Key, 0, strlen($Prefix))) {
  133. $this->delete($Key);
  134. $this->Time += (microtime(true) - $StartTime) * 1000;
  135. return false;
  136. }
  137. }
  138. $this->ClearedKeys[$Key] = true;
  139. }
  140. // For cases like the forums, if a key is already loaded, grab the existing pointer
  141. if (isset($this->CacheHits[$Key]) && !$NoCache) {
  142. $this->Time += (microtime(true) - $StartTime) * 1000;
  143. return $this->CacheHits[$Key];
  144. }
  145. $Return = $this->get($Key);
  146. if ($Return !== false) {
  147. $this->CacheHits[$Key] = $NoCache ? null : $Return;
  148. }
  149. $this->Time += (microtime(true) - $StartTime) * 1000;
  150. return $Return;
  151. }
  152. // Wrapper for Memcache::delete. For a reason, see above.
  153. public function delete_value($Key) {
  154. $StartTime = microtime(true);
  155. if (empty($Key)) {
  156. trigger_error('Cache deletion failed for empty key');
  157. }
  158. if (!$this->delete($Key)) {
  159. //trigger_error("Cache delete failed for key $Key");
  160. }
  161. unset($this->CacheHits[$Key]);
  162. $this->Time += (microtime(true) - $StartTime) * 1000;
  163. }
  164. public function increment_value($Key, $Value = 1) {
  165. $StartTime = microtime(true);
  166. $NewVal = $this->increment($Key, $Value);
  167. if (isset($this->CacheHits[$Key])) {
  168. $this->CacheHits[$Key] = $NewVal;
  169. }
  170. $this->Time += (microtime(true) - $StartTime) * 1000;
  171. }
  172. public function decrement_value($Key, $Value = 1) {
  173. $StartTime = microtime(true);
  174. $NewVal = $this->decrement($Key, $Value);
  175. if (isset($this->CacheHits[$Key])) {
  176. $this->CacheHits[$Key] = $NewVal;
  177. }
  178. $this->Time += (microtime(true) - $StartTime) * 1000;
  179. }
  180. //---------- memcachedb functions ----------//
  181. public function begin_transaction($Key) {
  182. $Value = $this->get($Key);
  183. if (!is_array($Value)) {
  184. $this->InTransaction = false;
  185. $this->MemcacheDBKey = [];
  186. $this->MemcacheDBKey = '';
  187. return false;
  188. }
  189. $this->MemcacheDBArray = $Value;
  190. $this->MemcacheDBKey = $Key;
  191. $this->InTransaction = true;
  192. return true;
  193. }
  194. public function cancel_transaction() {
  195. $this->InTransaction = false;
  196. $this->MemcacheDBKey = [];
  197. $this->MemcacheDBKey = '';
  198. }
  199. public function commit_transaction($Time = 2592000) {
  200. if (!$this->InTransaction) {
  201. return false;
  202. }
  203. $this->cache_value($this->MemcacheDBKey, $this->MemcacheDBArray, $Time);
  204. $this->InTransaction = false;
  205. }
  206. // Updates multiple rows in an array
  207. public function update_transaction($Rows, $Values) {
  208. if (!$this->InTransaction) {
  209. return false;
  210. }
  211. $Array = $this->MemcacheDBArray;
  212. if (is_array($Rows)) {
  213. $i = 0;
  214. $Keys = $Rows[0];
  215. $Property = $Rows[1];
  216. foreach ($Keys as $Row) {
  217. $Array[$Row][$Property] = $Values[$i];
  218. $i++;
  219. }
  220. } else {
  221. $Array[$Rows] = $Values;
  222. }
  223. $this->MemcacheDBArray = $Array;
  224. }
  225. // Updates multiple values in a single row in an array
  226. // $Values must be an associative array with key:value pairs like in the array we're updating
  227. public function update_row($Row, $Values) {
  228. if (!$this->InTransaction) {
  229. return false;
  230. }
  231. if ($Row === false) {
  232. $UpdateArray = $this->MemcacheDBArray;
  233. } else {
  234. $UpdateArray = $this->MemcacheDBArray[$Row];
  235. }
  236. foreach ($Values as $Key => $Value) {
  237. if (!array_key_exists($Key, $UpdateArray)) {
  238. trigger_error('Bad transaction key ('.$Key.') for cache '.$this->MemcacheDBKey);
  239. }
  240. if ($Value === '+1') {
  241. if (!is_number($UpdateArray[$Key])) {
  242. trigger_error('Tried to increment non-number ('.$Key.') for cache '.$this->MemcacheDBKey);
  243. }
  244. ++$UpdateArray[$Key]; // Increment value
  245. } elseif ($Value === '-1') {
  246. if (!is_number($UpdateArray[$Key])) {
  247. trigger_error('Tried to decrement non-number ('.$Key.') for cache '.$this->MemcacheDBKey);
  248. }
  249. --$UpdateArray[$Key]; // Decrement value
  250. } else {
  251. $UpdateArray[$Key] = $Value; // Otherwise, just alter value
  252. }
  253. }
  254. if ($Row === false) {
  255. $this->MemcacheDBArray = $UpdateArray;
  256. } else {
  257. $this->MemcacheDBArray[$Row] = $UpdateArray;
  258. }
  259. }
  260. // Increments multiple values in a single row in an array
  261. // $Values must be an associative array with key:value pairs like in the array we're updating
  262. public function increment_row($Row, $Values) {
  263. if (!$this->InTransaction) {
  264. return false;
  265. }
  266. if ($Row === false) {
  267. $UpdateArray = $this->MemcacheDBArray;
  268. } else {
  269. $UpdateArray = $this->MemcacheDBArray[$Row];
  270. }
  271. foreach ($Values as $Key => $Value) {
  272. if (!array_key_exists($Key, $UpdateArray)) {
  273. trigger_error("Bad transaction key ($Key) for cache ".$this->MemcacheDBKey);
  274. }
  275. if (!is_number($Value)) {
  276. trigger_error("Tried to increment with non-number ($Key) for cache ".$this->MemcacheDBKey);
  277. }
  278. $UpdateArray[$Key] += $Value; // Increment value
  279. }
  280. if ($Row === false) {
  281. $this->MemcacheDBArray = $UpdateArray;
  282. } else {
  283. $this->MemcacheDBArray[$Row] = $UpdateArray;
  284. }
  285. }
  286. // Insert a value at the beginning of the array
  287. public function insert_front($Key, $Value) {
  288. if (!$this->InTransaction) {
  289. return false;
  290. }
  291. if ($Key === '') {
  292. array_unshift($this->MemcacheDBArray, $Value);
  293. } else {
  294. $this->MemcacheDBArray = array($Key=>$Value) + $this->MemcacheDBArray;
  295. }
  296. }
  297. // Insert a value at the end of the array
  298. public function insert_back($Key, $Value) {
  299. if (!$this->InTransaction) {
  300. return false;
  301. }
  302. if ($Key === '') {
  303. array_push($this->MemcacheDBArray, $Value);
  304. } else {
  305. $this->MemcacheDBArray = $this->MemcacheDBArray + array($Key=>$Value);
  306. }
  307. }
  308. public function insert($Key, $Value) {
  309. if (!$this->InTransaction) {
  310. return false;
  311. }
  312. if ($Key === '') {
  313. $this->MemcacheDBArray[] = $Value;
  314. } else {
  315. $this->MemcacheDBArray[$Key] = $Value;
  316. }
  317. }
  318. public function delete_row($Row) {
  319. if (!$this->InTransaction) {
  320. return false;
  321. }
  322. if (!isset($this->MemcacheDBArray[$Row])) {
  323. trigger_error("Tried to delete non-existent row ($Row) for cache ".$this->MemcacheDBKey);
  324. }
  325. unset($this->MemcacheDBArray[$Row]);
  326. }
  327. public function update($Key, $Rows, $Values, $Time = 2592000) {
  328. if (!$this->InTransaction) {
  329. $this->begin_transaction($Key);
  330. $this->update_transaction($Rows, $Values);
  331. $this->commit_transaction($Time);
  332. } else {
  333. $this->update_transaction($Rows, $Values);
  334. }
  335. }
  336. /**
  337. * Tries to set a lock. Expiry time is one hour to avoid indefinite locks
  338. *
  339. * @param string $LockName name on the lock
  340. * @return true if lock was acquired
  341. */
  342. public function get_query_lock($LockName) {
  343. return $this->add_value('query_lock_'.$LockName, 1, 3600);
  344. }
  345. /**
  346. * Remove lock
  347. *
  348. * @param string $LockName name on the lock
  349. */
  350. public function clear_query_lock($LockName) {
  351. $this->delete_value('query_lock_'.$LockName);
  352. }
  353. /**
  354. * Get cache server status
  355. *
  356. * @return array (host => bool status, ...)
  357. */
  358. public function server_status() {
  359. $Status = [];
  360. foreach ($this->Servers as $Server) {
  361. $Status["$Server[host]:$Server[port]"] = $this->getServerStatus($Server['host'], $Server['port']);
  362. }
  363. return $Status;
  364. }
  365. }