Index: branches/RL2/extensions/Gadgets/Gadgets.php |
— | — | @@ -47,7 +47,7 @@ |
48 | 48 | * // TODO: Make this the default? |
49 | 49 | * 'dbFlags' => ( $wgDebugDumpSql ? DBO_DEBUG : 0 ) | DBO_DEFAULT // Use this value unless you know what you're doing |
50 | 50 | * 'tablePrefix' => 'mw_', // Table prefix for the foreign wiki's database, or '' if no prefix |
51 | | - //* 'hasSharedCache' => true, // Whether the foreign wiki's cache is accessible through $wgMemc // TODO: needed? |
| 51 | + * 'hasSharedCache' => true, // Whether the foreign wiki's cache is accessible through $wgMemc |
52 | 52 | * ); |
53 | 53 | * |
54 | 54 | * For foreign API-based gadget repositories, use: |
Index: branches/RL2/extensions/Gadgets/backend/LocalGadgetRepo.php |
— | — | @@ -1,9 +1,12 @@ |
2 | 2 | <?php |
| 3 | +// TODO: Make LocalGadgetRepo a singleton |
| 4 | + |
3 | 5 | /** |
4 | 6 | * Gadget repository that gets its gadgets from the local database. |
5 | 7 | */ |
6 | 8 | class LocalGadgetRepo extends GadgetRepo { |
7 | | - protected $data = null; |
| 9 | + protected $data = array(); |
| 10 | + protected $namesLoaded = false; |
8 | 11 | |
9 | 12 | /*** Public methods inherited from GadgetRepo ***/ |
10 | 13 | |
— | — | @@ -19,16 +22,16 @@ |
20 | 23 | } |
21 | 24 | |
22 | 25 | public function getGadgetIds() { |
23 | | - $this->loadData(); |
| 26 | + $this->loadIDs(); |
24 | 27 | return array_keys( $this->data ); |
25 | 28 | } |
26 | 29 | |
27 | 30 | public function getGadget( $id ) { |
28 | | - $this->loadData(); |
29 | | - if ( !isset( $this->data[$id] ) ) { |
| 31 | + $data = $this->loadDataFor( $id ); |
| 32 | + if ( !$data ) { |
30 | 33 | return null; |
31 | 34 | } |
32 | | - return new Gadget( $id, $this, $this->data[$id]['json'], $this->data[$id]['timestamp'] ); |
| 35 | + return new Gadget( $id, $this, $data['json'], $data['timestamp'] ); |
33 | 36 | } |
34 | 37 | |
35 | 38 | public function getSource() { |
— | — | @@ -44,12 +47,12 @@ |
45 | 48 | } |
46 | 49 | |
47 | 50 | public function modifyGadget( Gadget $gadget, $timestamp = null ) { |
| 51 | + global $wgMemc; |
48 | 52 | if ( !$this->isWriteable() ) { |
49 | 53 | return Status::newFatal( 'gadget-manager-readonly-repository' ); |
50 | 54 | } |
51 | 55 | |
52 | 56 | $dbw = $this->getMasterDB(); |
53 | | - $this->loadData(); |
54 | 57 | $id = $gadget->getId(); |
55 | 58 | $json = $gadget->getJSON(); |
56 | 59 | $ts = $dbw->timestamp( $gadget->getTimestamp() ); |
— | — | @@ -73,12 +76,14 @@ |
74 | 77 | 'gd_timestamp <= ' . $dbw->addQuotes( $ts ) // for conflict detection |
75 | 78 | ), __METHOD__ |
76 | 79 | ); |
| 80 | + $created = $dbw->affectedRows(); |
77 | 81 | } |
78 | 82 | $dbw->commit(); |
79 | 83 | |
80 | 84 | // Detect conflicts |
81 | | - if ( $dbw->affectedRows() === 0 ) { |
| 85 | + if ( !$created ) { |
82 | 86 | // Some conflict occurred |
| 87 | + wfDebug( __METHOD__ . ": conflict detected\n" ); |
83 | 88 | return Status::newFatal( 'gadgets-manager-modify-conflict', $id, $ts ); |
84 | 89 | } |
85 | 90 | |
— | — | @@ -88,24 +93,45 @@ |
89 | 94 | // a clone. If it returned a reference to a cached object, the caller could change |
90 | 95 | // that object and cause weird things to happen. |
91 | 96 | $this->data[$id] = array( 'json' => $json, 'timestamp' => $newTs ); |
| 97 | + // Write to memc too |
| 98 | + $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
| 99 | + if ( $key !== false ) { |
| 100 | + $wgMemc->set( $key, $this->data[$id] ); |
| 101 | + } |
| 102 | + // Clear the gadget names array in memc |
| 103 | + $namesKey = $this->getMemcKey( 'gadgets', 'localreponames' ); |
| 104 | + if ( $namesKey !== false ) { |
| 105 | + $wgMemc->delete( $namesKey ); |
| 106 | + } |
92 | 107 | |
93 | 108 | return Status::newGood(); |
94 | 109 | } |
95 | 110 | |
96 | 111 | public function deleteGadget( $id ) { |
| 112 | + global $wgMemc; |
97 | 113 | if ( !$this->isWriteable() ) { |
98 | 114 | return Status::newFatal( 'gadget-manager-readonly-repository' ); |
99 | 115 | } |
100 | 116 | |
101 | | - $this->loadData(); |
102 | | - if ( !isset( $this->data[$id] ) ) { |
103 | | - return Status::newFatal( 'gadgets-manager-nosuchgadget', $id ); |
| 117 | + // Remove gadget from database |
| 118 | + $dbw = $this->getMasterDB(); |
| 119 | + $dbw->delete( 'gadgets', array( 'gd_id' => $id ), __METHOD__ ); |
| 120 | + $affectedRows = $dbw->affectedRows(); |
| 121 | + |
| 122 | + // Remove gadget from in-object cache |
| 123 | + unset( $this->data[$id] ); |
| 124 | + // Remove from memc too |
| 125 | + $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
| 126 | + if ( $key !== false ) { |
| 127 | + $wgMemc->delete( $key ); |
104 | 128 | } |
| 129 | + // Clear the gadget names array in memc |
| 130 | + $namesKey = $this->getMemcKey( 'gadgets', 'localreponames' ); |
| 131 | + if ( $namesKey !== false ) { |
| 132 | + $wgMemc->delete( $namesKey ); |
| 133 | + } |
105 | 134 | |
106 | | - unset( $this->data[$id] ); |
107 | | - $dbw = $this->getMasterDB(); |
108 | | - $dbw->delete( 'gadgets', array( 'gd_id' => $id ), __METHOD__ ); |
109 | | - if ( $dbw->affectedRows() === 0 ) { |
| 135 | + if ( $affectedRows === 0 ) { |
110 | 136 | return Status::newFatal( 'gadgets-manager-nosuchgadget', $id ); |
111 | 137 | } |
112 | 138 | return Status::newGood(); |
— | — | @@ -129,36 +155,105 @@ |
130 | 156 | return wfGetDB( DB_SLAVE ); |
131 | 157 | } |
132 | 158 | |
| 159 | + |
133 | 160 | /*** Protected methods ***/ |
134 | 161 | |
135 | 162 | /** |
136 | | - * Populate $this->data from the DB, if that hasn't happened yet. All methods using |
137 | | - * $this->data must call this before accessing $this->data . |
| 163 | + * Get a memcached key. Subclasses can override this to use a foreign memc |
| 164 | + * @return string|bool Cache key, or false if this repo has no shared memc |
138 | 165 | */ |
139 | | - protected function loadData() { |
140 | | - // FIXME: Make the cache shared somehow, it's getting repopulated for every instance now |
141 | | - // FIXME: Reconsider the query-everything behavior; maybe use memc? |
142 | | - if ( is_array( $this->data ) ) { |
| 166 | + protected function getMemcKey( /* ... */ ) { |
| 167 | + $args = func_get_args(); |
| 168 | + return call_user_func_array( 'wfMemcKey', $args ); |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * Populate the keys in $this->data. Values are only populated when loading from the DB; |
| 173 | + * when loading from memc, all values are set to null and are lazy-loaded in loadDataFor(). |
| 174 | + * @return array Array of gadget IDs |
| 175 | + */ |
| 176 | + protected function loadIDs() { |
| 177 | + global $wgMemc; |
| 178 | + if ( $this->namesLoaded ) { |
143 | 179 | // Already loaded |
144 | | - return; |
| 180 | + return array_keys( $this->data ); |
145 | 181 | } |
146 | | - $this->data = array(); |
147 | 182 | |
148 | | - $query = $this->getLoadDataQuery(); |
| 183 | + // Try memc |
| 184 | + $key = $this->getMemcKey( 'gadgets', 'localreponames' ); |
| 185 | + $cached = $key !== false ? $wgMemc->get( $key ) : false; |
| 186 | + if ( is_array( $cached ) ) { |
| 187 | + // Yay, data is in cache |
| 188 | + // Add to $this->data , but let things already in $this->data take precedence |
| 189 | + $this->data += $cached; |
| 190 | + $this->namesLoaded = true; |
| 191 | + return array_keys( $this->data ); |
| 192 | + } |
| 193 | + |
| 194 | + // Get from DB |
| 195 | + $query = $this->getLoadIDsQuery(); |
149 | 196 | $dbr = $this->getDB(); |
150 | 197 | $res = $dbr->select( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
151 | 198 | $query['options'], $query['join_conds'] ); |
152 | 199 | |
| 200 | + $toCache = array(); |
153 | 201 | foreach ( $res as $row ) { |
154 | 202 | $this->data[$row->gd_id] = array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
| 203 | + $toCache[$row->gd_id] = null; |
155 | 204 | } |
| 205 | + // Write to memc |
| 206 | + $wgMemc->set( $key, $toCache ); |
| 207 | + $this->namesLoaded = true; |
| 208 | + return array_keys( $this->data ); |
156 | 209 | } |
157 | 210 | |
158 | 211 | /** |
159 | | - * Get the DB query to use in loadData(). Subclasses can override this to tweak the query. |
| 212 | + * Populate a given Gadget's data in $this->data . Tries memc first, then falls back to a DB query. |
| 213 | + * @param $id string Gadget ID |
| 214 | + * @return array( 'json' => JSON string, 'timestamp' => timestamp ) or empty array if the gadget doesn't exist. |
| 215 | + */ |
| 216 | + protected function loadDataFor( $id ) { |
| 217 | + global $wgMemc; |
| 218 | + if ( isset( $this->data[$id] ) && is_array( $this->data[$id] ) ) { |
| 219 | + // Already loaded, nothing to do here. |
| 220 | + return $this->data[$id]; |
| 221 | + } |
| 222 | + |
| 223 | + // Try cache |
| 224 | + $key = $this->getMemcKey( 'gadgets', 'localrepodata', $id ); |
| 225 | + $cached = $key !== false ? $wgMemc->get( $key ) : false; |
| 226 | + if ( is_array( $cached ) ) { |
| 227 | + // Yay, data is in cache |
| 228 | + $this->data[$id] = $cached; |
| 229 | + return $cached; |
| 230 | + } |
| 231 | + |
| 232 | + // Get from database |
| 233 | + $query = $this->getLoadDataForQuery( $id ); |
| 234 | + $dbr = $this->getDB(); |
| 235 | + $row = $dbr->selectRow( $query['tables'], $query['fields'], $query['conds'], __METHOD__, |
| 236 | + $query['options'], $query['join_conds'] |
| 237 | + ); |
| 238 | + if ( !$row ) { |
| 239 | + // Gadget doesn't exist |
| 240 | + // Use empty array to prevent confusion with $wgMemc->get() return values for missing keys |
| 241 | + $data = array(); |
| 242 | + } else { |
| 243 | + $data = array( 'json' => $row->gd_blob, 'timestamp' => $row->gd_timestamp ); |
| 244 | + } |
| 245 | + // Save to object cache |
| 246 | + $this->data[$id] = $data; |
| 247 | + // Save to memc |
| 248 | + $wgMemc->set( $key, $data ); |
| 249 | + |
| 250 | + return $data; |
| 251 | + } |
| 252 | + |
| 253 | + /** |
| 254 | + * Get the DB query to use in getLoadIDs(). Subclasses can override this to tweak the query. |
160 | 255 | * @return Array |
161 | 256 | */ |
162 | | - protected function getLoadDataQuery() { |
| 257 | + protected function getLoadIDsQuery() { |
163 | 258 | return array( |
164 | 259 | 'tables' => 'gadgets', |
165 | 260 | 'fields' => array( 'gd_id', 'gd_blob', 'gd_timestamp' ), |
— | — | @@ -167,4 +262,19 @@ |
168 | 263 | 'join_conds' => array(), |
169 | 264 | ); |
170 | 265 | } |
| 266 | + |
| 267 | + /** |
| 268 | + * Get the DB query to use in loadDataFor(). Subclasses can override this to tweak the query. |
| 269 | + * @param $id string Gadget ID |
| 270 | + * @return Array |
| 271 | + */ |
| 272 | + protected function getLoadDataForQuery( $id ) { |
| 273 | + return array( |
| 274 | + 'tables' => 'gadgets', |
| 275 | + 'fields' => array( 'gd_blob', 'gd_timestamp' ), |
| 276 | + 'conds' => array( 'gd_id' => $id ), |
| 277 | + 'options' => array(), |
| 278 | + 'join_conds' => array(), |
| 279 | + ); |
| 280 | + } |
171 | 281 | } |
Index: branches/RL2/extensions/Gadgets/backend/ForeignDBGadgetRepo.php |
— | — | @@ -11,7 +11,7 @@ |
12 | 12 | * 'dbName': Database name |
13 | 13 | * 'dbFlags': Bitmap of the DBO_* flags. Recommended value is ( $wgDebugDumpSql ? DBO_DEBUG : 0 ) | DBO_DEFAULT |
14 | 14 | * 'tablePrefix': Table prefix |
15 | | - //* 'hasSharedCache': Whether the foreign wiki's cache is accessible through $wgMemc // TODO: needed? |
| 15 | + * 'hasSharedCache': Whether the foreign wiki's cache is accessible through $wgMemc |
16 | 16 | */ |
17 | 17 | class ForeignDBGadgetRepo extends LocalGadgetRepo { |
18 | 18 | protected $db = null; |
— | — | @@ -26,7 +26,7 @@ |
27 | 27 | parent::__construct( $options ); |
28 | 28 | |
29 | 29 | $optionKeys = array( 'source', 'dbType', 'dbServer', 'dbUser', 'dbPassword', 'dbName', |
30 | | - 'dbFlags', 'tablePrefix'/*, 'hasSharedCache'*/ ); |
| 30 | + 'dbFlags', 'tablePrefix', 'hasSharedCache' ); |
31 | 31 | foreach ( $optionKeys as $optionKey ) { |
32 | 32 | $this->{$optionKey} = $options[$optionKey]; |
33 | 33 | } |
— | — | @@ -61,9 +61,30 @@ |
62 | 62 | return $this->db; |
63 | 63 | } |
64 | 64 | |
65 | | - protected function getLoadDataQuery() { |
66 | | - $query = parent::getLoadDataQuery(); |
| 65 | + protected function getMemcKey( /* ... */ ) { |
| 66 | + if ( $this->hasSharedCache ) { |
| 67 | + $args = func_get_args(); |
| 68 | + // FIXME: This is a dirty hack. Need to cache localrepo and foreignrepo name lists separately |
| 69 | + // because one includes non-shared gadgets and the other doesn't |
| 70 | + if ( $args[1] === 'localreponames' ) { |
| 71 | + $args[1] = 'foreignreponames'; |
| 72 | + } |
| 73 | + array_unshift( $args, $this->dbName, $this->tablePrefix ); |
| 74 | + return call_user_func_array( 'wfForeignMemcKey', $args ); |
| 75 | + } else { |
| 76 | + return false; |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + protected function getLoadIDsQuery() { |
| 81 | + $query = parent::getLoadIDsQuery(); |
67 | 82 | $query['conds']['gd_shared'] = 1; |
68 | 83 | return $query; |
69 | 84 | } |
| 85 | + |
| 86 | + protected function getLoadDataForQuery( $id ) { |
| 87 | + $query = parent::getLoadDataForQuery( $id ); |
| 88 | + $query['conds']['gd_shared'] = 1; |
| 89 | + return $query; |
| 90 | + } |
70 | 91 | } |