From a2273a082f7397ed746d776acae33e306745718f Mon Sep 17 00:00:00 2001 From: Aleksandr Bogdanov Date: Thu, 24 Mar 2016 15:44:20 +0100 Subject: [PATCH] Locking template object for non-parallel-safe operations --- app/Services/CloudFormer/Sparkle.php | 92 +++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/app/Services/CloudFormer/Sparkle.php b/app/Services/CloudFormer/Sparkle.php index b0e8425..fb927a6 100644 --- a/app/Services/CloudFormer/Sparkle.php +++ b/app/Services/CloudFormer/Sparkle.php @@ -18,6 +18,8 @@ class Sparkle implements TemplateEngine { const REMOTE_HIGHCORE = 'highcore'; const APP_SSH_PRIVKEY = 'app/id_rsa'; + const TEMPLATE_LOCK_TIMEOUT = 10 * 1000000; //10 seconds + const KEYGEN_TIMEOUT = 5000; protected $authProvider; @@ -26,7 +28,7 @@ protected function ensureKeyExists($storage_path) if(!file_exists($storage_path)){ $cmd = sprintf('ssh-keygen -b 2048 -t rsa -f %s -q -N ""', escapeshellarg($storage_path)); $process = new Process($cmd); - $process->setTimeout(5000); + $process->setTimeout(static::KEYGEN_TIMEOUT); $process->run(); if ($process->getExitCode() !== 0) { $text = 'Exit code: ' . $process->getExitCode(); @@ -48,13 +50,25 @@ public function updateTemplate(Template $template) $repo = Repository::openAuthenticated($repoPath, $this->getAuthProvider()); - $repo->isInitialized() or $repo->init(); - $repo->setRemote(self::REMOTE_HIGHCORE, $template->repository); - $repo->fetch(self::REMOTE_HIGHCORE); - $repo->reset(sprintf('%s/%s', self::REMOTE_HIGHCORE, $template->refspec), ResetCommand::RESET_HARD, true); - $repo->updateSubmodule(true, true, true); + return $this->exclusiveFor($template, function() use($template, $repo){ + $repo->isInitialized() or $repo->init(); + $repo->setRemote(self::REMOTE_HIGHCORE, $template->repository); + $repo->fetch(self::REMOTE_HIGHCORE); - return $repo->getCommit(); + try{ + $currentRevision = $repo->getCommit()->getSha(); //git show fails when missing refs/heads/master + }catch(\RuntimeException $e){ + $currentRevision = null; + } + + $targetRefspec = sprintf('%s/%s', self::REMOTE_HIGHCORE, $template->refspec); + + if($currentRevision != $repo->getCommit($targetRefspec)->getSha()) { + $repo->reset($targetRefspec, ResetCommand::RESET_HARD, true); + $repo->updateSubmodule(true, true, true); + } + return $repo; + }); } /** @@ -76,8 +90,10 @@ public function getTemplate(Template $template, $stack_definition, $update = tru Log::debug(sprintf('Calling sparke for template %s in project %s: %s', $template->name, $template->project->name, $command->getCommandLine())); - //$exitCode = 0; //$command->run(); - $exitCode = $command->run(); + $exitCode = $this->exclusiveFor($template, function() use ($command) { + return $command->run(); + }); + if ($exitCode) { throw new Exception($command->getErrorOutput()); } @@ -128,6 +144,9 @@ public function getComponents(Template $template) { private function getTemplatePath(Template $template) { return storage_path("app/projects/{$template->id}-{$template->name}"); } + private function getLockPath(Template $template){ + return storage_path(sprintf("app/locks/%d.lock", $template->id)); + } /** * @inheritdoc @@ -168,4 +187,59 @@ private function getAuthProvider() } return $this->authProvider; } + + /** + * @param Template $template + * @param callable $f + * @return mixed + * @throws Exception + */ + protected function exclusiveFor(Template $template, $f){ + $lock = $this->acquireLock($template); + try { + $retval = $f(); + }catch(\Exception $e){ + $this->releaseLock($lock); + throw $e; + } + $this->releaseLock($lock); + return $retval; + } + + protected function acquireLock(Template $template){ + $handle = fopen($this->getLockPath($template), 'c'); + if(!static::flock_t($handle, LOCK_EX, static::TEMPLATE_LOCK_TIMEOUT, 500)){ + throw new \RuntimeException('Could not acquire Template repository lock at '.$this->getLockPath($template)); + } + return $handle; + } + + protected function releaseLock($handle){ + fclose($handle); + } + + private function flock_t ($lockfile, $locktype, $timeout_micro, $sleep_by_micro = 10000) { + if (!is_resource($lockfile)) { + throw new \InvalidArgumentException ('The $lockfile was not a file resource or the resource was closed.'); + } + if ($sleep_by_micro < 1) { + throw new \InvalidArgumentException ('The $sleep_by_micro cannot be less than 1, or else an infinite loop.'); + } + if ($timeout_micro < 1) { + $locked = flock ($lockfile, $locktype | LOCK_NB); + } else { + $count_micro = 0; + $locked = true; + while (!flock($lockfile, $locktype | LOCK_NB, $blocking)) { + if ($blocking AND (($count_micro += $sleep_by_micro) <= $timeout_micro)) { + usleep($sleep_by_micro); + } else { + $locked = false; + break; + } + } + } + return $locked; + } + } \ No newline at end of file