share.py 8.3KB

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