taskman_config/config.inc.php

431 lines
12 KiB
PHP

<?php
namespace taskman;
use Exception;
require_once(__DIR__ . '/task.inc.php');
require_once(__DIR__ . '/scan.inc.php');
require_once(__DIR__ . '/cache.inc.php');
require_once(__DIR__ . '/pack.inc.php');
require_once(__DIR__ . '/util.inc.php');
require_once(__DIR__ . '/msgpack.inc.php');
class ConfigGlobals
{
public readonly array $base_dirs;
public readonly ?string $worker_init_fn;
public readonly int $workers_num;
public readonly string $base_class;
public readonly string $build_dir;
function __construct(
array $base_dirs,
string $build_dir,
?string $worker_init_fn = null,
int $workers_num = 1,
string $base_class = '\ConfBase'
)
{
$this->base_dirs = array_map(fn($path) => normalize_path($path), $base_dirs);
$this->build_dir = $build_dir;
$this->worker_init_fn = $worker_init_fn;
$this->workers_num = $workers_num;
$this->base_class = $base_class;
}
function initWorker(bool $is_master_proc)
{
$GLOBALS['CONFIG_GLOBALS'] = $this;
if($this->worker_init_fn !== null)
{
$fn = $this->worker_init_fn;
$fn($is_master_proc);
}
}
}
enum ConfigUpdateMode : int
{
case Force = 1;
case DetectChanged = 2;
case Patch = 3;
}
class ConfigUpdateRequest
{
public ConfigUpdateMode $mode;
public ?ConfigDirFiles $files;
public ?string $result_file;
private function __construct() {}
static function force(?ConfigDirFiles $files = null) : ConfigUpdateRequest
{
$req = new ConfigUpdateRequest();
$req->mode = ConfigUpdateMode::Force;
$req->files = $files;
return $req;
}
static function detectChanged(string $result_file, ?ConfigDirFiles $files = null) : ConfigUpdateRequest
{
$req = new ConfigUpdateRequest();
$req->mode = ConfigUpdateMode::DetectChanged;
$req->files = $files;
$req->result_file = $result_file;
return $req;
}
static function patch(ConfigDirFiles $files) : ConfigUpdateRequest
{
$req = new ConfigUpdateRequest();
$req->mode = ConfigUpdateMode::Patch;
$req->files = $files;
return $req;
}
}
class ConfigManager
{
private ConfigGlobals $globals;
private ConfigCache $cache;
private ?ConfigCacheFileMap $file_map;
function __construct(ConfigGlobals $globals)
{
$this->globals = $globals;
$this->cache = new ConfigCache($globals);
$this->file_map = null;
}
function getGlobals() : ConfigGlobals
{
return $this->globals;
}
function getCache() : ConfigCache
{
return $this->cache;
}
function getArtifactFilesSpec() : array
{
return [$this->globals->base_dirs, ['.js']];
}
function getFileMap() : ConfigCacheFileMap
{
if($this->file_map === null)
{
$this->file_map = $this->_tryLoadMap();
if($this->file_map === null)
{
$this->file_map = self::_makeMap($this->scanFiles(extension: '.js'));
$this->_saveFileMap();
}
}
return $this->file_map;
}
function updateCache(ConfigUpdateRequest $req, bool $return_entries = false, bool $verbose = false) : ConfigCacheUpdateResult
{
config_log("Updating cache, mode '{$req->mode->name}'...");
if($req->files === null)
$req->files = $this->scanFiles(extension: '.js', verbose: $verbose);
$added_files = [];
$removed_files = [];
$this->_checkFileMap($req, $added_files, $removed_files);
$affected_files = $this->_getAffectedFiles($req, $removed_files);
//NOTE: at this poine taking into account only config files
$affected_files->filter(fn($file) => str_ends_with($file, '.conf.js'));
config_log("Affected files: {$affected_files->count()}");
$update_params = new ConfigCacheUpdateParams(
globals: $this->globals,
affected_files: $affected_files,
verbose: $verbose
);
$update_result = self::_updateCache($update_params);
$this->cache->clear();
$this->_updateFileMap($req, $affected_files, $added_files, $removed_files);
if($return_entries)
{
foreach($affected_files as $file)
{
$entry = $this->cache->getOrLoadByPath($file);
$update_result->affected_entries[] = $entry;
}
}
return $update_result;
}
private function _updateCache(ConfigCacheUpdateParams $params) : ConfigCacheUpdateResult
{
if($params->affected_files->isEmpty())
return new ConfigCacheUpdateResult($params);
$t = microtime(true);
$result = _config_cache_update($params);
if($result->errors)
{
$errors = array();
foreach($result->errors as $file => $error)
{
$errors[] = (count($errors) + 1) . ") Error in file '$file': $error";
if(count($errors) > $params->max_errors_num)
break;
}
throw new Exception(implode("\n", $errors));
}
if($params->verbose)
config_log("Update cache: " . round(microtime(true) - $t,2) . " sec.");
return $result;
}
private function _checkFileMap(ConfigUpdateRequest $req, array &$added_files, array &$removed_files)
{
$fs_cache_map = $this->getFileMap();
if($req->mode === ConfigUpdateMode::Force)
{
$added_files = $req->files->getAllFiles();
//let's rebuild the file map
$fs_cache_map->init($added_files);
config_log("File map init: ".count($added_files));
}
else if($req->mode === ConfigUpdateMode::DetectChanged)
{
list($added_files, $removed_files) = $fs_cache_map->compare($req->files->getAllFiles());
config_log("File map compare, added: ".count($added_files).", removed: ".count($removed_files));
}
}
private function _updateFileMap(ConfigUpdateRequest $req, ConfigDirFiles $affected_files, array $added_files, array $removed_files)
{
$fs_cache_map = $this->getFileMap();
//TODO: traverse all affected files and update file map
foreach($affected_files as $file)
{
$cache_entry = $this->cache->getOrLoadByPath($file);
$fs_cache_map->updateDepsForEntry($cache_entry);
}
if($affected_files->count() > 0 || $added_files || $removed_files)
{
//in case of Force map was already cleared and initialized
if($req->mode != ConfigUpdateMode::Force)
$fs_cache_map->update($added_files, $removed_files);
$this->_saveFileMap();
}
}
private function _getAffectedFiles(ConfigUpdateRequest $req, array $removed_files) : ConfigDirFiles
{
$fs_cache_map = $this->getFileMap();
$affected_files = null;
if($req->mode === ConfigUpdateMode::Force)
{
$affected_files = $req->files;
}
else if($req->mode === ConfigUpdateMode::DetectChanged)
{
$affected_files = ConfigDirFiles::makeFor($this);
foreach($req->files->getMap() as $base_dir => $files)
{
foreach($files as $file)
{
if(need_to_regen($req->result_file, [$file]))
{
$affected_files->add($base_dir, $file);
$affected_by_file = $fs_cache_map->getAffectedFiles($file);
foreach($affected_by_file as $dep)
$affected_files->addFile($dep, unique: true);
}
}
}
//if there were removed files we need to rebuild affected files
foreach($removed_files as $file)
{
if(!str_ends_with($file, '.conf.js'))
{
$affected_by_file = $fs_cache_map->getAffectedFiles($file);
foreach($affected_by_file as $dep)
$affected_files->addFile($dep, unique: true);
}
else
{
//TODO: in case config file was removed do we actually need to rebuild all configs?
}
}
}
else if($req->mode === ConfigUpdateMode::Patch)
{
$affected_files = ConfigDirFiles::makeFor($this);
foreach($req->files as $file)
{
$affected_files->addFile($file, unique: true);
if(!str_ends_with($file, '.conf.js'))
{
$affected_by_file = $fs_cache_map->getAffectedFiles($file);
foreach($affected_by_file as $dep)
$affected_files->addFile($dep, unique: true);
}
}
}
return $affected_files;
}
function parseFile(string $file) : ConfigParseResult
{
return config_parse($this->globals->base_dirs, $file);
}
function scanFiles(string $extension = '.conf.js', bool $verbose = false) : ConfigDirFiles
{
return config_scan_files($this->globals->base_dirs, $extension, $verbose);
}
private function _getMapPath() : string
{
return $this->globals->build_dir . '/file_map.data';
}
private function _tryLoadMap() : ?ConfigCacheFileMap
{
if(!is_file($this->_getMapPath()))
return null;
config_log("Loading file map");
return ConfigCacheFileMap::unserialize(ensure_read($this->_getMapPath()));
}
private static function _makeMap(ConfigDirFiles $files) : ConfigCacheFileMap
{
config_log("Creating file map");
$map = new ConfigCacheFileMap();
$map->init($files->getAllFiles());
return $map;
}
private function _saveFileMap()
{
$data = ConfigCacheFileMap::serialize($this->getFileMap());
config_log("Saving file map: " . kb($data));
ensure_write($this->_getMapPath(), $data);
}
}
function config_log($msg)
{
echo "[CFG] $msg\n";
}
function config_get_cache_path(ConfigGlobals $globals, string $file) : string
{
return config_get_tmp_build_path($globals, $file . '.cache');
}
function config_get_cache_id_path(ConfigGlobals $globals, int $id) : string
{
return config_get_tmp_build_path($globals, $id . '.id');
}
function config_get_cache_strid_path(ConfigGlobals $globals, string $strid) : string
{
return config_get_tmp_build_path($globals, $strid . '.strid');
}
function config_get_cache_payload_path(ConfigGlobals $globals, string $file) : string
{
return config_get_tmp_build_path($globals, $file . '.payload');
}
function config_load(ConfigGlobals $globals, string $conf_path, ?string $conf_dir = null) : object
{
$conf_dir ??= config_map_base_dir($globals->base_dirs, $conf_path);
list($conf_id, $_) = config_ensure_header($conf_dir, $conf_path);
if(!$conf_id)
throw new Exception("Bad conf id: {$conf_id}");
$pres = config_parse(array($conf_dir), $conf_path);
if($pres->error !== 0)
throw new Exception("Error({$pres->error}) while loading JSON from {$conf_path}:\n" . $pres->error_descr);
return config_load_from_kv_array($globals, $conf_dir, $conf_path, $pres->parsed_arr, $conf_id);
}
function config_load_from_kv_array(
ConfigGlobals $globals, string $conf_dir, string $file,
array $arr, ?int $id = null) : object
{
if(!isset($arr['class']) || !isset($arr['class'][0]))
throw new Exception("Class is not set in file '$file'.");
$id ??= config_file2id($conf_dir, $file);
$cnf = null;
try
{
list($klass, $class_id, $norm_arr) = config_apply_class_normalization($arr);
if(!class_exists($klass))
throw new Exception("No such class '$klass'");
$cnf = new $klass;
if(!is_a($cnf, $globals->base_class))
throw new Exception("'$klass' is not subclass of '".ltrim($globals->base_class, '\\')."'");
$cnf->import($norm_arr);
}
catch(Exception $e)
{
throw new Exception($e->getMessage() . " in file '{$file}'"/* . $e->getTraceAsString()*/);
}
$cnf->id = $id;
$cnf->strid = config_file2strid($conf_dir, $file);
return $cnf;
}
function config_bench_load(ConfigGlobals $globals, string $file)
{
$base_dir = config_map_base_dir($globals->base_dirs, $file);
list($conf_id, $_) = config_ensure_header($base_dir, $file);
if(!$conf_id)
throw new Exception("Bad conf id: {$conf_id}");
$t = microtime(true);
$parse_res = config_parse($globals->base_dirs, $file);
if($parse_res->error !== 0)
throw new Exception("Error({$parse_res->error}) while loading JSON from {$file}:\n" . $parse_res->error_descr);
config_log("Parse: " . (microtime(true) - $t));
$t = microtime(true);
$config = config_load_from_kv_array($globals, $base_dir, $file, $parse_res->parsed_arr, $conf_id);
config_log("Load: " . (microtime(true) - $t));
}