Skip to content

Store data in a cache #495

@SamMousa

Description

@SamMousa

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions