1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from datetime import datetime, timedelta
6from file_system import FileNotFoundError
7from future import Future
8from patcher import Patcher
9
10_VERSION_CACHE_MAXAGE = timedelta(seconds=5)
11
12''' Append @version for keys to distinguish between different patchsets of
13an issue.
14'''
15def _MakeKey(path, version):
16  return '%s@%s' % (path, version)
17
18def _ToObjectStoreValue(raw_value, version):
19  return dict((_MakeKey(key, version), raw_value[key])
20              for key in raw_value)
21
22def _FromObjectStoreValue(raw_value):
23  return dict((key[0:key.rfind('@')], raw_value[key]) for key in raw_value)
24
25class _AsyncUncachedFuture(object):
26  def __init__(self,
27               version,
28               paths,
29               cached_value,
30               missing_paths,
31               fetch_delegate,
32               object_store):
33    self._version = version
34    self._paths = paths
35    self._cached_value = cached_value
36    self._missing_paths = missing_paths
37    self._fetch_delegate = fetch_delegate
38    self._object_store = object_store
39
40  def Get(self):
41    uncached_value = self._fetch_delegate.Get()
42    self._object_store.SetMulti(_ToObjectStoreValue(uncached_value,
43                                                    self._version))
44
45    for path in self._missing_paths:
46      if uncached_value.get(path) is None:
47        raise FileNotFoundError('File %s was not found in the patch.' % path)
48      self._cached_value[path] = uncached_value[path]
49
50    return self._cached_value
51
52class CachingRietveldPatcher(Patcher):
53  ''' CachingRietveldPatcher implements a caching layer on top of |patcher|.
54  In theory, it can be used with any class that implements Patcher. But this
55  class assumes that applying to all patched files at once is more efficient
56  than applying to individual files.
57  '''
58  def __init__(self,
59               rietveld_patcher,
60               object_store_creator,
61               test_datetime=datetime):
62    self._patcher = rietveld_patcher
63    def create_object_store(category):
64      return object_store_creator.Create(
65          CachingRietveldPatcher,
66          category='%s/%s' % (rietveld_patcher.GetIdentity(), category))
67    self._version_object_store = create_object_store('version')
68    self._list_object_store = create_object_store('list')
69    self._file_object_store = create_object_store('file')
70    self._datetime = test_datetime
71
72  def GetVersion(self):
73    key = 'version'
74    value = self._version_object_store.Get(key).Get()
75    if value is not None:
76      version, time = value
77      if self._datetime.now() - time < _VERSION_CACHE_MAXAGE:
78        return version
79
80    version = self._patcher.GetVersion()
81    self._version_object_store.Set(key,
82                                (version, self._datetime.now()))
83    return version
84
85  def GetPatchedFiles(self, version=None):
86    if version is None:
87      version = self.GetVersion()
88    patched_files = self._list_object_store.Get(version).Get()
89    if patched_files is not None:
90      return patched_files
91
92    patched_files = self._patcher.GetPatchedFiles(version)
93    self._list_object_store.Set(version, patched_files)
94    return patched_files
95
96  def Apply(self, paths, file_system, version=None):
97    if version is None:
98      version = self.GetVersion()
99    added, deleted, modified = self.GetPatchedFiles(version)
100    cached_value = _FromObjectStoreValue(self._file_object_store.
101        GetMulti([_MakeKey(path, version) for path in paths]).Get())
102    missing_paths = list(set(paths) - set(cached_value.keys()))
103    if len(missing_paths) == 0:
104      return Future(value=cached_value)
105
106    return _AsyncUncachedFuture(version,
107                                paths,
108                                cached_value,
109                                missing_paths,
110                                self._patcher.Apply(set(added) | set(modified),
111                                                    None,
112                                                    version),
113                                self._file_object_store)
114
115  def GetIdentity(self):
116    return self._patcher.GetIdentity()
117