refactoring memory and disk cache abstractions

This commit is contained in:
Vinicius Moreira
2019-08-30 17:26:35 -03:00
parent 71959e8e04
commit bd17594723
12 changed files with 215 additions and 63 deletions

View File

@@ -3,32 +3,36 @@ import sys
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from bauh_api.abstract.controller import ApplicationContext from bauh_api.abstract.controller import ApplicationContext
from bauh_api.util.cache import Cache
from bauh_api.util.disk import DiskCacheLoaderFactory
from bauh_api.util.http import HttpClient from bauh_api.util.http import HttpClient
from bauh import __version__, __app_name__, app_args, ROOT_DIR from bauh import __version__, __app_name__, app_args, ROOT_DIR
from bauh.core import resource, extensions from bauh.core import resource, extensions
from bauh.core.controller import GenericSoftwareManager from bauh.core.controller import GenericSoftwareManager
from bauh.util import util from bauh.util import util
from bauh.util.memory import CacheCleaner from bauh.util.cache import DefaultMemoryCacheFactory, CacheCleaner
from bauh.util.disk import DefaultDiskCacheLoaderFactory
from bauh.view.qt.systray import TrayIcon from bauh.view.qt.systray import TrayIcon
from bauh.view.qt.window import ManageWindow from bauh.view.qt.window import ManageWindow
args = app_args.read() args = app_args.read()
i18n = util.get_locale_keys(args.locale) i18n = util.get_locale_keys(args.locale)
caches, cache_map = [], {}
context = ApplicationContext(i18n=i18n, http_client=HttpClient(), args=args, app_root_dir=ROOT_DIR) cache_cleaner = CacheCleaner()
managers = extensions.load_managers(caches=caches, cache_map=cache_map, context=context) cache_factory = DefaultMemoryCacheFactory(expiration_time=args.cache_exp, cleaner=cache_cleaner)
icon_cache = cache_factory.new(args.icon_exp)
icon_cache = Cache(expiration_time=args.icon_exp) context = ApplicationContext(i18n=i18n,
caches.append(icon_cache) http_client=HttpClient(),
args=args,
app_root_dir=ROOT_DIR,
cache_factory=cache_factory,
disk_loader_factory=DefaultDiskCacheLoaderFactory(disk_cache_enabled=args.disk_cache))
disk_loader_factory = DiskCacheLoaderFactory(disk_cache=args.disk_cache, cache_map=cache_map) managers = extensions.load_managers(context=context)
manager = GenericSoftwareManager(managers, disk_loader_factory=disk_loader_factory, context=context)
manager = GenericSoftwareManager(managers, context=context)
manager.prepare() manager.prepare()
app = QApplication(sys.argv) app = QApplication(sys.argv)
@@ -56,5 +60,5 @@ else:
manage_window.refresh_apps() manage_window.refresh_apps()
manage_window.show() manage_window.show()
CacheCleaner(caches).start() cache_cleaner.start()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@@ -20,7 +20,7 @@ def read() -> Namespace:
parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__)) parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(__version__))
parser.add_argument('-e', '--cache-exp', action="store", parser.add_argument('-e', '--cache-exp', action="store",
default=int(os.getenv('BAUH_CACHE_EXPIRATION', 60 * 60)), type=int, default=int(os.getenv('BAUH_CACHE_EXPIRATION', 60 * 60)), type=int,
help='cached API data expiration time in SECONDS. Default: %(default)s') help='default memory caches expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('BAUH_ICON_EXPIRATION', 60 * 5)), parser.add_argument('-ie', '--icon-exp', action="store", default=int(os.getenv('BAUH_ICON_EXPIRATION', 60 * 5)),
type=int, help='cached icons expiration time in SECONDS. Default: %(default)s') type=int, help='cached icons expiration time in SECONDS. Default: %(default)s')
parser.add_argument('-l', '--locale', action="store", default=os.getenv('BAUH_LOCALE', 'en'), parser.add_argument('-l', '--locale', action="store", default=os.getenv('BAUH_LOCALE', 'en'),

View File

@@ -2,24 +2,23 @@ from threading import Thread
from typing import List, Set, Type from typing import List, Set, Type
from bauh_api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext from bauh_api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext
from bauh_api.abstract.disk import DiskCacheLoader
from bauh_api.abstract.handler import ProcessWatcher from bauh_api.abstract.handler import ProcessWatcher
from bauh_api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory from bauh_api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory
from bauh_api.util.disk import DiskCacheLoader
from bauh_api.util.disk import DiskCacheLoaderFactory
SUGGESTIONS_LIMIT = 6 SUGGESTIONS_LIMIT = 6
class GenericSoftwareManager(SoftwareManager): class GenericSoftwareManager(SoftwareManager):
def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, disk_loader_factory: DiskCacheLoaderFactory): def __init__(self, managers: List[SoftwareManager], context: ApplicationContext):
super(GenericSoftwareManager, self).__init__(context=context, app_cache=None) super(GenericSoftwareManager, self).__init__(context=context)
self.managers = managers self.managers = managers
self.map = {t: m for m in self.managers for t in m.get_managed_types()} self.map = {t: m for m in self.managers for t in m.get_managed_types()}
self.disk_loader_factory = disk_loader_factory
self._enabled_map = {} if context.args.check_packaging_once else None self._enabled_map = {} if context.args.check_packaging_once else None
self.thread_prepare = None self.thread_prepare = None
self.i18n = context.i18n self.i18n = context.i18n
self.disk_loader_factory = context.disk_loader_factory
def _sort(self, apps: List[SoftwarePackage], word: str) -> List[SoftwarePackage]: def _sort(self, apps: List[SoftwarePackage], word: str) -> List[SoftwarePackage]:
@@ -188,7 +187,7 @@ class GenericSoftwareManager(SoftwareManager):
return man if man and self._is_enabled(man) else None return man if man and self._is_enabled(man) else None
def cache_to_disk(self, app: SoftwarePackage, icon_bytes: bytes, only_icon: bool): def cache_to_disk(self, app: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
if self.disk_loader_factory.disk_cache and app.supports_disk_cache(): if self.context.args.disk_cache and app.supports_disk_cache():
man = self._get_manager_for(app) man = self._get_manager_for(app)
if man: if man:

View File

@@ -1,10 +1,9 @@
import inspect import inspect
import os import os
import pkgutil import pkgutil
from typing import List, Dict from typing import List
from bauh_api.abstract.controller import SoftwareManager, ApplicationContext from bauh_api.abstract.controller import SoftwareManager, ApplicationContext
from bauh_api.util.cache import Cache
from bauh import __app_name__ from bauh import __app_name__
from bauh.util import util from bauh.util import util
@@ -24,7 +23,7 @@ def find_manager(member):
return manager_found return manager_found
def load_managers(caches: List[Cache], cache_map: Dict[type, Cache], context: ApplicationContext) -> List[SoftwareManager]: def load_managers(context: ApplicationContext) -> List[SoftwareManager]:
managers = [] managers = []
for m in pkgutil.iter_modules(): for m in pkgutil.iter_modules():
@@ -40,13 +39,6 @@ def load_managers(caches: List[Cache], cache_map: Dict[type, Cache], context: Ap
if os.path.exists(locale_path): if os.path.exists(locale_path):
context.i18n.update(util.get_locale_keys(context.args.locale, locale_path)) context.i18n.update(util.get_locale_keys(context.args.locale, locale_path))
app_cache = Cache(expiration_time=context.args.cache_exp) managers.append(manager_class(context=context))
man = manager_class(context=context, app_cache=app_cache)
for t in man.get_managed_types():
cache_map[t] = app_cache
caches.append(app_cache)
managers.append(man)
return managers return managers

108
bauh/util/cache.py Normal file
View File

@@ -0,0 +1,108 @@
import datetime
import time
from threading import Lock, Thread
from typing import List
from bauh_api.abstract.cache import MemoryCache, MemoryCacheFactory
class DefaultMemoryCache(MemoryCache):
"""
A synchronized cache implementation
"""
def __init__(self, expiration_time: int):
super(DefaultMemoryCache, self).__init__()
self.expiration_time = expiration_time
self._cache = {}
self.lock = Lock()
def is_enabled(self):
return self.expiration_time < 0 or self.expiration_time > 0
def add(self, key: str, val: object):
if key and self.is_enabled():
self.lock.acquire()
self._add(key, val)
self.lock.release()
def _add(self, key: str, val: object):
if key:
self._cache[key] = {'val': val, 'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=self.expiration_time) if self.expiration_time > 0 else None}
def add_non_existing(self, key: str, val: object):
if key and self. is_enabled():
self.lock.acquire()
cur_val = self.get(key)
if cur_val is None:
self._add(key, val)
self.lock.release()
def get(self, key: str):
if key and self.is_enabled():
val = self._cache.get(key)
if val:
expiration = val.get('expires_at')
if expiration and expiration <= datetime.datetime.utcnow():
self.lock.acquire()
del self._cache[key]
self.lock.release()
return None
return val['val']
def delete(self, key):
if key and self.is_enabled():
if key in self._cache:
self.lock.acquire()
del self._cache[key]
self.lock.release()
def keys(self):
return set(self._cache.keys()) if self.is_enabled() else set()
def clean_expired(self):
if self.is_enabled():
for key in self.keys():
self.get(key)
class CacheCleaner(Thread):
def __init__(self, check_interval: int = 15):
super(CacheCleaner, self).__init__(daemon=True)
self.caches = []
self.check_interval = check_interval
def register(self, cache: MemoryCache):
if cache.is_enabled():
self.caches.append(cache)
def run(self):
if self.caches:
while True:
for cache in self.caches:
cache.clean_expired()
time.sleep(self.check_interval)
class DefaultMemoryCacheFactory(MemoryCacheFactory):
def __init__(self, expiration_time: int, cleaner: CacheCleaner):
"""
:param expiration_time: default expiration time for all instantiated caches
:param cleaner
"""
super(DefaultMemoryCacheFactory, self).__init__()
self.expiration_time = expiration_time
self.cleaner = cleaner
def new(self, expiration: int = None) -> MemoryCache:
instance = DefaultMemoryCache(expiration if expiration is not None else self.expiration_time)
self.cleaner.register(instance)
return instance

72
bauh/util/disk.py Normal file
View File

@@ -0,0 +1,72 @@
import json
import os
from threading import Thread, Lock
from typing import Type, Dict
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.disk import DiskCacheLoader, DiskCacheLoaderFactory
from bauh_api.abstract.model import SoftwarePackage
class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
def __init__(self, enabled: bool, cache_map: Dict[Type[SoftwarePackage], MemoryCache]):
super(AsyncDiskCacheLoader, self).__init__(daemon=True)
self.apps = []
self.stop = False
self.lock = Lock()
self.cache_map = cache_map
self.enabled = enabled
def fill(self, pkg: SoftwarePackage):
"""
Adds a package which data must be read from the disk to a queue.
:param pkg:
:return:
"""
if self.enabled:
if pkg and pkg.supports_disk_cache():
self.lock.acquire()
self.apps.append(pkg)
self.lock.release()
def run(self):
if self.enabled:
while True:
if self.apps:
self.lock.acquire()
app = self.apps[0]
del self.apps[0]
self.lock.release()
self._fill_cached_data(app)
elif self.stop:
break
def _fill_cached_data(self, pkg: SoftwarePackage):
if self.enabled:
if os.path.exists(pkg.get_disk_data_path()):
with open(pkg.get_disk_data_path()) as f:
cached_data = json.loads(f.read())
pkg.fill_cached_data(cached_data)
cache = self.cache_map.get(pkg.__class__)\
if cache:
cache.add_non_existing(pkg.base_data.id, cached_data)
class DefaultDiskCacheLoaderFactory(DiskCacheLoaderFactory):
def __init__(self, disk_cache_enabled: bool):
super(DefaultDiskCacheLoaderFactory, self).__init__()
self.disk_cache_enabled = disk_cache_enabled
self.cache_map = {}
def map(self, pkg_type: Type[SoftwarePackage], cache: MemoryCache):
if pkg_type:
if pkg_type in self.cache_map:
raise Exception('{} is already mapped')
self.cache_map[pkg_type] = cache
def new(self) -> AsyncDiskCacheLoader:
return AsyncDiskCacheLoader(enabled=self.disk_cache_enabled, cache_map=self.cache_map)

View File

@@ -1,23 +0,0 @@
import time
from threading import Thread
from typing import List
from bauh_api.util.cache import Cache
class CacheCleaner(Thread):
def __init__(self, caches: List[Cache], check_interval: int = 15):
super(CacheCleaner, self).__init__(daemon=True)
self.caches = [c for c in caches if c.is_enabled()]
self.check_interval = check_interval
def run(self):
if self.caches:
while True:
for cache in self.caches:
cache.clean_expired()
time.sleep(self.check_interval)

View File

@@ -7,8 +7,8 @@ from PyQt5.QtGui import QPixmap, QIcon, QCursor
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \ from PyQt5.QtWidgets import QTableWidget, QTableView, QMenu, QAction, QTableWidgetItem, QToolButton, QWidget, \
QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar QHeaderView, QLabel, QHBoxLayout, QPushButton, QToolBar
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.model import PackageStatus from bauh_api.abstract.model import PackageStatus
from bauh_api.util.cache import Cache
from bauh.core import resource from bauh.core import resource
from bauh.util import util from bauh.util import util
@@ -75,7 +75,7 @@ class UpdateToggleButton(QWidget):
class AppsTable(QTableWidget): class AppsTable(QTableWidget):
def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool, download_icons: bool): def __init__(self, parent: QWidget, icon_cache: MemoryCache, disk_cache: bool, download_icons: bool):
super(AppsTable, self).__init__() super(AppsTable, self).__init__()
self.setParent(parent) self.setParent(parent)
self.window = parent self.window = parent

View File

@@ -4,13 +4,13 @@ from functools import reduce
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.model import PackageHistory from bauh_api.abstract.model import PackageHistory
from bauh_api.util.cache import Cache
class HistoryDialog(QDialog): class HistoryDialog(QDialog):
def __init__(self, history: PackageHistory, icon_cache: Cache, locale_keys: dict): def __init__(self, history: PackageHistory, icon_cache: MemoryCache, locale_keys: dict):
super(HistoryDialog, self).__init__() super(HistoryDialog, self).__init__()
self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], history.pkg.base_data.name)) self.setWindowTitle('{} - {} '.format(locale_keys['popup.history.title'], history.pkg.base_data.name))

View File

@@ -2,7 +2,7 @@ from PyQt5.QtCore import QSize
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar
from bauh_api.util.cache import Cache from bauh_api.abstract.cache import MemoryCache
from bauh.util import util from bauh.util import util
@@ -11,7 +11,7 @@ IGNORED_ATTRS = {'name', '__app__'}
class InfoDialog(QDialog): class InfoDialog(QDialog):
def __init__(self, app: dict, icon_cache: Cache, locale_keys: dict, screen_size: QSize()): def __init__(self, app: dict, icon_cache: MemoryCache, locale_keys: dict, screen_size: QSize()):
super(InfoDialog, self).__init__() super(InfoDialog, self).__init__()
self.setWindowTitle(app['__app__'].model.base_data.name) self.setWindowTitle(app['__app__'].model.base_data.name)
self.screen_size = screen_size self.screen_size = screen_size

View File

@@ -5,12 +5,12 @@ from typing import List, Type, Set
import requests import requests
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.controller import SoftwareManager from bauh_api.abstract.controller import SoftwareManager
from bauh_api.abstract.handler import ProcessWatcher from bauh_api.abstract.handler import ProcessWatcher
from bauh_api.abstract.model import PackageStatus, SoftwarePackage from bauh_api.abstract.model import PackageStatus, SoftwarePackage
from bauh_api.abstract.view import InputViewComponent, MessageType from bauh_api.abstract.view import InputViewComponent, MessageType
from bauh_api.exception import NoInternetException from bauh_api.exception import NoInternetException
from bauh_api.util.cache import Cache
from bauh.view.qt.view_model import PackageView from bauh.view.qt.view_model import PackageView
@@ -127,7 +127,7 @@ class RefreshApps(AsyncAction):
class UninstallApp(AsyncAction): class UninstallApp(AsyncAction):
def __init__(self, manager: SoftwareManager, icon_cache: Cache, app: PackageView = None): def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, app: PackageView = None):
super(UninstallApp, self).__init__() super(UninstallApp, self).__init__()
self.app = app self.app = app
self.manager = manager self.manager = manager
@@ -223,7 +223,7 @@ class SearchApps(AsyncAction):
class InstallApp(AsyncAction): class InstallApp(AsyncAction):
def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: Cache, locale_keys: dict, pkg: PackageView = None): def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: MemoryCache, locale_keys: dict, pkg: PackageView = None):
super(InstallApp, self).__init__() super(InstallApp, self).__init__()
self.pkg = pkg self.pkg = pkg
self.manager = manager self.manager = manager

View File

@@ -6,10 +6,10 @@ from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QPixmap
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QCheckBox, QHeaderView, QToolButton, QToolBar, \
QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox
from bauh_api.abstract.cache import MemoryCache
from bauh_api.abstract.controller import SoftwareManager from bauh_api.abstract.controller import SoftwareManager
from bauh_api.abstract.model import SoftwarePackage from bauh_api.abstract.model import SoftwarePackage
from bauh_api.abstract.view import MessageType from bauh_api.abstract.view import MessageType
from bauh_api.util.cache import Cache
from bauh.core import resource from bauh.core import resource
from bauh.util import util from bauh.util import util
@@ -38,7 +38,7 @@ class ManageWindow(QWidget):
def _toolbar_button_style(self, bg: str): def _toolbar_button_style(self, bg: str):
return 'QPushButton { color: white; font-weight: bold; background: ' + bg + '}' return 'QPushButton { color: white; font-weight: bold; background: ' + bg + '}'
def __init__(self, locale_keys: dict, icon_cache: Cache, manager: SoftwareManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, tray_icon=None): def __init__(self, locale_keys: dict, icon_cache: MemoryCache, manager: SoftwareManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, tray_icon=None):
super(ManageWindow, self).__init__() super(ManageWindow, self).__init__()
self.locale_keys = locale_keys self.locale_keys = locale_keys
self.manager = manager self.manager = manager