Shared Memory Dictionary utilizing Posix IPC semaphores and shared memory segments and offering permanent disk storage of data if required.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
11KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Standard library imports
  4. import base64
  5. try:
  6. from collections.abc import MutableMapping
  7. except ImportError:
  8. from collections import MutableMapping
  9. from contextlib import contextmanager
  10. import hashlib
  11. import logging
  12. from math import ceil
  13. import mmap
  14. import os
  15. import pickle # nosec
  16. import sys
  17. import threading
  18. # Related third party imports (If you used pip/apt/yum to install)
  19. import posix_ipc
  20. import six
  21. # Local application/library specific imports (Look ma! I wrote it myself!)
  22. from ._version import __version__
  23. __author__ = "Nate Bohman"
  24. __credits__ = ["Nate Bohman"]
  25. __license__ = "LGPL-3"
  26. __maintainer__ = "Nate Bohman"
  27. __email__ = "natrinicle@natrinicle.com"
  28. __status__ = "Production"
  29. logger = logging.getLogger(__name__) # pylint: disable=invalid-name
  30. class SHMDict(MutableMapping):
  31. """Python shared memory dictionary."""
  32. def __init__(self, name, persist=False, lock_timeout=30, auto_unlock=False):
  33. """Standard init method.
  34. :param name: Name for shared memory and semaphore if volatile
  35. or path to file if persistent.
  36. :param persist: True if name is the path to a file and this
  37. shared memory dictionary should be written
  38. out to the file for persistence between runs
  39. and/or processes.
  40. :param lock_timeout: Time in seconds before giving up on
  41. acquiring an exclusive lock to the
  42. dictionary.
  43. :param auto_unlock: If the lock_timeout is hit, and this
  44. is True, automatically bypass the
  45. lock and use the dictionary anyway.
  46. :type name: :class:`str`
  47. :type persist: :class:`bool`
  48. :type lock_timeout: :class:`int` or :class:`float`
  49. :type auto_unlock: :class:`bool`
  50. """
  51. self.name = name
  52. self.persist_file = None
  53. self.lock_timeout = lock_timeout
  54. self.auto_unlock = auto_unlock
  55. self._semaphore = None
  56. self._map_file = None
  57. self.__thread_local = threading.local()
  58. self.__thread_local.semaphore = False
  59. self.__internal_dict = None
  60. self.__dirty = False
  61. if persist is True:
  62. self.persist_file = self.name
  63. if self.persist_file.startswith("~"):
  64. self.persist_file = os.path.expanduser(self.persist_file)
  65. self.persist_file = os.path.abspath(os.path.realpath(self.persist_file))
  66. super(SHMDict, self).__init__()
  67. def _safe_name(self, prefix=""):
  68. """IPC object safe name creator.
  69. Semaphores and Shared Mmeory names allow up to 256 characters (dependong on OS) and must
  70. begin with a /.
  71. :param prefix: A string to prepend followed by _ and
  72. then the dictionary's name.
  73. :type prefix: :class:`str`
  74. """
  75. # Hash lengths
  76. # SHA1: 28
  77. # SHA256: 44
  78. # SHA512: 88
  79. sha_hash = hashlib.sha512()
  80. sha_hash.update("_".join([prefix, str(self.name)]).encode("utf-8"))
  81. b64_encode = base64.urlsafe_b64encode(sha_hash.digest())
  82. return "/{}".format(b64_encode)
  83. @property
  84. def safe_sem_name(self):
  85. """Unique semaphore name based on the dictionary name."""
  86. return self._safe_name("sem")
  87. @property
  88. def safe_shm_name(self):
  89. """Unique shared memory segment name based on the dictionary name."""
  90. return self._safe_name("shm")
  91. @property
  92. def semaphore(self):
  93. """Create or return already existing semaphore."""
  94. if self._semaphore is not None:
  95. return self._semaphore
  96. try:
  97. self._semaphore = posix_ipc.Semaphore(self.safe_sem_name)
  98. except posix_ipc.ExistentialError:
  99. self._semaphore = posix_ipc.Semaphore(
  100. self.safe_sem_name, flags=posix_ipc.O_CREAT, initial_value=1
  101. )
  102. return self._semaphore
  103. @property
  104. def shared_mem(self):
  105. """Create or return already existing shared memory object."""
  106. try:
  107. return posix_ipc.SharedMemory(
  108. self.safe_shm_name, size=len(pickle.dumps(self.__internal_dict))
  109. )
  110. except posix_ipc.ExistentialError:
  111. return posix_ipc.SharedMemory(
  112. self.safe_shm_name, flags=posix_ipc.O_CREX, size=posix_ipc.PAGE_SIZE
  113. )
  114. @property
  115. def map_file(self):
  116. """Create or return mmap file resizing if necessary."""
  117. if self._map_file is None:
  118. self._map_file = mmap.mmap(self.shared_mem.fd, self.shared_mem.size)
  119. self.shared_mem.close_fd()
  120. self._map_file.resize(
  121. int(
  122. ceil(float(len(pickle.dumps(self.__internal_dict, 2))) / mmap.PAGESIZE)
  123. * mmap.PAGESIZE
  124. )
  125. )
  126. return self._map_file
  127. def __load_dict(self):
  128. """Load dictionary from shared memory or file if persistent and memory empty."""
  129. # Read in internal data from map_file
  130. self.map_file.seek(0)
  131. try:
  132. self.__internal_dict = pickle.load(self.map_file) # nosec
  133. except (KeyError, pickle.UnpicklingError, EOFError):
  134. # Curtis Pullen found that Python 3.4 throws EOFError
  135. # instead of UnpicklingError that Python 3.6 throws
  136. # when attempting to unpickle an empty file.
  137. pass
  138. # If map_file is empty and persist_file is true, treat
  139. # self.name as filename and attempt to load from disk.
  140. if self.__internal_dict is None and self.persist_file is not None:
  141. try:
  142. with open(self.persist_file, "rb") as pfile:
  143. self.__internal_dict = pickle.load(pfile) # nosec
  144. except IOError:
  145. pass
  146. # If map_file is empty, persist_file is False or
  147. # self.name is empty create a new empty dictionary.
  148. if self.__internal_dict is None:
  149. self.__internal_dict = {}
  150. def __save_dict(self):
  151. """Save dictionary into shared memory and file if persistent."""
  152. # Write out internal dict to map_file
  153. if self.__dirty is True:
  154. self.map_file.seek(0)
  155. pickle.dump(self.__internal_dict, self.map_file, 2)
  156. if self.persist_file is not None:
  157. with open(self.persist_file, "wb") as pfile:
  158. pickle.dump(self.__internal_dict, pfile, 2)
  159. self.__dirty = False
  160. def _acquire_lock(self):
  161. """Acquire an exclusive dict lock.
  162. Loads dictionary data from memory or disk (if persistent) to
  163. ensure data is up to date when lock is requested.
  164. .. warnings also::
  165. MacOS has a number of shortcomings with regards to
  166. semaphores and shared memory segments, this is one
  167. method contains one of them.
  168. When the timeout is > 0, the call will wait no longer than
  169. timeout seconds before either returning (having acquired
  170. the semaphore) or raising a BusyError.
  171. On platforms that don't support the sem_timedwait() API,
  172. a timeout > 0 is treated as infinite. The call will not
  173. return until its wait condition is satisfied.
  174. Most platforms provide sem_timedwait(). macOS is a notable
  175. exception. The module's Boolean constant
  176. SEMAPHORE_TIMEOUT_SUPPORTED is True on platforms that
  177. support sem_timedwait().
  178. -- http://semanchuk.com/philip/posix_ipc/
  179. """
  180. if self.__thread_local.semaphore is False:
  181. try:
  182. self.semaphore.acquire(self.lock_timeout)
  183. self.__thread_local.semaphore = True
  184. except posix_ipc.BusyError:
  185. if self.auto_unlock is True:
  186. self.__thread_local.semaphore = True
  187. else:
  188. six.reraise(*sys.exc_info())
  189. self.__load_dict()
  190. def _release_lock(self):
  191. """Release the exclusive semaphore lock."""
  192. if self.__thread_local.semaphore is True:
  193. self.__save_dict()
  194. self.semaphore.release()
  195. self.__thread_local.semaphore = False
  196. @contextmanager
  197. def exclusive_lock(self):
  198. """A context manager for the lock to allow with statements for exclusive access."""
  199. self._acquire_lock()
  200. yield
  201. self._release_lock()
  202. def __del__(self):
  203. """Destroy the object nicely."""
  204. self.map_file.close()
  205. self.shared_mem.unlink()
  206. self.semaphore.unlink()
  207. def __setitem__(self, key, value):
  208. """Set a key in the dictionary to a value."""
  209. with self.exclusive_lock():
  210. self.__internal_dict[key] = value
  211. self.__dirty = True
  212. def __getitem__(self, key):
  213. """Get the value of a key from the dictionary."""
  214. with self.exclusive_lock():
  215. return self.__internal_dict[key]
  216. def __repr__(self):
  217. """Represent the dictionary in a human readable format."""
  218. with self.exclusive_lock():
  219. return repr(self.__internal_dict)
  220. def __len__(self):
  221. """Return the length of the dictionary."""
  222. with self.exclusive_lock():
  223. return len(self.__internal_dict)
  224. def __delitem__(self, key):
  225. """Remove an item from the dictionary."""
  226. with self.exclusive_lock():
  227. del self.__internal_dict[key]
  228. self.__dirty = True
  229. def clear(self):
  230. """Completely clear the dictionary."""
  231. with self.exclusive_lock():
  232. self.__dirty = True
  233. return self.__internal_dict.clear()
  234. def copy(self):
  235. """Create and return a copy of the internal dictionary."""
  236. with self.exclusive_lock():
  237. return self.__internal_dict.copy()
  238. def has_key(self, key):
  239. """Return true if a key is in the internal dictionary."""
  240. with self.exclusive_lock():
  241. return key in self.__internal_dict
  242. def __eq__(self, other):
  243. """Shared memory dictionary equality check with another shared memory dictionary."""
  244. return isinstance(other, SHMDict) and self.safe_shm_name == other.safe_shm_name
  245. def __ne__(self, other):
  246. """Shared memory dictionary non-equality check with another shared memory dictionary."""
  247. return not isinstance(other, SHMDict) or (
  248. isinstance(other, SHMDict) and self.safe_shm_name != other.safe_shm_name
  249. )
  250. def __contains__(self, key):
  251. """Check if a key exists inside the dictionary."""
  252. with self.exclusive_lock():
  253. return key in self.__internal_dict
  254. def __iter__(self):
  255. """Iterate through the dictionary keys."""
  256. with self.exclusive_lock():
  257. return iter(self.__internal_dict)