share.py 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. def remove(self, object_):
  79. super().remove(object_)
  80. self.shared.set(self.shared_data.get_final_key(self.instance), list(self))
  81. def extend(self, iterable) -> None:
  82. super().extend(iterable)
  83. self.shared.set(self.shared_data.get_final_key(self.instance), list(self))
  84. # TODO: Cover all methods
  85. class SharedDataManager(object):
  86. """
  87. This object is designed to own shared memory between processes. It must be feed (with set method) before
  88. start of processes. Processes will only be able to access shared memory filled here before start.
  89. """
  90. def __init__(self, clear: bool=True):
  91. self._r = redis.StrictRedis(host='localhost', port=6379, db=0) # TODO: configs
  92. self._shared_data_list = [] # type: typing.List[SharedData]
  93. self._data = {}
  94. self._modified_keys = set()
  95. self._default_values = {}
  96. self._special_types = {} # type: typing.Dict[str, typing.Union[typing.Type[TrackedDict], typing.Type[TrackedList]]] # nopep8
  97. if clear:
  98. self.clear()
  99. def clear(self) -> None:
  100. self._r.flushdb()
  101. self._data = {}
  102. self._modified_keys = set()
  103. def reset(self) -> None:
  104. for key, value in self._default_values.items():
  105. self.set(key, value)
  106. self.commit()
  107. self._data = {}
  108. def purge_data(self):
  109. self._data = {}
  110. def set(self, key: str, value: typing.Any) -> None:
  111. self._data[key] = value
  112. self._modified_keys.add(key)
  113. def get(self, key: str) -> typing.Any:
  114. try:
  115. return self._data[key]
  116. except KeyError:
  117. database_value = self._r.get(key)
  118. if database_value is None:
  119. # We not allow None value storage
  120. raise UnknownSharedData('No shared data for key "{}"'.format(key))
  121. value = pickle.loads(database_value)
  122. self._data[key] = value
  123. return self._data[key]
  124. def commit(self) -> None:
  125. for key in self._modified_keys:
  126. value = self.get(key)
  127. self._r.set(key, pickle.dumps(value))
  128. self._modified_keys = set()
  129. def refresh(self) -> None:
  130. self._data = {}
  131. def make_index(
  132. self,
  133. shared_data_index_class: typing.Type[SharedDataIndex],
  134. key: str,
  135. *args: typing.Any,
  136. **kwargs: typing.Any
  137. ) -> SharedDataIndex:
  138. return shared_data_index_class(self, key, *args, **kwargs)
  139. def create_self(
  140. self,
  141. key: str,
  142. default: typing.Any,
  143. indexes: typing.List[SharedDataIndex]=None,
  144. ):
  145. return self.create(key, self_type=True, value=default, indexes=indexes)
  146. def create(
  147. self,
  148. key: str,
  149. value: typing.Any,
  150. self_type: bool=False,
  151. indexes: typing.List[SharedDataIndex]=None,
  152. ):
  153. # TODO: Store all keys and forbid re-use one
  154. indexes = indexes or []
  155. shared_data = SharedData(
  156. key=key,
  157. self_type=self_type,
  158. default=value,
  159. )
  160. self._shared_data_list.append(shared_data)
  161. def fget(instance):
  162. final_key = shared_data.get_final_key(instance)
  163. try:
  164. value_ = self.get(final_key)
  165. if not shared_data.is_special_type:
  166. return value_
  167. else:
  168. return shared_data.special_type(value_, shared_data=shared_data, shared=self, instance=instance)
  169. except UnknownSharedData:
  170. # If no data in database, value for this shared_data have been never set
  171. self.set(final_key, shared_data.default_value)
  172. self._default_values[final_key] = shared_data.default_value
  173. return self.get(final_key)
  174. def fset(instance, value_):
  175. final_key = shared_data.get_final_key(instance)
  176. try:
  177. previous_value = self.get(final_key)
  178. for index in indexes:
  179. index.remove(previous_value)
  180. except UnknownSharedData:
  181. pass # If no shared data, no previous value to remove
  182. self.set(final_key, value_)
  183. for index in indexes:
  184. index.add(value_)
  185. def fdel(self_):
  186. raise SynergineException('You cannot delete a shared data: not implemented yet')
  187. shared_property = property(
  188. fget=fget,
  189. fset=fset,
  190. fdel=fdel,
  191. )
  192. # A simple shared data can be set now because no need to build key with instance id
  193. if not self_type:
  194. self.set(key, shared_data.default_value)
  195. self._default_values[key] = shared_data.default_value
  196. return shared_property
  197. # TODO: Does exist a way to permit overload of SharedDataManager class ?
  198. shared = SharedDataManager()
  199. class ListIndex(SharedDataIndex):
  200. def add(self, value):
  201. try:
  202. values = self.shared_data_manager.get(self.key)
  203. except UnknownSharedData:
  204. values = []
  205. values.append(value)
  206. self.shared_data_manager.set(self.key, values)
  207. def remove(self, value):
  208. values = self.shared_data_manager.get(self.key)
  209. values.remove(value)
  210. self.shared_data_manager.set(self.key, values)