Skip to content

Commit

Permalink
feat(loader): lock load class on file loader to avoid race condition
Browse files Browse the repository at this point in the history
  • Loading branch information
joelwurtz committed Jul 16, 2024
1 parent 4ba0372 commit e1b0fe4
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Fixed
- [GH#174](https://github.com/jolicode/automapper/pull/174) Fix race condition when writing generated mappers
- [GH#167](https://github.com/jolicode/automapper/pull/167) Fix property metadata attribute name in docs
- [GH#166](https://github.com/jolicode/automapper/pull/166) Remove cache for property info, use specific services instead

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"symfony/deprecation-contracts": "^3.0",
"symfony/event-dispatcher": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/lock": "^6.4 || ^7.0",
"symfony/property-info": "^6.4 || ^7.0",
"symfony/property-access": "^6.4 || ^7.0",
"phpdocumentor/type-resolver": "^1.7"
Expand Down
6 changes: 5 additions & 1 deletion src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
Expand Down Expand Up @@ -181,10 +183,12 @@ public static function create(
$expressionLanguage,
);

$lockFactory = new LockFactory(new FlockStore());

if (null === $cacheDirectory) {
$loader = new EvalLoader($mapperGenerator, $metadataFactory);
} else {
$loader = new FileLoader($mapperGenerator, $metadataFactory, $cacheDirectory);
$loader = new FileLoader($mapperGenerator, $metadataFactory, $cacheDirectory, $lockFactory);
}

return new self($loader, $customTransformerRegistry, $metadataRegistry, $providerRegistry, $expressionLanguageProvider);
Expand Down
40 changes: 25 additions & 15 deletions src/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
use AutoMapper\Metadata\MetadataRegistry;
use PhpParser\PrettyPrinter\Standard;
use PhpParser\PrettyPrinterAbstract;
use Symfony\Component\Lock\LockFactory;

/**
* Use file system to load mapper, and persist them using a registry.
*
* @author Joel Wurtz <[email protected]>
*
* @internal
* @internal
*/
final class FileLoader implements ClassLoaderInterface
{
Expand All @@ -30,6 +31,7 @@ public function __construct(
private readonly MapperGenerator $generator,
private readonly MetadataFactory $metadataFactory,
private readonly string $directory,
private readonly LockFactory $lockFactory,
private readonly FileReloadStrategy $reloadStrategy = FileReloadStrategy::ON_CHANGE,
) {
$this->printer = new Standard();
Expand All @@ -40,25 +42,33 @@ public function loadClass(MapperMetadata $mapperMetadata): void
$className = $mapperMetadata->className;
$classPath = $this->directory . \DIRECTORY_SEPARATOR . $className . '.php';

if ($this->reloadStrategy === FileReloadStrategy::NEVER && file_exists($classPath)) {
require $classPath;
// We lock the file here, because another process could be writing the file at the same time
$lock = $this->lockFactory->createLock($className);
$lock->acquire(true);

return;
}
try {
if ($this->reloadStrategy === FileReloadStrategy::NEVER && file_exists($classPath)) {
require $classPath;

$shouldBuildMapper = true;
return;
}

if ($this->reloadStrategy === FileReloadStrategy::ON_CHANGE) {
$registry = $this->getRegistry();
$hash = $mapperMetadata->getHash();
$shouldBuildMapper = !isset($registry[$className]) || $registry[$className] !== $hash || !file_exists($classPath);
}
$shouldBuildMapper = true;

if ($shouldBuildMapper) {
$this->createGeneratedMapper($mapperMetadata);
}
if ($this->reloadStrategy === FileReloadStrategy::ON_CHANGE) {
$registry = $this->getRegistry();
$hash = $mapperMetadata->getHash();
$shouldBuildMapper = !isset($registry[$className]) || $registry[$className] !== $hash || !file_exists($classPath);
}

if ($shouldBuildMapper) {
$this->createGeneratedMapper($mapperMetadata);
}

require $classPath;
require $classPath;
} finally {
$lock->release();
}
}

public function buildMappers(MetadataRegistry $registry): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function load(array $configs, ContainerBuilder $container): void

$container
->getDefinition(FileLoader::class)
->replaceArgument(3, $generateStrategy);
->replaceArgument(4, $generateStrategy);

$container
->setAlias(ClassLoaderInterface::class, FileLoader::class)
Expand Down
13 changes: 12 additions & 1 deletion src/Symfony/Bundle/Resources/config/automapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
use AutoMapper\Loader\ClassLoaderInterface;
use AutoMapper\Loader\EvalLoader;
use AutoMapper\Loader\FileLoader;
use AutoMapper\Loader\FileReloadStrategy;
use AutoMapper\Metadata\MetadataFactory;
use AutoMapper\Provider\ProviderRegistry;
use AutoMapper\Symfony\ExpressionLanguageProvider;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;

return static function (ContainerConfigurator $container) {
$container->services()
Expand All @@ -30,6 +33,13 @@
->alias(AutoMapperInterface::class, AutoMapper::class)->public()
->alias(AutoMapperRegistryInterface::class, AutoMapper::class)->public()

->set('automapper.file_loader_lock_factory_store')
->class(FlockStore::class)
->set('automapper.file_loader_lock_factory')
->class(LockFactory::class)
->args([
service('automapper.file_loader_lock_factory_store'),
])
->set(EvalLoader::class)
->args([
service(MapperGenerator::class),
Expand All @@ -41,7 +51,8 @@
service(MapperGenerator::class),
service(MetadataFactory::class),
'%kernel.cache_dir%/automapper',
true,
service('automapper.file_loader_lock_factory'),
FileReloadStrategy::ALWAYS,
])

->set(Configuration::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function testItCorrectlyConfiguresReloadStrategy(array $config, bool $deb
$this->container->setParameter('kernel.debug', $debug);
$this->load(['loader' => $config]);

$this->assertContainerBuilderHasServiceDefinitionWithArgument(FileLoader::class, 3, $expectedValue);
$this->assertContainerBuilderHasServiceDefinitionWithArgument(FileLoader::class, 4, $expectedValue);
}

public static function provideReloadStrategyConfiguration(): iterable
Expand Down

0 comments on commit e1b0fe4

Please sign in to comment.