Skip to content

Commit 32942f6

Browse files
author
epriestley
committedFeb 2, 2021
Introduce storage patch "phases" to allow index-rebuilding patches to execute after worker queue schema changes
Summary: Ref T13591. Some storage patches queue worker tasks, currently always to rebuild search indexes. These patches can not execute in creation order if a later patch modifies the worker task table, since they'll try to perform a modern INSERT against an out-of-date table schema. Such a modification is desirable in the context of T13591, but making it causes these patches to fail. Patches have an existing "after" mechanism which allows them to have explicit dependencies. This mechanism could be used to resolve this issue, but all patches with a dependency like this would need to be updated every time the queue table changes. Instead, introduce "phases" to provide broader ordering rules. There are now two phases: "default" and "worker". Patches in the "worker" phase execute after patches in the "default" phase. Phases may eventually be further separated, but Test Plan: - Ran `bin/storage status`, saw patches annotated with phases. - Will apply `containerPHID` changes on top of this. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13591 Differential Revision: https://secure.phabricator.com/D21529
1 parent 6d5920f commit 32942f6

8 files changed

+215
-17
lines changed
 

‎resources/sql/autopatches/20190412.dashboard.13.rebuild.php

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
// @phase worker
4+
35
PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery(
46
'PhabricatorDashboardQuery');
57

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
<?php
22

3+
// @phase worker
4+
35
PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery('HeraldRuleQuery');
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
<?php
22

3+
// @phase worker
4+
35
PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery('HeraldRuleQuery');
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<?php
22

3+
// @phase worker
4+
35
PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery(
46
'PhabricatorRepositoryQuery');

‎src/infrastructure/storage/management/PhabricatorStoragePatch.php

+36
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ final class PhabricatorStoragePatch extends Phobject {
99
private $after;
1010
private $legacy;
1111
private $dead;
12+
private $phase;
13+
14+
const PHASE_DEFAULT = 'default';
15+
const PHASE_WORKER = 'worker';
1216

1317
public function __construct(array $dict) {
1418
$this->key = $dict['key'];
@@ -18,6 +22,7 @@ public function __construct(array $dict) {
1822
$this->name = $dict['name'];
1923
$this->after = $dict['after'];
2024
$this->dead = $dict['dead'];
25+
$this->phase = $dict['phase'];
2126
}
2227

2328
public function getLegacy() {
@@ -44,6 +49,10 @@ public function getKey() {
4449
return $this->key;
4550
}
4651

52+
public function getPhase() {
53+
return $this->phase;
54+
}
55+
4756
public function isDead() {
4857
return $this->dead;
4958
}
@@ -52,4 +61,31 @@ public function getIsGlobalPatch() {
5261
return ($this->getType() == 'php');
5362
}
5463

64+
public static function getPhaseList() {
65+
return array_keys(self::getPhaseMap());
66+
}
67+
68+
public static function getDefaultPhase() {
69+
return self::PHASE_DEFAULT;
70+
}
71+
72+
private static function getPhaseMap() {
73+
return array(
74+
self::PHASE_DEFAULT => array(
75+
'order' => 0,
76+
),
77+
self::PHASE_WORKER => array(
78+
'order' => 1,
79+
),
80+
);
81+
}
82+
83+
public function newSortVector() {
84+
$map = self::getPhaseMap();
85+
$phase = $this->getPhase();
86+
87+
return id(new PhutilSortVector())
88+
->addInt($map[$phase]['order']);
89+
}
90+
5591
}

‎src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php

+17-10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function didExecute(PhutilArgumentParser $args) {
3333
$table = id(new PhutilConsoleTable())
3434
->setShowHeader(false)
3535
->addColumn('id', array('title' => pht('ID')))
36+
->addColumn('phase', array('title' => pht('Phase')))
3637
->addColumn('host', array('title' => pht('Host')))
3738
->addColumn('status', array('title' => pht('Status')))
3839
->addColumn('duration', array('title' => pht('Duration')))
@@ -49,16 +50,22 @@ public function didExecute(PhutilArgumentParser $args) {
4950
$duration = pht('%s us', new PhutilNumber($duration));
5051
}
5152

52-
$table->addRow(array(
53-
'id' => $patch->getFullKey(),
54-
'host' => $ref->getRefKey(),
55-
'status' => in_array($patch->getFullKey(), $applied)
56-
? pht('Applied')
57-
: pht('Not Applied'),
58-
'duration' => $duration,
59-
'type' => $patch->getType(),
60-
'name' => $patch->getName(),
61-
));
53+
if (in_array($patch->getFullKey(), $applied)) {
54+
$status = pht('Applied');
55+
} else {
56+
$status = pht('Not Applied');
57+
}
58+
59+
$table->addRow(
60+
array(
61+
'id' => $patch->getFullKey(),
62+
'phase' => $patch->getPhase(),
63+
'host' => $ref->getRefKey(),
64+
'status' => $status,
65+
'duration' => $duration,
66+
'type' => $patch->getType(),
67+
'name' => $patch->getName(),
68+
));
6269
}
6370

6471
$table->draw();

‎src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php

+4
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,10 @@ final private function doUpgradeSchemata(
922922
$patches = $this->patches;
923923
$is_dryrun = $this->dryRun;
924924

925+
// We expect that patches should already be sorted properly. However,
926+
// phase behavior will be wrong if they aren't, so make sure.
927+
$patches = msortv($patches, 'newSortVector');
928+
925929
$api_map = array();
926930
foreach ($apis as $api) {
927931
$api_map[$api->getRef()->getRefKey()] = $api;

‎src/infrastructure/storage/patch/PhabricatorSQLPatchList.php

+150-7
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,20 @@ protected function buildPatchesFromDirectory($directory) {
2727
$directory));
2828
}
2929

30+
$patch_type = $matches[1];
31+
$patch_full_path = rtrim($directory, '/').'/'.$patch;
32+
33+
$attributes = array();
34+
if ($patch_type === 'php') {
35+
$attributes = $this->getPHPPatchAttributes(
36+
$patch,
37+
$patch_full_path);
38+
}
39+
3040
$patches[$patch] = array(
31-
'type' => $matches[1],
32-
'name' => rtrim($directory, '/').'/'.$patch,
33-
);
41+
'type' => $patch_type,
42+
'name' => $patch_full_path,
43+
) + $attributes;
3444
}
3545

3646
return $patches;
@@ -45,8 +55,16 @@ final public static function buildAllPatches() {
4555
$specs = array();
4656
$seen_namespaces = array();
4757

58+
$phases = PhabricatorStoragePatch::getPhaseList();
59+
$phases = array_fuse($phases);
60+
61+
$default_phase = PhabricatorStoragePatch::getDefaultPhase();
62+
4863
foreach ($patch_lists as $patch_list) {
49-
$last_key = null;
64+
$last_keys = array_fill_keys(
65+
array_keys($phases),
66+
null);
67+
5068
foreach ($patch_list->getPatches() as $key => $patch) {
5169
if (!is_array($patch)) {
5270
throw new Exception(
@@ -63,6 +81,7 @@ final public static function buildAllPatches() {
6381
'after' => true,
6482
'legacy' => true,
6583
'dead' => true,
84+
'phase' => true,
6685
);
6786

6887
foreach ($patch as $pkey => $pval) {
@@ -128,8 +147,26 @@ final public static function buildAllPatches() {
128147
$patch['legacy'] = false;
129148
}
130149

150+
if (!array_key_exists('phase', $patch)) {
151+
$patch['phase'] = $default_phase;
152+
}
153+
154+
$patch_phase = $patch['phase'];
155+
156+
if (!isset($phases[$patch_phase])) {
157+
throw new Exception(
158+
pht(
159+
'Storage patch "%s" specifies it should apply in phase "%s", '.
160+
'but this phase is unrecognized. Valid phases are: %s.',
161+
$full_key,
162+
$patch_phase,
163+
implode(', ', array_keys($phases))));
164+
}
165+
166+
$last_key = $last_keys[$patch_phase];
167+
131168
if (!array_key_exists('after', $patch)) {
132-
if ($last_key === null) {
169+
if ($last_key === null && $patch_phase === $default_phase) {
133170
throw new Exception(
134171
pht(
135172
"Patch '%s' is missing key 'after', and is the first patch ".
@@ -139,10 +176,14 @@ final public static function buildAllPatches() {
139176
$full_key,
140177
get_class($patch_list)));
141178
} else {
142-
$patch['after'] = array($last_key);
179+
if ($last_key === null) {
180+
$patch['after'] = array();
181+
} else {
182+
$patch['after'] = array($last_key);
183+
}
143184
}
144185
}
145-
$last_key = $full_key;
186+
$last_keys[$patch_phase] = $full_key;
146187

147188
foreach ($patch['after'] as $after_key => $after) {
148189
if (strpos($after, ':') === false) {
@@ -186,6 +227,21 @@ final public static function buildAllPatches() {
186227
$key,
187228
$after));
188229
}
230+
231+
$patch_phase = $patch['phase'];
232+
$after_phase = $specs[$after]['phase'];
233+
234+
if ($patch_phase !== $after_phase) {
235+
throw new Exception(
236+
pht(
237+
'Storage patch "%s" executes in phase "%s", but depends on '.
238+
'patch "%s" which is in a different phase ("%s"). Patches '.
239+
'may not have dependencies across phases.',
240+
$key,
241+
$patch_phase,
242+
$after,
243+
$after_phase));
244+
}
189245
}
190246
}
191247

@@ -196,7 +252,94 @@ final public static function buildAllPatches() {
196252

197253
// TODO: Detect cycles?
198254
255+
$patches = msortv($patches, 'newSortVector');
256+
199257
return $patches;
200258
}
201259

260+
private function getPHPPatchAttributes($patch_name, $full_path) {
261+
$data = Filesystem::readFile($full_path);
262+
263+
$phase_list = PhabricatorStoragePatch::getPhaseList();
264+
$phase_map = array_fuse($phase_list);
265+
266+
$attributes = array();
267+
268+
$lines = phutil_split_lines($data, false);
269+
foreach ($lines as $line) {
270+
// Skip over the "PHP" line.
271+
if (preg_match('(^<\?)', $line)) {
272+
continue;
273+
}
274+
275+
// Skip over blank lines.
276+
if (!strlen(trim($line))) {
277+
continue;
278+
}
279+
280+
// If this is a "//" comment...
281+
if (preg_match('(^\s*//)', $line)) {
282+
$matches = null;
283+
if (preg_match('(^\s*//\s*@(\S+)(?:\s+(.*))?\z)', $line, $matches)) {
284+
$attr_key = $matches[1];
285+
$attr_value = trim(idx($matches, 2));
286+
287+
switch ($attr_key) {
288+
case 'phase':
289+
$phase_name = $attr_value;
290+
291+
if (!strlen($phase_name)) {
292+
throw new Exception(
293+
pht(
294+
'Storage patch "%s" specifies a "@phase" attribute with '.
295+
'no phase value. Phase attributes must specify a value, '.
296+
'like "@phase default".',
297+
$patch_name));
298+
}
299+
300+
if (!isset($phase_map[$phase_name])) {
301+
throw new Exception(
302+
pht(
303+
'Storage patch "%s" specifies a "@phase" value ("%s"), '.
304+
'but this is not a recognized phase. Valid phases '.
305+
'are: %s.',
306+
$patch_name,
307+
$phase_name,
308+
implode(', ', $phase_list)));
309+
}
310+
311+
if (isset($attributes['phase'])) {
312+
throw new Exception(
313+
pht(
314+
'Storage patch "%s" specifies a "@phase" value ("%s"), '.
315+
'but it already has a specified phase ("%s"). Patches '.
316+
'may not specify multiple phases.',
317+
$patch_name,
318+
$phase_name,
319+
$attributes['phase']));
320+
}
321+
322+
$attributes[$attr_key] = $phase_name;
323+
break;
324+
default:
325+
throw new Exception(
326+
pht(
327+
'Storage patch "%s" specifies attribute "%s", but this '.
328+
'attribute is unknown.',
329+
$patch_name,
330+
$attr_key));
331+
}
332+
}
333+
continue;
334+
}
335+
336+
// If this is anything else, we're all done. Attributes must be marked
337+
// in the header of the file.
338+
break;
339+
}
340+
341+
342+
return $attributes;
343+
}
344+
202345
}

0 commit comments

Comments
 (0)
Failed to load comments.