Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -565,11 +565,12 @@ public function getCacheMessagingFileNameBasedOnConfig(

foreach ($this->registeredClasses() as $class) {
$filePath = (new ReflectionClass($class))->getFileName();
$fileSha .= sha1_file($filePath);
$fileSha .= $class . sha1_file($filePath);
}

if (file_exists($pathToRootCatalog . 'composer.lock')) {
$fileSha .= sha1_file($pathToRootCatalog . 'composer.lock');
$composerLockPath = rtrim($pathToRootCatalog, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'composer.lock';
if (file_exists($composerLockPath)) {
$fileSha .= sha1_file($composerLockPath);
}

$fileSha .= sha1(serialize($serviceConfiguration));
Expand Down
20 changes: 19 additions & 1 deletion packages/Ecotone/src/SymfonyContainer/ContainerCacheLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,27 @@ public static function resolve(
$annotationFinder,
new ServiceCacheConfiguration(
$useHashSubDirectory ? $cacheDirectory . DIRECTORY_SEPARATOR . $configHash : $cacheDirectory,
$shouldUseCache,
$shouldUseCache && ! self::containsAnonymousClass($annotationFinder->registeredClasses()),
),
$configHash,
);
}

/**
* An anonymous class is named after the file and a counter that changes
* between processes, so a dumped container referencing one can never be
* resolved again. Configurations built from them are always rebuilt.
*
* @param class-string[] $registeredClasses
*/
private static function containsAnonymousClass(array $registeredClasses): bool
{
foreach ($registeredClasses as $registeredClass) {
if (str_contains($registeredClass, '@anonymous')) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ private static function defaultRuntimeServices(
}

/**
* The dumped container holds the cache path from the machine that built it.
* Deployments that warm the cache in one directory and run it from another
* must resolve the path from the configuration given here, not from the dump.
*
* @param array<string, object> $runtimeServices
*/
public static function loadCached(
Expand All @@ -134,6 +138,8 @@ public static function loadCached(
return null;
}

$runtimeServices = [ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration] + $runtimeServices;

return self::wrapWithExternalFallback($container, $externalContainer, $runtimeServices, $serviceCacheConfiguration->getPath());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Test\Ecotone\SymfonyContainer;

use Ecotone\Lite\EcotoneLite;
use Ecotone\Messaging\Attribute\Parameter\Header;
use Ecotone\Messaging\Config\ConfiguredMessagingSystem;
use Ecotone\Messaging\Config\ModulePackageList;
use Ecotone\Messaging\Config\ServiceConfiguration;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;
use PHPUnit\Framework\TestCase;

/**
* licence Apache-2.0
* @internal
*/
final class CachedBootstrapIsolationTest extends TestCase
{
public function test_handlers_defined_as_anonymous_classes_are_not_served_from_another_bootstrap_cache(): void
{
$firstService = new class () {
private array $notes = [];

#[CommandHandler('first.store')]
public function store(#[Header('note')] string $note): void
{
$this->notes[] = $note;
}

#[QueryHandler('first.retrieve')]
public function retrieve(): array
{
return $this->notes;
}
};

$secondService = new class () {
private array $labels = [];

#[CommandHandler('second.store')]
public function store(#[Header('label')] string $label): void
{
$this->labels[] = $label;
}

#[QueryHandler('second.retrieve')]
public function retrieve(): array
{
return $this->labels;
}
};

$firstEcotone = $this->bootstrapWithCache($firstService);
$secondEcotone = $this->bootstrapWithCache($secondService);

$firstEcotone->getCommandBus()->sendWithRouting('first.store', metadata: ['note' => 'from first']);
$secondEcotone->getCommandBus()->sendWithRouting('second.store', metadata: ['label' => 'from second']);

self::assertSame(['from first'], $firstEcotone->getQueryBus()->sendWithRouting('first.retrieve'));
self::assertSame(['from second'], $secondEcotone->getQueryBus()->sendWithRouting('second.retrieve'));
}

private function bootstrapWithCache(object $service): ConfiguredMessagingSystem
{
return EcotoneLite::bootstrap(
[$service::class],
[$service],
ServiceConfiguration::createWithDefaults()
->withSkippedModulePackageNames(ModulePackageList::allPackages())
->withCacheDirectoryPath(sys_get_temp_dir() . '/ecotone_cached_bootstrap_isolation'),
useCachedVersion: true,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,43 @@ classesToResolve: [self::class],
self::assertTrue($cacheLayout->serviceCacheConfiguration->shouldUseCache());
}

public function test_it_resolves_different_config_hash_for_different_classes_declared_in_the_same_file(): void
{
$firstLayout = $this->resolveFor([FirstCacheKeyFixture::class]);
$secondLayout = $this->resolveFor([SecondCacheKeyFixture::class]);

self::assertNotSame($firstLayout->configHash, $secondLayout->configHash);
}

public function test_it_disables_cache_when_anonymous_class_is_registered(): void
{
$anonymousClass = new class () {
};

$layout = $this->resolveFor([$anonymousClass::class]);

self::assertFalse($layout->serviceCacheConfiguration->shouldUseCache());
}

public function test_it_resolves_different_config_hash_when_installed_dependencies_change(): void
{
$rootCatalog = sys_get_temp_dir() . '/ecotone_composer_lock_test_' . bin2hex(random_bytes(6));
mkdir($rootCatalog, 0777, true);

try {
file_put_contents($rootCatalog . '/composer.lock', '{"packages":[{"name":"ecotone/ecotone","version":"1.322.0"}]}');
$beforeUpgrade = $this->resolveFor([FirstCacheKeyFixture::class], $rootCatalog);

file_put_contents($rootCatalog . '/composer.lock', '{"packages":[{"name":"ecotone/ecotone","version":"1.323.0"}]}');
$afterUpgrade = $this->resolveFor([FirstCacheKeyFixture::class], $rootCatalog);

self::assertNotSame($beforeUpgrade->configHash, $afterUpgrade->configHash);
} finally {
@unlink($rootCatalog . '/composer.lock');
@rmdir($rootCatalog);
}
}

public function test_it_resolves_fixed_cache_directory_without_hash_sub_directory(): void
{
$cacheDirectory = sys_get_temp_dir() . '/ecotone_cache_layout_test_fixed';
Expand All @@ -60,4 +97,33 @@ classesToResolve: [self::class],

self::assertSame($cacheDirectory, $cacheLayout->serviceCacheConfiguration->getPath());
}

/**
* @param class-string[] $classesToResolve
*/
private function resolveFor(array $classesToResolve, ?string $rootCatalog = null): ContainerCacheLayout
{
return ContainerCacheLayout::resolve(
$rootCatalog ?? __DIR__ . '/../../',
ServiceConfiguration::createWithDefaults()
->withSkippedModulePackageNames(ModulePackageList::allPackages()),
sys_get_temp_dir() . '/ecotone_cache_layout_test',
shouldUseCache: true,
classesToResolve: $classesToResolve,
);
}
}

/**
* licence Apache-2.0
*/
class FirstCacheKeyFixture
{
}

/**
* licence Apache-2.0
*/
class SecondCacheKeyFixture
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ public function process(ContainerBuilder $builder): void
self::assertSame($cacheConfiguration, $loaded->get(ServiceCacheConfiguration::REFERENCE_NAME));
}

public function test_it_overrides_dumped_cache_configuration_with_the_one_used_to_load_from(): void
{
$buildDirectory = $this->uniqueCacheDirectory();
$buildCacheConfiguration = new ServiceCacheConfiguration($buildDirectory, true);
$builder = new ContainerBuilder();
$builder->register(ServiceCacheConfiguration::REFERENCE_NAME, $buildCacheConfiguration);
$builder->replace('aService', new Definition(ACachedService::class, ['someName', new Reference(ServiceCacheConfiguration::REFERENCE_NAME)]));
EcotoneSymfonyContainerFactory::build($builder, $buildCacheConfiguration);

$runtimeDirectory = $this->uniqueCacheDirectory();
$this->relocate($buildDirectory, $runtimeDirectory);
$runtimeCacheConfiguration = new ServiceCacheConfiguration($runtimeDirectory, true);

$loaded = EcotoneSymfonyContainerFactory::loadCached($runtimeCacheConfiguration);

self::assertSame($runtimeCacheConfiguration, $loaded->get(ServiceCacheConfiguration::REFERENCE_NAME));
self::assertSame($runtimeDirectory, $loaded->get('aService')->dependency->getPath());
}

private function relocate(string $buildDirectory, string $runtimeDirectory): void
{
mkdir($runtimeDirectory, 0777, true);
foreach (glob($buildDirectory . '/*') as $file) {
rename($file, $runtimeDirectory . '/' . basename($file));
}
rmdir($buildDirectory);
}

private function uniqueCacheDirectory(): string
{
return sys_get_temp_dir() . '/ecotone_container_cache_test/' . uniqid('', true);
Expand Down
82 changes: 82 additions & 0 deletions packages/Symfony/tests/phpunit/RelocatedContainerCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Test;

use Ecotone\Lite\InMemoryPSRContainer;
use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences;
use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass;
use Ecotone\Messaging\Config\Container\ContainerBuilder;
use Ecotone\Messaging\Config\Container\GatewayProxyReference;
use Ecotone\Messaging\Config\MessagingSystemConfiguration;
use Ecotone\Messaging\Config\ServiceCacheConfiguration;
use Ecotone\Messaging\Handler\Gateway\ProxyFactory;
use Ecotone\Modelling\QueryBus;
use Ecotone\SymfonyBundle\DependencyInjection\EcotoneContainerLoader;
use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Filesystem\Filesystem;

/**
* @internal
*/
/**
* licence Apache-2.0
* @internal
*/
final class RelocatedContainerCacheTest extends TestCase
{
private Filesystem $filesystem;
private string $temporaryDirectory;

protected function setUp(): void
{
$this->filesystem = new Filesystem();
$this->temporaryDirectory = sys_get_temp_dir() . '/ecotone-relocation-' . bin2hex(random_bytes(6));
}

protected function tearDown(): void
{
$this->filesystem->remove($this->temporaryDirectory);
}

public function test_cached_proxies_are_written_next_to_the_relocated_container(): void
{
$buildCacheDirectory = $this->temporaryDirectory . '/build/ecotone';
$runtimeCacheDirectory = $this->temporaryDirectory . '/runtime/ecotone';

$this->buildEcotoneContainerIn($buildCacheDirectory);
$this->relocateCacheFrom($buildCacheDirectory, $runtimeCacheDirectory);

$container = EcotoneContainerLoader::load($runtimeCacheDirectory, InMemoryPSRContainer::createEmpty());

$proxyFile = $container
->get(ProxyFactory::class)
->generateCachedProxyFileFor(new GatewayProxyReference(QueryBus::class, QueryBus::class), true);

self::assertStringStartsWith($runtimeCacheDirectory . '/', $proxyFile);
self::assertFileExists($proxyFile);
}

private function buildEcotoneContainerIn(string $cacheDirectory): void
{
$serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true);

$containerBuilder = new ContainerBuilder();
$containerBuilder->register(ServiceCacheConfiguration::REFERENCE_NAME, $serviceCacheConfiguration);
$containerBuilder->addCompilerPass(MessagingSystemConfiguration::prepareWithDefaultsForTesting());
$containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences());
$containerBuilder->addCompilerPass(new ValidityCheckPass());

MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration);
EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration);
}

private function relocateCacheFrom(string $buildCacheDirectory, string $runtimeCacheDirectory): void
{
$this->filesystem->mirror($buildCacheDirectory, $runtimeCacheDirectory);
$this->filesystem->remove($this->temporaryDirectory . '/build');
$this->filesystem->touch($this->temporaryDirectory . '/build');
}
}
Loading