From f0044a21ea7bbabb633057804e83df884196012b Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 31 Aug 2022 09:25:40 +0200 Subject: [proxy] make sure that dependency tree recomputation by default activates the same resources that were marked as required before --- src/hydrilla/item_infos.py | 2 +- src/hydrilla/proxy/simple_dependency_satisfying.py | 218 ++++++++++++--------- .../_operations/recompute_dependencies.py | 74 +++++-- 3 files changed, 188 insertions(+), 106 deletions(-) diff --git a/src/hydrilla/item_infos.py b/src/hydrilla/item_infos.py index 525c6e3..3a25f6c 100644 --- a/src/hydrilla/item_infos.py +++ b/src/hydrilla/item_infos.py @@ -612,7 +612,7 @@ class MultirepoItemInfo( """ assert not self.is_empty() - return self.get_all(reverse_versions=True)[-1] + return self.get_all(reverse_repos=True)[-1] def options(self, reverse: bool = False) -> t.Sequence[tuple[str, int]]: return sorted( diff --git a/src/hydrilla/proxy/simple_dependency_satisfying.py b/src/hydrilla/proxy/simple_dependency_satisfying.py index 78a1197..a5431f9 100644 --- a/src/hydrilla/proxy/simple_dependency_satisfying.py +++ b/src/hydrilla/proxy/simple_dependency_satisfying.py @@ -37,6 +37,9 @@ from __future__ import annotations import dataclasses as dc import typing as t +import functools as ft + +from immutables import Map from ..exceptions import HaketiloException from .. import item_infos @@ -73,8 +76,19 @@ class MappingVersionRequirement(MappingRequirement): return info == self.version_info +@dc.dataclass(frozen=True) +class ResourceVersionRequirement: + mapping_identifier: str + version_info: item_infos.ResourceInfo + + def is_fulfilled_by(self, info: item_infos.ResourceInfo) -> bool: + return info == self.version_info + + @dc.dataclass class ComputedPayload: + mapping_identifier: str + resources: list[item_infos.ResourceInfo] = dc.field(default_factory=list) allows_eval: bool = False @@ -109,38 +123,76 @@ def _mark_mappings( ComputedChoices = dict[str, MappingChoice] +def _compute_inter_mapping_deps(choices: ComputedChoices) \ + -> dict[str, frozenset[str]]: + mapping_deps: dict[str, frozenset[str]] = {} + + for mapping_choice in choices.values(): + specs_to_resolve = [*mapping_choice.info.required_mappings] + + for computed_payload in mapping_choice.payloads.values(): + for resource_info in computed_payload.resources: + specs_to_resolve.extend(resource_info.required_mappings) + + depended = frozenset(spec.identifier for spec in specs_to_resolve) + mapping_deps[mapping_choice.info.identifier] = depended + + return mapping_deps + @dc.dataclass(frozen=True) class _ComputationData: - resources: t.Mapping[str, item_infos.ResourceInfo] - mappings: t.Mapping[str, item_infos.MappingInfo] - required: frozenset[str] + resources_map: item_infos.MultirepoResourceInfoMap + mappings_map: item_infos.MultirepoMappingInfoMap + + mappings_to_reqs: t.Mapping[str, t.Sequence[MappingRequirement]] + + mappings_resources_to_reqs: t.Mapping[ + tuple[str, str], + t.Sequence[ResourceVersionRequirement] + ] def _satisfy_payload_resource_rec( self, resource_identifier: str, processed_resources: set[str], - computed_payload: ComputedPayload + computed_payload: ComputedPayload ) -> t.Optional[ComputedPayload]: if resource_identifier in processed_resources: # We forbid circular dependencies. return None - resource_info = self.resources.get(resource_identifier) - if resource_info is None: + multirepo_info = self.resources_map.get(resource_identifier) + if multirepo_info is None: return None - if resource_info in computed_payload.resources: + key = (computed_payload.mapping_identifier, resource_identifier) + resource_reqs = self.mappings_resources_to_reqs.get(key) + + if resource_reqs is None: + info = multirepo_info.default_info + else: + found = False + # From newest to oldest version. + for info in multirepo_info.get_all(reverse_versions=True): + if all(req.is_fulfilled_by(info) for req in resource_reqs): + found = True + break + + if not found: + return None + + if info in computed_payload.resources: return computed_payload processed_resources.add(resource_identifier) - if resource_info.allows_eval: + if info.allows_eval: computed_payload.allows_eval = True - if resource_info.allows_cors_bypass: + if info.allows_cors_bypass: computed_payload.allows_cors_bypass = True - for dependency_spec in resource_info.dependencies: + for dependency_spec in info.dependencies: if self._satisfy_payload_resource_rec( dependency_spec.identifier, processed_resources, @@ -150,67 +202,68 @@ class _ComputationData: processed_resources.remove(resource_identifier) - computed_payload.resources.append(resource_info) + computed_payload.resources.append(info) return computed_payload - def _satisfy_payload_resource(self, resource_identifier: str) \ - -> t.Optional[ComputedPayload]: + def _satisfy_payload_resource( + self, + mapping_identifier: str, + resource_identifier: str + ) -> t.Optional[ComputedPayload]: return self._satisfy_payload_resource_rec( resource_identifier, set(), - ComputedPayload() + ComputedPayload(mapping_identifier) ) - def _compute_payloads_no_mapping_requirements(self) -> ComputedChoices: - computed_result: ComputedChoices = ComputedChoices() + def _compute_best_choices(self) -> ComputedChoices: + choices = ComputedChoices() - for mapping_info in self.mappings.values(): - mapping_choice = MappingChoice(mapping_info) + for multirepo_info in self.mappings_map.values(): + choice: t.Optional[MappingChoice] = None + + reqs = self.mappings_to_reqs.get(multirepo_info.identifier) + if reqs is None: + choice = MappingChoice(multirepo_info.default_info) + else: + # From newest to oldest version. + for info in multirepo_info.get_all(reverse_versions=True): + if all(req.is_fulfilled_by(info) for req in reqs): + choice = MappingChoice(info=info, required=True) + break + + if choice is None: + continue failure = False - for pattern, resource_spec in mapping_info.payloads.items(): + for pattern, resource_spec in choice.info.payloads.items(): computed_payload = self._satisfy_payload_resource( - resource_spec.identifier + mapping_identifier = choice.info.identifier, + resource_identifier = resource_spec.identifier ) if computed_payload is None: failure = True break - if mapping_info.allows_eval: + if choice.info.allows_eval: computed_payload.allows_eval = True - if mapping_info.allows_cors_bypass: + if choice.info.allows_cors_bypass: computed_payload.allows_cors_bypass = True - mapping_choice.payloads[pattern] = computed_payload + choice.payloads[pattern] = computed_payload if not failure: - computed_result[mapping_info.identifier] = mapping_choice - - return computed_result - - def _compute_inter_mapping_deps(self, choices: ComputedChoices) \ - -> dict[str, frozenset[str]]: - mapping_deps: dict[str, frozenset[str]] = {} - - for mapping_choice in choices.values(): - specs_to_resolve = [*mapping_choice.info.required_mappings] + choices[choice.info.identifier] = choice - for computed_payload in mapping_choice.payloads.values(): - for resource_info in computed_payload.resources: - specs_to_resolve.extend(resource_info.required_mappings) - - depended = frozenset(spec.identifier for spec in specs_to_resolve) - mapping_deps[mapping_choice.info.identifier] = depended - - return mapping_deps + return choices def compute_payloads(self) -> ComputedChoices: - choices = self._compute_payloads_no_mapping_requirements() + choices = self._compute_best_choices() - mapping_deps = self._compute_inter_mapping_deps(choices) + mapping_deps = _compute_inter_mapping_deps(choices) reverse_deps: dict[str, set[str]] = {} @@ -221,12 +274,12 @@ class _ComputationData: bad_mappings: set[str] = set() for depended_identifier in reverse_deps.keys(): - if self.mappings.get(depended_identifier) not in choices: + if depended_identifier not in choices: _mark_mappings(depended_identifier, reverse_deps, bad_mappings) bad_required_mappings: list[str] = [] - for identifier in self.required: + for identifier in self.mappings_to_reqs.keys(): if identifier in bad_mappings or identifier not in choices: bad_required_mappings.append(identifier) @@ -234,12 +287,11 @@ class _ComputationData: raise ImpossibleSituation(frozenset(bad_required_mappings)) for identifier in bad_mappings: - if identifier in self.mappings: - choices.pop(identifier, None) + choices.pop(identifier, None) required_mappings: set[str] = set() - for identifier in self.required: + for identifier in self.mappings_to_reqs.keys(): _mark_mappings(identifier, mapping_deps, required_mappings) for identifier in required_mappings: @@ -247,45 +299,35 @@ class _ComputationData: return choices - -AnyInfoVar = t.TypeVar( - 'AnyInfoVar', - item_infos.ResourceInfo, - item_infos.MappingInfo -) - -def _choose_newest(infos: t.Iterable[AnyInfoVar]) -> dict[str, AnyInfoVar]: - best_versions: dict[str, AnyInfoVar] = {} - - for info in infos: - other = best_versions.setdefault(info.identifier, info) - - if (other.version, other.repo, info.repo_iteration) < \ - (info.version, info.repo, other.repo_iteration): - best_versions[info.identifier] = info - - return best_versions - def compute_payloads( - resources: t.Iterable[item_infos.ResourceInfo], - mappings: t.Iterable[item_infos.MappingInfo], - requirements: t.Iterable[MappingRequirement] + resources: t.Iterable[item_infos.ResourceInfo], + mappings: t.Iterable[item_infos.MappingInfo], + mapping_requirements: t.Iterable[MappingRequirement], + resource_requirements: t.Iterable[ResourceVersionRequirement] ) -> ComputedChoices: - reqs_by_identifier = dict((req.identifier, req) for req in requirements) - - filtered_mappings = [] - - for mapping_info in mappings: - req = reqs_by_identifier.get(mapping_info.identifier) - if req is not None and not req.is_fulfilled_by(mapping_info): - continue - - filtered_mappings.append(mapping_info) - - best_resources = _choose_newest(resources) - best_mappings = _choose_newest(filtered_mappings) - - required = frozenset(reqs_by_identifier.keys()) - - return _ComputationData(best_resources, best_mappings, required)\ - .compute_payloads() + resources_map: item_infos.MultirepoResourceInfoMap = \ + ft.reduce(item_infos.register_in_multirepo_map, resources, Map()) + mappings_map: item_infos.MultirepoMappingInfoMap = \ + ft.reduce(item_infos.register_in_multirepo_map, mappings, Map()) + + mappings_to_reqs: dict[str, list[MappingRequirement]] = {} + for mapping_req in mapping_requirements: + mappings_to_reqs.setdefault(mapping_req.identifier, [])\ + .append(mapping_req) + + mappings_resources_to_reqs: dict[ + tuple[str, str], + list[ResourceVersionRequirement] + ] = {} + for resource_req in resource_requirements: + info = resource_req.version_info + key = (resource_req.mapping_identifier, info.identifier) + mappings_resources_to_reqs.setdefault(key, [])\ + .append(resource_req) + + return _ComputationData( + mappings_map = mappings_map, + resources_map = resources_map, + mappings_to_reqs = mappings_to_reqs, + mappings_resources_to_reqs = mappings_resources_to_reqs + ).compute_payloads() diff --git a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py index 2ec3600..5403ec3 100644 --- a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py +++ b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py @@ -78,7 +78,7 @@ def _get_infos_of_type(cursor: sqlite3.Cursor, info_type: t.Type[AnyInfoVar],) \ def _get_current_required_state( cursor: sqlite3.Cursor, unlocked_required_mappings: t.Sequence[int] -) -> list[sds.MappingRequirement]: +) -> tuple[list[sds.MappingRequirement], list[sds.ResourceVersionRequirement]]: # For mappings explicitly enabled by the user (+ all mappings they # recursively depend on) let's make sure that their exact same versions will # be enabled after the change. Make exception for mappings specified by the @@ -88,6 +88,7 @@ def _get_current_required_state( ids = unlocked_required_mappings, table_name = '__unlocked_ids' ): + # Describe all required mappings using requirement objects. cursor.execute( ''' SELECT @@ -101,14 +102,52 @@ def _get_current_required_state( rows = cursor.fetchall() - requirements: list[sds.MappingRequirement] = [] + mapping_requirements: list[sds.MappingRequirement] = [] - for definition, repo, iteration in rows: - info = item_infos.MappingInfo.load(definition, repo, iteration) - req = sds.MappingVersionRequirement(info.identifier, info) - requirements.append(req) + for definition, repo, iteration in rows: + mapping_info = \ + item_infos.MappingInfo.load(definition, repo, iteration) + mapping_req = sds.MappingVersionRequirement( + identifier = mapping_info.identifier, + version_info = mapping_info + ) + mapping_requirements.append(mapping_req) + + # Describe all required resources using requirement objects. + cursor.execute( + ''' + SELECT + i_m.identifier, + ive_r.definition, ive_r.repo, ive_r.repo_iteration + FROM + resolved_depended_resources AS rdd + JOIN item_versions_extra AS ive_r + ON rdd.resource_item_id = ive_r.item_version_id + JOIN payloads AS p + USING (payload_id) + JOIN item_versions AS iv_m + ON p.mapping_item_id = iv_m.item_version_id + JOIN items AS i_m + ON iv_m.item_id = i_m.item_id + WHERE + i_m.item_id NOT IN __unlocked_ids AND iv_m.active = 'R'; + ''', + ) + + rows = cursor.fetchall() + + resource_requirements: list[sds.ResourceVersionRequirement] = [] - return requirements + for mapping_identifier, definition, repo, iteration in rows: + resource_info = \ + item_infos.ResourceInfo.load(definition, repo, iteration) + resource_req = sds.ResourceVersionRequirement( + mapping_identifier = mapping_identifier, + version_info = resource_info + ) + resource_requirements.append(resource_req) + + return (mapping_requirements, resource_requirements) def _mark_version_installed(cursor: sqlite3.Cursor, version_id: int) -> None: cursor.execute( @@ -135,13 +174,13 @@ def _recompute_dependencies_no_state_update_no_pull_files( resources_to_ids = dict((info, id) for id, info in ids_to_resources.items()) mappings_to_ids = dict((info, id) for id, info in ids_to_mappings.items()) - if unlocked_required_mappings == 'all_mappings_unlocked': - requirements = [] - else: - requirements = _get_current_required_state( + if unlocked_required_mappings != 'all_mappings_unlocked': + mapping_reqs, resource_reqs = _get_current_required_state( cursor = cursor, unlocked_required_mappings = unlocked_required_mappings ) + else: + mapping_reqs, resource_reqs = [], [] cursor.execute( ''' @@ -156,7 +195,7 @@ def _recompute_dependencies_no_state_update_no_pull_files( ) for mapping_identifier, in cursor.fetchall(): - requirements.append(sds.MappingRequirement(mapping_identifier)) + mapping_reqs.append(sds.MappingRequirement(mapping_identifier)) cursor.execute( ''' @@ -179,12 +218,13 @@ def _recompute_dependencies_no_state_update_no_pull_files( else: requirement = sds.MappingVersionRequirement(info.identifier, info) - requirements.append(requirement) + mapping_reqs.append(requirement) mapping_choices = sds.compute_payloads( - ids_to_resources.values(), - ids_to_mappings.values(), - requirements + resources = ids_to_resources.values(), + mappings = ids_to_mappings.values(), + mapping_requirements = mapping_reqs, + resource_requirements = resource_reqs ) cursor.execute( @@ -243,7 +283,7 @@ def _recompute_dependencies_no_state_update_no_pull_files( WHERE item_version_id = ?; ''', - (mapping_ver_id, 'R' if choice.required else 'A') + ('R' if choice.required else 'A', mapping_ver_id) ) for num, (pattern, payload) in enumerate(choice.payloads.items()): -- cgit v1.2.3