Index: branches/RL2/extensions/Gadgets/Gadgets.php |
— | — | @@ -126,6 +126,7 @@ |
127 | 127 | $wgAutoloadClasses['ApiQueryGadgets'] = $dir . 'api/ApiQueryGadgets.php'; |
128 | 128 | $wgAutoloadClasses['ApiGetGadgetPrefs'] = $dir . 'api/ApiGetGadgetPrefs.php'; |
129 | 129 | $wgAutoloadClasses['ApiSetGadgetPrefs'] = $dir . 'api/ApiSetGadgetPrefs.php'; |
| 130 | +$wgAutoloadClasses['CachedGadgetRepo'] = $dir . 'backend/CachedGadgetRepo.php'; |
130 | 131 | $wgAutoloadClasses['ForeignDBGadgetRepo'] = $dir . 'backend/ForeignDBGadgetRepo.php'; |
131 | 132 | $wgAutoloadClasses['Gadget'] = $dir . 'backend/Gadget.php'; |
132 | 133 | $wgAutoloadClasses['GadgetsHooks'] = $dir . 'Gadgets.hooks.php'; |
Index: branches/RL2/extensions/Gadgets/backend/CachedGadgetRepo.php |
— | — | @@ -0,0 +1,194 @@ |
| 2 | +<?php |
| 3 | +// TODO: Reconsider memc key naming |
| 4 | +/** |
| 5 | + * Abstract class that enhances GadgetRepo with caching. This is useful for |
| 6 | + * repos that obtain gadget data from a database or a REST API. Currently, all |
| 7 | + * repos use this. |
| 8 | + */ |
| 9 | +abstract class CachedGadgetRepo extends GadgetRepo { |
| 10 | + /*** Protected members ***/ |
| 11 | + |
| 12 | + /** Cache for EXISTING gadgets. Nonexistent gadgets must not be cached here, |
| 13 | + * use $missCache instead. Values may be null, in which case only the gadget's |
| 14 | + * existence is cached and the data must still be retrieved from memc or the DB. |
| 15 | + * |
| 16 | + * array( id => null|array( 'json' => JSON string, 'timestamp' => timestamp ) ) |
| 17 | + */ |
| 18 | + protected $data = array(); |
| 19 | + |
| 20 | + /** Cache for gadget IDs that have been queried and found to be nonexistent. |
| 21 | + * |
| 22 | + * array( id => ignored ) |
| 23 | + */ |
| 24 | + protected $missCache = array(); |
| 25 | + |
| 26 | + /** If true, $data is assumed to contain all existing gadget IDs. |
| 27 | + */ |
| 28 | + protected $idsLoaded = false; |
| 29 | + |
| 30 | + /** |
| 31 | + * Memcached key of the gadget names list. Subclasses must set this in their constructor. |
| 32 | + * This could've been a static member if we had PHP 5.3's late static binding. |
| 33 | + * |
| 34 | + * Set to false if memcached is not available. |
| 35 | + * |
| 36 | + * TODO: make this an abstract getter, and redesign the whole memc key naming scheme |
| 37 | + */ |
| 38 | + protected $namesKey; |
| 39 | + |
| 40 | + /*** Abstract methods ***/ |
| 41 | + |
| 42 | + /** |
| 43 | + * Load the full data for all gadgets. |
| 44 | + * @return array( id => array( 'json' => JSON string, 'timestamp' => timestamp ) ) |
| 45 | + */ |
| 46 | + abstract protected function loadAllData(); |
| 47 | + |
| 48 | + /** |
| 49 | + * Load the full data for one gadget. |
| 50 | + * @param $id string Gadget ID |
| 51 | + * @return array( 'json' => JSON string, 'timestamp' => timestamp ) or empty array if the gadget doesn't exist. |
| 52 | + */ |
| 53 | + abstract protected function loadDataFor( $id ); |
| 54 | + |
| 55 | + /*** Protected methods ***/ |
| 56 | + |
| 57 | + /** |
| 58 | + * Update the cache to account for the fact that a gadget has been |
| 59 | + * added, modified or deleted. |
| 60 | + * @param $id string Gadget ID |
| 61 | + * @param $data array array( 'json' => JSON string, 'timestamp' => timestamp ) if added or modified, or null if deleted |
| 62 | + */ |
| 63 | + protected function updateCache( $id, $data ) { |
| 64 | + global $wgMemc; |
| 65 | + $toCache = $data; |
| 66 | + if ( $data !== null ) { |
| 67 | + // Added or modified |
| 68 | + // Store in in-object cache |
| 69 | + $this->data[$id] = $data; |
| 70 | + // Remove from the missing cache if present there |
| 71 | + unset( $this->missCache[$id] ); |
| 72 | + } else { |
| 73 | + // Deleted |
| 74 | + // Remove from in-object cache |
| 75 | + unset( $this->data[$id] ); |
| 76 | + // Add it to the missing cache |
| 77 | + $this->missCache[$id] = true; |
| 78 | + // Store nonexistence in memc as an empty array |
| 79 | + $toCache = array(); |
| 80 | + } |
| 81 | + |
| 82 | + // Write to memc |
| 83 | + $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
| 84 | + if ( $key !== false ) { |
| 85 | + $wgMemc->set( $key, $toCache ); |
| 86 | + } |
| 87 | + // Clear the gadget names array in memc so it'll be regenerated later |
| 88 | + if ( $this->namesKey !== false ) { |
| 89 | + $wgMemc->delete( $this->namesKey ); |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + /*** Public methods inherited from GadgetRepo ***/ |
| 94 | + |
| 95 | + public function getGadgetIds() { |
| 96 | + $this->maybeLoadIDs(); |
| 97 | + return array_keys( $this->data ); |
| 98 | + } |
| 99 | + |
| 100 | + public function getGadget( $id ) { |
| 101 | + $data = $this->maybeLoadDataFor( $id ); |
| 102 | + if ( !$data ) { |
| 103 | + return null; |
| 104 | + } |
| 105 | + return new Gadget( $id, $this, $data['json'], $data['timestamp'] ); |
| 106 | + } |
| 107 | + |
| 108 | + /*** Private methods ***/ |
| 109 | + |
| 110 | + /** |
| 111 | + * Populate the keys in $this->data. Values are only populated if loadAllData() is called, |
| 112 | + * when loading from memc, all values are set to null and are lazy-loaded in loadDataFor(). |
| 113 | + * @return array Array of gadget IDs |
| 114 | + */ |
| 115 | + private function maybeLoadIDs() { |
| 116 | + global $wgMemc; |
| 117 | + if ( $this->idsLoaded ) { |
| 118 | + return array_keys( $this->data ); |
| 119 | + } |
| 120 | + |
| 121 | + // Try memc |
| 122 | + $cached = $this->namesKey !== false ? $wgMemc->get( $this->namesKey ) : false; |
| 123 | + if ( is_array( $cached ) ) { |
| 124 | + // Yay, data is in cache |
| 125 | + // Add to $this->data , but let things already in $this->data take precedence |
| 126 | + $this->data += $cached; |
| 127 | + $this->idsLoaded = true; |
| 128 | + return array_keys( $this->data ); |
| 129 | + } |
| 130 | + |
| 131 | + $this->data = $this->loadAllData(); |
| 132 | + // For memc, prepare an array with the IDs as keys but with each value set to null |
| 133 | + $toCache = array_combine( array_keys( $data ), array_fill( 0, count( $this->data ), null ) ); |
| 134 | + |
| 135 | + $wgMemc->set( $this->namesKey, $toCache ); |
| 136 | + $this->idsLoaded = true; |
| 137 | + return array_keys( $this->data ); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Populate a given Gadget's data in $this->data . Tries memc first, then falls back to loadDataFor() |
| 142 | + * @param $id string Gadget ID |
| 143 | + * @return array( 'json' => JSON string, 'timestamp' => timestamp ) or empty array if the gadget doesn't exist. |
| 144 | + */ |
| 145 | + private function maybeLoadDataFor( $id ) { |
| 146 | + global $wgMemc; |
| 147 | + if ( isset( $this->data[$id] ) && is_array( $this->data[$id] ) ) { |
| 148 | + // Already loaded, nothing to do here. |
| 149 | + return $this->data[$id]; |
| 150 | + } |
| 151 | + if ( isset( $this->missCache[$id] ) ) { |
| 152 | + // Gadget is already known to be missing |
| 153 | + return array(); |
| 154 | + } |
| 155 | + // Need to use array_key_exists() here because isset() returns true for nulls. !@#$ you, PHP |
| 156 | + if ( $this->idsLoaded && !array_key_exists( $id, $this->data ) ) { |
| 157 | + // All IDs have been loaded into $this->data but $id isn't in there, |
| 158 | + // therefore it doesn't exist. |
| 159 | + $this->missCache[$id] = true; |
| 160 | + return array(); |
| 161 | + } |
| 162 | + |
| 163 | + // Try cache |
| 164 | + // FIXME getMemcKey is defined in LocalGadgetRepo but not abstract. Cache key naming needs redesign |
| 165 | + $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
| 166 | + $cached = $key !== false ? $wgMemc->get( $key ) : false; |
| 167 | + if ( is_array( $cached ) ) { |
| 168 | + // Yay, data is in cache |
| 169 | + if ( count( $cached ) ) { |
| 170 | + // Cache entry contains data |
| 171 | + $this->data[$id] = $cached; |
| 172 | + } else { |
| 173 | + // Cache entry signals nonexistence |
| 174 | + $this->missCache[$id] = true; |
| 175 | + } |
| 176 | + return $cached; |
| 177 | + } |
| 178 | + |
| 179 | + $data = $this->loadDataFor( $id ); |
| 180 | + if ( !$data ) { |
| 181 | + // Gadget doesn't exist |
| 182 | + // Use empty array to prevent confusion with $wgMemc->get() return values for missing keys |
| 183 | + $data = array(); |
| 184 | + // DO NOT store $data in $this->data, because it's supposed to contain existing gadgets only |
| 185 | + $this->missCache[$id] = true; |
| 186 | + } else { |
| 187 | + // Save to object cache |
| 188 | + $this->data[$id] = $data; |
| 189 | + } |
| 190 | + // Save to memc |
| 191 | + $wgMemc->set( $key, $data ); |
| 192 | + |
| 193 | + return $data; |
| 194 | + } |
| 195 | +} |
\ No newline at end of file |
Property changes on: branches/RL2/extensions/Gadgets/backend/CachedGadgetRepo.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 196 | + native |
Index: branches/RL2/extensions/Gadgets/backend/LocalGadgetRepo.php |
— | — | @@ -2,30 +2,8 @@ |
3 | 3 | /** |
4 | 4 | * Gadget repository that gets its gadgets from the local database. |
5 | 5 | */ |
6 | | -class LocalGadgetRepo extends GadgetRepo { |
7 | | - /** Cache for EXISTING gadgets. Nonexistent gadgets must not be cached here, |
8 | | - * use $missCache instead. Values may be null, in which case only the gadget's |
9 | | - * existence is cached and the data must still be retrieved from memc or the DB. |
10 | | - * |
11 | | - * array( id => null|array( 'json' => JSON string, 'timestamp' => timestamp ) ) |
12 | | - */ |
13 | | - protected $data = array(); |
| 6 | +class LocalGadgetRepo extends CachedGadgetRepo { |
14 | 7 | |
15 | | - /** Cache for gadget IDs that have been queried and found to be nonexistent. |
16 | | - * |
17 | | - * array( id => ignored ) |
18 | | - */ |
19 | | - protected $missCache = array(); |
20 | | - |
21 | | - /** If true, $data is assumed to contain all existing gadget IDs. |
22 | | - */ |
23 | | - protected $idsLoaded = false; |
24 | | - |
25 | | - /** Memcached key of the gadget names list. Subclasses may override this in their constructor. |
26 | | - * This could've been a static member if we had PHP 5.3's late static binding. |
27 | | - */ |
28 | | - protected $namesKey; |
29 | | - |
30 | 8 | /*** Public static methods ***/ |
31 | 9 | |
32 | 10 | /** |
— | — | @@ -40,6 +18,35 @@ |
41 | 19 | return $instance; |
42 | 20 | } |
43 | 21 | |
| 22 | + /*** Protected methods inherited from CachedGadgetRepo ***/ |
| 23 | + |
| 24 | + protected function loadAllData() { |
| 25 | + $query = $this->getLoadAllDataQuery(); |
| 26 | + $dbr = $this->getDB(); |
| 27 | + $res = $dbr->select( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
| 28 | + $query['options'], $query['join_conds'] ); |
| 29 | + |
| 30 | + $data = array(); |
| 31 | + foreach ( $res as $row ) { |
| 32 | + $data[$row->gd_id] = array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
| 33 | + } |
| 34 | + return $data; |
| 35 | + } |
| 36 | + |
| 37 | + protected function loadDataFor( $id ) { |
| 38 | + $query = $this->getLoadDataForQuery( $id ); |
| 39 | + $dbr = $this->getDB(); |
| 40 | + $row = $dbr->selectRow( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
| 41 | + $query['options'], $query['join_conds'] |
| 42 | + ); |
| 43 | + if ( !$row ) { |
| 44 | + // Gadget doesn't exist |
| 45 | + return array(); |
| 46 | + } else { |
| 47 | + return array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
| 48 | + } |
| 49 | + } |
| 50 | + |
44 | 51 | /*** Public methods inherited from GadgetRepo ***/ |
45 | 52 | |
46 | 53 | /** |
— | — | @@ -51,29 +58,10 @@ |
52 | 59 | $this->namesKey = $this->getMemcKey( 'gadgets', 'localreponames' ); |
53 | 60 | } |
54 | 61 | |
55 | | - public function getGadgetIds() { |
56 | | - $this->loadIDs(); |
57 | | - return array_keys( $this->data ); |
58 | | - } |
59 | | - |
60 | | - public function getGadget( $id ) { |
61 | | - $data = $this->loadDataFor( $id ); |
62 | | - if ( !$data ) { |
63 | | - return null; |
64 | | - } |
65 | | - return new Gadget( $id, $this, $data['json'], $data['timestamp'] ); |
66 | | - } |
67 | | - |
68 | 62 | public function getSource() { |
69 | 63 | return 'local'; |
70 | 64 | } |
71 | 65 | |
72 | | - public function clearInObjectCache() { |
73 | | - $this->data = array(); |
74 | | - $this->missCache = array(); |
75 | | - $this->idsLoaded = false; |
76 | | - } |
77 | | - |
78 | 66 | public function isWriteable() { |
79 | 67 | return true; |
80 | 68 | } |
— | — | @@ -119,23 +107,7 @@ |
120 | 108 | return Status::newFatal( 'gadgets-manager-modify-conflict', $id, $ts ); |
121 | 109 | } |
122 | 110 | |
123 | | - // Update our in-object cache |
124 | | - // This looks stupid: we have an object that we could be caching. But I prefer |
125 | | - // to keep $this->data in a consistent format and have getGadget() always return |
126 | | - // a clone. If it returned a reference to a cached object, the caller could change |
127 | | - // that object and cause weird things to happen. |
128 | | - $this->data[$id] = array( 'json' => $json, 'timestamp' => $newTs ); |
129 | | - // Remove from the missing cache if present there |
130 | | - unset( $this->missCache[$id] ); |
131 | | - // Write to memc too |
132 | | - $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
133 | | - if ( $key !== false ) { |
134 | | - $wgMemc->set( $key, $this->data[$id] ); |
135 | | - } |
136 | | - // Clear the gadget names array in memc |
137 | | - if ( $this->namesKey !== false ) { |
138 | | - $wgMemc->delete( $this->namesKey ); |
139 | | - } |
| 111 | + $this->updateCache( $id, array( 'json' => $json, 'timestamp' => $newTs ) ); |
140 | 112 | |
141 | 113 | return Status::newGood(); |
142 | 114 | } |
— | — | @@ -151,19 +123,7 @@ |
152 | 124 | $dbw->delete( 'gadgets', array( 'gd_id' => $id ), __METHOD__ ); |
153 | 125 | $affectedRows = $dbw->affectedRows(); |
154 | 126 | |
155 | | - // Remove gadget from in-object cache |
156 | | - unset( $this->data[$id] ); |
157 | | - // Add it to the missing cache |
158 | | - $this->missCache[$id] = true; |
159 | | - // Store nonexistence in memc too |
160 | | - $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
161 | | - if ( $key !== false ) { |
162 | | - $wgMemc->set( $key, array() ); |
163 | | - } |
164 | | - // Clear the gadget names array in memc so it'll be regenerated later |
165 | | - if ( $this->namesKey !== false ) { |
166 | | - $wgMemc->delete( $this->namesKey ); |
167 | | - } |
| 127 | + $this->updateCache( $id, null ); |
168 | 128 | |
169 | 129 | if ( $affectedRows === 0 ) { |
170 | 130 | return Status::newFatal( 'gadgets-manager-nosuchgadget', $id ); |
— | — | @@ -229,110 +189,10 @@ |
230 | 190 | } |
231 | 191 | |
232 | 192 | /** |
233 | | - * Populate the keys in $this->data. Values are only populated when loading from the DB; |
234 | | - * when loading from memc, all values are set to null and are lazy-loaded in loadDataFor(). |
235 | | - * @return array Array of gadget IDs |
236 | | - */ |
237 | | - protected function loadIDs() { |
238 | | - global $wgMemc; |
239 | | - if ( $this->idsLoaded ) { |
240 | | - // Already loaded |
241 | | - return array_keys( $this->data ); |
242 | | - } |
243 | | - |
244 | | - // Try memc |
245 | | - $cached = $this->namesKey !== false ? $wgMemc->get( $this->namesKey ) : false; |
246 | | - if ( is_array( $cached ) ) { |
247 | | - // Yay, data is in cache |
248 | | - // Add to $this->data , but let things already in $this->data take precedence |
249 | | - $this->data += $cached; |
250 | | - $this->idsLoaded = true; |
251 | | - return array_keys( $this->data ); |
252 | | - } |
253 | | - |
254 | | - // Get from DB |
255 | | - $query = $this->getLoadIDsQuery(); |
256 | | - $dbr = $this->getDB(); |
257 | | - $res = $dbr->select( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
258 | | - $query['options'], $query['join_conds'] ); |
259 | | - |
260 | | - $toCache = array(); |
261 | | - foreach ( $res as $row ) { |
262 | | - $this->data[$row->gd_id] = array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
263 | | - $toCache[$row->gd_id] = null; |
264 | | - } |
265 | | - // Write to memc |
266 | | - $wgMemc->set( $this->namesKey, $toCache ); |
267 | | - $this->idsLoaded = true; |
268 | | - return array_keys( $this->data ); |
269 | | - } |
270 | | - |
271 | | - /** |
272 | | - * Populate a given Gadget's data in $this->data . Tries memc first, then falls back to a DB query. |
273 | | - * @param $id string Gadget ID |
274 | | - * @return array( 'json' => JSON string, 'timestamp' => timestamp ) or empty array if the gadget doesn't exist. |
275 | | - */ |
276 | | - protected function loadDataFor( $id ) { |
277 | | - global $wgMemc; |
278 | | - if ( isset( $this->data[$id] ) && is_array( $this->data[$id] ) ) { |
279 | | - // Already loaded, nothing to do here. |
280 | | - return $this->data[$id]; |
281 | | - } |
282 | | - if ( isset( $this->missCache[$id] ) ) { |
283 | | - // Gadget is already known to be missing |
284 | | - return array(); |
285 | | - } |
286 | | - // Need to use array_key_exists() here because isset() returns true for nulls. !@#$ you, PHP |
287 | | - if ( $this->idsLoaded && !array_key_exists( $id, $this->data ) ) { |
288 | | - // All IDs have been loaded into $this->data but $id isn't in there, |
289 | | - // therefore it doesn't exist. |
290 | | - $this->missCache[$id] = true; |
291 | | - return array(); |
292 | | - } |
293 | | - |
294 | | - // Try cache |
295 | | - $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
296 | | - $cached = $key !== false ? $wgMemc->get( $key ) : false; |
297 | | - if ( is_array( $cached ) ) { |
298 | | - // Yay, data is in cache |
299 | | - if ( count( $cached ) ) { |
300 | | - // Cache entry contains data |
301 | | - $this->data[$id] = $cached; |
302 | | - } else { |
303 | | - // Cache entry signals nonexistence |
304 | | - $this->missCache[$id] = true; |
305 | | - } |
306 | | - return $cached; |
307 | | - } |
308 | | - |
309 | | - // Get from database |
310 | | - $query = $this->getLoadDataForQuery( $id ); |
311 | | - $dbr = $this->getDB(); |
312 | | - $row = $dbr->selectRow( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
313 | | - $query['options'], $query['join_conds'] |
314 | | - ); |
315 | | - if ( !$row ) { |
316 | | - // Gadget doesn't exist |
317 | | - // Use empty array to prevent confusion with $wgMemc->get() return values for missing keys |
318 | | - $data = array(); |
319 | | - // DO NOT store this in $this->data, because it's supposed to contain existing gadgets only |
320 | | - $this->missCache[$id] = true; |
321 | | - } else { |
322 | | - $data = array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
323 | | - // Save to object cache |
324 | | - $this->data[$id] = $data; |
325 | | - } |
326 | | - // Save to memc |
327 | | - $wgMemc->set( $key, $data ); |
328 | | - |
329 | | - return $data; |
330 | | - } |
331 | | - |
332 | | - /** |
333 | | - * Get the DB query to use in getLoadIDs(). Subclasses can override this to tweak the query. |
| 193 | + * Get the DB query to use in loadAllData(). Subclasses can override this to tweak the query. |
334 | 194 | * @return Array |
335 | 195 | */ |
336 | | - protected function getLoadIDsQuery() { |
| 196 | + protected function getLoadAllDataQuery() { |
337 | 197 | return array( |
338 | 198 | 'tables' => 'gadgets', |
339 | 199 | 'fields' => array( 'gd_id', 'gd_blob', 'gd_timestamp' ), |
Index: branches/RL2/extensions/Gadgets/backend/ForeignDBGadgetRepo.php |
— | — | @@ -77,7 +77,7 @@ |
78 | 78 | } |
79 | 79 | } |
80 | 80 | |
81 | | - protected function getLoadIDsQuery() { |
| 81 | + protected function getLoadAllDataQuery() { |
82 | 82 | $query = parent::getLoadIDsQuery(); |
83 | 83 | $query['conds']['gd_shared'] = 1; |
84 | 84 | return $query; |
Index: branches/RL2/extensions/Gadgets/backend/GadgetRepo.php |
— | — | @@ -34,14 +34,6 @@ |
35 | 35 | abstract public function getGadget( $id ); |
36 | 36 | |
37 | 37 | /** |
38 | | - * Clear any in-object caches this repository may have. In particular, |
39 | | - * the return values of getGadgetIds() and getGadget() may be cached. |
40 | | - * Callers may wish to clear this cache and reobtain a Gadget object |
41 | | - * when they get a conflict error. |
42 | | - */ |
43 | | - abstract public function clearInObjectCache(); |
44 | | - |
45 | | - /** |
46 | 38 | * Check whether this repository allows write actions. If this method returns false, |
47 | 39 | * methods that modify the state of the repository or the gadgets in it (i.e. addGadget(), |
48 | 40 | * modifyGadget() and deleteGadget()) will always fail. |