From 2ba399922434571f6da23ccd652b4050dabf8891 Mon Sep 17 00:00:00 2001
From: phenaproxima <phenaproxima@205645.no-reply.drupal.org>
Date: Tue, 3 May 2022 14:10:19 +0000
Subject: [PATCH] Issue #3277014 by phenaproxima, lhridley, p.ayekumi,
 capysara, percoction: Increase timeouts during create and apply

---
 package_manager/src/Stage.php                 | 18 ++++++++++----
 .../package_manager_bypass/src/Beginner.php   |  2 +-
 .../package_manager_bypass/src/Committer.php  |  2 +-
 .../tests/src/Kernel/StageTest.php            | 24 +++++++++++++++++++
 src/CronUpdater.php                           | 18 +++++++++-----
 src/Updater.php                               |  8 +++++--
 6 files changed, 58 insertions(+), 14 deletions(-)

diff --git a/package_manager/src/Stage.php b/package_manager/src/Stage.php
index 229d2eb882..516d08f9d1 100644
--- a/package_manager/src/Stage.php
+++ b/package_manager/src/Stage.php
@@ -238,6 +238,11 @@ class Stage {
    * call ::claim(). However, if it was created during another request, the
    * stage must be claimed before operations can be performed on it.
    *
+   * @param int|null $timeout
+   *   (optional) How long to allow the file copying operation to run before
+   *   timing out, in seconds, or NULL to never time out. Defaults to 300
+   *   seconds.
+   *
    * @return string
    *   Unique ID for the stage, which can be used to claim the stage before
    *   performing other operations on it. Calling code should store this ID for
@@ -248,7 +253,7 @@ class Stage {
    *
    * @see ::claim()
    */
-  public function create(): string {
+  public function create(?int $timeout = 300): string {
     if (!$this->isAvailable()) {
       throw new StageException('Cannot create a new stage because one already exists.');
     }
@@ -270,7 +275,7 @@ class Stage {
     // available.
     $this->dispatch($event, [$this, 'markAsAvailable']);
 
-    $this->beginner->begin($active_dir, $stage_dir, $event->getExcludedPaths());
+    $this->beginner->begin($active_dir, $stage_dir, $event->getExcludedPaths(), NULL, $timeout);
     $this->dispatch(new PostCreateEvent($this));
     return $id;
   }
@@ -313,8 +318,13 @@ class Stage {
 
   /**
    * Applies staged changes to the active directory.
+   *
+   * @param int|null $timeout
+   *   (optional) How long to allow the file copying operation to run before
+   *   timing out, in seconds, or NULL to never time out. Defaults to 600
+   *   seconds.
    */
-  public function apply(): void {
+  public function apply(?int $timeout = 600): void {
     $this->checkOwnership();
 
     $active_dir = $this->pathLocator->getProjectRoot();
@@ -331,7 +341,7 @@ class Stage {
     $this->tempStore->set(self::TEMPSTORE_APPLY_TIME_KEY, $this->time->getRequestTime());
     $this->dispatch($event, $release_apply);
 
-    $this->committer->commit($stage_dir, $active_dir, $event->getExcludedPaths());
+    $this->committer->commit($stage_dir, $active_dir, $event->getExcludedPaths(), NULL, $timeout);
 
     // Rebuild the container and clear all caches, to ensure that new services
     // are picked up.
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
index e9ff25509f..5fc291a1ee 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Beginner.php
@@ -14,7 +14,7 @@ class Beginner extends InvocationRecorderBase implements BeginnerInterface {
    * {@inheritdoc}
    */
   public function begin(string $activeDir, string $stagingDir, ?array $exclusions = [], ?OutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
-    $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
+    $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions, $timeout);
   }
 
 }
diff --git a/package_manager/tests/modules/package_manager_bypass/src/Committer.php b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
index 2feb0d0f70..f5f40847e4 100644
--- a/package_manager/tests/modules/package_manager_bypass/src/Committer.php
+++ b/package_manager/tests/modules/package_manager_bypass/src/Committer.php
@@ -31,7 +31,7 @@ class Committer extends InvocationRecorderBase implements CommitterInterface {
    * {@inheritdoc}
    */
   public function commit(string $stagingDir, string $activeDir, ?array $exclusions = [], ?OutputCallbackInterface $callback = NULL, ?int $timeout = 120): void {
-    $this->saveInvocationArguments($activeDir, $stagingDir, $exclusions);
+    $this->saveInvocationArguments($stagingDir, $activeDir, $exclusions, $timeout);
   }
 
   /**
diff --git a/package_manager/tests/src/Kernel/StageTest.php b/package_manager/tests/src/Kernel/StageTest.php
index bf68cc8f39..5d029db83e 100644
--- a/package_manager/tests/src/Kernel/StageTest.php
+++ b/package_manager/tests/src/Kernel/StageTest.php
@@ -208,6 +208,30 @@ class StageTest extends PackageManagerKernelTestBase {
     $stage->apply();
   }
 
+  /**
+   * Tests that Composer Stager is invoked with a long timeout.
+   */
+  public function testTimeouts(): void {
+    $stage = $this->createStage();
+    $stage->create(420);
+    $stage->apply();
+
+    $timeouts = [
+      // The beginner was given an explicit timeout.
+      'package_manager.beginner' => 420,
+      // The committer should have been called with a longer timeout than
+      // Composer Stager's default of 120 seconds.
+      'package_manager.committer' => 600,
+    ];
+    foreach ($timeouts as $service_id => $expected_timeout) {
+      $invocations = $this->container->get($service_id)->getInvocationArguments();
+
+      // The service should have been called with the expected timeout.
+      $this->assertCount(1, $invocations);
+      $this->assertSame($expected_timeout, end($invocations[0]));
+    }
+  }
+
 }
 
 /**
diff --git a/src/CronUpdater.php b/src/CronUpdater.php
index 34bd9fa608..c78b9954ea 100644
--- a/src/CronUpdater.php
+++ b/src/CronUpdater.php
@@ -76,15 +76,20 @@ class CronUpdater extends Updater {
 
   /**
    * Handles updates during cron.
+   *
+   * @param int|null $timeout
+   *   (optional) How long to allow the file copying operation to run before
+   *   timing out, in seconds, or NULL to never time out. Defaults to 300
+   *   seconds.
    */
-  public function handleCron(): void {
+  public function handleCron(?int $timeout = 300): void {
     if ($this->getMode() === static::DISABLED) {
       return;
     }
 
     $next_release = $this->releaseChooser->getLatestInInstalledMinor();
     if ($next_release) {
-      $this->performUpdate($next_release->getVersion());
+      $this->performUpdate($next_release->getVersion(), $timeout);
     }
   }
 
@@ -93,8 +98,11 @@ class CronUpdater extends Updater {
    *
    * @param string $update_version
    *   The version to which to update.
+   * @param int|null $timeout
+   *   How long to allow the operation to run before timing out, in seconds, or
+   *   NULL to never time out.
    */
-  private function performUpdate(string $update_version): void {
+  private function performUpdate(string $update_version, ?int $timeout): void {
     $installed_version = (new ProjectInfo('drupal'))->getInstalledVersion();
     if (empty($installed_version)) {
       $this->logger->error('Unable to determine the current version of Drupal core.');
@@ -105,9 +113,7 @@ class CronUpdater extends Updater {
     // handle any exceptions or validation errors consistently, and destroy the
     // stage regardless of whether the update succeeds.
     try {
-      $this->begin([
-        'drupal' => $update_version,
-      ]);
+      $this->begin(['drupal' => $update_version], $timeout);
       $this->stage();
       $this->apply();
 
diff --git a/src/Updater.php b/src/Updater.php
index c333f6c376..83fe7b3dfe 100644
--- a/src/Updater.php
+++ b/src/Updater.php
@@ -22,6 +22,10 @@ class Updater extends Stage {
    *
    * @param string[] $project_versions
    *   The versions of the packages to update to, keyed by package name.
+   * @param int|null $timeout
+   *   (optional) How long to allow the file copying operation to run before
+   *   timing out, in seconds, or NULL to never time out. Defaults to 300
+   *   seconds.
    *
    * @return string
    *   The unique ID of the stage.
@@ -29,7 +33,7 @@ class Updater extends Stage {
    * @throws \InvalidArgumentException
    *   Thrown if no project version for Drupal core is provided.
    */
-  public function begin(array $project_versions): string {
+  public function begin(array $project_versions, ?int $timeout = 300): string {
     if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) {
       throw new \InvalidArgumentException("Currently only updates to Drupal core are supported.");
     }
@@ -54,7 +58,7 @@ class Updater extends Stage {
     $this->tempStore->set(static::TEMPSTORE_METADATA_KEY, [
       'packages' => $package_versions,
     ]);
-    return $this->create();
+    return $this->create($timeout);
   }
 
   /**
-- 
GitLab