Problem/Motivation
In #3277034: Unhandled Composer Stager exceptions leave the update process in an indeterminate state we are wrapping the line $this->beginner->begin($active_dir, $stage_dir, new PathList($event->getExcludedPaths()), NULL, $timeout); and $this->stager->stage($command, $active_dir, $stage_dir, NULL, $timeout);in try-catch structures which can handle Composer Stager exceptions.
But, if an event subscriber to the stage lifecycle throws an exception, it can result in bad, incongruous UX. For example, if a PostRequireEvent subscriber throws an exception, the user is still redirected to the confirmation page to continue the update (/admin/automatic-update-ready). They will see the error message, and they won't be able to continue the update...but if a PreRequireEvent subscriber throws an exception, users are redirected to the start page!
Worse yet, there may not be any way to recover from that error. The only recourse is to press the "Delete existing update" button and try again, and hope it works this time. That sucks.
Also there is a inconsistency in handling of composer stager exceptions in StageBase that is the require() catches all throwable exceptions and then catch it checks for the two classes( InvalidArgumentException and PreconditionException) while in apply() there are two catches one for the two classes and other for the all throwable exceptions
Proposed resolution
When an exception is thrown by a stage lifecycle event subscriber, we should destroy the stage on behalf of the user, before returning them to an appropriate page for them to continue their work. Change StageBase::create(), StageBase::require() and StageBase::postApply() to destroy stage when there is an error in dispatching the events. With this change the user will now won't have to click "Cancel update" button on the UpdateReady when there is an error because we now have destroyed the stage for them.
Currently BatchProcessor::postApply() throws the exception which causes it the site to be redirected to UpdateReady form when there is an error. Change this behaviour to match with code>BatchProcessor::clean() because if we got successfully till BatchProcessor::postApply() the update is already done and we don't need to redirect it back to update ready form when there is an error.
Also make handling of composer stager exceptions in StageBase::require() consistent with StageBase::apply().
Remaining tasks
file a follow-up to repeat the solution for Automatic Updates Extensions
| Comment | File | Size | Author |
|---|---|---|---|
| #21 | testUpdateStoppedByEventSubscriber-output.zip | 35.84 KB | tedbow |
| #21 | testUpdateStoppedByEventSubscriber-debug.patch | 2.18 KB | tedbow |
Issue fork automatic_updates-3346644
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
tedbowComment #3
phenaproximaMerging in some stuff from #3354699: If a PostRequireEvent subscriber throws an exception, users are redirected to the confirmation page, which I'm closing as a duplicate.
Comment #4
phenaproximaRe-titling.
Comment #5
kunal.sachdev commentedComment #7
kunal.sachdev commentedComment #8
kunal.sachdev commentedI think the problem that we want to solve in this issue is that if there are any errors during
PreCreateEvent/PostCreateEventorPreRequireEvent/PostRequireEventthen we want to destroy the stage inUpdateStagefor the user. If that's right then why shouldn’t we do it inStageBaseso that the same thing will not have to be repeated for Project Browser @tedbow, @phenaproxima? Actually @Wim Leers and I were discussing about this issue and he suggested this.Comment #9
wim leersYep.
The rationale for doing it only for
UpdateStageis not documented on this issue nor on #3277034: Unhandled Composer Stager exceptions leave the update process in an indeterminate state.Furthermore, @tedbow wrote at #3277034:
… but implementing it in
UpdateStageaffects both manually triggered updates (i.e. "user is present") as well unattended updates, becauseCronUpdateStage extends UpdateStage.Tagging for clarifying this.
@kunal.sachdev: Can you think of a reason why it should be implemented as you did, @kunal.sachdev? Like I explained on our meeting earlier, that'd help inform the discussion.
#8 is too succinct: it doesn't try to articulate a rationale for the current approach. I expected more detail.
Comment #10
kunal.sachdev commented@Wim Leers I don't think I had any reason for implementing it this way, I just followed the same way I implemented this in #3277034: Unhandled Composer Stager exceptions leave the update process in an indeterminate state in which @tedbow while reviewing initially requested changes in UpdateStage( Updater) https://git.drupalcode.org/project/automatic_updates/-/merge_requests/63... but later in https://git.drupalcode.org/project/automatic_updates/-/merge_requests/74... he suggested we should do this in separate issue and that is the current issue.
Comment #11
kunal.sachdev commentedDiscussed with @tedbow and @Wim Leers and decided that we should destroy the stage in the
StageBaseonly. I reverted the changes inUpdateStageand added the logic to destroy the stage inStageBaseitself.Comment #12
kunal.sachdev commented\Drupal\Tests\package_manager\Kernel\LockFileValidatorTest::testLockFileChangedand\Drupal\Tests\package_manager\Kernel\LockFileValidatorTest::testLockFileDeletedis on Pre-require event while asserting status check results because now we are deleting stage on error during Pre-require event in StageBase seeand we pass the same stage while checking the status check results in test see
but now that stage is not available the error is not added. I think this behaviour is right as already during pre-require event we have shown the error and destroy the stage so we should have error added during status-check event after that.
So for now I think instead of
we should do
because this method test pre-apply event also during which we don't destroy the stage and we should see error during status check event.
Also while debugging this I found some inconsistencies handling composer stager exceptions in
\Drupal\package_manager\StageBase::requireand\Drupal\package_manager\StageBase::applyi.e. require() doeswhile apply() does
. The inconsistency is that the require() catches all throwable exceptions and then catch it checks for the two classes( InvalidArgumentException and PreconditionException) while in apply() there are two catches one for the two classes and other for the all throwable exceptions. So tagging this issue as Needs architectural review.
Comment #13
kunal.sachdev commentedComment #14
phenaproximaAfter reading #12, I too am wondering why they act differently. On its face, I can't see any reason for the discrepancy.
I'm going to open a separate MR on this same issue fork to change it and see what breaks. I think that what apply() does is cleaner.
Comment #16
phenaproximaLooks like changing require() to be consistent with the way apply() works won't break anything. So let's go ahead and make them consistent, either in this issue or a follow-up.
Comment #17
kunal.sachdev commentedComment #18
kunal.sachdev commentedI think we can make require() and apply() consistent in this issue. So, updating the issue summary.
Comment #19
kunal.sachdev commentedComment #20
kunal.sachdev commentedComment #21
tedbowI don't think the "Proposed resolution" actually covers what this issue is doing. It talks about "lifecycle event subscriber" but we are only handling "require". Is that right?
What happens if there is error on preApply()? In PostApply?
I think we should handle the other events in this issue or rescope the "Proposed resolution" make follow-ups for other events. I think we should handle them here.
Also even if we only handled require I think an issue like this would need functional test coverage
from the summary
If that is not the case anymore we should have test coverage confirming this.
\Drupal\Tests\automatic_updates\Functional\UpdateErrorTest::testUpdateStoppedByEventSubscribermight be good place for this test coverage if we don't have it anywhere else. We actually already have todo in the test referencing this issueIn the "Proposed resolution" we should detail what the changes will be from the user's perspective now that they we will be deleting the stage for them. Will they be redirected to different page? Will they not have to click the "Cancel update" button on the UpdateReady from because we now have destroyed the stage for them?
for instance I debugged
testUpdateStoppedByEventSubscriber. If an error happens PostApply the user is redirected back to UpdateReady and there only option is "Cancel update". They also get the LockFileValidator error because the stage has already been applied. In this case I think it seems reasonable that we would just delete the stage for them.testUpdateStoppedByEventSubscriberalso showed me that if an error happens in PreApply the user is also redirected back to UpdateReady but they have the option to "Continue" or "Cancel Update". In this case I think it is reasonable not to delete the stage automatically because the validation error might be something they could fix. But we may want to look into updating\Drupal\automatic_updates\BatchProcessor::finishCommitto tell the user they should try to resolve this issue if possible before continuing. That can be another issue.Also in PostCreate if we get an error we don't delete the stage. There is already a
@todofor this in the test to solve it in this issue.Basically we need to figure out the situations where the only thing the user can do is "Cancel Update" and start over. In those situations we should probably be deleting the stage for them.
I am including a patch I used to output the page for all the test cases in
testUpdateStoppedByEventSubscriberwhere the user is left. This can be used to determine when we should delete the stage.Also including the zip file of the debug output I got from the current MR with this patch. It has all 14 test cases. Some of them are probably fine but I think at least PostApply and PostCreate are not great UX
Comment #22
tedbowComment #23
kunal.sachdev commentedCreated #3371076: [PP-1] There is an error shown along with 'Update complete!' on update page if there is an error. as per discussion with @tedbow during scrum.
Comment #24
kunal.sachdev commentedI think we still need to destroy stage in
\Drupal\automatic_updates\BatchProcessor::stage()for the case if$this->stager->stage($command, $active_dir, $stage_dir, NULL, $timeout);fails in\Drupal\package_manager\StageBase::require()withInvalidArgumentExceptionorPreconditionExceptionwhere we don't destroy stage.Comment #25
kunal.sachdev commentedComment #26
wim leersA new quarter, time to increase the expectations! 🤓 I know you can do it! 😊
Comment #27
kunal.sachdev commentedCreated #3372940: Tell the user to try resolve the issue if possible before continuing if there is an error in PreApply as per #21
Comment #28
kunal.sachdev commentedThe test is already there i.e.
\Drupal\Tests\automatic_updates\Functional\UpdateErrorTest::testUpdateStoppedByEventSubscriberwhich is updated accordingly.Comment #29
kunal.sachdev commentedUpdated the issue summary.
Comment #30
kunal.sachdev commentedComment #31
tedbowlooking good
Comment #32
kunal.sachdev commentedComment #33
kunal.sachdev commented