-
-
Notifications
You must be signed in to change notification settings - Fork 150
Closed
Description
What steps will reproduce the problem?
Try running an app on a cluster. If you have multiple nodes running the debug data for each tag is not available everywhere.
What's expected?
Some way to solve that, which is easily possible.
What do you get instead?
1/n of your requests for the debugger will fail due to the data not existing.
Proof of concept
<?php
declare(strict_types=1);
namespace collecthor\components;
use yii\base\InvalidConfigException;
use yii\caching\CacheInterface;
use yii\debug\FlattenException;
use yii\debug\LogTarget;
use yii\di\Instance;
class DebugCacheLogTarget extends LogTarget
{
/** @var CacheInterface */
public $cache;
public function init()
{
parent::init();
if (!isset($this->cache)) {
throw new InvalidConfigException("A cache must be configured");
}
$this->cache = Instance::ensure($this->cache, CacheInterface::class);
}
/**
* Exports log messages to a specific destination.
* Child classes must implement this method.
* @throws \yii\base\Exception
*/
public function export()
{
$summary = $this->collectSummary();
$data = [];
$exceptions = [];
foreach ($this->module->panels as $id => $panel) {
try {
$panelData = $panel->save();
if ($id === 'profiling') {
$summary['peakMemory'] = $panelData['memory'];
$summary['processingTime'] = $panelData['time'];
}
$data[$id] = serialize($panelData);
} catch (\Exception $exception) {
$exceptions[$id] = new FlattenException($exception);
}
}
$data['summary'] = $summary;
$data['exceptions'] = $exceptions;
$this->save($this->tag, $data);
$this->updateIndex($summary);
}
private function retrieve(string $tag): array
{
return $this->cache->get(['YII2_DEBUG' . $tag]);
}
private function save(string $tag, array $data): void
{
$this->cache->set(['YII2_DEBUG' . $tag], $data);
}
private function remove(string $tag): void
{
$this->cache->delete(['YII2_DEBUG' . $tag]);
}
private function updateIndex($summary): void
{
$key = ['YII2_DEBUG_INDEX', $this->module->dataPath];
// We have a race condition here that could cause us to lose entries. This is acceptable.
$manifest = $this->cache->exists('YII2_DEBUG_INDEX') ? $this->cache->get('YII2_DEBUG_INDEX') : [];
$manifest[$this->tag] = $summary;
$this->cache->set($key, $this->truncate($manifest));
}
/**
* Remove entries exceeding the history size from the manifest.
* @param array $manifest
* @return array
*/
private function truncate(array $manifest): array
{
$result = [];
foreach($manifest as $tag => $entry) {
if (count($result) <= $this->module->historySize) {
$result[$tag] = $entry;
} else {
$this->remove($tag);
// We do not support the mail panel and thus its files are not deleted.
}
}
return $result;
}
public function loadManifest(): array
{
$key = ['YII2_DEBUG_INDEX', $this->module->dataPath];
$result = $this->cache->get($key);
return $result ?: [];
}
public function loadTagToPanels($tag): array
{
if (!is_string($tag)) {
throw new \Exception('Tag must be a string');
}
$data = $this->retrieve($tag);
$exceptions = $data['exceptions'];
foreach ($this->module->panels as $id => $panel) {
if (isset($data[$id])) {
$panel->tag = $tag;
$panel->load(unserialize($data[$id]));
} else {
unset($this->module->panels[$id]);
}
if (isset($exceptions[$id])) {
$panel->setError($exceptions[$id]);
}
}
return $data;
}
}
This is a proof of concept implementation; if anyone wants to pick this up and transform it into a PR please feel free to!
I won't have the time to polish it properly.
- The manifest is stored in a separate key that uses the
$dataPath
module config for namespacing. - Each request (tag) is stored in a separate key
- Cleanup works for the tags
- Mail files are not cleaned up
- Mail files are still stored on the node local disk
This will work with any cache implementation. One could argue that the default implementation could even be adapted to use a FileCache
. By using a common CacheInterface
for storage one implementation could serve both use cases.
I've tested this using a Redis cache.
Metadata
Metadata
Assignees
Labels
No labels