Skip to content

Commit 5e4081a

Browse files
committed
Retry stale manual runtime refreshes
1 parent 92a89f1 commit 5e4081a

6 files changed

Lines changed: 86 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.17.0 (2026-08-28)
44

5+
- Retry manual runtime refreshes invalidated by concurrent file changes
56
- Keep generic installation examples unchanged during releases
67
- Integrate headless diagnostics with Symfony CLI project-aware PHP defaults
78
- Find Twig function and filter usages from PHP declarations

src/Feature/Configuration/ProjectConfigurationValidationSnapshotLoader.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Symfony\Lsp\Feature\Configuration;
44

5-
use Amp\CancelledException;
65
use Symfony\Lsp\Project\Project;
76

87
final class ProjectConfigurationValidationSnapshotLoader
@@ -21,7 +20,7 @@ public function load(Project $project, array $snapshot): void
2120
{
2221
$generation = $snapshot['configurationGeneration'] ?? 0;
2322
if (!\is_int($generation) || $generation !== $this->validations->generation($project)) {
24-
throw new CancelledException();
23+
throw new StaleConfigurationValidationSnapshotException();
2524
}
2625
$validation = $snapshot['configurationValidation'] ?? null;
2726
$projectMetadata = $snapshot['project'] ?? null;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Lsp\Feature\Configuration;
4+
5+
use Amp\CancelledException;
6+
7+
final class StaleConfigurationValidationSnapshotException extends CancelledException
8+
{
9+
}

src/Index/IndexCommandHandler.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Symfony\Lsp\Index;
44

55
use Amp\Cancellation;
6+
use Symfony\Lsp\Feature\Configuration\StaleConfigurationValidationSnapshotException;
67
use Symfony\Lsp\Project\Project;
78
use Symfony\Lsp\Project\ProjectRegistry;
89
use Symfony\Lsp\Project\TrustStatus;
@@ -50,15 +51,15 @@ public function execute(array $params, ?Cancellation $cancellation = null): ?arr
5051
$cancellation?->throwIfRequested();
5152
$this->configuration->setEnvironment($project, $environment);
5253
if ($this->configuration->runtimeIndexing($project) && TrustStatus::Trusted === $this->workspaceTrust->status($project)) {
53-
$this->runtimeInitializer->initialize($project, new RuntimeRefreshPlan(RuntimeRefreshMode::Clear), $cancellation);
54+
$this->initializeRuntime($project, new RuntimeRefreshPlan(RuntimeRefreshMode::Clear), $cancellation);
5455
}
5556
}
5657
} elseif (self::REFRESH_COMMAND === $command) {
5758
foreach ($projects as $project) {
5859
$cancellation?->throwIfRequested();
5960
$this->sourceScanner->refreshProject($project, $cancellation);
6061
if ($this->configuration->runtimeIndexing($project) && TrustStatus::Trusted === $this->workspaceTrust->status($project)) {
61-
$this->runtimeInitializer->initialize($project, cancellation: $cancellation);
62+
$this->initializeRuntime($project, cancellation: $cancellation);
6263
}
6364
}
6465
}
@@ -71,6 +72,22 @@ public function execute(array $params, ?Cancellation $cancellation = null): ?arr
7172
], $projects);
7273
}
7374

75+
private function initializeRuntime(Project $project, ?RuntimeRefreshPlan $plan = null, ?Cancellation $cancellation = null): void
76+
{
77+
while (true) {
78+
try {
79+
$this->runtimeInitializer->initialize($project, $plan, $cancellation);
80+
81+
return;
82+
} catch (StaleConfigurationValidationSnapshotException $error) {
83+
$cancellation?->throwIfRequested();
84+
if (!$this->projects->contains($project)) {
85+
throw $error;
86+
}
87+
}
88+
}
89+
}
90+
7491
/** @param array<array-key, mixed> $params */
7592
private function environment(array $params): ?string
7693
{

tests/Feature/Configuration/ConfigurationValidationRegistryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace Symfony\Lsp\Tests\Feature\Configuration;
44

5-
use Amp\CancelledException;
65
use PHPUnit\Framework\TestCase;
76
use Symfony\Lsp\Feature\Configuration\ConfigurationValidationException;
87
use Symfony\Lsp\Feature\Configuration\ConfigurationValidationRegistry;
98
use Symfony\Lsp\Feature\Configuration\ConfigurationValidationResult;
109
use Symfony\Lsp\Feature\Configuration\ProjectConfigurationValidationSnapshotLoader;
10+
use Symfony\Lsp\Feature\Configuration\StaleConfigurationValidationSnapshotException;
1111
use Symfony\Lsp\Project\Project;
1212

1313
final class ConfigurationValidationRegistryTest extends TestCase
@@ -69,7 +69,7 @@ public function testRejectsResultsFromAnOlderConfigurationGeneration(): void
6969
'configurationValidation' => ['status' => 'valid'],
7070
]);
7171
self::fail('The stale configuration validation was accepted.');
72-
} catch (CancelledException) {
72+
} catch (StaleConfigurationValidationSnapshotException) {
7373
}
7474

7575
self::assertSame(ConfigurationValidationResult::PENDING, $registry->result($project)->state);

tests/Index/IndexCommandHandlerTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Amp\Sync\LocalKeyedMutex;
77
use PHPUnit\Framework\TestCase;
88
use Symfony\Lsp\Document\DocumentStore;
9+
use Symfony\Lsp\Feature\Configuration\StaleConfigurationValidationSnapshotException;
910
use Symfony\Lsp\Index\ApplicationSourceScanner;
1011
use Symfony\Lsp\Index\IndexCommandHandler;
1112
use Symfony\Lsp\Index\PhpRuntimeStructureHasher;
@@ -113,6 +114,50 @@ public function testManuallyRefreshesSourceAndTrustedRuntimeIndexes(): void
113114
]);
114115
self::assertFalse($untrusted[0]['trusted'] ?? null);
115116
}
117+
118+
public function testRetriesManualRuntimeRefreshAfterAConfigurationChangeInvalidatesTheSnapshot(): void
119+
{
120+
$projects = new ProjectRegistry();
121+
$projects->replace([$project = new Project(
122+
$this->temporaryDirectory,
123+
'file://'.$this->temporaryDirectory,
124+
'^8.0',
125+
)]);
126+
$statuses = new ProjectIndexStatusRegistry();
127+
$sourceScanner = new ApplicationSourceScanner(
128+
$projects,
129+
new DocumentStore(),
130+
$statuses,
131+
new NullProgressReporter(),
132+
new InMemorySourceIndexStore(),
133+
new SourceIndexPayloadCodec(),
134+
new PhpRuntimeStructureHasher(),
135+
new UriToPathConverter(),
136+
new SourceFileEnumerator(new GitignoreMatcher(), new ProjectFileScopeRegistry()),
137+
new LocalKeyedMutex(),
138+
new ServerLogger(null, new SensitiveDataRedactor()),
139+
[],
140+
);
141+
$runtime = new RecordingRuntimeInitializer(1);
142+
$workspaceTrust = new WorkspaceTrust();
143+
$workspaceTrust->set($project, TrustStatus::Trusted);
144+
$handler = new IndexCommandHandler(
145+
$projects,
146+
$workspaceTrust,
147+
$sourceScanner,
148+
new StatusRuntimeInitializer($runtime, $statuses, $projects),
149+
$statuses,
150+
new RuntimeConfiguration(),
151+
);
152+
153+
$result = $handler->execute([
154+
'command' => IndexCommandHandler::REFRESH_COMMAND,
155+
'arguments' => [$project->rootUri()],
156+
]);
157+
158+
self::assertSame([$this->temporaryDirectory, $this->temporaryDirectory], $runtime->projects);
159+
self::assertSame('ready', $result[0]['runtime']['state'] ?? null);
160+
}
116161
}
117162

118163
final class RecordingRuntimeInitializer implements RuntimeInitializerInterface
@@ -123,9 +168,18 @@ final class RecordingRuntimeInitializer implements RuntimeInitializerInterface
123168
/** @var list<RuntimeRefreshPlan> */
124169
public array $plans = [];
125170

171+
public function __construct(private int $staleSnapshots = 0)
172+
{
173+
}
174+
126175
public function initialize(Project $project, ?RuntimeRefreshPlan $plan = null, ?Cancellation $cancellation = null): void
127176
{
128177
$this->projects[] = $project->rootPath();
129178
$this->plans[] = $plan ?? new RuntimeRefreshPlan();
179+
if (0 < $this->staleSnapshots) {
180+
--$this->staleSnapshots;
181+
182+
throw new StaleConfigurationValidationSnapshotException();
183+
}
130184
}
131185
}

0 commit comments

Comments
 (0)