diff --git a/README.md b/README.md index 1b6942e..eaa8bc6 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,11 @@ pci "maj projet" prel -C -commit="$(git log --grep="Init changelog . version ${version}p82" --format=%H)" +commit="$(git log --grep="Init changelog . version ${version}p82" --format=%H)" && +echo "commit=$commit" + git checkout dev74 + git cherry-pick "$commit" pp -a ~~~ diff --git a/bin/nucache.php b/bin/nucache.php new file mode 100755 index 0000000..290263c --- /dev/null +++ b/bin/nucache.php @@ -0,0 +1,7 @@ +#!/usr/bin/php + "varchar(64) not null", + "id" => "varchar(64) not null", + "date_start" => "datetime", + "duration_" => "text", + "primary key (group_id, id)", + ]; + + static function get_cache_ids($id): array { + if (is_array($id)) { + $keys = array_keys($id); + if (array_key_exists("group_id", $id)) $groupIdKey = "group_id"; + else $groupIdKey = $keys[1] ?? null; + $groupId = $id[$groupIdKey] ?? ""; + if (array_key_exists("id", $id)) $idKey = "id"; + else $idKey = $keys[0] ?? null; + $id = $id[$idKey] ?? ""; + } else { + $groupId = ""; + } + if (preg_match('/^(.*\\\\)?([^\\\\]+)$/', $groupId, $ms)) { + # si le groupe est une classe, faire un hash du package pour limiter la + # longueur du groupe + [$package, $groupId] = [$ms[1], $ms[2]]; + $package = substr(md5($package), 0, 4); + $groupId = "${groupId}_$package"; + } + return ["group_id" => $groupId, "id" => $id]; + } + + function __construct(?string $duration=null, ?string $name=null) { + parent::__construct($name); + $this->duration = $duration ?? static::DURATION; + $this->includes = static::INCLUDES; + $this->excludes = static::EXCLUDES; + } + + protected string $duration; + + protected ?array $includes; + + protected ?array $excludes; + + function getItemValues($item): ?array { + return cl::merge(self::get_cache_ids($item), [ + "item" => null, + ]); + } + + function onCreate($item, array $values, ?array $alwaysNull, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function onUpdate($item, array $values, array $pvalues, ?string $duration=null): ?array { + $now = new DateTime(); + $duration ??= $this->duration; + return [ + "date_start" => $now, + "duration" => new Delay($duration, $now), + ]; + } + + function shouldUpdate($id, bool $noCache=false): bool { + if ($noCache) return true; + + $cacheIds = self::get_cache_ids($id); + $groupId = $cacheIds["group_id"]; + if ($groupId) { + $includes = $this->includes; + $shouldInclude = $includes !== null && in_array($groupId, $includes); + $excludes = $this->excludes; + $shouldExclude = $excludes !== null && in_array($groupId, $excludes); + if (!$shouldInclude || $shouldExclude) return true; + } + + $found = false; + $expired = false; + $this->each($cacheIds, + function($item, $values) use (&$found, &$expired) { + $found = true; + $expired = $values["duration"]->isElapsed(); + }); + return !$found || $expired; + } + + function setCached($id, ?string $duration=null): void { + $cacheIds = self::get_cache_ids($id); + $this->charge($cacheIds, null, [$duration]); + } + + function resetCached($id) { + $cacheIds = self::get_cache_ids($id); + $this->delete($cacheIds); + } +} diff --git a/src/cache/CacheData.php b/src/cache/CacheData.php new file mode 100644 index 0000000..ae0ea6e --- /dev/null +++ b/src/cache/CacheData.php @@ -0,0 +1,50 @@ +name = $name ?? static::NAME ?? ""; + $this->compute = func::withn($compute ?? static::COMPUTE); + } + + protected string $name; + + protected ?func $compute; + + protected function compute() { + $compute = $this->compute; + return $compute !== null? $compute->invoke(): null; + } + + function getName() : string { + return $this->name; + } + + /** obtenir la donnée, en l'itérant au préalable si elle est traversable */ + function get($compute=null) { + $this->compute ??= func::withn($compute); + $data = $this->compute(); + if ($data instanceof Traversable) { + $data = cl::all($data); + } + return $data; + } + + /** obtenir un itérateur sur la donnée ou null s'il n'y a pas de données */ + function all($compute=null): ?iterable { + $this->compute ??= func::withn($compute); + $data = $this->compute(); + if ($data !== null && !is_iterable($data)) $data = [$data]; + return $data; + } +} diff --git a/src/cache/CacheDataChannel.php b/src/cache/CacheDataChannel.php new file mode 100644 index 0000000..768b6d0 --- /dev/null +++ b/src/cache/CacheDataChannel.php @@ -0,0 +1,53 @@ + "varchar(128) primary key not null", + "all_values" => "mediumtext", + ]; + + function __construct($id, callable $builder, ?string $duration=null) { + $this->cacheIds = $cacheIds = CacheChannel::get_cache_ids($id); + $this->builder = Closure::fromCallable($builder); + $this->duration = $duration; + $name = "{$cacheIds["group_id"]}-{$cacheIds["id"]}"; + parent::__construct($name); + } + + protected array $cacheIds; + + protected Closure $builder; + + protected ?string $duration = null; + + function getItemValues($item): ?array { + $key = array_keys($item)[0]; + $row = $item[$key]; + return [ + "key" => $key, + "item" => $row, + "all_values" => implode(" ", cl::filter_n(cl::with($row))), + ]; + } + + function getIterator(): Traversable { + $cm = storage_cache::get(); + if ($cm->shouldUpdate($this->cacheIds)) { + $this->capacitor->reset(); + foreach (($this->builder)() as $key => $row) { + $this->charge([$key => $row]); + } + $cm->setCached($this->cacheIds, $this->duration); + } + return $this->discharge(false); + } +} diff --git a/src/cache/CacheFile.php b/src/cache/CacheFile.php new file mode 100644 index 0000000..3feb150 --- /dev/null +++ b/src/cache/CacheFile.php @@ -0,0 +1,368 @@ +basedir = path::dirname($file); + $basename = path::filename($file); + $this->basename = str::without_suffix(self::EXT, $basename); + $this->initialDuration = Delay::with($params["duration"] ?? static::DURATION); + $this->overrideDuration = $params["override_duration"] ?? false; + $this->readonly = $params["readonly"] ?? false; + $this->cacheNull = $params["cache_null"] ?? false; + $data = $params["data"] ?? null; + $this->sources = null; + if ($data === null) { + $this->source = null; + } elseif ($data instanceof CacheData) { + $this->source = $data; + } elseif (func::is_callable($data)) { + $this->source = new CacheData($data); + } elseif (!is_array($data)) { + $this->source = self::ensure_source($data); + } else { + $sources = []; + $index = 0; + foreach ($data as $key => $source) { + $source = self::ensure_source($source); + if ($key === $index) { + $index++; + $key = $source->getName(); + } + $sources[$key] = $source; + } + $this->sources = $sources; + $this->source = null; + } + parent::__construct($file); + } + + /** @var string répertoire de base des fichiers de cache */ + protected string $basedir; + + /** @var string nom de base des fichiers de cache */ + protected string $basename; + + protected Delay $initialDuration; + + protected bool $overrideDuration; + + protected bool $readonly; + + protected bool $cacheNull; + + protected ?array $sources; + + protected ?CacheData $source; + + protected function getSource($data): CacheData { + if ($data === null) { + return $this->source ??= new CacheData(function() { + return $this->compute(); + }); + } + $source = $data; + if (is_string($source) || is_int($source)) { + $source = $this->sources[$source] ?? null; + if ($source === null) throw ValueException::invalid_key($data); + } + if ($source instanceof CacheData) return $source; + throw ValueException::invalid_type($data, CacheData::class); + } + + /** + * vérifier si le fichier est valide. s'il est invalide, il faut le recréer. + * + * on assume que le fichier existe, vu qu'il a été ouvert en c+b + */ + function isValid(): bool { + # considèrer que le fichier est invalide s'il est de taille nulle + return $this->getSize() > 0; + } + + protected ?DateTime $start; + + protected ?Delay $duration; + + protected ?array $datafilenames; + + /** charger les données. le fichier a déjà été verrouillé en lecture */ + protected function loadMetadata(): void { + if ($this->isValid()) { + $this->rewind(); + [ + "start" => $start, + "duration" => $duration, + "datafiles" => $datafilenames, + ] = $this->unserialize(null, false, true); + if ($this->overrideDuration) { + $duration = Delay::with($this->initialDuration, $start); + } + $datafilenames = array_fill_keys($datafilenames, true); + } else { + $start = null; + $duration = null; + $datafilenames = []; + } + $this->start = $start; + $this->duration = $duration; + $this->datafilenames = $datafilenames; + } + + /** + * tester s'il faut mettre les données à jour. le fichier a déjà été + * verrouillé en lecture + */ + protected function shouldUpdate(bool $noCache=false): bool { + if ($this->isValid()) { + $expired = $this->duration->isElapsed(); + } else { + $expired = false; + $noCache = true; + } + return $noCache || $expired; + } + + /** sauvegarder les données. le fichier a déjà été verrouillé en écriture */ + protected function saveMetadata(): void { + $this->duration ??= $this->initialDuration; + if ($this->start === null) { + $this->start = new DateTime(); + $this->duration = Delay::with($this->duration, $this->start); + } + $datafilenames = array_keys($this->datafilenames); + $this->ftruncate(); + $this->serialize([ + "start" => $this->start, + "duration" => $this->duration, + "datafiles" => $datafilenames, + ], false, true); + } + + protected function loadData(string $datafile) { + return file::reader($datafile)->unserialize(); + } + + protected function saveData(string $datafile, $data): void { + file::writer($datafile)->serialize($data); + } + + protected function unlinkDatafile(string $datafilename): void { + $datafile = path::join($this->basedir, $datafilename); + @unlink($datafile); + unset($this->datafilenames[$datafilename]); + } + + protected function unlinkFiles(bool $datafilesOnly=false): void { + if (!$datafilesOnly) @unlink($this->file); + foreach ($this->datafilenames as $datafilename) { + $this->unlinkDatafile($datafilename); + } + } + + /** tester si $value peut être mis en cache */ + protected function shouldCache($value): bool { + return $this->cacheNull || $value !== null; + } + + protected ?DateTime $ostart; + + protected ?Delay $oduration; + + protected ?array $odatafilenames; + + protected function beforeAction() { + $this->loadMetadata(); + $this->ostart = cv::clone($this->start); + $this->oduration = cv::clone($this->duration); + $this->odatafilenames = cv::clone($this->datafilenames); + } + + protected function afterAction() { + $start = $this->start; + $duration = $this->duration; + $oduration = $this->oduration; + $datafilenames = $this->datafilenames; + $modified = false; + if ($start != $this->ostart) $modified = true; + if ($duration === null || $oduration === null) $modified = true; + elseif ($duration->getDest() != $oduration->getDest()) $modified = true; + # égalité stricte uniquement pour $datafiles qui est un array + if ($datafilenames !== $this->odatafilenames) $modified = true; + if ($modified && !$this->readonly) { + $this->lockWrite(); + $this->saveMetadata(); + } + } + + protected function action(callable $callback, bool $willWrite=false) { + if ($willWrite && !$this->readonly) $this->lockWrite(); + else $this->lockRead(); + try { + $this->beforeAction(); + $result = $callback(); + $this->afterAction(); + return $result; + } finally { + $this->start = null; + $this->duration = null; + $this->datafilenames = null; + $this->unlock(true); + } + } + + protected function compute() { + return null; + } + + function get($data=null, bool $noCache=false) { + return $this->action(function () use ($data, $noCache) { + $source = $this->getSource($data); + $dataname = $source->getName(); + $datafilename = ".{$this->basename}.{$dataname}".self::EXT; + $datafile = path::join($this->basedir, $datafilename); + + $updateMetadata = $this->shouldUpdate($noCache); + $updateData = !array_key_exists($datafilename, $this->datafilenames); + if (!$this->readonly && ($updateMetadata || $updateData)) { + $this->lockWrite(); + if ($updateMetadata) { + # il faut refaire tout le cache + $this->unlinkFiles(true); + $this->start = null; + $this->duration = null; + $updateData = true; + } + if ($updateData) { + # calculer un fichier + try { + $data = $source->get(); + } catch (Exception $e) { + # ne pas garder le fichier en cas d'exception + $this->unlinkDatafile($datafile); + throw $e; + } + } elseif (file_exists($datafile)) { + $data = $this->loadData($datafile); + } else { + $data = null; + } + if ($this->shouldCache($data)) { + $this->saveData($datafile, $data); + $this->datafilenames[$datafilename] = true; + } else { + # ne pas garder le fichier s'il ne faut pas mettre en cache + $this->unlinkDatafile($datafile); + } + } elseif (file_exists($datafile)) { + $data = $this->loadData($datafile); + } else { + $data = null; + } + return $data; + }); + } + + function all($data=null, bool $noCache=false): ?iterable { + $data = $this->get($data, $noCache); + if ($data !== null && !is_iterable($data)) $data = [$data]; + return $data; + } + + function delete($data=null): void { + $source = $this->getSource($data); + $dataname = $source->getName(); + $datafilename = ".{$this->basename}.{$dataname}".self::EXT; + $this->unlinkDatafile($datafilename); + } + + /** obtenir les informations sur le fichier */ + function getInfos(): array { + return $this->action(function () { + if (!$this->isValid()) { + return ["valid" => false]; + } + $start = $this->start; + $duration = $this->duration; + $datafilenames = $this->datafilenames; + $datafiles = []; + foreach (array_keys($datafilenames) as $datafilename) { + $datafile = path::join($this->basedir, $datafilename); + $name = $datafilename; + str::del_prefix($name, ".{$this->basename}."); + str::del_suffix($name, self::EXT); + if (file_exists($datafile)) { + $size = filesize($datafile); + } else { + $size = null; + } + $datafiles[$name] = $size; + } + return [ + "valid" => true, + "start" => $start, + "duration" => strval($duration), + "date_start" => $start->format(), + "date_end" => $duration->getDest()->format(), + "datafiles" => $datafiles, + ]; + }); + } + + const UPDATE_SUB = -1, UPDATE_SET = 0, UPDATE_ADD = 1; + + /** + * mettre à jour la durée de validité du fichier + * + * XXX UPDATE_SET n'est pas implémenté + */ + function updateDuration($nduration, int $action=self::UPDATE_ADD): void { + if ($this->readonly) return; + $this->action(function () use ($nduration, $action) { + if (!$this->isValid()) return; + $duration = $this->duration; + if ($action < 0) $duration->subDuration($nduration); + elseif ($action > 0) $duration->addDuration($nduration); + }, true); + } + + /** supprimer les fichiers s'ils ont expiré */ + function deleteExpired(bool $force=false): bool { + if ($this->readonly) return false; + return $this->action(function () use ($force) { + if ($force || $this->shouldUpdate()) { + $this->unlinkFiles(); + return true; + } + return false; + }, true); + } +} diff --git a/src/cache/storage_cache.php b/src/cache/storage_cache.php new file mode 100644 index 0000000..1fc6814 --- /dev/null +++ b/src/cache/storage_cache.php @@ -0,0 +1,39 @@ + parent::ARGS, + "purpose" => "gestion de fichiers cache", + ["-r", "--read", "name" => "action", "value" => self::ACTION_READ, + "help" => "Afficher le contenu d'un fichier cache", + ], + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur le fichier cache", + ], + ["-k", "--clean", "name" => "action", "value" => self::ACTION_CLEAN, + "help" => "Supprimer le fichier cache s'il a expiré", + ], + ["-a", "--add-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_ADD], + "help" => "Ajouter le nombre de secondes spécifié à la durée du cache", + ], + ["-b", "--sub-duration", "args" => 1, + "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SUB], + "help" => "Enlever le nombre de secondes spécifié à la durée du cache", + ], + #XXX pas encore implémenté + //["-s", "--set-duration", "args" => 1, + // "action" => [null, "->setActionUpdate", self::ACTION_UPDATE_SET], + // "help" => "Mettre à jour la durée du cache à la valeur spécifiée", + //], + ]; + + protected $action = self::ACTION_READ; + + protected $updateAction, $updateDuration; + + protected $args; + + function setActionUpdate(int $action, $updateDuration): void { + $this->action = self::ACTION_UPDATE; + switch ($action) { + case self::ACTION_UPDATE_SUB: + $this->updateAction = CacheFile::UPDATE_SUB; + break; + case self::ACTION_UPDATE_SET: + $this->updateAction = CacheFile::UPDATE_SET; + break; + case self::ACTION_UPDATE_ADD: + $this->updateAction = CacheFile::UPDATE_ADD; + break; + } + $this->updateDuration = $updateDuration; + } + + protected function findCaches(string $dir, ?array &$files): void { + foreach (glob("$dir/*") as $file) { + if (is_dir($file)) { + $this->findCaches($file, $files); + } elseif (is_file($file) && fnmatch("*.cache", $file)) { + $files[] = $file; + } + } + } + + function main() { + $files = []; + foreach ($this->args as $arg) { + if (is_dir($arg)) { + $this->findCaches($arg, $files); + } elseif (is_file($arg)) { + $files[] = $arg; + } else { + msg::warning("$arg: fichier introuvable"); + } + } + $showSection = count($files) > 1; + foreach ($files as $file) { + switch ($this->action) { + case self::ACTION_READ: + if ($showSection) msg::section($file); + $cache = new CacheFile($file, [ + "readonly" => true, + "duration" => "INF", + "override_duration" => true, + ]); + yaml::dump($cache->get()); + break; + case self::ACTION_INFOS: + if ($showSection) msg::section($file); + $cache = new CacheFile($file, [ + "readonly" => true, + ]); + yaml::dump($cache->getInfos()); + break; + case self::ACTION_CLEAN: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + if ($cache->deleteExpired()) msg::asuccess("fichier supprimé"); + else msg::adone("fichier non expiré"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + case self::ACTION_UPDATE: + msg::action(path::ppath($file)); + $cache = new CacheFile($file); + try { + $cache->updateDuration($this->updateDuration, $this->updateAction); + msg::asuccess("fichier mis à jour"); + } catch (Exception $e) { + msg::afailure($e); + } + break; + default: + self::die("$this->action: action non implémentée"); + } + } + } +} diff --git a/tbin/.gitignore b/tbin/.gitignore index 74dd8df..f9f3cae 100644 --- a/tbin/.gitignore +++ b/tbin/.gitignore @@ -1,2 +1,3 @@ /output-forever.log /devel/ +/*.cache diff --git a/tbin/test-nucache.php b/tbin/test-nucache.php new file mode 100755 index 0000000..802926f --- /dev/null +++ b/tbin/test-nucache.php @@ -0,0 +1,76 @@ +#!/usr/bin/php +get()); + if ($dumpInfos) { + yaml::dump($cache->getInfos()); + } +} + +//system("rm -f *.cache .*.cache"); + +$what = [ + //"null", + "one", + //"two", + //"three", +]; +$duration = 10; + +if (in_array("null", $what)) { + $null = new CacheFile("null", [ + "duration" => $duration, + ]); + show("null", $null); +} + +if (in_array("one", $what)) { + $one = new class("one", [ + "duration" => $duration, + ]) extends CacheFile { + protected function compute() { + return 1; + } + }; + show("one", $one); +} + +if (in_array("two", $what)) { + $two = new CacheFile("two", [ + "duration" => $duration, + "data" => new CacheData(function () { + return 2; + }), + ]); + show("two", $two); +} + +if (in_array("three", $what)) { + $data31 = new CacheData(function () { + return 31; + }, "data31name"); + + $data32 = new CacheData(function () { + return 32; + }); + + $three = new CacheFile("three", [ + "data" => [ + "data31" => $data31, + $data31, # name=data31name + "data32" => $data32, + $data32, # name="" + ], + ]); + Txx("three.0=", $three->get("data31")); + Txx("three.1=", $three->get("data31name")); + Txx("three.2=", $three->get("data32")); + Txx("three.3=", $three->get("")); +}