share.py 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # coding: utf-8
  2. import pickle
  3. import typing
  4. import redis
  5. from synergine2.base import IdentifiedObject
  6. from synergine2.exceptions import SynergineException
  7. from synergine2.exceptions import UnknownSharedData
  8. class NoSharedDataInstance(SynergineException):
  9. pass
  10. class SharedDataIndex(object):
  11. def __init__(
  12. self,
  13. shared_data_manager: 'SharedDataManager',
  14. key: str,
  15. ) -> None:
  16. self.shared_data_manager = shared_data_manager
  17. self.key = key
  18. def add(self, value: typing.Any) -> None:
  19. raise NotImplementedError()
  20. def remove(self, value: typing.Any) -> None:
  21. raise NotImplementedError()
  22. class SharedData(object):
  23. def __init__(
  24. self, key: str,
  25. self_type: bool=False,
  26. default: typing.Any=None,
  27. ) -> None:
  28. """
  29. :param key: shared data key
  30. :param self_type: if it is a magic shared data where real key is association of key and instance id
  31. :param default: default/initial value to shared data. Can be a callable to return list or dict
  32. """
  33. self._key = key
  34. self.self_type = self_type
  35. self._default = default
  36. self.is_special_type = isinstance(self.default_value, (list, dict))
  37. if self.is_special_type:
  38. if isinstance(self.default_value, list):
  39. self.special_type = TrackedList
  40. elif isinstance(self.default_value, dict):
  41. self.special_type = TrackedDict
  42. else:
  43. raise NotImplementedError()
  44. def get_final_key(self, instance: IdentifiedObject) -> str:
  45. if self.self_type:
  46. return '{}_{}'.format(instance.id, self._key)
  47. return self._key
  48. @property
  49. def default_value(self) -> typing.Any:
  50. if callable(self._default):
  51. return self._default()
  52. return self._default
  53. class TrackedDict(dict):
  54. base = dict
  55. def __init__(self, seq=None, **kwargs):
  56. self.shared_data = kwargs.pop('shared_data')
  57. self.shared = kwargs.pop('shared')
  58. self.instance = kwargs.pop('instance')
  59. super().__init__(seq, **kwargs)
  60. def __setitem__(self, key, value):
  61. super().__setitem__(key, value)
  62. self.shared.set(self.shared_data.get_final_key(self.instance), dict(self))
  63. def setdefault(self, k, d=None):
  64. v = super().setdefault(k, d)
  65. self.shared.set(self.shared_data.get_final_key(self.instance), dict(self))
  66. return v
  67. # TODO: Cover all methods
  68. class TrackedList(list):
  69. base = list
  70. def __init__(self, seq=(), **kwargs):
  71. self.shared_data = kwargs.pop('shared_data')
  72. self.shared = kwargs.pop('shared')
  73. self.instance = kwargs.pop('instance')
  74. super().__init__(seq)
  75. def append(self, p_object):
  76. super().append(p_object)
  77. self.shared.set(self.shared_data.get_final_key(self.instance), list(self))
  78. # TODO: Cover all methods
  79. class SharedDataManager(object):
  80. """
  81. This object is designed to own shared memory between processes. It must be feed (with set method) before
  82. start of processes. Processes will only be able to access shared memory filled here before start.
  83. """
  84. def __init__(self, clear: bool=True):
  85. self._r = redis.StrictRedis(host='localhost', port=6379, db=0) # TODO: configs
  86. self._shared_data_list = [] # type: typing.List[SharedData]
  87. self._data = {}
  88. self._modified_keys = set()
  89. self._default_values = {}
  90. self._special_types = {} # type: typing.Dict[str, typing.Union[typing.Type[TrackedDict], typing.Type[TrackedList]]] # nopep8
  91. if clear:
  92. self.clear()
  93. def clear(self) -> None:
  94. self._r.flushdb()
  95. self._data = {}
  96. self._modified_keys = set()
  97. def reset(self) -> None:
  98. for key, value in self._default_values.items():
  99. self.set(key, value)
  100. self.commit()
  101. self._data = {}
  102. def purge_data(self):
  103. self._data = {}
  104. def set(self, key: str, value: typing.Any) -> None:
  105. self._data[key] = value
  106. self._modified_keys.add(key)
  107. def get(self, key: str) -> typing.Any:
  108. try:
  109. return self._data[key]
  110. except KeyError:
  111. database_value = self._r.get(key)
  112. if database_value is None:
  113. # We not allow None value storage
  114. raise UnknownSharedData('No shared data for key "{}"'.format(key))
  115. value = pickle.loads(database_value)
  116. self._data[key] = value
  117. return self._data[key]
  118. def commit(self) -> None:
  119. for key in self._modified_keys:
  120. value = self.get(key)
  121. self._r.set(key, pickle.dumps(value))
  122. self._modified_keys = set()
  123. def refresh(self) -> None:
  124. self._data = {}
  125. def make_index(
  126. self,
  127. shared_data_index_class: typing.Type[SharedDataIndex],
  128. key: str,
  129. *args: typing.Any,
  130. **kwargs: typing.Any
  131. ) -> SharedDataIndex:
  132. return shared_data_index_class(self, key, *args, **kwargs)
  133. def create_self(
  134. self,
  135. key: str,
  136. default: typing.Any,
  137. indexes: typing.List[SharedDataIndex]=None,
  138. ):
  139. return self.create(key, self_type=True, value=default, indexes=indexes)
  140. def create(
  141. self,
  142. key: str,
  143. value: typing.Any,
  144. self_type: bool=False,
  145. indexes: typing.List[SharedDataIndex]=None,
  146. ):
  147. # TODO: Store all keys and forbid re-use one
  148. indexes = indexes or []
  149. shared_data = SharedData(
  150. key=key,
  151. self_type=self_type,
  152. default=value,
  153. )
  154. self._shared_data_list.append(shared_data)
  155. def fget(instance):
  156. final_key = shared_data.get_final_key(instance)
  157. try:
  158. value_ = self.get(final_key)
  159. if not shared_data.is_special_type:
  160. return value_
  161. else:
  162. return shared_data.special_type(value_, shared_data=shared_data, shared=self, instance=instance)
  163. except UnknownSharedData:
  164. # If no data in database, value for this shared_data have been never set
  165. self.set(final_key, shared_data.default_value)
  166. self._default_values[final_key] = shared_data.default_value
  167. return self.get(final_key)
  168. def fset(instance, value_):
  169. final_key = shared_data.get_final_key(instance)
  170. try:
  171. previous_value = self.get(final_key)
  172. for index in indexes:
  173. index.remove(previous_value)
  174. except UnknownSharedData:
  175. pass # If no shared data, no previous value to remove
  176. self.set(final_key, value_)
  177. for index in indexes:
  178. index.add(value_)
  179. def fdel(self_):
  180. raise SynergineException('You cannot delete a shared data: not implemented yet')
  181. shared_property = property(
  182. fget=fget,
  183. fset=fset,
  184. fdel=fdel,
  185. )
  186. # A simple shared data can be set now because no need to build key with instance id
  187. if not self_type:
  188. self.set(key, shared_data.default_value)
  189. self._default_values[key] = shared_data.default_value
  190. return shared_property
  191. # TODO: Does exist a way to permit overload of SharedDataManager class ?
  192. shared = SharedDataManager()
  193. class ListIndex(SharedDataIndex):
  194. def add(self, value):
  195. try:
  196. values = self.shared_data_manager.get(self.key)
  197. except UnknownSharedData:
  198. values = []
  199. values.append(value)
  200. self.shared_data_manager.set(self.key, values)
  201. def remove(self, value):
  202. values = self.shared_data_manager.get(self.key)
  203. values.remove(value)
  204. self.shared_data_manager.set(self.key, values)