2025-02-24 19:22:01 +03:00
|
|
|
<?php
|
|
|
|
namespace taskman;
|
|
|
|
use Exception;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @property object $config
|
|
|
|
* @property string $payload
|
|
|
|
*/
|
|
|
|
class ConfigCacheEntry
|
|
|
|
{
|
|
|
|
const FMT_BINARY = 0;
|
|
|
|
const FMT_LZ4 = 1;
|
|
|
|
const FMT_FILE_REF = 2;
|
|
|
|
|
|
|
|
public string $class;
|
|
|
|
public int $class_id;
|
|
|
|
public int $id;
|
|
|
|
public string $strid;
|
|
|
|
public string $cache_file;
|
|
|
|
//NOTE: actual payload is stored in a separate file for faster incremental retrievals
|
|
|
|
public string $payload_file;
|
|
|
|
public string $file;
|
|
|
|
public array $includes = array();
|
|
|
|
public array $refs = array();
|
|
|
|
public ?ConfigCacheEntryExtras $extras;
|
|
|
|
|
|
|
|
public $_config;
|
|
|
|
public $_payload;
|
|
|
|
|
|
|
|
static function serialize(ConfigCacheEntry $ce) : string
|
|
|
|
{
|
|
|
|
$d = $ce->export();
|
|
|
|
return serialize($d);
|
|
|
|
}
|
|
|
|
|
|
|
|
static function unserialize(string $str) : ?ConfigCacheEntry
|
|
|
|
{
|
|
|
|
$d = @unserialize($str);
|
|
|
|
if(!is_array($d))
|
|
|
|
return null;
|
|
|
|
$ce = new ConfigCacheEntry();
|
|
|
|
$ce->import($d);
|
|
|
|
return $ce;
|
|
|
|
}
|
|
|
|
|
|
|
|
function __construct()
|
|
|
|
{
|
|
|
|
$this->extras = ConfigCacheEntryExtras::create();
|
|
|
|
}
|
|
|
|
|
|
|
|
function __get(string $name)
|
|
|
|
{
|
|
|
|
if($name === "config")
|
|
|
|
{
|
|
|
|
if($this->_config === null)
|
|
|
|
{
|
|
|
|
$klass = $this->class;
|
|
|
|
$data = config_msgpack_unpack($this->payload);
|
|
|
|
$this->_config = new $klass();
|
|
|
|
$this->_config->import($data);
|
|
|
|
}
|
|
|
|
return $this->_config;
|
|
|
|
}
|
|
|
|
else if($name === "payload")
|
|
|
|
{
|
|
|
|
if($this->_payload !== null)
|
|
|
|
return $this->_payload;
|
|
|
|
//if payload not set directly not storing it
|
|
|
|
return ensure_read($this->payload_file);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
throw new Exception("No such property '$name'");
|
|
|
|
}
|
|
|
|
|
|
|
|
function __set(string $name, $v)
|
|
|
|
{
|
|
|
|
if($name === "config")
|
|
|
|
$this->_config = $v;
|
|
|
|
else if($name === "payload")
|
|
|
|
$this->_payload = $v;
|
|
|
|
else
|
|
|
|
throw new Exception("No such property '$name'");
|
|
|
|
}
|
|
|
|
|
|
|
|
function export() : array
|
|
|
|
{
|
|
|
|
$d = array();
|
|
|
|
$d[] = $this->class;
|
|
|
|
$d[] = $this->class_id;
|
|
|
|
$d[] = $this->id;
|
|
|
|
$d[] = $this->strid;
|
|
|
|
$d[] = $this->cache_file;
|
|
|
|
$d[] = $this->payload_file;
|
|
|
|
$d[] = $this->file;
|
|
|
|
$d[] = $this->includes;
|
|
|
|
$d[] = $this->refs;
|
|
|
|
$d[] = $this->extras->export();
|
|
|
|
return $d;
|
|
|
|
}
|
|
|
|
|
|
|
|
function import(array $d)
|
|
|
|
{
|
|
|
|
$extras = array();
|
|
|
|
|
|
|
|
list(
|
|
|
|
$this->class,
|
|
|
|
$this->class_id,
|
|
|
|
$this->id,
|
|
|
|
$this->strid,
|
|
|
|
$this->cache_file,
|
|
|
|
$this->payload_file,
|
|
|
|
$this->file,
|
|
|
|
$this->includes,
|
|
|
|
$this->refs,
|
|
|
|
$extras,
|
|
|
|
) = $d;
|
|
|
|
|
|
|
|
$this->extras->import($extras);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ConfigCacheEntryExtras extends \stdClass
|
|
|
|
{
|
|
|
|
private static string $klass = ConfigCacheEntryExtras::class;
|
|
|
|
|
|
|
|
static function init(string $project_specific_klass)
|
|
|
|
{
|
|
|
|
self::$klass = $project_specific_klass;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function create() : object
|
|
|
|
{
|
|
|
|
return new self::$klass();
|
|
|
|
}
|
|
|
|
|
|
|
|
function export() : array
|
|
|
|
{
|
|
|
|
$as_array = get_object_vars($this);
|
|
|
|
return $as_array;
|
|
|
|
}
|
|
|
|
|
|
|
|
function import(array $as_array)
|
|
|
|
{
|
|
|
|
foreach($as_array as $field_name => $field_value)
|
|
|
|
$this->$field_name = $field_value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ConfigFetchParams
|
|
|
|
{
|
|
|
|
public ConfigGlobals $globals;
|
|
|
|
public ConfigScanResult $scanned;
|
|
|
|
public bool $force_stale = false;
|
|
|
|
public bool $verbose = false;
|
|
|
|
public ?int $max_workers = null;
|
|
|
|
public bool $touch_files_with_includes = true;
|
2025-02-27 20:26:42 +03:00
|
|
|
public bool $check_junk = true;
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
function __construct(ConfigGlobals $globals, ConfigScanResult $scanned,
|
|
|
|
bool $force_stale = false, bool $verbose = false, ?int $max_workers = null)
|
|
|
|
{
|
|
|
|
$this->globals = $globals;
|
|
|
|
$this->scanned = $scanned;
|
|
|
|
$this->force_stale = $force_stale;
|
|
|
|
$this->verbose = $verbose;
|
|
|
|
$this->max_workers = $max_workers;
|
|
|
|
}
|
|
|
|
|
|
|
|
function calcMaxWorkers() : int
|
|
|
|
{
|
|
|
|
$max_workers = $this->max_workers;
|
|
|
|
if($max_workers === null)
|
2025-02-27 01:35:16 +03:00
|
|
|
$max_workers = $this->scanned->count() < 100 ? 1 : 5;
|
2025-02-24 19:22:01 +03:00
|
|
|
return $max_workers;
|
|
|
|
}
|
|
|
|
|
2025-02-27 01:35:16 +03:00
|
|
|
function splitFilesByChunks(int $max_workers, bool $sort = true) : array
|
2025-02-24 19:22:01 +03:00
|
|
|
{
|
2025-02-27 01:35:16 +03:00
|
|
|
$flat = $this->scanned->getFlatArray();
|
|
|
|
if($sort)
|
|
|
|
usort($flat, fn($a, $b) => $a[1] <=> $b[1]);
|
|
|
|
|
|
|
|
$chunk_size = (int)ceil(count($flat)/$max_workers);
|
|
|
|
return array_chunk($flat, $chunk_size);
|
2025-02-24 19:22:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
//returns [[idx, time, [[[base_dir, file1], [base_dir, file2], ..]]], ]
|
2025-02-27 01:35:16 +03:00
|
|
|
function splitJobs(bool $sort = true) : array
|
2025-02-24 19:22:01 +03:00
|
|
|
{
|
|
|
|
$max_workers = $this->calcMaxWorkers();
|
|
|
|
$jobs = array();
|
2025-02-27 01:35:16 +03:00
|
|
|
$chunks = $this->splitFilesByChunks($max_workers, $sort);
|
|
|
|
foreach($chunks as $idx => $chunk)
|
2025-02-24 19:22:01 +03:00
|
|
|
$jobs[] = array($idx, microtime(true), $chunk);
|
|
|
|
return $jobs;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ConfigFetchResult
|
|
|
|
{
|
|
|
|
/** @var ConfigCacheEntry[] */
|
|
|
|
public array $all = array();
|
|
|
|
/** @var array<int, ConfigCacheEntry> */
|
|
|
|
public array $by_id = array();
|
|
|
|
/** @var array<string, ConfigCacheEntry> */
|
|
|
|
public array $by_path = array();
|
|
|
|
/** @var array<string, ConfigCacheEntry> */
|
|
|
|
public array $by_alias = array();
|
2025-02-25 18:05:12 +03:00
|
|
|
/** @var array<string, string> */
|
|
|
|
public array $errors = array();
|
2025-02-24 19:22:01 +03:00
|
|
|
public int $stales = 0;
|
|
|
|
public int $corruptions = 0;
|
|
|
|
public int $fast_jsons = 0;
|
|
|
|
|
|
|
|
function getByAlias(string $strid) : ConfigCacheEntry
|
|
|
|
{
|
|
|
|
if(array_key_exists($strid, $this->by_alias))
|
|
|
|
return $this->by_alias[$strid];
|
|
|
|
throw new Exception("Failed to find config by alias '$strid'!");
|
|
|
|
}
|
|
|
|
|
|
|
|
function getById(int $id) : ConfigCacheEntry
|
|
|
|
{
|
|
|
|
if(array_key_exists($id, $this->by_id))
|
|
|
|
return $this->by_id[$id];
|
|
|
|
throw new Exception("Failed to find config by id '$id'!");
|
|
|
|
}
|
|
|
|
|
|
|
|
function getByPath(string $path) : ConfigCacheEntry
|
|
|
|
{
|
|
|
|
if(array_key_exists($path, $this->by_path))
|
|
|
|
return $this->by_path[$path];
|
|
|
|
throw new Exception("Failed to find config by path '$path'!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function config_cache_fetch(ConfigFetchParams $params) : ConfigFetchResult
|
|
|
|
{
|
|
|
|
$t = microtime(true);
|
|
|
|
|
|
|
|
$result = _config_cache_fetch($params);
|
|
|
|
|
2025-02-25 18:05:12 +03:00
|
|
|
if($result->errors)
|
|
|
|
{
|
|
|
|
$errors = array();
|
|
|
|
foreach($result->errors as $file => $error)
|
|
|
|
{
|
|
|
|
$errors[] = (count($errors) + 1) . ") Error in file '$file': $error";
|
|
|
|
if(count($errors) > 15)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Exception(implode("\n", $errors));
|
|
|
|
}
|
|
|
|
|
2025-02-24 19:22:01 +03:00
|
|
|
if($params->verbose)
|
2025-02-25 15:48:28 +03:00
|
|
|
config_log("Fetch from cache: " . round(microtime(true) - $t,2) . " sec.");
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _config_cache_fetch(ConfigFetchParams $params) : ConfigFetchResult
|
|
|
|
{
|
|
|
|
if($params->scanned->isEmpty())
|
|
|
|
return new ConfigFetchResult();
|
|
|
|
|
2025-02-27 01:35:16 +03:00
|
|
|
$jobs = $params->splitJobs(sort: true);
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
$serial = sizeof($jobs) == 1;
|
|
|
|
|
|
|
|
$results_by_job = _config_worker_run_procs($params, $jobs, $serial);
|
|
|
|
|
|
|
|
if(!$serial)
|
|
|
|
{
|
2025-02-27 15:53:16 +03:00
|
|
|
//NOTE: in case result unserialize error try serial processing
|
2025-02-24 19:22:01 +03:00
|
|
|
if(array_search(false, $results_by_job, true/*strict*/) !== false)
|
|
|
|
{
|
|
|
|
if($params->verbose)
|
2025-02-27 15:53:16 +03:00
|
|
|
config_log("Corrupted result, trying serial processing...");
|
2025-02-24 19:22:01 +03:00
|
|
|
$results_by_job = _config_worker_run_procs($params, $jobs, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$t = microtime(true);
|
|
|
|
$result = _config_merge_fetch_results($params, $results_by_job);
|
|
|
|
if($params->verbose)
|
2025-02-25 15:48:28 +03:00
|
|
|
config_log("Merge results: " . round(microtime(true) - $t,2) . " sec.");
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
if($params->verbose)
|
2025-02-25 15:48:28 +03:00
|
|
|
config_log("Miss(Fast)/Total: {$result->stales}($result->fast_jsons)/" . sizeof($result->all));
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function config_cache_fetch_by_path(ConfigGlobals $globals, string $path, bool $force_stale = false) : ConfigCacheEntry
|
|
|
|
{
|
|
|
|
$path = realpath($path);
|
|
|
|
$base_dir = config_map_base_dir($globals->base_dirs, $path);
|
|
|
|
|
|
|
|
$scanned = new ConfigScanResult();
|
|
|
|
$scanned->add($base_dir, $path);
|
|
|
|
|
|
|
|
$ces = config_cache_fetch(new ConfigFetchParams(
|
|
|
|
globals: $globals,
|
|
|
|
scanned: $scanned,
|
|
|
|
force_stale: $force_stale
|
|
|
|
));
|
|
|
|
|
|
|
|
if(empty($ces->all))
|
|
|
|
throw new Exception("Config not found at path '$path'");
|
|
|
|
return $ces->all[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
function _config_merge_fetch_results(ConfigFetchParams $params, array $results_by_job) : ConfigFetchResult
|
|
|
|
{
|
|
|
|
$result = new ConfigFetchResult();
|
|
|
|
$total_stales = 0;
|
|
|
|
$total_fast_jsons = 0;
|
|
|
|
|
|
|
|
foreach($results_by_job as $results)
|
|
|
|
{
|
|
|
|
foreach($results as $item)
|
|
|
|
{
|
2025-02-25 18:05:12 +03:00
|
|
|
list($base_dir, $file, $cache_file, $is_stale, $parser_type, $error) = $item;
|
|
|
|
|
|
|
|
if($error !== null)
|
|
|
|
{
|
|
|
|
$result->errors[$file] = $error;
|
|
|
|
continue;
|
|
|
|
}
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
if($parser_type === 1)
|
|
|
|
++$total_fast_jsons;
|
|
|
|
|
|
|
|
$cache_entry = ConfigCacheEntry::unserialize(ensure_read($cache_file));
|
|
|
|
|
|
|
|
//NOTE: handling any cache corruption errors
|
|
|
|
if($cache_entry === null)
|
|
|
|
{
|
|
|
|
$is_stale = true;
|
|
|
|
$cache_entry = _config_invalidate_cache($params, $base_dir, $file, $cache_file);
|
|
|
|
++$result->corruptions;
|
|
|
|
}
|
|
|
|
|
|
|
|
$includes = $cache_entry->includes;
|
|
|
|
|
|
|
|
if(!$is_stale && count($includes) > 0 && need_to_regen($file, $includes))
|
|
|
|
{
|
|
|
|
$is_stale = true;
|
|
|
|
$cache_entry = _config_invalidate_cache($params, $base_dir, $file, $cache_file);
|
|
|
|
//NOTE: let's change the mtime of the file which include other files,
|
|
|
|
// so that on tne next build it will be 'older' than its includes
|
|
|
|
// and won't trigger rebuild
|
|
|
|
if($params->touch_files_with_includes)
|
|
|
|
touch($file);
|
|
|
|
}
|
|
|
|
|
|
|
|
if($is_stale)
|
|
|
|
++$total_stales;
|
|
|
|
|
|
|
|
//we want results to be returned in the same order, so
|
|
|
|
//we store entries by the file key and later retrieve array values
|
|
|
|
$result->by_path[$file] = $cache_entry;
|
|
|
|
$result->by_id[$cache_entry->id] = $cache_entry;
|
|
|
|
$result->by_alias[$cache_entry->strid] = $cache_entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$result->all = array_values($result->by_path);
|
|
|
|
$result->stales = $total_stales;
|
|
|
|
$result->fast_jsons = $total_fast_jsons;
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2025-02-25 15:41:22 +03:00
|
|
|
function _config_invalidate_cache(
|
|
|
|
ConfigFetchParams $params, string $base_dir, string $file,
|
|
|
|
string $cache_file, ?int &$parser_type = null) : ConfigCacheEntry
|
2025-02-24 19:22:01 +03:00
|
|
|
{
|
|
|
|
list($conf_id, $conf_strid) = config_ensure_header($base_dir, $file);
|
|
|
|
if(!$conf_id)
|
|
|
|
throw new Exception("Bad conf id: {$conf_id}");
|
|
|
|
|
|
|
|
//TODO: this is a bit ugly but kinda works for now
|
|
|
|
$GLOBALS['CONFIG_CURRENT_FILE'] = $file;
|
|
|
|
$GLOBALS['CONFIG_CURRENT_PROTO_ID'] = $conf_id;
|
|
|
|
$GLOBALS['CONFIG_EXTRAS'] = ConfigCacheEntryExtras::create();
|
|
|
|
|
|
|
|
$pres = config_parse($params->globals->base_dirs, $file);
|
|
|
|
if($pres->error !== 0)
|
2025-02-25 15:41:22 +03:00
|
|
|
throw new Exception("Parse error({$pres->error}):\n" . $pres->error_descr);
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
$parser_type = $pres->parser_type;
|
|
|
|
$parsed_arr = $pres->parsed_arr;
|
|
|
|
//TODO: these hardcoded keys below probably shouldn't be part of data?
|
|
|
|
$parsed_arr['id'] = $conf_id;
|
|
|
|
$parsed_arr['strid'] = $conf_strid;
|
2025-02-25 15:41:22 +03:00
|
|
|
list($class, $class_id, $normalized_data)
|
|
|
|
= config_apply_class_normalization($parsed_arr, $params->check_junk);
|
2025-02-24 19:22:01 +03:00
|
|
|
|
|
|
|
$cache_payload_file = config_get_cache_payload_path($params->globals, $file);
|
|
|
|
|
|
|
|
$cache_entry = new ConfigCacheEntry();
|
|
|
|
$cache_entry->id = $conf_id;
|
|
|
|
$cache_entry->strid = $conf_strid;
|
|
|
|
$cache_entry->cache_file = $cache_file;
|
|
|
|
$cache_entry->class = $class;
|
|
|
|
$cache_entry->class_id = $class_id;
|
|
|
|
$cache_entry->payload_file = $cache_payload_file;
|
|
|
|
//TODO: pass flag if file is actually normalized?
|
|
|
|
$cache_entry->file = normalize_path($file);
|
|
|
|
$cache_entry->includes = config_get_module_includes($pres->jsm_module);
|
|
|
|
$cache_entry->extras = $GLOBALS['CONFIG_EXTRAS'];
|
|
|
|
//TODO: do we really need these refs?
|
|
|
|
$cache_entry->refs = config_content_match_refs($pres->normalized_jzon);
|
|
|
|
|
|
|
|
ensure_write($cache_file, ConfigCacheEntry::serialize($cache_entry));
|
|
|
|
ensure_write($cache_payload_file, config_msgpack_pack($normalized_data));
|
|
|
|
|
|
|
|
return $cache_entry;
|
|
|
|
}
|
|
|
|
|