| # Copyright 2014-2015 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import errno |
| import io |
| import json |
| import os |
| |
| from portage import _encodings |
| from portage.util import atomic_ofstream |
| from portage.versions import cpv_getkey |
| |
| class VdbMetadataDelta(object): |
| |
| _format_version = "1" |
| |
| def __init__(self, vardb): |
| self._vardb = vardb |
| |
| def initialize(self, timestamp): |
| f = atomic_ofstream(self._vardb._cache_delta_filename, 'w', |
| encoding=_encodings['repo.content'], errors='strict') |
| json.dump({ |
| "version": self._format_version, |
| "timestamp": timestamp |
| }, f, ensure_ascii=False) |
| f.close() |
| |
| def load(self): |
| |
| if not os.path.exists(self._vardb._aux_cache_filename): |
| # If the primary cache doesn't exist yet, then |
| # we can't record a delta against it. |
| return None |
| |
| try: |
| with io.open(self._vardb._cache_delta_filename, 'r', |
| encoding=_encodings['repo.content'], |
| errors='strict') as f: |
| cache_obj = json.load(f) |
| except EnvironmentError as e: |
| if e.errno not in (errno.ENOENT, errno.ESTALE): |
| raise |
| except (SystemExit, KeyboardInterrupt): |
| raise |
| except Exception: |
| # Corrupt, or not json format. |
| pass |
| else: |
| try: |
| version = cache_obj["version"] |
| except KeyError: |
| pass |
| else: |
| # Verify that the format version is compatible, |
| # since a newer version of portage may have |
| # written an incompatible file. |
| if version == self._format_version: |
| try: |
| deltas = cache_obj["deltas"] |
| except KeyError: |
| cache_obj["deltas"] = deltas = [] |
| |
| if isinstance(deltas, list): |
| return cache_obj |
| |
| return None |
| |
| def loadRace(self): |
| """ |
| This calls self.load() and validates the timestamp |
| against the currently loaded self._vardb._aux_cache. If a |
| concurrent update causes the timestamps to be inconsistent, |
| then it reloads the caches and tries one more time before |
| it aborts. In practice, the race is very unlikely, so |
| this will usually succeed on the first try. |
| """ |
| |
| tries = 2 |
| while tries: |
| tries -= 1 |
| cache_delta = self.load() |
| if cache_delta is not None and \ |
| cache_delta.get("timestamp") != \ |
| self._vardb._aux_cache.get("timestamp", False): |
| self._vardb._aux_cache_obj = None |
| else: |
| return cache_delta |
| |
| return None |
| |
| def recordEvent(self, event, cpv, slot, counter): |
| |
| self._vardb.lock() |
| try: |
| deltas_obj = self.load() |
| |
| if deltas_obj is None: |
| # We can't record meaningful deltas without |
| # a pre-existing state. |
| return |
| |
| delta_node = { |
| "event": event, |
| "package": cpv.cp, |
| "version": cpv.version, |
| "slot": slot, |
| "counter": "%s" % counter |
| } |
| |
| deltas_obj["deltas"].append(delta_node) |
| |
| # Eliminate earlier nodes cancelled out by later nodes |
| # that have identical package and slot attributes. |
| filtered_list = [] |
| slot_keys = set() |
| version_keys = set() |
| for delta_node in reversed(deltas_obj["deltas"]): |
| slot_key = (delta_node["package"], |
| delta_node["slot"]) |
| version_key = (delta_node["package"], |
| delta_node["version"]) |
| if not (slot_key in slot_keys or \ |
| version_key in version_keys): |
| filtered_list.append(delta_node) |
| slot_keys.add(slot_key) |
| version_keys.add(version_key) |
| |
| filtered_list.reverse() |
| deltas_obj["deltas"] = filtered_list |
| |
| f = atomic_ofstream(self._vardb._cache_delta_filename, |
| mode='w', encoding=_encodings['repo.content']) |
| json.dump(deltas_obj, f, ensure_ascii=False) |
| f.close() |
| |
| finally: |
| self._vardb.unlock() |
| |
| def applyDelta(self, data): |
| packages = self._vardb._aux_cache["packages"] |
| deltas = {} |
| for delta in data["deltas"]: |
| cpv = delta["package"] + "-" + delta["version"] |
| deltas[cpv] = delta |
| event = delta["event"] |
| if event == "add": |
| # Use aux_get to populate the cache |
| # for this cpv. |
| if cpv not in packages: |
| try: |
| self._vardb.aux_get(cpv, ["DESCRIPTION"]) |
| except KeyError: |
| pass |
| elif event == "remove": |
| packages.pop(cpv, None) |
| |
| if deltas: |
| # Delete removed or replaced versions from affected slots |
| for cached_cpv, (mtime, metadata) in list(packages.items()): |
| if cached_cpv in deltas: |
| continue |
| |
| removed = False |
| for cpv, delta in deltas.items(): |
| if (cached_cpv.startswith(delta["package"]) and |
| metadata.get("SLOT") == delta["slot"] and |
| cpv_getkey(cached_cpv) == delta["package"]): |
| removed = True |
| break |
| |
| if removed: |
| del packages[cached_cpv] |
| del deltas[cpv] |
| if not deltas: |
| break |