@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorRepositoryManagementThawWorkflow
4 extends PhabricatorRepositoryManagementWorkflow {
5
6 protected function didConstruct() {
7 $this
8 ->setName('thaw')
9 ->setExamples('**thaw** [options] __repository__ ...')
10 ->setSynopsis(
11 pht(
12 'Resolve issues with frozen cluster repositories. Very advanced '.
13 'and dangerous.'))
14 ->setArguments(
15 array(
16 array(
17 'name' => 'demote',
18 'param' => 'device|service',
19 'help' => pht(
20 'Demote a device (or all devices in a service) discarding '.
21 'unsynchronized changes. Clears stuck write locks and recovers '.
22 'from lost leaders.'),
23 ),
24 array(
25 'name' => 'promote',
26 'param' => 'device',
27 'help' => pht(
28 'Promote a device, discarding changes on other devices. '.
29 'Resolves ambiguous leadership and recovers from demotion '.
30 'mistakes.'),
31 ),
32 array(
33 'name' => 'force',
34 'help' => pht('Run operations without asking for confirmation.'),
35 ),
36 array(
37 'name' => 'all-repositories',
38 'help' => pht(
39 'Apply the promotion or demotion to all repositories hosted '.
40 'on the device.'),
41 ),
42 array(
43 'name' => 'repositories',
44 'wildcard' => true,
45 ),
46 ));
47 }
48
49 public function execute(PhutilArgumentParser $args) {
50 $viewer = $this->getViewer();
51
52 $promote = $args->getArg('promote');
53 $demote = $args->getArg('demote');
54
55 if (!$promote && !$demote) {
56 throw new PhutilArgumentUsageException(
57 pht('You must choose a device to --promote or --demote.'));
58 }
59
60 if ($promote && $demote) {
61 throw new PhutilArgumentUsageException(
62 pht('Specify either --promote or --demote, but not both.'));
63 }
64
65 $target_name = nonempty($promote, $demote);
66
67 $devices = id(new AlmanacDeviceQuery())
68 ->setViewer($viewer)
69 ->withNames(array($target_name))
70 ->execute();
71 if (!$devices) {
72 $service = id(new AlmanacServiceQuery())
73 ->setViewer($viewer)
74 ->withNames(array($target_name))
75 ->executeOne();
76
77 if (!$service) {
78 throw new PhutilArgumentUsageException(
79 pht('No device or service named "%s" exists.', $target_name));
80 }
81
82 if ($promote) {
83 throw new PhutilArgumentUsageException(
84 pht(
85 'You can not "--promote" an entire service ("%s"). Only a single '.
86 'device may be promoted.',
87 $target_name));
88 }
89
90 $bindings = id(new AlmanacBindingQuery())
91 ->setViewer($viewer)
92 ->withServicePHIDs(array($service->getPHID()))
93 ->execute();
94 if (!$bindings) {
95 throw new PhutilArgumentUsageException(
96 pht(
97 'Service "%s" is not bound to any devices.',
98 $target_name));
99 }
100
101 $interfaces = id(new AlmanacInterfaceQuery())
102 ->setViewer($viewer)
103 ->withPHIDs(mpull($bindings, 'getInterfacePHID'))
104 ->execute();
105
106 $device_phids = mpull($interfaces, 'getDevicePHID');
107
108 $devices = id(new AlmanacDeviceQuery())
109 ->setViewer($viewer)
110 ->withPHIDs($device_phids)
111 ->execute();
112 }
113
114 $repository_names = $args->getArg('repositories');
115 $all_repositories = $args->getArg('all-repositories');
116 if ($repository_names && $all_repositories) {
117 throw new PhutilArgumentUsageException(
118 pht(
119 'Specify a list of repositories or "--all-repositories", '.
120 'but not both.'));
121 } else if (!$repository_names && !$all_repositories) {
122 throw new PhutilArgumentUsageException(
123 pht(
124 'Select repositories to affect by providing a list of repositories '.
125 'or using the "--all-repositories" flag.'));
126 }
127
128 if ($repository_names) {
129 $repositories = $this->loadRepositories($args, 'repositories');
130 if (!$repositories) {
131 throw new PhutilArgumentUsageException(
132 pht('Specify one or more repositories to thaw.'));
133 }
134 } else {
135 $repositories = array();
136
137 $services = id(new AlmanacServiceQuery())
138 ->setViewer($viewer)
139 ->withDevicePHIDs(mpull($devices, 'getPHID'))
140 ->execute();
141 if ($services) {
142 $repositories = id(new PhabricatorRepositoryQuery())
143 ->setViewer($viewer)
144 ->withAlmanacServicePHIDs(mpull($services, 'getPHID'))
145 ->execute();
146 }
147
148 if (!$repositories) {
149 throw new PhutilArgumentUsageException(
150 pht('There are no repositories on the selected device or service.'));
151 }
152 }
153
154 $display_list = new PhutilConsoleList();
155 foreach ($repositories as $repository) {
156 $display_list->addItem(
157 pht(
158 '%s %s',
159 $repository->getMonogram(),
160 $repository->getName()));
161 }
162
163 echo tsprintf(
164 "%s\n\n%B\n",
165 pht('These repositories will be thawed:'),
166 $display_list->drawConsoleString());
167
168 if ($promote) {
169 $risk_message = pht(
170 'Promoting a device can cause the loss of any repository data which '.
171 'only exists on other devices. The version of the repository on the '.
172 'promoted device will become authoritative.');
173 } else {
174 $risk_message = pht(
175 'Demoting a device can cause the loss of any repository data which '.
176 'only exists on the demoted device. The version of the repository '.
177 'on some other device will become authoritative.');
178 }
179
180 echo tsprintf(
181 "**<bg:red> %s </bg>** %s\n",
182 pht('DATA AT RISK'),
183 $risk_message);
184
185 $is_force = $args->getArg('force');
186 $prompt = pht('Accept the possibility of permanent data loss?');
187 if (!$is_force && !phutil_console_confirm($prompt)) {
188 throw new PhutilArgumentUsageException(
189 pht('User aborted the workflow.'));
190 }
191
192 foreach ($devices as $device) {
193 foreach ($repositories as $repository) {
194 $repository_phid = $repository->getPHID();
195
196 $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
197 $repository_phid);
198
199 echo tsprintf(
200 "%s\n",
201 pht(
202 'Waiting to acquire write lock for "%s"...',
203 $repository->getDisplayName()));
204
205 $write_lock->lock(phutil_units('5 minutes in seconds'));
206 try {
207
208 $service = $repository->loadAlmanacService();
209 if (!$service) {
210 throw new PhutilArgumentUsageException(
211 pht(
212 'Repository "%s" is not a cluster repository: it is not '.
213 'bound to an Almanac service.',
214 $repository->getDisplayName()));
215 }
216
217 if ($promote) {
218 // You can only promote active devices. (You may demote active or
219 // inactive devices.)
220 $bindings = $service->getActiveBindings();
221 $bindings = mpull($bindings, null, 'getDevicePHID');
222 if (empty($bindings[$device->getPHID()])) {
223 throw new PhutilArgumentUsageException(
224 pht(
225 'Repository "%s" has no active binding to device "%s". '.
226 'Only actively bound devices can be promoted.',
227 $repository->getDisplayName(),
228 $device->getName()));
229 }
230
231 $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
232 $repository->getPHID());
233 $versions = mpull($versions, null, 'getDevicePHID');
234
235 // Before we promote, make sure there are no outstanding versions
236 // on devices with inactive bindings. If there are, you need to
237 // demote these first.
238 $inactive = array();
239 foreach ($versions as $device_phid => $version) {
240 if (isset($bindings[$device_phid])) {
241 continue;
242 }
243 $inactive[$device_phid] = $version;
244 }
245
246 if ($inactive) {
247 $handles = $viewer->loadHandles(array_keys($inactive));
248
249 $handle_list = iterator_to_array($handles);
250 $handle_list = mpull($handle_list, 'getName');
251 $handle_list = implode(', ', $handle_list);
252
253 throw new PhutilArgumentUsageException(
254 pht(
255 'Repository "%s" has versions on inactive devices. Demote '.
256 '(or reactivate) these devices before promoting a new '.
257 'leader: %s.',
258 $repository->getDisplayName(),
259 $handle_list));
260 }
261
262 // Now, make sure there are no outstanding versions on devices with
263 // active bindings. These also need to be demoted (or promoting is
264 // a mistake or already happened).
265 $active = array_select_keys($versions, array_keys($bindings));
266 if ($active) {
267 $handles = $viewer->loadHandles(array_keys($active));
268
269 $handle_list = iterator_to_array($handles);
270 $handle_list = mpull($handle_list, 'getName');
271 $handle_list = implode(', ', $handle_list);
272
273 throw new PhutilArgumentUsageException(
274 pht(
275 'Unable to promote "%s" for repository "%s" because this '.
276 'cluster already has one or more unambiguous leaders: %s.',
277 $device->getName(),
278 $repository->getDisplayName(),
279 $handle_list));
280 }
281
282 PhabricatorRepositoryWorkingCopyVersion::updateVersion(
283 $repository->getPHID(),
284 $device->getPHID(),
285 0);
286
287 echo tsprintf(
288 "%s\n",
289 pht(
290 'Promoted "%s" to become a leader for "%s".',
291 $device->getName(),
292 $repository->getDisplayName()));
293 }
294
295 if ($demote) {
296 PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
297 $repository->getPHID(),
298 $device->getPHID());
299
300 echo tsprintf(
301 "%s\n",
302 pht(
303 'Demoted "%s" from leadership of repository "%s".',
304 $device->getName(),
305 $repository->getDisplayName()));
306 }
307 } catch (Exception $ex) {
308 $write_lock->unlock();
309 throw $ex;
310 }
311
312 $write_lock->unlock();
313 }
314 }
315
316 return 0;
317 }
318
319}