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