Skip to content

Commit 098ed0a

Browse files
committed
Simplify the handling of package updates with the PackageUpdateSource class
Signed-off-by: Tim Goudriaan <tim@codedmonkey.com>
1 parent fb8d81f commit 098ed0a

File tree

9 files changed

+142
-62
lines changed

9 files changed

+142
-62
lines changed

src/Command/PackagesUpdateCommand.php

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace CodedMonkey\Dirigent\Command;
44

55
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
6+
use CodedMonkey\Dirigent\Entity\PackageUpdateSource;
67
use CodedMonkey\Dirigent\Message\SchedulePackageUpdate;
8+
use CodedMonkey\Dirigent\Message\UpdatePackage;
79
use Symfony\Component\Console\Attribute\AsCommand;
810
use Symfony\Component\Console\Command\Command;
911
use Symfony\Component\Console\Input\InputArgument;
@@ -16,6 +18,32 @@
1618
#[AsCommand(
1719
name: 'packages:update',
1820
description: 'Schedules packages for update',
21+
help: <<<'TXT'
22+
The <info>%command.name%</info> command schedules packages in the registry for update:
23+
24+
<info>%command.full_name%</info>
25+
26+
<fg=black;bg=yellow> </>
27+
<fg=black;bg=yellow> Make sure a worker is running, or use the --sync option. </>
28+
<fg=black;bg=yellow> </>
29+
30+
By default, only packages that have passed the periodic update interval will be scheduled for update.
31+
32+
Use the <comment>--all</comment> option to schedule all packages for update instead:
33+
34+
<info>%command.full_name% --all</info>
35+
36+
Package updates are scheduled somewhere in the next 12 minutes, except when specifying package names, then they are
37+
scheduled immediately. Check the worker's message queue if updates are not being executed.
38+
39+
It's possible to update specific packages by passing their name as arguments:
40+
41+
<info>%command.full_name% psr/cache psr/log</info>
42+
43+
Use the <comment>--sync</comment> option to skip the worker and update packages synchronously:
44+
45+
<info>%command.full_name% psr/cache psr/log --sync</info>
46+
TXT,
1947
)]
2048
class PackagesUpdateCommand extends Command
2149
{
@@ -30,48 +58,67 @@ protected function configure(): void
3058
{
3159
$this
3260
->addArgument('package', InputArgument::OPTIONAL, 'Package to update')
33-
->addOption('force', null, InputOption::VALUE_NONE, 'Forces a re-crawl of all packages');
61+
->addOption('all', null, InputOption::VALUE_NONE, 'Update all packages')
62+
->addOption('sync', null, InputOption::VALUE_NONE, 'Updates packages synchronously');
3463
}
3564

3665
protected function execute(InputInterface $input, OutputInterface $output): int
3766
{
3867
$io = new SymfonyStyle($input, $output);
3968

40-
$force = $input->getOption('force');
41-
$packageName = $input->getArgument('package');
69+
$all = $input->getOption('all');
70+
$packageNames = $input->getArgument('package');
71+
$sync = $input->getOption('sync');
4272

43-
$randomTimes = true;
44-
$reschedule = false;
73+
if ($sync && !count($packageNames)) {
74+
$io->error('Specify a package to update when using the --sync option.');
4575

46-
if ($packageName) {
47-
if (null === $package = $this->packageRepository->findOneByName($packageName)) {
48-
$io->error("Package $packageName not found");
76+
return Command::FAILURE;
77+
}
4978

50-
return Command::FAILURE;
51-
}
79+
$randomTimes = true; // Randomize time of updates
80+
$source = PackageUpdateSource::Stale;
81+
82+
if (count($packageNames)) {
83+
$packageIds = [];
84+
foreach ($packageNames as $packageName) {
85+
if (null === $package = $this->packageRepository->findOneByName($packageName)) {
86+
$io->error("Package $packageName not found");
5287

53-
$io->writeln("Scheduling package $packageName for update...");
88+
return Command::FAILURE;
89+
}
5490

55-
$packages = [['id' => $package->getId()]];
91+
$io->writeln("Scheduling package $packageName for update...");
92+
$packageIds[] = $package->getId();
93+
}
5694

5795
$randomTimes = false;
58-
$reschedule = true;
59-
} elseif ($force) {
96+
$source = PackageUpdateSource::Manual;
97+
} elseif ($all) {
6098
$io->writeln('Scheduling all packages for update...');
61-
$packages = $this->packageRepository->getAllPackageIds();
99+
$packageIds = $this->packageRepository->getAllPackageIds();
62100

63-
$reschedule = true;
101+
$source = PackageUpdateSource::Manual;
64102
} else {
65103
$io->writeln('Scheduling stale packages for update...');
66-
$packages = $this->packageRepository->getStalePackageIds();
104+
$packageIds = $this->packageRepository->getStalePackageIds();
67105
}
68106

69-
foreach ($packages as $package) {
70-
$this->messenger->dispatch(new SchedulePackageUpdate($package['id'], randomTime: $randomTimes, reschedule: $reschedule, forceRefresh: $force));
71-
}
107+
$packageCount = count($packageIds);
108+
109+
if ($sync) {
110+
foreach ($packageIds as $packageId) {
111+
$this->messenger->dispatch(new UpdatePackage($packageId, $source));
112+
}
72113

73-
$packageCount = count($packages);
74-
$io->success("Scheduled $packageCount package(s) for update.");
114+
$io->success("Updated $packageCount package(s).");
115+
} else {
116+
foreach ($packageIds as $packageId) {
117+
$this->messenger->dispatch(new SchedulePackageUpdate($packageId, $source, $randomTimes));
118+
}
119+
120+
$io->success("Scheduled $packageCount package(s) for update.");
121+
}
75122

76123
return Command::SUCCESS;
77124
}

src/Controller/ApiController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use CodedMonkey\Dirigent\Doctrine\Entity\PackageFetchStrategy;
99
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
1010
use CodedMonkey\Dirigent\Doctrine\Repository\VersionRepository;
11+
use CodedMonkey\Dirigent\Entity\PackageUpdateSource;
1112
use CodedMonkey\Dirigent\Message\TrackInstallations;
1213
use CodedMonkey\Dirigent\Message\UpdatePackage;
1314
use CodedMonkey\Dirigent\Package\PackageDistributionResolver;
@@ -87,7 +88,7 @@ public function packageMetadata(Request $request): Response
8788
throw $this->createNotFoundException();
8889
}
8990

90-
$this->messenger->dispatch(new UpdatePackage($package->getId()));
91+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Dynamic));
9192

9293
if (!$this->providerManager->exists($packageName)) {
9394
throw $this->createNotFoundException();
@@ -125,7 +126,7 @@ public function packageDistribution(Request $request, string $reference, string
125126
throw $this->createNotFoundException();
126127
}
127128

128-
$this->messenger->dispatch(new UpdatePackage($package->getId()));
129+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Dynamic));
129130

130131
if (null === $version = $this->versionRepository->findOneByNormalizedVersion($package, $versionName)) {
131132
throw $this->createNotFoundException();

src/Controller/Dashboard/DashboardPackagesController.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use CodedMonkey\Dirigent\Doctrine\Entity\PackageFetchStrategy;
99
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
1010
use CodedMonkey\Dirigent\EasyAdmin\PackagePaginator;
11+
use CodedMonkey\Dirigent\Entity\PackageUpdateSource;
1112
use CodedMonkey\Dirigent\Form\PackageAddMirroringFormType;
1213
use CodedMonkey\Dirigent\Form\PackageAddVcsFormType;
1314
use CodedMonkey\Dirigent\Form\PackageFormType;
@@ -112,7 +113,7 @@ public function addMirroring(Request $request): Response
112113

113114
$this->packageRepository->save($package, true);
114115

115-
$this->messenger->dispatch(new UpdatePackage($package->getId()));
116+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Manual));
116117

117118
$results[] = [
118119
'packageName' => $packageName,
@@ -148,7 +149,7 @@ public function addVcsRepository(Request $request): Response
148149
$package = $form->getData();
149150
$this->packageRepository->save($package, true);
150151

151-
$this->messenger->dispatch(new UpdatePackage($package->getId()));
152+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Manual));
152153

153154
return $this->redirectToRoute('dashboard_packages');
154155
}
@@ -171,7 +172,7 @@ public function edit(Request $request, #[MapPackage] Package $package): Response
171172
$package = $form->getData();
172173
$this->packageRepository->save($package, true);
173174

174-
$this->messenger->dispatch(new UpdatePackage($package->getId()));
175+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Manual));
175176

176177
return $this->redirectToRoute('dashboard_packages_info', ['package' => $package->getName()]);
177178
}
@@ -186,7 +187,7 @@ public function edit(Request $request, #[MapPackage] Package $package): Response
186187
#[IsGranted('ROLE_ADMIN')]
187188
public function update(#[MapPackage] Package $package): Response
188189
{
189-
$this->messenger->dispatch(new UpdatePackage($package->getId(), forceRefresh: true));
190+
$this->messenger->dispatch(new UpdatePackage($package->getId(), PackageUpdateSource::Manual));
190191

191192
return $this->redirectToRoute('dashboard_packages_info', ['package' => $package->getName()]);
192193
}

src/Doctrine/Repository/PackageRepository.php

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*/
2020
class PackageRepository extends ServiceEntityRepository
2121
{
22-
private \DateInterval $updateInterval;
22+
private \DateInterval $periodicUpdateInterval;
2323

2424
/**
2525
* @var array<string, Package>
@@ -29,11 +29,11 @@ class PackageRepository extends ServiceEntityRepository
2929
public function __construct(
3030
ManagerRegistry $registry,
3131
#[Autowire(param: 'dirigent.packages.periodic_update_interval')]
32-
string $updateInterval,
32+
string $periodicUpdateInterval,
3333
) {
3434
parent::__construct($registry, Package::class);
3535

36-
$this->updateInterval = new \DateInterval($updateInterval);
36+
$this->periodicUpdateInterval = new \DateInterval($periodicUpdateInterval);
3737
}
3838

3939
public function save(Package $entity, bool $flush = false): void
@@ -83,16 +83,29 @@ public function getStalePackageIds(): array
8383
{
8484
$connection = $this->getEntityManager()->getConnection();
8585

86-
$now = (new \DateTimeImmutable())->setTimezone(new \DateTimeZone('UTC'));
87-
$before = $now->sub($this->updateInterval);
88-
89-
return $connection->fetchAllAssociative(
90-
'SELECT p.id FROM package p
91-
WHERE p.update_scheduled_at IS NULL
92-
AND (p.updated_at IS NULL OR p.updated_at < :crawled)
93-
ORDER BY p.id',
94-
[
95-
'crawled' => $before->format('Y-m-d H:i:s'),
86+
$staleFrom = (new \DateTimeImmutable())->setTimezone(new \DateTimeZone('UTC'));
87+
$staleFrom = $staleFrom->sub($this->periodicUpdateInterval);
88+
89+
// Find package (id)s that have:
90+
// - never been updated or are stale
91+
// - and either:
92+
// - no update is scheduled
93+
// - the schedule date has gone stale
94+
// This should cover all packages. Packages may never come into
95+
// a state where they can't be stale,
96+
// until functionality is created to disable updates altogether.
97+
return $connection->fetchFirstColumn(
98+
<<<'SQL'
99+
SELECT p.id FROM package p
100+
WHERE
101+
(p.updated_at IS NULL OR p.updated_at < :staleFrom)
102+
AND (
103+
p.update_scheduled_at IS NULL
104+
OR p.update_scheduled_at < :staleFrom
105+
)
106+
ORDER BY p.id
107+
SQL, [
108+
'staleFrom' => $staleFrom->format('Y-m-d H:i:s'),
96109
]
97110
);
98111
}

src/Entity/PackageUpdateSource.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace CodedMonkey\Dirigent\Entity;
4+
5+
enum PackageUpdateSource: string
6+
{
7+
case Manual = 'manual';
8+
case Stale = 'stale';
9+
case Dynamic = 'dynamic';
10+
11+
public function isManual(): bool
12+
{
13+
return self::Manual === $this;
14+
}
15+
16+
public function isStale(): bool
17+
{
18+
return self::Stale === $this;
19+
}
20+
21+
public function isDynamic(): bool
22+
{
23+
return self::Dynamic === $this;
24+
}
25+
}

src/Message/SchedulePackageUpdate.php

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

33
namespace CodedMonkey\Dirigent\Message;
44

5+
use CodedMonkey\Dirigent\Entity\PackageUpdateSource;
56
use Symfony\Component\Messenger\Attribute\AsMessage;
67

78
#[AsMessage]
89
readonly class SchedulePackageUpdate
910
{
1011
public function __construct(
1112
public int $packageId,
13+
public PackageUpdateSource $source,
1214
public bool $randomTime = false,
13-
public bool $reschedule = false,
14-
public bool $forceRefresh = false,
1515
) {
1616
}
1717
}

src/Message/SchedulePackageUpdateHandler.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use CodedMonkey\Dirigent\Doctrine\Repository\PackageRepository;
66
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
7-
use Symfony\Component\Messenger\Envelope;
87
use Symfony\Component\Messenger\MessageBusInterface;
98
use Symfony\Component\Messenger\Stamp\DelayStamp;
109
use Symfony\Component\Messenger\Stamp\TransportNamesStamp;
@@ -22,26 +21,18 @@ public function __invoke(SchedulePackageUpdate $message): void
2221
{
2322
$package = $this->packageRepository->find($message->packageId);
2423

25-
if (!$message->reschedule && null !== $package->getUpdateScheduledAt()) {
26-
return;
27-
}
28-
29-
$updateMessage = new UpdatePackage($message->packageId, scheduled: true, forceRefresh: $message->forceRefresh);
30-
$updateEnvelope = new Envelope($updateMessage, [
31-
new TransportNamesStamp('async'),
32-
]);
24+
$stamps = [new TransportNamesStamp('async')];
3325

3426
if ($message->randomTime) {
3527
// Delay message up to 12 minutes
36-
$updateEnvelope = $updateEnvelope->with(
37-
new DelayStamp(random_int(1, 720) * 1000),
38-
);
28+
$stamps[] = new DelayStamp(random_int(1, 720) * 1000);
3929
}
4030

41-
$package->setUpdateScheduledAt(new \DateTimeImmutable());
31+
$this->messenger->dispatch(new UpdatePackage($message->packageId, $message->source, scheduled: true), $stamps);
4232

33+
// todo prevent flush for every scheduled update but make sure scheduled updates
34+
// are only performed when the scheduled message was delivered
35+
$package->setUpdateScheduledAt(new \DateTimeImmutable());
4336
$this->packageRepository->save($package, true);
44-
45-
$this->messenger->dispatch($updateEnvelope);
4637
}
4738
}

src/Message/UpdatePackage.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
namespace CodedMonkey\Dirigent\Message;
44

5+
use CodedMonkey\Dirigent\Entity\PackageUpdateSource;
56
use Symfony\Component\Messenger\Attribute\AsMessage;
67

78
#[AsMessage]
89
readonly class UpdatePackage
910
{
1011
public function __construct(
1112
public int $packageId,
13+
public PackageUpdateSource $source,
1214
public bool $scheduled = false,
13-
public bool $forceRefresh = false,
1415
) {
1516
}
1617
}

src/Message/UpdatePackageHandler.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,20 @@ public function __construct(
2626

2727
public function __invoke(UpdatePackage $message): void
2828
{
29-
if (!$message->scheduled && !$message->forceRefresh && !$this->dynamicUpdatesEnabled) {
29+
if ($message->source->isDynamic() && !$this->dynamicUpdatesEnabled) {
3030
// Dynamic updates are disabled
3131
return;
3232
}
3333

3434
$package = $this->packageRepository->find($message->packageId);
3535

3636
if ($message->scheduled && null === $package->getUpdateScheduledAt()) {
37-
// Package was already updated between being scheduled and now
37+
// Package was already updated between being scheduled and now,
38+
// so stop the update to prevent excessive requests
3839
return;
3940
}
4041

41-
if (!$message->forceRefresh && $this->isFresh($package)) {
42+
if (!$message->source->isManual() && $this->isFresh($package)) {
4243
// Package was recently updated
4344
return;
4445
}

0 commit comments

Comments
 (0)