/**
  * @task sync
  */
 public function synchronizeWorkingCopyAfterWrite()
 {
     if (!$this->shouldEnableSynchronization()) {
         return;
     }
     if (!$this->clusterWriteLock) {
         throw new Exception(pht('Trying to synchronize after write, but not holding a write ' . 'lock!'));
     }
     $repository = $this->getRepository();
     $repository_phid = $repository->getPHID();
     $device = AlmanacKeys::getLiveDevice();
     $device_phid = $device->getPHID();
     // It is possible that we've lost the global lock while receiving the push.
     // For example, the master database may have been restarted between the
     // time we acquired the global lock and now, when the push has finished.
     // We wrote a durable lock while we were holding the the global lock,
     // essentially upgrading our lock. We can still safely release this upgraded
     // lock even if we're no longer holding the global lock.
     // If we fail to release the lock, the repository will be frozen until
     // an operator can figure out what happened, so we try pretty hard to
     // reconnect to the database and release the lock.
     $now = PhabricatorTime::getNow();
     $duration = phutil_units('5 minutes in seconds');
     $try_until = $now + $duration;
     $did_release = false;
     $already_failed = false;
     while (PhabricatorTime::getNow() <= $try_until) {
         try {
             // NOTE: This means we're still bumping the version when pushes fail. We
             // could select only un-rejected events instead to bump a little less
             // often.
             $new_log = id(new PhabricatorRepositoryPushEventQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withRepositoryPHIDs(array($repository_phid))->setLimit(1)->executeOne();
             $old_version = $this->clusterWriteVersion;
             if ($new_log) {
                 $new_version = $new_log->getID();
             } else {
                 $new_version = $old_version;
             }
             PhabricatorRepositoryWorkingCopyVersion::didWrite($repository_phid, $device_phid, $this->clusterWriteVersion, $new_version, $this->clusterWriteOwner);
             $did_release = true;
             break;
         } catch (AphrontConnectionQueryException $ex) {
             $connection_exception = $ex;
         } catch (AphrontConnectionLostQueryException $ex) {
             $connection_exception = $ex;
         }
         if (!$already_failed) {
             $already_failed = true;
             $this->logLine(pht('CRITICAL. Failed to release cluster write lock!'));
             $this->logLine(pht('The connection to the master database was lost while receiving ' . 'the write.'));
             $this->logLine(pht('This process will spend %s more second(s) attempting to ' . 'recover, then give up.', new PhutilNumber($duration)));
         }
         sleep(1);
     }
     if ($did_release) {
         if ($already_failed) {
             $this->logLine(pht('RECOVERED. Link to master database was restored.'));
         }
         $this->logLine(pht('Released cluster write lock.'));
     } else {
         throw new Exception(pht('Failed to reconnect to master database and release held write ' . 'lock ("%s") on device "%s" for repository "%s" after trying ' . 'for %s seconds(s). This repository will be frozen.', $this->clusterWriteOwner, $device->getName(), $this->getDisplayName(), new PhutilNumber($duration)));
     }
     // We can continue even if we've lost this lock, everything is still
     // consistent.
     try {
         $this->clusterWriteLock->unlock();
     } catch (Exception $ex) {
         // Ignore.
     }
     $this->clusterWriteLock = null;
     $this->clusterWriteOwner = null;
 }
 public function execute(PhutilArgumentParser $args)
 {
     $viewer = $this->getViewer();
     $repositories = $this->loadRepositories($args, 'repositories');
     if (!$repositories) {
         throw new PhutilArgumentUsageException(pht('Specify one or more repositories to thaw.'));
     }
     $promote = $args->getArg('promote');
     $demote = $args->getArg('demote');
     if (!$promote && !$demote) {
         throw new PhutilArgumentUsageException(pht('You must choose a device to --promote or --demote.'));
     }
     if ($promote && $demote) {
         throw new PhutilArgumentUsageException(pht('Specify either --promote or --demote, but not both.'));
     }
     $device_name = nonempty($promote, $demote);
     $device = id(new AlmanacDeviceQuery())->setViewer($viewer)->withNames(array($device_name))->executeOne();
     if (!$device) {
         throw new PhutilArgumentUsageException(pht('No device "%s" exists.', $device_name));
     }
     if ($promote) {
         $risk_message = pht('Promoting a device can cause the loss of any repository data which ' . 'only exists on other devices. The version of the repository on the ' . 'promoted device will become authoritative.');
     } else {
         $risk_message = pht('Demoting a device can cause the loss of any repository data which ' . 'only exists on the demoted device. The version of the repository ' . 'on some other device will become authoritative.');
     }
     echo tsprintf("**<bg:red> %s </bg>** %s\n", pht('DATA AT RISK'), $risk_message);
     $is_force = $args->getArg('force');
     $prompt = pht('Accept the possibilty of permanent data loss?');
     if (!$is_force && !phutil_console_confirm($prompt)) {
         throw new PhutilArgumentUsageException(pht('User aborted the workflow.'));
     }
     foreach ($repositories as $repository) {
         $repository_phid = $repository->getPHID();
         $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock($repository_phid);
         echo tsprintf("%s\n", pht('Waiting to acquire write lock for "%s"...', $repository->getDisplayName()));
         $write_lock->lock(phutil_units('5 minutes in seconds'));
         try {
             $service = $repository->loadAlmanacService();
             if (!$service) {
                 throw new PhutilArgumentUsageException(pht('Repository "%s" is not a cluster repository: it is not ' . 'bound to an Almanac service.', $repository->getDisplayName()));
             }
             $bindings = $service->getActiveBindings();
             $bindings = mpull($bindings, null, 'getDevicePHID');
             if (empty($bindings[$device->getPHID()])) {
                 throw new PhutilArgumentUsageException(pht('Repository "%s" has no active binding to device "%s". Only ' . 'actively bound devices can be promoted or demoted.', $repository->getDisplayName(), $device->getName()));
             }
             $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions($repository->getPHID());
             $versions = mpull($versions, null, 'getDevicePHID');
             $versions = array_select_keys($versions, array_keys($bindings));
             if ($versions && $promote) {
                 throw new PhutilArgumentUsageException(pht('Unable to promote "%s" for repository "%s": the leaders for ' . 'this cluster are not ambiguous.', $device->getName(), $repository->getDisplayName()));
             }
             if ($promote) {
                 PhabricatorRepositoryWorkingCopyVersion::updateVersion($repository->getPHID(), $device->getPHID(), 0);
                 echo tsprintf("%s\n", pht('Promoted "%s" to become a leader for "%s".', $device->getName(), $repository->getDisplayName()));
             }
             if ($demote) {
                 PhabricatorRepositoryWorkingCopyVersion::demoteDevice($repository->getPHID(), $device->getPHID());
                 echo tsprintf("%s\n", pht('Demoted "%s" from leadership of repository "%s".', $device->getName(), $repository->getDisplayName()));
             }
         } catch (Exception $ex) {
             $write_lock->unlock();
             throw $ex;
         }
         $write_lock->unlock();
     }
     return 0;
 }
 private function buildClusterStatusPanel()
 {
     $repository = $this->getRepository();
     $viewer = $this->getViewer();
     $service_phid = $repository->getAlmanacServicePHID();
     if ($service_phid) {
         $service = id(new AlmanacServiceQuery())->setViewer($viewer)->withServiceTypes(array(AlmanacClusterRepositoryServiceType::SERVICETYPE))->withPHIDs(array($service_phid))->needBindings(true)->executeOne();
         if (!$service) {
             // TODO: Viewer may not have permission to see the service, or it may
             // be invalid? Raise some more useful error here?
             throw new Exception(pht('Unable to load cluster service.'));
         }
     } else {
         $service = null;
     }
     Javelin::initBehavior('phabricator-tooltips');
     $rows = array();
     if ($service) {
         $bindings = $service->getBindings();
         $bindings = mgroup($bindings, 'getDevicePHID');
         // This is an unusual read which always comes from the master.
         if (PhabricatorEnv::isReadOnly()) {
             $versions = array();
         } else {
             $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions($repository->getPHID());
         }
         $versions = mpull($versions, null, 'getDevicePHID');
         foreach ($bindings as $binding_group) {
             $all_disabled = true;
             foreach ($binding_group as $binding) {
                 if (!$binding->getIsDisabled()) {
                     $all_disabled = false;
                     break;
                 }
             }
             $any_binding = head($binding_group);
             if ($all_disabled) {
                 $binding_icon = 'fa-times grey';
                 $binding_tip = pht('Disabled');
             } else {
                 $binding_icon = 'fa-folder-open green';
                 $binding_tip = pht('Active');
             }
             $binding_icon = id(new PHUIIconView())->setIcon($binding_icon)->addSigil('has-tooltip')->setMetadata(array('tip' => $binding_tip));
             $device = $any_binding->getDevice();
             $version = idx($versions, $device->getPHID());
             if ($version) {
                 $version_number = $version->getRepositoryVersion();
                 $href = null;
                 if ($repository->isHosted()) {
                     $href = "/diffusion/pushlog/view/{$version_number}/";
                 } else {
                     $commit = id(new DiffusionCommitQuery())->setViewer($viewer)->withIDs(array($version_number))->executeOne();
                     if ($commit) {
                         $href = $commit->getURI();
                     }
                 }
                 if ($href) {
                     $version_number = phutil_tag('a', array('href' => $href), $version_number);
                 }
             } else {
                 $version_number = '-';
             }
             if ($version && $version->getIsWriting()) {
                 $is_writing = id(new PHUIIconView())->setIcon('fa-pencil green');
             } else {
                 $is_writing = id(new PHUIIconView())->setIcon('fa-pencil grey');
             }
             $write_properties = null;
             if ($version) {
                 $write_properties = $version->getWriteProperties();
                 if ($write_properties) {
                     try {
                         $write_properties = phutil_json_decode($write_properties);
                     } catch (Exception $ex) {
                         $write_properties = null;
                     }
                 }
             }
             if ($write_properties) {
                 $writer_phid = idx($write_properties, 'userPHID');
                 $last_writer = $viewer->renderHandle($writer_phid);
                 $writer_epoch = idx($write_properties, 'epoch');
                 $writer_epoch = phabricator_datetime($writer_epoch, $viewer);
             } else {
                 $last_writer = null;
                 $writer_epoch = null;
             }
             $rows[] = array($binding_icon, phutil_tag('a', array('href' => $device->getURI()), $device->getName()), $version_number, $is_writing, $last_writer, $writer_epoch);
         }
     }
     $table = id(new AphrontTableView($rows))->setNoDataString(pht('This is not a cluster repository.'))->setHeaders(array(null, pht('Device'), pht('Version'), pht('Writing'), pht('Last Writer'), pht('Last Write At')))->setColumnClasses(array(null, null, null, 'right wide', null, 'date'));
     $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories');
     $header = id(new PHUIHeaderView())->setHeader(pht('Cluster Status'))->addActionLink(id(new PHUIButtonView())->setIcon('fa-book')->setHref($doc_href)->setTag('a')->setText(pht('Documentation')));
     return id(new PHUIObjectBoxView())->setHeader($header)->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)->setTable($table);
 }