@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
at recaptime-dev/main 418 lines 12 kB view raw
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}