1<?php
2/**
3 * Object caching using PHP's APCU accelerator.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Cache
22 */
23
24/**
25 * This is a wrapper for APCU's shared memory functions
26 *
27 * Use PHP serialization to avoid bugs and easily create CAS tokens.
28 * APCu has a memory corruption bug when the serializer is set to 'default'.
29 * See T120267, and upstream bug reports:
30 *  - https://github.com/krakjoe/apcu/issues/38
31 *  - https://github.com/krakjoe/apcu/issues/35
32 *  - https://github.com/krakjoe/apcu/issues/111
33 *
34 * @ingroup Cache
35 */
36class APCUBagOStuff extends MediumSpecificBagOStuff {
37	/** @var bool Whether to trust the APC implementation to serialization */
38	private $nativeSerialize;
39	/** @var bool */
40	private $useIncrTTLArg;
41
42	/**
43	 * @var string String to append to each APC key. This may be changed
44	 *  whenever the handling of values is changed, to prevent existing code
45	 *  from encountering older values which it cannot handle.
46	 */
47	private const KEY_SUFFIX = ':4';
48
49	/** @var int Max attempts for implicit CAS operations */
50	private static $CAS_MAX_ATTEMPTS = 100;
51
52	public function __construct( array $params = [] ) {
53		$params['segmentationSize'] = $params['segmentationSize'] ?? INF;
54		parent::__construct( $params );
55		// The extension serializer is still buggy, unlike "php" and "igbinary"
56		$this->nativeSerialize = ( ini_get( 'apc.serializer' ) !== 'default' );
57		$this->useIncrTTLArg = version_compare( phpversion( 'apcu' ), '5.1.12', '>=' );
58		// Avoid back-dated values that expire too soon. In particular, regenerating a hot
59		// key before it expires should never have the end-result of purging that key. Using
60		// the web request time becomes increasingly problematic the longer the request lasts.
61		ini_set( 'apc.use_request_time', '0' );
62	}
63
64	protected function doGet( $key, $flags = 0, &$casToken = null ) {
65		$getToken = ( $casToken === self::PASS_BY_REF );
66		$casToken = null;
67
68		$blob = apcu_fetch( $key . self::KEY_SUFFIX );
69		$value = $this->nativeSerialize ? $blob : $this->unserialize( $blob );
70		if ( $getToken && $value !== false ) {
71			$casToken = $blob; // don't bother hashing this
72		}
73
74		return $value;
75	}
76
77	protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
78		$blob = $this->nativeSerialize ? $value : $this->getSerialized( $value, $key );
79		$success = apcu_store( $key . self::KEY_SUFFIX, $blob, $exptime );
80		return $success;
81	}
82
83	protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) {
84		$blob = $this->nativeSerialize ? $value : $this->getSerialized( $value, $key );
85		$success = apcu_add( $key . self::KEY_SUFFIX, $blob, $exptime );
86		return $success;
87	}
88
89	protected function doDelete( $key, $flags = 0 ) {
90		apcu_delete( $key . self::KEY_SUFFIX );
91
92		return true;
93	}
94
95	public function incr( $key, $value = 1, $flags = 0 ) {
96		$result = false;
97
98		// https://github.com/krakjoe/apcu/issues/166
99		for ( $i = 0; $i < self::$CAS_MAX_ATTEMPTS; ++$i ) {
100			$oldCount = apcu_fetch( $key . self::KEY_SUFFIX );
101			if ( !is_int( $oldCount ) ) {
102				break;
103			}
104			$count = $oldCount + (int)$value;
105			if ( apcu_cas( $key . self::KEY_SUFFIX, $oldCount, $count ) ) {
106				$result = $count;
107				break;
108			}
109		}
110
111		return $result;
112	}
113
114	public function decr( $key, $value = 1, $flags = 0 ) {
115		$result = false;
116
117		// https://github.com/krakjoe/apcu/issues/166
118		for ( $i = 0; $i < self::$CAS_MAX_ATTEMPTS; ++$i ) {
119			$oldCount = apcu_fetch( $key . self::KEY_SUFFIX );
120			if ( !is_int( $oldCount ) ) {
121				break;
122			}
123			$count = $oldCount - (int)$value;
124			if ( apcu_cas( $key . self::KEY_SUFFIX, $oldCount, $count ) ) {
125				$result = $count;
126				break;
127			}
128		}
129
130		return $result;
131	}
132
133	public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
134		$init = is_int( $init ) ? $init : $value;
135		// Use apcu 5.1.12 $ttl argument if apcu_inc() will initialize to $init:
136		// https://www.php.net/manual/en/function.apcu-inc.php
137		if ( $value === $init && $this->useIncrTTLArg ) {
138			/** @noinspection PhpMethodParametersCountMismatchInspection */
139			$result = apcu_inc( $key . self::KEY_SUFFIX, $value, $success, $exptime );
140		} else {
141			$result = false;
142			for ( $i = 0; $i < self::$CAS_MAX_ATTEMPTS; ++$i ) {
143				$oldCount = apcu_fetch( $key . self::KEY_SUFFIX );
144				if ( $oldCount === false ) {
145					$count = (int)$init;
146					if ( apcu_add( $key . self::KEY_SUFFIX, $count, $exptime ) ) {
147						$result = $count;
148						break;
149					}
150				} elseif ( is_int( $oldCount ) ) {
151					$count = $oldCount + (int)$value;
152					if ( apcu_cas( $key . self::KEY_SUFFIX, $oldCount, $count ) ) {
153						$result = $count;
154						break;
155					}
156				} else {
157					break;
158				}
159			}
160		}
161
162		return $result;
163	}
164
165	public function makeKeyInternal( $keyspace, $components ) {
166		return $this->genericKeyFromComponents( $keyspace, ...$components );
167	}
168
169	protected function convertGenericKey( $key ) {
170		return $key; // short-circuit; already uses "generic" keys
171	}
172}
173