Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 80 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
SqlModuleDependencyStore | |
0.00% |
0 / 80 |
|
0.00% |
0 / 8 |
552 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
retrieveMulti | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
storeMulti | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
remove | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
fetchDependencyBlobs | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
getReplicaDb | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPrimaryDb | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getEntityNameComponents | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace Wikimedia\DependencyStore; |
22 | |
23 | use InvalidArgumentException; |
24 | use Wikimedia\Rdbms\IDatabase; |
25 | use Wikimedia\Rdbms\ILoadBalancer; |
26 | use Wikimedia\Rdbms\IReadableDatabase; |
27 | use Wikimedia\Rdbms\OrExpressionGroup; |
28 | |
29 | /** |
30 | * Track per-module file dependencies in the core module_deps table |
31 | * |
32 | * Wiki farms that are too big for maintenance/update.php, can clean up |
33 | * unneeded data for modules that no longer exist after a MW upgrade, |
34 | * by running maintenance/cleanupRemovedModules.php. |
35 | * |
36 | * To force a rebuild and incurr a small penalty in browser cache churn, |
37 | * run maintenance/purgeModuleDeps.php instead. |
38 | * |
39 | * @internal For use by ResourceLoader\Module only |
40 | * @since 1.35 |
41 | */ |
42 | class SqlModuleDependencyStore extends DependencyStore { |
43 | /** @var ILoadBalancer */ |
44 | private $lb; |
45 | |
46 | /** |
47 | * @param ILoadBalancer $lb Storage backend |
48 | */ |
49 | public function __construct( ILoadBalancer $lb ) { |
50 | $this->lb = $lb; |
51 | } |
52 | |
53 | public function retrieveMulti( $type, array $entities ) { |
54 | $dbr = $this->getReplicaDb(); |
55 | |
56 | $depsBlobByEntity = $this->fetchDependencyBlobs( $entities, $dbr ); |
57 | |
58 | $storedPathsByEntity = []; |
59 | foreach ( $depsBlobByEntity as $entity => $depsBlob ) { |
60 | $storedPathsByEntity[$entity] = json_decode( $depsBlob, true ); |
61 | } |
62 | |
63 | $results = []; |
64 | foreach ( $entities as $entity ) { |
65 | $paths = $storedPathsByEntity[$entity] ?? []; |
66 | $results[$entity] = $this->newEntityDependencies( $paths, null ); |
67 | } |
68 | |
69 | return $results; |
70 | } |
71 | |
72 | public function storeMulti( $type, array $dataByEntity, $ttl ) { |
73 | // Avoid opening a primary DB connection when it's not needed. |
74 | // ResourceLoader::saveModuleDependenciesInternal calls this method unconditionally |
75 | // with empty values most of the time. |
76 | if ( !$dataByEntity ) { |
77 | return; |
78 | } |
79 | |
80 | $dbw = $this->getPrimaryDb(); |
81 | $depsBlobByEntity = $this->fetchDependencyBlobs( array_keys( $dataByEntity ), $dbw ); |
82 | |
83 | $rows = []; |
84 | foreach ( $dataByEntity as $entity => $data ) { |
85 | [ $module, $variant ] = $this->getEntityNameComponents( $entity ); |
86 | if ( !is_array( $data[self::KEY_PATHS] ) ) { |
87 | throw new InvalidArgumentException( "Invalid entry for '$entity'" ); |
88 | } |
89 | |
90 | // Normalize the list by removing duplicates and sortings |
91 | $paths = array_values( array_unique( $data[self::KEY_PATHS] ) ); |
92 | sort( $paths, SORT_STRING ); |
93 | $blob = json_encode( $paths, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); |
94 | |
95 | $existingBlob = $depsBlobByEntity[$entity] ?? null; |
96 | if ( $blob !== $existingBlob ) { |
97 | $rows[] = [ |
98 | 'md_module' => $module, |
99 | 'md_skin' => $variant, |
100 | 'md_deps' => $blob |
101 | ]; |
102 | } |
103 | } |
104 | |
105 | // @TODO: use a single query with VALUES()/aliases support in DB wrapper |
106 | // See https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html |
107 | foreach ( $rows as $row ) { |
108 | $dbw->newInsertQueryBuilder() |
109 | ->insertInto( 'module_deps' ) |
110 | ->row( $row ) |
111 | ->onDuplicateKeyUpdate() |
112 | ->uniqueIndexFields( [ 'md_module', 'md_skin' ] ) |
113 | ->set( [ 'md_deps' => $row['md_deps'] ] ) |
114 | ->caller( __METHOD__ )->execute(); |
115 | } |
116 | } |
117 | |
118 | public function remove( $type, $entities ) { |
119 | // Avoid opening a primary DB connection when it's not needed. |
120 | // ResourceLoader::saveModuleDependenciesInternal calls this method unconditionally |
121 | // with empty values most of the time. |
122 | if ( !$entities ) { |
123 | return; |
124 | } |
125 | |
126 | $dbw = $this->getPrimaryDb(); |
127 | $disjunctionConds = []; |
128 | foreach ( (array)$entities as $entity ) { |
129 | [ $module, $variant ] = $this->getEntityNameComponents( $entity ); |
130 | $disjunctionConds[] = $dbw |
131 | ->expr( 'md_skin', '=', $variant ) |
132 | ->and( 'md_module', '=', $module ); |
133 | } |
134 | |
135 | if ( $disjunctionConds ) { |
136 | $dbw->newDeleteQueryBuilder() |
137 | ->deleteFrom( 'module_deps' ) |
138 | ->where( new OrExpressionGroup( ...$disjunctionConds ) ) |
139 | ->caller( __METHOD__ )->execute(); |
140 | } |
141 | } |
142 | |
143 | /** |
144 | * @param string[] $entities |
145 | * @param IReadableDatabase $db |
146 | * @return string[] |
147 | */ |
148 | private function fetchDependencyBlobs( array $entities, IReadableDatabase $db ) { |
149 | $modulesByVariant = []; |
150 | foreach ( $entities as $entity ) { |
151 | [ $module, $variant ] = $this->getEntityNameComponents( $entity ); |
152 | $modulesByVariant[$variant][] = $module; |
153 | } |
154 | |
155 | $disjunctionConds = []; |
156 | foreach ( $modulesByVariant as $variant => $modules ) { |
157 | $disjunctionConds[] = $db |
158 | ->expr( 'md_skin', '=', $variant ) |
159 | ->and( 'md_module', '=', $modules ); |
160 | } |
161 | |
162 | $depsBlobByEntity = []; |
163 | |
164 | if ( $disjunctionConds ) { |
165 | $res = $db->newSelectQueryBuilder() |
166 | ->select( [ 'md_module', 'md_skin', 'md_deps' ] ) |
167 | ->from( 'module_deps' ) |
168 | ->where( new OrExpressionGroup( ...$disjunctionConds ) ) |
169 | ->caller( __METHOD__ )->fetchResultSet(); |
170 | |
171 | foreach ( $res as $row ) { |
172 | $entity = "{$row->md_module}|{$row->md_skin}"; |
173 | $depsBlobByEntity[$entity] = $row->md_deps; |
174 | } |
175 | } |
176 | |
177 | return $depsBlobByEntity; |
178 | } |
179 | |
180 | /** |
181 | * @return IReadableDatabase |
182 | */ |
183 | private function getReplicaDb() { |
184 | return $this->lb |
185 | ->getConnection( DB_REPLICA, [], false, ( $this->lb )::CONN_TRX_AUTOCOMMIT ); |
186 | } |
187 | |
188 | /** |
189 | * @return IDatabase |
190 | */ |
191 | private function getPrimaryDb() { |
192 | return $this->lb |
193 | ->getConnection( DB_PRIMARY, [], false, ( $this->lb )::CONN_TRX_AUTOCOMMIT ); |
194 | } |
195 | |
196 | /** |
197 | * @param string $entity |
198 | * @return string[] |
199 | */ |
200 | private function getEntityNameComponents( $entity ) { |
201 | $parts = explode( '|', $entity, 2 ); |
202 | if ( count( $parts ) !== 2 ) { |
203 | throw new InvalidArgumentException( "Invalid module entity '$entity'" ); |
204 | } |
205 | |
206 | return $parts; |
207 | } |
208 | } |