diff --git a/deploy/activate-incremental-release.sh b/deploy/activate-incremental-release.sh index 038b0f15..08949bad 100644 --- a/deploy/activate-incremental-release.sh +++ b/deploy/activate-incremental-release.sh @@ -65,6 +65,14 @@ if ! flock -n 9; then exit 1 fi +# 容器替换会终止其中的同步进程;与生产刷新任务共享同一把锁。 +exec 8>/run/caiwu-upstream-sync.lock +echo "Waiting for active upstream synchronization before replacing the application container" +if ! flock -w 1800 8; then + echo "Upstream synchronization is still running; application container was not replaced" >&2 + exit 1 +fi + PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)" PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)" diff --git a/deploy/activate-release.sh b/deploy/activate-release.sh index 6260783d..84a97c2f 100644 --- a/deploy/activate-release.sh +++ b/deploy/activate-release.sh @@ -69,6 +69,14 @@ if ! flock -n 9; then exit 1 fi +# 容器替换会终止其中的同步进程;与生产刷新任务共享同一把锁。 +exec 8>/run/caiwu-upstream-sync.lock +echo "Waiting for active upstream synchronization before replacing the application container" +if ! flock -w 1800 8; then + echo "Upstream synchronization is still running; application container was not replaced" >&2 + exit 1 +fi + PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)" PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)" diff --git a/tests/Unit/Deployment/DeploymentSyncLockTest.php b/tests/Unit/Deployment/DeploymentSyncLockTest.php new file mode 100644 index 00000000..2e6b71f4 --- /dev/null +++ b/tests/Unit/Deployment/DeploymentSyncLockTest.php @@ -0,0 +1,57 @@ +/run/caiwu-upstream-sync.lock'); + $end = strpos($contents, 'PREVIOUS_IMAGE=', $start ?: 0); + $this->assertNotFalse($start, $script); + $this->assertNotFalse($end, $script); + $this->assertLessThan(strpos($contents, 'docker compose -f compose.jiangling.yml up'), $end); + $guard = substr($contents, $start, $end - $start); + + $directory = sys_get_temp_dir() . '/caiwu-deploy-lock-' . bin2hex(random_bytes(8)); + mkdir($directory, 0700); + $holder = new Process(['sh', '-c', 'exec 8>"$1"; flock 8; touch "$2"; sleep 2', 'holder', $directory . '/sync.lock', $directory . '/held']); + try { + $holder->start(); + $deadline = microtime(true) + 3; + while (! file_exists($directory . '/held') && microtime(true) < $deadline) { + usleep(10000); + } + $this->assertFileExists($directory . '/held'); + $guard = str_replace('/run/caiwu-upstream-sync.lock', $directory . '/sync.lock', $guard); + $guard .= "touch '$directory/activated'\n"; + + $timeout = new Process(['sh', '-c', str_replace('-w 1800', '-w 0', $guard)]); + $timeout->run(); + $this->assertSame(1, $timeout->getExitCode()); + $this->assertStringContainsString('application container was not replaced', $timeout->getErrorOutput()); + $this->assertFileDoesNotExist($directory . '/activated'); + + $release = new Process(['sh', '-c', $guard]); + $release->start(); + usleep(100000); + $this->assertTrue($release->isRunning()); + $this->assertFileDoesNotExist($directory . '/activated'); + $release->wait(); + $this->assertTrue($release->isSuccessful(), $release->getErrorOutput()); + $this->assertFileExists($directory . '/activated'); + } finally { + $holder->stop(); + foreach (glob($directory . '/*') as $file) { + unlink($file); + } + rmdir($directory); + } + } + } +}