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; public bool $check_junk = false; 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) $max_workers = $this->scanned->count() < 20 ? 1 : 5; return $max_workers; } function splitFilesByChunks(int $max_workers) : array { $chunk_size = (int)ceil($this->scanned->count()/$max_workers); return array_chunk($this->scanned->getFlatArray(), $chunk_size); } //returns [[idx, time, [[[base_dir, file1], [base_dir, file2], ..]]], ] function splitJobs() : array { $max_workers = $this->calcMaxWorkers(); $jobs = array(); foreach($this->splitFilesByChunks($max_workers) as $idx => $chunk) $jobs[] = array($idx, microtime(true), $chunk); return $jobs; } } class ConfigFetchResult { /** @var ConfigCacheEntry[] */ public array $all = array(); /** @var array */ public array $by_id = array(); /** @var array */ public array $by_path = array(); /** @var array */ public array $by_alias = array(); 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); if($params->verbose) echo "[CFG] Fetch from cache: " . round(microtime(true) - $t,2) . " sec.\n"; return $result; } function _config_cache_fetch(ConfigFetchParams $params) : ConfigFetchResult { if($params->scanned->isEmpty()) return new ConfigFetchResult(); //TODO: not sure if it's the best place for this one $GLOBALS['CONFIG_GLOBALS'] = $params->globals; $params->scanned->apply(function($base_dir, $files) { sort($files); return $files;}); $jobs = $params->splitJobs(); $serial = sizeof($jobs) == 1; $results_by_job = _config_worker_run_procs($params, $jobs, $serial); if(!$serial) { //NOTE: in case of any result error try serial processing if(array_search(false, $results_by_job, true/*strict*/) !== false) { if($params->verbose) echo "[CFG] Result error detected, trying serial processing...\n"; $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) echo "[CFG] Merge results: " . round(microtime(true) - $t,2) . " sec.\n"; if($params->verbose) echo "[CFG] Miss(Fast)/Total: {$result->stales}($result->fast_jsons)/" . sizeof($result->all) . "\n"; 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) { list($base_dir, $file, $cache_file, $is_stale, $parser_type) = $item; 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; } function _config_invalidate_cache( ConfigFetchParams $params, string $base_dir, string $file, string $cache_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_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; }