*/ private array $by_path = []; /** @var array */ private array $by_id = []; /** @var array */ private array $by_alias = []; function __construct(ConfigGlobals $globals) { $this->globals = $globals; } function clear() { $this->by_path = []; $this->by_id = []; $this->by_alias = []; } function tryGetOrLoadByPath(string $path) : ?ConfigCacheEntry { if(!array_key_exists($path, $this->by_path)) { $cache_file = config_get_cache_path($this->globals, $path); if(!is_file($cache_file)) return null; $ce = ConfigCacheEntry::unserialize(ensure_read($cache_file)); $this->by_path[$path] = $ce; return $ce; } return $this->by_path[$path]; } function getOrLoadByPath(string $path) : ConfigCacheEntry { $ce = $this->tryGetOrLoadByPath($path); if($ce === null) throw new Exception("Failed to find entry by path '$path'"); return $ce; } function tryGetOrLoadById(int $id) : ?ConfigCacheEntry { if(!array_key_exists($id, $this->by_id)) { $cache_id_file = config_get_cache_id_path($this->globals, $id); if(!is_file($cache_id_file)) return null; $file = ensure_read($cache_id_file); $ce = $this->tryGetOrLoadByPath($file); $this->by_id[$id] = $ce; return $ce; } return $this->by_id[$id]; } function getOrLoadById(int $id) : ConfigCacheEntry { $ce = $this->tryGetOrLoadById($id); if($ce === null) throw new Exception("Failed to find entry by id '$id'"); return $ce; } function tryGetOrLoadByStrid(string $strid) : ?ConfigCacheEntry { if(!array_key_exists($strid, $this->by_alias)) { $cache_strid_file = config_get_cache_strid_path($this->globals, $strid); if(!is_file($cache_strid_file)) return null; $file = ensure_read($cache_strid_file); $ce = $this->tryGetOrLoadByPath($file); $this->by_alias[$strid] = $ce; return $ce; } return $this->by_alias[$strid]; } function getOrLoadByStrid(string $strid) : ConfigCacheEntry { $ce = $this->tryGetOrLoadByStrid($strid); if($ce === null) throw new Exception("Failed to find entry by strid '$strid'"); return $ce; } } /** * @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 ConfigCacheFileMap { private array $file2deps = array(); static function serialize(ConfigCacheFileMap $map) : string { $d = $map->export(); return serialize($d); } static function unserialize(string $str) : ?ConfigCacheFileMap { $d = @unserialize($str); if(!is_array($d)) return null; $map = new ConfigCacheFileMap(); $map->import($d); return $map; } function count() : int { return count($this->file2deps); } function exists(string $file) : bool { return isset($this->file2deps[$file]); } function getAffectedFiles(string $file) : array { if(!isset($this->file2deps[$file])) return []; return array_keys($this->file2deps[$file]); } function differs(ConfigDirFiles|\taskman\artefact\TaskmanDirFiles|array $files) : bool { if(!is_array($files)) $files = $files->getAllFiles(); $this_files = array_keys($this->file2deps); return count($files) !== count($this_files) || count(array_intersect($files, $this_files)) != count($this_files); } function compare(array $files) : array { $this_files = array_keys($this->file2deps); $added = array_diff($files, $this_files); $removed = array_diff($this_files, $files); return [$added, $removed]; } function init(array $files) { $this->file2deps = []; foreach($files as $file) $this->file2deps[$file] = []; } function update(array $added, array $removed) { foreach($added as $file) { //NOTE: we preserve existing files if(!isset($this->file2deps[$file])) $this->file2deps[$file] = []; } foreach($removed as $file) unset($this->file2deps[$file]); } function updateDepsForEntry(ConfigCacheEntry $entry) { foreach($entry->includes as $include) { if(isset($this->file2deps[$include])) $this->file2deps[$include][$entry->file] = true; else $this->file2deps[$include] = [$entry->file => true]; } } function export() : array { $d = array(); $d[] = $this->file2deps; return $d; } function import(array $d) { list($this->file2deps) = $d; } } class ConfigCacheUpdateResult { public ConfigDirFiles $affected_files; public array $affected_entries = array(); /** @var array */ public array $errors = array(); public int $corruptions = 0; public int $fast_jsons = 0; function __construct(ConfigCacheUpdateParams $params) { $this->affected_files = $params->affected_files; } } class ConfigCacheUpdateParams { const MAX_DEFAULT_WORKERS = 5; const FILES_THRESHOLD_ONE_WORKER = 100; public ConfigGlobals $globals; public ConfigDirFiles $affected_files; public bool $verbose = false; public bool $check_junk = true; public int $max_errors_num = 15; public bool $return_affected_entries = false; function __construct(ConfigGlobals $globals, ConfigDirFiles $affected_files, bool $verbose = false) { $this->globals = $globals; $this->affected_files = $affected_files; $this->verbose = $verbose; } function calcMaxWorkers() : int { if($this->affected_files->count() < self::FILES_THRESHOLD_ONE_WORKER) return 1; return $this->globals->max_workers ?? self::MAX_DEFAULT_WORKERS; } function splitFilesByChunks(int $max_workers, bool $sort = true) : array { $flat = $this->affected_files->getFlatArray(); if(!$flat) return array(); 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); } //returns [[idx, time, [[[base_dir, file1], [base_dir, file2], ..]]], ] function splitJobs(bool $sort = true) : array { $max_workers = $this->calcMaxWorkers(); $jobs = array(); $chunks = $this->splitFilesByChunks($max_workers, $sort); foreach($chunks as $idx => $chunk) $jobs[] = array($idx, microtime(true), $chunk); return $jobs; } } function _config_cache_update(ConfigCacheUpdateParams $params) : ConfigCacheUpdateResult { $jobs = $params->splitJobs(sort: true); $serial = sizeof($jobs) == 1; $results_by_job = _config_update_worker_run_procs($params, $jobs, $serial); if(!$serial) { //NOTE: in case result unserialize error try serial processing if(array_search(false, $results_by_job, true/*strict*/) !== false) { if($params->verbose) config_log("Corrupted result, trying serial processing..."); $results_by_job = _config_update_worker_run_procs(params: $params, jobs: $jobs, serial: true); } } $result = _config_merge_update_results($params, $results_by_job); if($params->verbose) config_log("Miss(Fast): {$result->affected_files->count()}({$result->fast_jsons})"); return $result; } function _config_merge_update_results(ConfigCacheUpdateParams $params, array $results_by_job) : ConfigCacheUpdateResult { $result = new ConfigCacheUpdateResult($params); $total_fast_jsons = 0; foreach($results_by_job as $results) { foreach($results as $item) { $base_dir = $item['base_dir']; $file = $item['file']; $cache_file = $item['cache_file']; $parser_type = $item['parser_type']; $error = $item['error']; if($error !== null) { $result->errors[$file] = $error; continue; } if($parser_type === 1) ++$total_fast_jsons; } } $result->fast_jsons = $total_fast_jsons; return $result; } function _config_update_cache_entry(ConfigCacheUpdateParams $params, $base_dir, string $file, ?int &$parser_type = null) : ConfigCacheEntry { 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) throw new Exception("Parse error({$pres->error}):\n" . $pres->error_descr); $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; list($class, $class_id, $normalized_data) = config_apply_class_normalization($parsed_arr, $params->check_junk); $cache_payload_file = config_get_cache_payload_path($params->globals, $file); $cache_file = config_get_cache_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(cm: $pres->jsm_module); //TODO: do we really need these refs? $cache_entry->refs = config_content_match_refs($pres->normalized_jzon); $cache_entry->extras = $GLOBALS['CONFIG_EXTRAS']; ensure_write($cache_file, ConfigCacheEntry::serialize($cache_entry)); ensure_write($cache_payload_file, config_msgpack_pack($normalized_data)); ensure_write(config_get_cache_id_path($params->globals, $conf_id), $cache_entry->file); ensure_write(config_get_cache_strid_path($params->globals, $conf_strid), $cache_entry->file); return $cache_entry; } function _config_update_worker_run_procs(ConfigCacheUpdateParams $params, array $jobs, bool $serial) : array { if($serial) { $results_by_job = array(); foreach($jobs as $job) $results_by_job[] = _config_update_worker_func($params, $job, $serial); return $results_by_job; } else { //initializing worker for master process anyway $params->globals->initWorker(true); $workers_args = array(); foreach($jobs as $job) $workers_args[] = array($params, $job); return run_background_gamectl_workers('config_update_worker', $workers_args); } } function _config_update_worker_func(ConfigCacheUpdateParams $params, array $job, bool $is_master_proc = false) : array { $start_time = microtime(true); $params->globals->initWorker($is_master_proc); list($idx, $start_time, $chunk) = $job; if($params->verbose) config_log("Worker $idx (" . sizeof($chunk) . ") started (".round(microtime(true)-$start_time, 2)." sec)"); $fast_parser_num = 0; $results = array(); foreach($chunk as $file_idx => $chunk_data) { list($base_dir, $file) = $chunk_data; try { if($params->verbose && $file_idx > 0 && ($file_idx % 500) == 0) config_log("Worker $idx progress: " . round($file_idx / sizeof($chunk) * 100) . "% ..."); $cache_file = config_get_cache_path($params->globals, $file); $parser_type = null; $entry = _config_update_cache_entry($params, $base_dir, $file, $parser_type); $results[] = [ 'base_dir' => $base_dir, 'file' => $file, 'cache_file' => $entry->cache_file, 'parser_type' => $parser_type, 'error' => null ]; } catch(\Throwable $e) { $results[] = [ 'base_dir' => $base_dir, 'file' => $file, 'cache_file' => null, 'parser_type' => null, 'error' => $e->getMessage() ]; } } if($params->verbose) config_log("Worker $idx done (".round(microtime(true)-$start_time, 2)." sec)"); return $results; }