Skip to content

Commit 53d6b7b

Browse files
authored
Merge pull request #19 from dmstr/feature/harden-dbless-publishing
harden DbAsset less publishing:
2 parents 15f8a55 + 2dd5b52 commit 53d6b7b

File tree

1 file changed

+80
-7
lines changed

1 file changed

+80
-7
lines changed

src/assets/DbAsset.php

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@
1717
use yii\helpers\Json;
1818
use yii\web\AssetBundle;
1919

20+
/**
21+
* Class DbAsset
22+
* @package extensions\dmstr\prototype\assets
23+
*
24+
* This class implements an AssetBundle for Less "files" stored in DB as prototype\models\Less models
25+
* In init() we get the less models from DB, build an overall checksum that will be compared with a cached value
26+
* previous run.
27+
*
28+
* If nothing has changed (checksum match, and db less was yet exported to disc) nothing will be done here
29+
* If "something" changed (or in the first run):
30+
* - if not exists, create empty sourcePath dir to prevent race condition with next req which will try to init
31+
* this bundle again...
32+
* - write contents of less models as *.less files in a tmp dir
33+
* - To prevent multiple converter runs while asset publishing the main less will be converted in this tmp dir
34+
* - To prevent multiple publishing runs we use another tmp file and simple renames while changing the sourcePath contents
35+
* - To prevent unnecessary publishing you should configure a persistent cacheComponent to store the checksum
36+
* which survive a restart (eg. do not use a memory cache which is part of a docker-compose stack)
37+
* - if something went wrong the previous state (prev. checksum in cache) is restored
38+
*
39+
*/
2040
class DbAsset extends AssetBundle
2141
{
2242
const CACHE_ID = 'app\assets\SettingsAsset';
@@ -26,6 +46,15 @@ class DbAsset extends AssetBundle
2646

2747
public $sourcePath = '@runtime/settings-asset';
2848
public $tmpPath = '@runtime/settings-asset-tmp';
49+
/**
50+
*
51+
* name of the cache component that should be used for the less checksum cache
52+
* for high volume sites this should be set to a persistent cache which survive a
53+
* restart
54+
*
55+
* @var string
56+
*/
57+
public $cacheComponent = 'cache';
2958

3059
public $settingsKey = 'registerPrototypeAssetKey';
3160

@@ -34,9 +63,18 @@ class DbAsset extends AssetBundle
3463
// if a full BootstrapAsset (CSS) is compiled, it's recommended to disable it in assetManager configuration
3564
'yii\bootstrap\BootstrapPluginAsset', // (JS)
3665
];
66+
/**
67+
* internal cache property
68+
*
69+
* @var
70+
*/
71+
protected $cache;
3772

3873
public function init()
3974
{
75+
// init configured cache component
76+
$this->cache = Yii::$app->{$this->cacheComponent};
77+
4078
$this->css[] = Yii::$app->settings->get($this->settingsKey, self::SETTINGS_SECTION).'-'.self::MAIN_LESS_FILE;
4179

4280
parent::init();
@@ -49,21 +87,56 @@ public function init()
4987

5088
$models = Less::find()->all();
5189
$hash = sha1(Json::encode($models));
52-
if (!is_dir($sourcePath) || ($hash !== Yii::$app->cache->get(self::CACHE_ID))) {
90+
$prevHash = $this->cache->get(self::CACHE_ID);
91+
$sourcePathExists = is_dir($sourcePath);
92+
if (($hash !== $prevHash) || ! $sourcePathExists) {
93+
94+
// create empty sourcePath dir to prevent race condition with next req which will init again...
95+
if ( ! $sourcePathExists) {
96+
FileHelper::createDirectory($sourcePath);
97+
}
98+
$dependency = new FileDependency();
99+
$dependency->fileName = __FILE__;
100+
$this->cache->set(self::CACHE_ID, $hash, 0, $dependency);
101+
53102
$tmpPath = uniqid($sourcePath.'-');
54103
FileHelper::createDirectory($tmpPath);
55-
56104
foreach ($models as $model) {
57105
file_put_contents("$tmpPath/{$model->key}.less", $model->value);
58106
}
59107

60-
$dependency = new FileDependency();
61-
$dependency->fileName = __FILE__;
62-
Yii::$app->cache->set(self::CACHE_ID, $hash, 0, $dependency);
108+
// convert less with new files in tmp folder before replacing bundle sourcePath
109+
// to prevent multiple conversions while republishing on high-traffic sites
110+
$converter = Yii::$app->assetManager->getConverter();
111+
try {
112+
foreach ($this->css as $cssFile) {
113+
$result = $converter->convert($cssFile, $tmpPath);
114+
}
115+
} catch (\Exception $exception) {
116+
$this->cache->set(self::CACHE_ID, $prevHash, 0, $dependency);
117+
Yii::error($exception->getMessage(), __METHOD__);
118+
return false;
119+
}
63120

64121
// force republishing of asset files by Yii Framework
65-
FileHelper::removeDirectory($sourcePath);
66-
rename($tmpPath, $sourcePath);
122+
// to prevent race conditions, use 2 rename cmds to switch dir and remove prev. dir afterwards
123+
$sourcePathToDelete = uniqid($sourcePath.'-to-delete-');
124+
$sourcePathRenamed = false;
125+
if ($sourcePathExists) {
126+
if (rename($sourcePath, $sourcePathToDelete)) {
127+
$sourcePathRenamed = true;
128+
} else {
129+
$this->cache->set(self::CACHE_ID, $prevHash, 0, $dependency);
130+
return false;
131+
}
132+
}
133+
if ( ! rename($tmpPath, $sourcePath)) {
134+
$this->cache->set(self::CACHE_ID, $prevHash, 0, $dependency);
135+
return false;
136+
}
137+
if ($sourcePathRenamed) {
138+
FileHelper::removeDirectory($sourcePathToDelete);
139+
}
67140
}
68141
}
69142
}

0 commit comments

Comments
 (0)