@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 PhabricatorConfigClusterRepositoriesController
4 extends PhabricatorConfigServicesController {
5
6 public function handleRequest(AphrontRequest $request) {
7 $title = pht('Repository Services');
8
9 $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories');
10 $button = id(new PHUIButtonView())
11 ->setIcon('fa-book')
12 ->setHref($doc_href)
13 ->setTag('a')
14 ->setText(pht('Documentation'));
15
16 $header = $this->buildHeaderView($title, $button);
17
18 $repository_status = $this->buildClusterRepositoryStatus();
19 $repo_status = $this->buildConfigBoxView(
20 pht('Repository Status'), $repository_status);
21
22 $repository_errors = $this->buildClusterRepositoryErrors();
23 $repo_errors = $this->buildConfigBoxView(
24 pht('Repository Errors'), $repository_errors);
25
26 $crumbs = $this->newCrumbs()
27 ->addTextCrumb($title);
28
29 $content = id(new PHUITwoColumnView())
30 ->setHeader($header)
31 ->setFooter(
32 array(
33 $repo_status,
34 $repo_errors,
35 ));
36
37 $nav = $this->newNavigation('repository-servers');
38
39 return $this->newPage()
40 ->setTitle($title)
41 ->setCrumbs($crumbs)
42 ->setNavigation($nav)
43 ->appendChild($content);
44 }
45
46 private function buildClusterRepositoryStatus() {
47 $viewer = $this->getViewer();
48
49 Javelin::initBehavior('phabricator-tooltips');
50
51 $all_services = id(new AlmanacServiceQuery())
52 ->setViewer($viewer)
53 ->withServiceTypes(
54 array(
55 AlmanacClusterRepositoryServiceType::SERVICETYPE,
56 ))
57 ->needBindings(true)
58 ->needProperties(true)
59 ->execute();
60 $all_services = mpull($all_services, null, 'getPHID');
61
62 $all_repositories = id(new PhabricatorRepositoryQuery())
63 ->setViewer($viewer)
64 ->withTypes(
65 array(
66 PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
67 ))
68 ->execute();
69 $all_repositories = mpull($all_repositories, null, 'getPHID');
70
71 $all_versions = id(new PhabricatorRepositoryWorkingCopyVersion())
72 ->loadAll();
73
74 $all_devices = $this->getDevices($all_services, false);
75 $all_active_devices = $this->getDevices($all_services, true);
76
77 $leader_versions = $this->getLeaderVersionsByRepository(
78 $all_repositories,
79 $all_versions,
80 $all_active_devices);
81
82 $push_times = $this->loadLeaderPushTimes($leader_versions);
83
84 $repository_groups = mgroup($all_repositories, 'getAlmanacServicePHID');
85 $repository_versions = mgroup($all_versions, 'getRepositoryPHID');
86
87 $rows = array();
88 foreach ($all_services as $service) {
89 $service_phid = $service->getPHID();
90
91 if ($service->getAlmanacPropertyValue('closed')) {
92 $status_icon = 'fa-folder';
93 $status_tip = pht('Closed');
94 } else {
95 $status_icon = 'fa-folder-open green';
96 $status_tip = pht('Open');
97 }
98
99 $status_icon = id(new PHUIIconView())
100 ->setIcon($status_icon)
101 ->addSigil('has-tooltip')
102 ->setMetadata(
103 array(
104 'tip' => $status_tip,
105 ));
106
107 $devices = idx($all_devices, $service_phid, array());
108 $active_devices = idx($all_active_devices, $service_phid, array());
109
110 $device_icon = 'fa-server green';
111
112 $device_label = pht(
113 '%s Active',
114 phutil_count($active_devices));
115
116 $device_status = array(
117 id(new PHUIIconView())->setIcon($device_icon),
118 ' ',
119 $device_label,
120 );
121
122 $repositories = idx($repository_groups, $service_phid, array());
123
124 $repository_status = pht(
125 '%s',
126 phutil_count($repositories));
127
128 $no_leader = array();
129 $full_sync = array();
130 $partial_sync = array();
131 $no_sync = array();
132 $lag = array();
133
134 // Threshold in seconds before we start complaining that repositories
135 // are not synchronized when there is only one leader.
136 $threshold = phutil_units('5 minutes in seconds');
137
138 $messages = array();
139
140 foreach ($repositories as $repository) {
141 $repository_phid = $repository->getPHID();
142
143 $leader_version = idx($leader_versions, $repository_phid);
144 if ($leader_version === null) {
145 $no_leader[] = $repository;
146 $messages[] = pht(
147 'Repository %s has an ambiguous leader.',
148 $viewer->renderHandle($repository_phid)->render());
149 continue;
150 }
151
152 $versions = idx($repository_versions, $repository_phid, array());
153
154 // Filter out any versions for devices which are no longer active.
155 foreach ($versions as $key => $version) {
156 $version_device_phid = $version->getDevicePHID();
157 if (empty($active_devices[$version_device_phid])) {
158 unset($versions[$key]);
159 }
160 }
161
162 $leaders = 0;
163 foreach ($versions as $version) {
164 if ($version->getRepositoryVersion() == $leader_version) {
165 $leaders++;
166 }
167 }
168
169 if ($leaders == count($active_devices)) {
170 $full_sync[] = $repository;
171 } else {
172 $push_epoch = idx($push_times, $repository_phid);
173 if ($push_epoch) {
174 $duration = (PhabricatorTime::getNow() - $push_epoch);
175 $lag[] = $duration;
176 } else {
177 $duration = null;
178 }
179
180 if ($leaders >= 2 || ($duration && ($duration < $threshold))) {
181 $partial_sync[] = $repository;
182 } else {
183 $no_sync[] = $repository;
184 if ($push_epoch) {
185 $messages[] = pht(
186 'Repository %s has unreplicated changes (for %s).',
187 $viewer->renderHandle($repository_phid)->render(),
188 phutil_format_relative_time($duration));
189 } else {
190 $messages[] = pht(
191 'Repository %s has unreplicated changes.',
192 $viewer->renderHandle($repository_phid)->render());
193 }
194 }
195
196 }
197 }
198
199 $with_lag = false;
200
201 if ($no_leader) {
202 $replication_icon = 'fa-times red';
203 $replication_label = pht('Ambiguous Leader');
204 } else if ($no_sync) {
205 $replication_icon = 'fa-refresh yellow';
206 $replication_label = pht('Unsynchronized');
207 $with_lag = true;
208 } else if ($partial_sync) {
209 $replication_icon = 'fa-refresh green';
210 $replication_label = pht('Partial');
211 $with_lag = true;
212 } else if ($full_sync) {
213 $replication_icon = 'fa-check green';
214 $replication_label = pht('Synchronized');
215 } else {
216 $replication_icon = 'fa-times grey';
217 $replication_label = pht('No Repositories');
218 }
219
220 if ($with_lag && $lag) {
221 $lag_status = phutil_format_relative_time(max($lag));
222 $lag_status = pht(' (%s)', $lag_status);
223 } else {
224 $lag_status = null;
225 }
226
227 $replication_status = array(
228 id(new PHUIIconView())->setIcon($replication_icon),
229 ' ',
230 $replication_label,
231 $lag_status,
232 );
233
234 $messages = phutil_implode_html(phutil_tag('br'), $messages);
235
236 $rows[] = array(
237 $status_icon,
238 $viewer->renderHandle($service->getPHID()),
239 $device_status,
240 $repository_status,
241 $replication_status,
242 $messages,
243 );
244 }
245
246 return id(new AphrontTableView($rows))
247 ->setNoDataString(
248 pht('No repository cluster services are configured.'))
249 ->setHeaders(
250 array(
251 null,
252 pht('Service'),
253 pht('Devices'),
254 pht('Repos'),
255 pht('Sync'),
256 pht('Messages'),
257 ))
258 ->setColumnClasses(
259 array(
260 null,
261 'pri',
262 null,
263 null,
264 null,
265 'wide',
266 ));
267 }
268
269 private function getDevices(
270 array $all_services,
271 $only_active) {
272
273 $devices = array();
274 foreach ($all_services as $service) {
275 $map = array();
276 foreach ($service->getBindings() as $binding) {
277 if ($only_active && $binding->getIsDisabled()) {
278 continue;
279 }
280
281 $device = $binding->getDevice();
282 $device_phid = $device->getPHID();
283
284 $map[$device_phid] = $device;
285 }
286 $devices[$service->getPHID()] = $map;
287 }
288
289 return $devices;
290 }
291
292 private function getLeaderVersionsByRepository(
293 array $all_repositories,
294 array $all_versions,
295 array $active_devices) {
296
297 $version_map = mgroup($all_versions, 'getRepositoryPHID');
298
299 $result = array();
300 foreach ($all_repositories as $repository_phid => $repository) {
301 $service_phid = $repository->getAlmanacServicePHID();
302 if (!$service_phid) {
303 continue;
304 }
305
306 $devices = idx($active_devices, $service_phid);
307 if (!$devices) {
308 continue;
309 }
310
311 $versions = idx($version_map, $repository_phid, array());
312 $versions = mpull($versions, null, 'getDevicePHID');
313 $versions = array_select_keys($versions, array_keys($devices));
314 if (!$versions) {
315 continue;
316 }
317
318 $leader = (int)max(mpull($versions, 'getRepositoryVersion'));
319 $result[$repository_phid] = $leader;
320 }
321
322 return $result;
323 }
324
325 private function loadLeaderPushTimes(array $leader_versions) {
326 $viewer = $this->getViewer();
327
328 if (!$leader_versions) {
329 return array();
330 }
331
332 $events = id(new PhabricatorRepositoryPushEventQuery())
333 ->setViewer($viewer)
334 ->withIDs($leader_versions)
335 ->execute();
336 $events = mpull($events, null, 'getID');
337
338 $result = array();
339 foreach ($leader_versions as $key => $version) {
340 $event = idx($events, $version);
341 if (!$event) {
342 continue;
343 }
344
345 $result[$key] = $event->getEpoch();
346 }
347
348 return $result;
349 }
350
351
352 private function buildClusterRepositoryErrors() {
353 $viewer = $this->getViewer();
354
355 $messages = id(new PhabricatorRepositoryStatusMessage())->loadAllWhere(
356 'statusCode IN (%Ls)',
357 array(
358 PhabricatorRepositoryStatusMessage::CODE_ERROR,
359 ));
360
361 $repository_ids = mpull($messages, 'getRepositoryID');
362 if ($repository_ids) {
363 // NOTE: We're bypassing policies when loading repositories because we
364 // want to show errors exist even if the viewer can't see the repository.
365 // We use handles to describe the repository below, so the viewer won't
366 // actually be able to see any particulars if they can't see the
367 // repository.
368 $repositories = id(new PhabricatorRepositoryQuery())
369 ->setViewer(PhabricatorUser::getOmnipotentUser())
370 ->withIDs($repository_ids)
371 ->execute();
372 $repositories = mpull($repositories, null, 'getID');
373 }
374
375 $rows = array();
376 foreach ($messages as $message) {
377 $repository = idx($repositories, $message->getRepositoryID());
378 if (!$repository) {
379 continue;
380 }
381
382 if (!$repository->isTracked()) {
383 continue;
384 }
385
386 $icon = id(new PHUIIconView())
387 ->setIcon('fa-exclamation-triangle red');
388
389 $rows[] = array(
390 $icon,
391 $viewer->renderHandle($repository->getPHID()),
392 phutil_tag(
393 'a',
394 array(
395 'href' => $repository->getPathURI('manage/status/'),
396 ),
397 $message->getStatusTypeName()),
398 );
399 }
400
401 return id(new AphrontTableView($rows))
402 ->setNoDataString(
403 pht('No active repositories have outstanding errors.'))
404 ->setHeaders(
405 array(
406 null,
407 pht('Repository'),
408 pht('Error'),
409 ))
410 ->setColumnClasses(
411 array(
412 null,
413 'pri',
414 'wide',
415 ));
416 }
417
418}