Bump RL filter version to 4 to keep it in sync with the cluster. WMF-centrism, I...
[lhc/web/wiklou.git] / includes / objectcache / EhcacheBagOStuff.php
1 <?php
2
3 /**
4 * Client for the Ehcache RESTful web service - http://ehcache.org/documentation/cache_server.html
5 * TODO: Simplify configuration and add to the installer.
6 */
7 class EhcacheBagOStuff extends BagOStuff {
8 var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions,
9 $requestData, $requestDataPos;
10
11 var $curls = array();
12
13 function __construct( $params ) {
14 if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) {
15 throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' );
16 }
17 if ( !extension_loaded( 'zlib' ) ) {
18 throw new MWException( __CLASS__.' requires the zlib extension' );
19 }
20 if ( !isset( $params['servers'] ) ) {
21 throw new MWException( __METHOD__.': servers parameter is required' );
22 }
23 $this->servers = $params['servers'];
24 $this->cacheName = isset( $params['cache'] ) ? $params['cache'] : 'mw';
25 $this->connectTimeout = isset( $params['connectTimeout'] )
26 ? $params['connectTimeout'] : 1;
27 $this->timeout = isset( $params['timeout'] ) ? $params['timeout'] : 1;
28 $this->curlOptions = array(
29 CURLOPT_CONNECTTIMEOUT_MS => intval( $this->connectTimeout * 1000 ),
30 CURLOPT_TIMEOUT_MS => intval( $this->timeout * 1000 ),
31 CURLOPT_RETURNTRANSFER => 1,
32 CURLOPT_CUSTOMREQUEST => 'GET',
33 CURLOPT_POST => 0,
34 CURLOPT_POSTFIELDS => '',
35 CURLOPT_HTTPHEADER => array(),
36 );
37 }
38
39 public function get( $key ) {
40 wfProfileIn( __METHOD__ );
41 $response = $this->doItemRequest( $key );
42 if ( !$response || $response['http_code'] == 404 ) {
43 wfProfileOut( __METHOD__ );
44 return false;
45 }
46 if ( $response['http_code'] >= 300 ) {
47 wfDebug( __METHOD__.": GET failure, got HTTP {$response['http_code']}\n" );
48 wfProfileOut( __METHOD__ );
49 return false;
50 }
51 $body = $response['body'];
52 $type = $response['content_type'];
53 if ( $type == 'application/vnd.php.serialized+deflate' ) {
54 $body = gzinflate( $body );
55 if ( !$body ) {
56 wfDebug( __METHOD__.": error inflating $key\n" );
57 wfProfileOut( __METHOD__ );
58 return false;
59 }
60 $data = unserialize( $body );
61 } elseif ( $type == 'application/vnd.php.serialized' ) {
62 $data = unserialize( $body );
63 } else {
64 wfDebug( __METHOD__.": unknown content type \"$type\"\n" );
65 wfProfileOut( __METHOD__ );
66 return false;
67 }
68
69 wfProfileOut( __METHOD__ );
70 return $data;
71 }
72
73 public function set( $key, $value, $expiry = 0 ) {
74 wfProfileIn( __METHOD__ );
75 $expiry = $this->convertExpiry( $expiry );
76 $ttl = $expiry ? $expiry - time() : 2147483647;
77 $blob = serialize( $value );
78 if ( strlen( $blob ) > 100 ) {
79 $blob = gzdeflate( $blob );
80 $contentType = 'application/vnd.php.serialized+deflate';
81 } else {
82 $contentType = 'application/vnd.php.serialized';
83 }
84
85 $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
86
87 if ( $code == 404 ) {
88 // Maybe the cache does not exist yet, let's try creating it
89 if ( !$this->createCache( $key ) ) {
90 wfDebug( __METHOD__.": cache creation failed\n" );
91 wfProfileOut( __METHOD__ );
92 return false;
93 }
94 $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
95 }
96
97 $result = false;
98 if ( !$code ) {
99 wfDebug( __METHOD__.": PUT failure for key $key\n" );
100 } elseif ( $code >= 300 ) {
101 wfDebug( __METHOD__.": PUT failure for key $key: HTTP $code\n" );
102 } else {
103 $result = true;
104 }
105
106 wfProfileOut( __METHOD__ );
107 return $result;
108 }
109
110 public function delete( $key, $time = 0 ) {
111 wfProfileIn( __METHOD__ );
112 $response = $this->doItemRequest( $key,
113 array( CURLOPT_CUSTOMREQUEST => 'DELETE' ) );
114 $code = isset( $response['http_code'] ) ? $response['http_code'] : 0;
115 if ( !$response || ( $code != 404 && $code >= 300 ) ) {
116 wfDebug( __METHOD__.": DELETE failure for key $key\n" );
117 $result = false;
118 } else {
119 $result = true;
120 }
121 wfProfileOut( __METHOD__ );
122 return $result;
123 }
124
125 protected function getCacheUrl( $key ) {
126 if ( count( $this->servers ) == 1 ) {
127 $server = reset( $this->servers );
128 } else {
129 // Use consistent hashing
130 $hashes = array();
131 foreach ( $this->servers as $server ) {
132 $hashes[$server] = md5( $server . '/' . $key );
133 }
134 asort( $hashes );
135 reset( $hashes );
136 $server = key( $hashes );
137 }
138 return "http://$server/ehcache/rest/{$this->cacheName}";
139 }
140
141 /**
142 * Get a cURL handle for the given cache URL.
143 * We cache the handles to allow keepalive.
144 */
145 protected function getCurl( $cacheUrl ) {
146 if ( !isset( $this->curls[$cacheUrl] ) ) {
147 $this->curls[$cacheUrl] = curl_init();
148 }
149 return $this->curls[$cacheUrl];
150 }
151
152 protected function attemptPut( $key, $data, $type, $ttl ) {
153 // In initial benchmarking, it was 30 times faster to use CURLOPT_POST
154 // than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because
155 // CURLOPT_UPLOAD was pushing the request headers first, then waiting
156 // for an ACK packet, then sending the data, whereas CURLOPT_POST just
157 // sends the headers and the data in a single send().
158 $response = $this->doItemRequest( $key,
159 array(
160 CURLOPT_POST => 1,
161 CURLOPT_CUSTOMREQUEST => 'PUT',
162 CURLOPT_POSTFIELDS => $data,
163 CURLOPT_HTTPHEADER => array(
164 'Content-Type: ' . $type,
165 'ehcacheTimeToLiveSeconds: ' . $ttl
166 )
167 )
168 );
169 if ( !$response ) {
170 return 0;
171 } else {
172 return $response['http_code'];
173 }
174 }
175
176 protected function createCache( $key ) {
177 wfDebug( __METHOD__.": creating cache for $key\n" );
178 $response = $this->doCacheRequest( $key,
179 array(
180 CURLOPT_POST => 1,
181 CURLOPT_CUSTOMREQUEST => 'PUT',
182 CURLOPT_POSTFIELDS => '',
183 ) );
184 if ( !$response ) {
185 wfDebug( __CLASS__.": failed to create cache for $key\n" );
186 return false;
187 }
188 if ( $response['http_code'] == 201 /* created */
189 || $response['http_code'] == 409 /* already there */ )
190 {
191 return true;
192 } else {
193 return false;
194 }
195 }
196
197 protected function doCacheRequest( $key, $curlOptions = array() ) {
198 $cacheUrl = $this->getCacheUrl( $key );
199 $curl = $this->getCurl( $cacheUrl );
200 return $this->doRequest( $curl, $cacheUrl, $curlOptions );
201 }
202
203 protected function doItemRequest( $key, $curlOptions = array() ) {
204 $cacheUrl = $this->getCacheUrl( $key );
205 $curl = $this->getCurl( $cacheUrl );
206 $url = $cacheUrl . '/' . rawurlencode( $key );
207 return $this->doRequest( $curl, $url, $curlOptions );
208 }
209
210 protected function doRequest( $curl, $url, $curlOptions = array() ) {
211 if ( array_diff_key( $curlOptions, $this->curlOptions ) ) {
212 // var_dump( array_diff_key( $curlOptions, $this->curlOptions ) );
213 throw new MWException( __METHOD__.": to prevent options set in one doRequest() " .
214 "call from affecting subsequent doRequest() calls, only options listed " .
215 "in \$this->curlOptions may be specified in the \$curlOptions parameter." );
216 }
217 $curlOptions += $this->curlOptions;
218 $curlOptions[CURLOPT_URL] = $url;
219
220 curl_setopt_array( $curl, $curlOptions );
221 $result = curl_exec( $curl );
222 if ( $result === false ) {
223 wfDebug( __CLASS__.": curl error: " . curl_error( $curl ) . "\n" );
224 return false;
225 }
226 $info = curl_getinfo( $curl );
227 $info['body'] = $result;
228 return $info;
229 }
230 }