403b09
From beff42632d1db674802c817afd49a3ac8bcd8fb6 Mon Sep 17 00:00:00 2001
403b09
From: David Kupka <dkupka@redhat.com>
403b09
Date: Wed, 27 Jul 2016 10:46:40 +0200
403b09
Subject: [PATCH] schema: Speed up schema cache
403b09
403b09
Check presence of schema in cache (and download it if necessary) on
403b09
__init__ instead of with each __getitem__ call. Prefill internal
403b09
dictionary with empty record for each command to be able to quickly
403b09
determine if requested command exist in schema or not. Rest of schema
403b09
data are read from cache on first attempt to retrive them.
403b09
403b09
https://fedorahosted.org/freeipa/ticket/6048
403b09
https://fedorahosted.org/freeipa/ticket/6069
403b09
403b09
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
403b09
---
403b09
 ipaclient/remote_plugins/schema.py | 301 ++++++++++++++++++++++---------------
403b09
 1 file changed, 177 insertions(+), 124 deletions(-)
403b09
403b09
diff --git a/ipaclient/remote_plugins/schema.py b/ipaclient/remote_plugins/schema.py
403b09
index 0301e54127dc236ebc14e1409484626f1427800d..d039fb41991c26a9c7b7f76f6959668efb677586 100644
403b09
--- a/ipaclient/remote_plugins/schema.py
403b09
+++ b/ipaclient/remote_plugins/schema.py
403b09
@@ -5,10 +5,8 @@
403b09
 import collections
403b09
 import errno
403b09
 import fcntl
403b09
-import glob
403b09
 import json
403b09
 import os
403b09
-import re
403b09
 import sys
403b09
 import time
403b09
 import types
403b09
@@ -65,8 +63,6 @@ USER_CACHE_PATH = (
403b09
         '.cache'
403b09
     )
403b09
 )
403b09
-SCHEMA_DIR = os.path.join(USER_CACHE_PATH, 'ipa', 'schema')
403b09
-SERVERS_DIR = os.path.join(USER_CACHE_PATH, 'ipa', 'servers')
403b09
 
403b09
 logger = log_mgr.get_logger(__name__)
403b09
 
403b09
@@ -274,15 +270,6 @@ class _SchemaObjectPlugin(_SchemaPlugin):
403b09
     schema_key = 'classes'
403b09
 
403b09
 
403b09
-def _ensure_dir_created(d):
403b09
-    try:
403b09
-        os.makedirs(d)
403b09
-    except OSError as e:
403b09
-        if e.errno != errno.EEXIST:
403b09
-            raise RuntimeError("Unable to create cache directory: {}"
403b09
-                               "".format(e))
403b09
-
403b09
-
403b09
 class _LockedZipFile(zipfile.ZipFile):
403b09
     """ Add locking to zipfile.ZipFile
403b09
     Shared lock is used with read mode, exclusive with write mode.
403b09
@@ -308,7 +295,10 @@ class _SchemaNameSpace(collections.Mapping):
403b09
         self._schema = schema
403b09
 
403b09
     def __getitem__(self, key):
403b09
-        return self._schema.read_namespace_member(self.name, key)
403b09
+        try:
403b09
+            return self._schema.read_namespace_member(self.name, key)
403b09
+        except KeyError:
403b09
+            raise KeyError(key)
403b09
 
403b09
     def __iter__(self):
403b09
         for key in self._schema.iter_namespace(self.name):
403b09
@@ -322,6 +312,62 @@ class NotAvailable(Exception):
403b09
     pass
403b09
 
403b09
 
403b09
+class ServerInfo(collections.MutableMapping):
403b09
+    _DIR = os.path.join(USER_CACHE_PATH, 'ipa', 'servers')
403b09
+
403b09
+    def __init__(self, api):
403b09
+        hostname = DNSName(api.env.server).ToASCII()
403b09
+        self._path = os.path.join(self._DIR, hostname)
403b09
+        self._dict = {}
403b09
+        self._dirty = False
403b09
+
403b09
+        self._read()
403b09
+
403b09
+    def __enter__(self):
403b09
+        return self
403b09
+
403b09
+    def __exit__(self, *_exc_info):
403b09
+        if self._dirty:
403b09
+            self._write()
403b09
+
403b09
+    def _read(self):
403b09
+        try:
403b09
+            with open(self._path, 'r') as sc:
403b09
+                self._dict = json.load(sc)
403b09
+        except EnvironmentError as e:
403b09
+            if e.errno != errno.ENOENT:
403b09
+                logger.warning('Failed to read server info: {}'.format(e))
403b09
+
403b09
+    def _write(self):
403b09
+        try:
403b09
+            try:
403b09
+                os.makedirs(self._DIR)
403b09
+            except EnvironmentError as e:
403b09
+                if e.errno != errno.EEXIST:
403b09
+                    raise
403b09
+            with open(self._path, 'w') as sc:
403b09
+                json.dump(self._dict, sc)
403b09
+        except EnvironmentError as e:
403b09
+            logger.warning('Failed to write server info: {}'.format(e))
403b09
+
403b09
+    def __getitem__(self, key):
403b09
+        return self._dict[key]
403b09
+
403b09
+    def __setitem__(self, key, value):
403b09
+        self._dirty = key not in self._dict or self._dict[key] != value
403b09
+        self._dict[key] = value
403b09
+
403b09
+    def __delitem__(self, key):
403b09
+        del self._dict[key]
403b09
+        self._dirty = True
403b09
+
403b09
+    def __iter__(self):
403b09
+        return iter(self._dict)
403b09
+
403b09
+    def __len__(self):
403b09
+        return len(self._dict)
403b09
+
403b09
+
403b09
 class Schema(object):
403b09
     """
403b09
     Store and provide schema for commands and topics
403b09
@@ -342,38 +388,76 @@ class Schema(object):
403b09
     u'Ping the remote IPA server to ...'
403b09
 
403b09
     """
403b09
-    schema_path_template = os.path.join(SCHEMA_DIR, '{}')
403b09
-    servers_path_template = os.path.join(SERVERS_DIR, '{}')
403b09
-    ns_member_pattern_template = '^{}/(?P<name>.+)$'
403b09
-    ns_member_path_template = '{}/{}'
403b09
     namespaces = {'classes', 'commands', 'topics'}
403b09
     schema_info_path = 'schema'
403b09
+    _DIR = os.path.join(USER_CACHE_PATH, 'ipa', 'schema')
403b09
 
403b09
-    @classmethod
403b09
-    def _list(cls):
403b09
-        for f in glob.glob(cls.schema_path_template.format('*')):
403b09
-            yield os.path.splitext(os.path.basename(f))[0]
403b09
+    def __init__(self, api, server_info, client):
403b09
+        self._dict = {}
403b09
+        self._namespaces = {}
403b09
+        self._help = None
403b09
 
403b09
-    @classmethod
403b09
-    def _in_cache(cls, fingeprint):
403b09
-        return os.path.exists(cls.schema_path_template.format(fingeprint))
403b09
+        for ns in self.namespaces:
403b09
+            self._dict[ns] = {}
403b09
+            self._namespaces[ns] = _SchemaNameSpace(self, ns)
403b09
 
403b09
-    def __init__(self, api, client):
403b09
-        self._api = api
403b09
-        self._client = client
403b09
-        self._dict = {}
403b09
+        is_known = False
403b09
+        if not api.env.force_schema_check:
403b09
+            try:
403b09
+                self._fingerprint = server_info['fingerprint']
403b09
+                self._expiration = server_info['expiration']
403b09
+            except KeyError:
403b09
+                pass
403b09
+            else:
403b09
+                is_known = True
403b09
+
403b09
+        if is_known:
403b09
+            try:
403b09
+                self._read_schema()
403b09
+            except Exception:
403b09
+                pass
403b09
+            else:
403b09
+                return
403b09
 
403b09
-    def _open_server_info(self, hostname, mode):
403b09
-        encoded_hostname = DNSName(hostname).ToASCII()
403b09
-        path = self.servers_path_template.format(encoded_hostname)
403b09
-        return open(path, mode)
403b09
+        try:
403b09
+            self._fetch(client)
403b09
+        except NotAvailable:
403b09
+            raise
403b09
+        else:
403b09
+            self._write_schema()
403b09
+        finally:
403b09
+            try:
403b09
+                server_info['fingerprint'] = self._fingerprint
403b09
+                server_info['expiration'] = self._expiration
403b09
+            except AttributeError:
403b09
+                pass
403b09
 
403b09
-    def _get_schema(self):
403b09
-        client = self._client
403b09
+    def _open_schema(self, filename, mode):
403b09
+        path = os.path.join(self._DIR, filename)
403b09
+        return _LockedZipFile(path, mode)
403b09
+
403b09
+    def _get_schema_fingerprint(self, schema):
403b09
+        schema_info = json.loads(schema.read(self.schema_info_path))
403b09
+        return schema_info['fingerprint']
403b09
+
403b09
+    def _fetch(self, client):
403b09
         if not client.isconnected():
403b09
             client.connect(verbose=False)
403b09
 
403b09
-        fps = [unicode(f) for f in Schema._list()]
403b09
+        fps = []
403b09
+        try:
403b09
+            files = os.listdir(self._DIR)
403b09
+        except EnvironmentError:
403b09
+            pass
403b09
+        else:
403b09
+            for filename in files:
403b09
+                try:
403b09
+                    with self._open_schema(filename, 'r') as schema:
403b09
+                        fps.append(
403b09
+                            unicode(self._get_schema_fingerprint(schema)))
403b09
+                except Exception:
403b09
+                    continue
403b09
+
403b09
         kwargs = {u'version': u'2.170'}
403b09
         if fps:
403b09
             kwargs[u'known_fingerprints'] = fps
403b09
@@ -386,110 +470,80 @@ class Schema(object):
403b09
             ttl = e.ttl
403b09
         else:
403b09
             fp = schema['fingerprint']
403b09
-            ttl = schema['ttl']
403b09
-            self._store(fp, schema)
403b09
-        finally:
403b09
-            client.disconnect()
403b09
+            ttl = schema.pop('ttl', 0)
403b09
 
403b09
-        exp = ttl + time.time()
403b09
-        return (fp, exp)
403b09
+            for key, value in schema.items():
403b09
+                if key in self.namespaces:
403b09
+                    value = {m['full_name']: m for m in value}
403b09
+                self._dict[key] = value
403b09
 
403b09
-    def _ensure_cached(self):
403b09
-        no_info = False
403b09
-        try:
403b09
-            # pylint: disable=access-member-before-definition
403b09
-            fp = self._server_schema_fingerprint
403b09
-            exp = self._server_schema_expiration
403b09
-        except AttributeError:
403b09
-            try:
403b09
-                with self._open_server_info(self._api.env.server, 'r') as sc:
403b09
-                    si = json.load(sc)
403b09
-
403b09
-                fp = si['fingerprint']
403b09
-                exp = si['expiration']
403b09
-            except Exception as e:
403b09
-                no_info = True
403b09
-                if not (isinstance(e, EnvironmentError) and
403b09
-                        e.errno == errno.ENOENT):  # pylint: disable=no-member
403b09
-                    logger.warning('Failed to load server properties: {}'
403b09
-                                   ''.format(e))
403b09
-
403b09
-        force_check = ((not getattr(self, '_schema_checked', False)) and
403b09
-                       self._api.env.force_schema_check)
403b09
-
403b09
-        if (force_check or
403b09
-                no_info or exp < time.time() or not Schema._in_cache(fp)):
403b09
-            (fp, exp) = self._get_schema()
403b09
-            self._schema_checked = True
403b09
-            _ensure_dir_created(SERVERS_DIR)
403b09
-            try:
403b09
-                with self._open_server_info(self._api.env.server, 'w') as sc:
403b09
-                    json.dump(dict(fingerprint=fp, expiration=exp), sc)
403b09
-            except Exception as e:
403b09
-                logger.warning('Failed to store server properties: {}'
403b09
-                               ''.format(e))
403b09
-
403b09
-        if not self._dict:
403b09
-            self._dict['fingerprint'] = fp
403b09
-            schema_info = self._read(self.schema_info_path)
403b09
+        self._fingerprint = fp
403b09
+        self._expiration = ttl + time.time()
403b09
+
403b09
+    def _read_schema(self):
403b09
+        with self._open_schema(self._fingerprint, 'r') as schema:
403b09
+            self._dict['fingerprint'] = self._get_schema_fingerprint(schema)
403b09
+            schema_info = json.loads(schema.read(self.schema_info_path))
403b09
             self._dict['version'] = schema_info['version']
403b09
-            for ns in self.namespaces:
403b09
-                self._dict[ns] = _SchemaNameSpace(self, ns)
403b09
 
403b09
-        self._server_schema_fingerprintr = fp
403b09
-        self._server_schema_expiration = exp
403b09
+            for name in schema.namelist():
403b09
+                ns, _slash, key = name.partition('/')
403b09
+                if ns in self.namespaces:
403b09
+                    self._dict[ns][key] = {}
403b09
 
403b09
     def __getitem__(self, key):
403b09
-        self._ensure_cached()
403b09
-        return self._dict[key]
403b09
+        try:
403b09
+            return self._namespaces[key]
403b09
+        except KeyError:
403b09
+            return self._dict[key]
403b09
 
403b09
-    def _open_archive(self, mode, fp=None):
403b09
-        if not fp:
403b09
-            fp = self['fingerprint']
403b09
-        arch_path = self.schema_path_template.format(fp)
403b09
-        return _LockedZipFile(arch_path, mode)
403b09
-
403b09
-    def _store(self, fingerprint, schema={}):
403b09
-        _ensure_dir_created(SCHEMA_DIR)
403b09
-
403b09
-        schema_info = dict(version=schema['version'],
403b09
-                           fingerprint=schema['fingerprint'])
403b09
-
403b09
-        with self._open_archive('w', fingerprint) as zf:
403b09
-            # store schema information
403b09
-            zf.writestr(self.schema_info_path, json.dumps(schema_info))
403b09
-            # store namespaces
403b09
-            for namespace in self.namespaces:
403b09
-                for member in schema[namespace]:
403b09
-                    path = self.ns_member_path_template.format(
403b09
-                        namespace,
403b09
-                        member['full_name']
403b09
-                    )
403b09
-                    zf.writestr(path, json.dumps(member))
403b09
+    def _write_schema(self):
403b09
+        try:
403b09
+            os.makedirs(self._DIR)
403b09
+        except EnvironmentError as e:
403b09
+            if e.errno != errno.EEXIST:
403b09
+                logger.warning("Failed ti write schema: {}".format(e))
403b09
+                return
403b09
+
403b09
+        with self._open_schema(self._fingerprint, 'w') as schema:
403b09
+            schema_info = {}
403b09
+            for key, value in self._dict.items():
403b09
+                if key in self.namespaces:
403b09
+                    ns = value
403b09
+                    for member in ns:
403b09
+                        path = '{}/{}'.format(key, member)
403b09
+                        schema.writestr(path, json.dumps(ns[member]))
403b09
+                else:
403b09
+                    schema_info[key] = value
403b09
+
403b09
+            schema.writestr(self.schema_info_path, json.dumps(schema_info))
403b09
 
403b09
     def _read(self, path):
403b09
-        with self._open_archive('r') as zf:
403b09
+        with self._open_schema(self._fingerprint, 'r') as zf:
403b09
             return json.loads(zf.read(path))
403b09
 
403b09
     def read_namespace_member(self, namespace, member):
403b09
-        path = self.ns_member_path_template.format(namespace, member)
403b09
-        return self._read(path)
403b09
+        value = self._dict[namespace][member]
403b09
+
403b09
+        if (not value) or ('full_name' not in value):
403b09
+            path = '{}/{}'.format(namespace, member)
403b09
+            value = self._dict[namespace].setdefault(
403b09
+                member, {}
403b09
+            ).update(self._read(path))
403b09
+
403b09
+        return value
403b09
 
403b09
     def iter_namespace(self, namespace):
403b09
-        pattern = self.ns_member_pattern_template.format(namespace)
403b09
-        with self._open_archive('r') as zf:
403b09
-            for name in zf.namelist():
403b09
-                r = re.match(pattern, name)
403b09
-                if r:
403b09
-                    yield r.groups('name')[0]
403b09
+        return iter(self._dict[namespace])
403b09
 
403b09
 
403b09
 def get_package(api, client):
403b09
     try:
403b09
         schema = api._schema
403b09
     except AttributeError:
403b09
-        schema = Schema(api, client)
403b09
-        object.__setattr__(api, '_schema', schema)
403b09
+        with ServerInfo(api.env.hostname) as server_info:
403b09
+            schema = Schema(api, server_info, client)
403b09
+            object.__setattr__(api, '_schema', schema)
403b09
 
403b09
     fingerprint = str(schema['fingerprint'])
403b09
     package_name = '{}${}'.format(__name__, fingerprint)
403b09
@@ -509,10 +563,9 @@ def get_package(api, client):
403b09
     module = types.ModuleType(module_name)
403b09
     module.__file__ = os.path.join(package_dir, 'plugins.py')
403b09
     module.register = plugable.Registry()
403b09
-    for key, plugin_cls in (('commands', _SchemaCommandPlugin),
403b09
-                            ('classes', _SchemaObjectPlugin)):
403b09
-        for full_name in schema[key]:
403b09
-            plugin = plugin_cls(full_name)
403b09
+    for plugin_cls in (_SchemaCommandPlugin, _SchemaObjectPlugin):
403b09
+        for full_name in schema[plugin_cls.schema_key]:
403b09
+            plugin = plugin_cls(str(full_name))
403b09
             plugin = module.register()(plugin)
403b09
     sys.modules[module_name] = module
403b09
 
403b09
-- 
403b09
2.7.4
403b09