base_lmdb_store.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import hashlib
  2. from abc import ABCMeta, abstractmethod
  3. from contextlib import contextmanager
  4. from os import makedirs, path
  5. import lmdb
  6. from lakesuperior import env
  7. class BaseLmdbStore(metaclass=ABCMeta):
  8. """
  9. Generic LMDB store abstract class.
  10. This class contains convenience method to create an LMDB store for any
  11. purpose and provides some convenience methods to wrap cursors and
  12. transactions into contexts.
  13. This interface can be subclassed for specific storage back ends. It is
  14. *not* used for :py:class:`~lakesuperior.store.ldp_rs.lmdb_store.LmdbStore`
  15. which has a more complex lifecycle and setup.
  16. Example usage::
  17. >>> class MyStore(BaseLmdbStore):
  18. ... path = '/base/store/path'
  19. ... db_labels = ('db1', 'db2')
  20. ...
  21. >>> ms = MyStore()
  22. >>> # "with" wraps the operation in a transaction.
  23. >>> with ms.cur(index='db1', write=True):
  24. ... cur.put(b'key1', b'val1')
  25. True
  26. """
  27. path = None
  28. """
  29. Filesystem path where the database environment is stored.
  30. This is a mandatory value for implementations.
  31. :rtype: str
  32. """
  33. db_labels = None
  34. """
  35. List of databases in the DB environment by label.
  36. If the environment has only one database, do not override this value (i.e.
  37. leave it to ``None``).
  38. :rtype: tuple(str)
  39. """
  40. options = {}
  41. """
  42. LMDB environment option overrides. Setting this is not required.
  43. See `LMDB documentation
  44. <http://lmdb.readthedocs.io/en/release/#environment-class`_ for details
  45. on available options.
  46. Default values are available for the following options:
  47. - ``map_size``: 1 Gib
  48. - ``max_dbs``: dependent on the number of DBs defined in
  49. :py:meth:``db_labels``. Only override if necessary.
  50. - ``max_spare_txns``: dependent on the number of threads, if accessed via
  51. WSGI, or ``1`` otherwise. Only override if necessary.
  52. :rtype: dict
  53. """
  54. def __init__(self, create=True):
  55. """
  56. Initialize DB environment and databases.
  57. """
  58. if not path.exists(self.path) and create is True:
  59. try:
  60. makedirs(self.path)
  61. except Exception as e:
  62. raise IOError(
  63. 'Could not create the database at {}. Error: {}'.format(
  64. self.path, e))
  65. options = self.options
  66. if not options.get('max_dbs'):
  67. options['max_dbs'] = len(self.db_labels)
  68. if options.get('max_spare_txns', False):
  69. options['max_spare_txns'] = (
  70. env.wsgi_options['workers']
  71. if getattr(env, 'wsgi_options', False)
  72. else 1)
  73. logger.info('Max LMDB readers: {}'.format(
  74. options['max_spare_txns']))
  75. self._dbenv = lmdb.open(self.path, **options)
  76. if self.db_labels is not None:
  77. self._dbs = {
  78. label: self._dbenv.open_db(
  79. label.encode('ascii'), create=create)
  80. for label in self.db_labels}
  81. @property
  82. def dbenv(self):
  83. """
  84. LMDB environment handler.
  85. :rtype: :py:class:`lmdb.Environment`
  86. """
  87. return self._dbenv
  88. @property
  89. def dbs(self):
  90. """
  91. List of databases in the environment, as LMDB handles.
  92. These handles can be used to begin transactions.
  93. :rtype: tuple
  94. """
  95. return self._dbs
  96. @contextmanager
  97. def txn(self, write=False):
  98. """
  99. Transaction context manager.
  100. :param bool write: Whether a write transaction is to be opened.
  101. :rtype: lmdb.Transaction
  102. """
  103. try:
  104. txn = self.dbenv.begin(write=write)
  105. yield txn
  106. txn.commit()
  107. except:
  108. txn.abort()
  109. raise
  110. finally:
  111. txn = None
  112. @contextmanager
  113. def cur(self, index=None, txn=None, write=False):
  114. """
  115. Handle a cursor on a database by its index as a context manager.
  116. An existing transaction can be used, otherwise a new one will be
  117. automatically opened and closed within the cursor context.
  118. :param str index: The database index. If not specified, a cursor is
  119. opened for the main database environment.
  120. :param lmdb.Transaction txn: Existing transaction to use. If not
  121. specified, a new transaction will be opened.
  122. :param bool write: Whether a write transaction is to be opened. Only
  123. meaningful if ``txn`` is ``None``.
  124. :rtype: lmdb.Cursor
  125. """
  126. db = None if index is None else self.dbs[index]
  127. if txn is None:
  128. with self.txn(write=write) as _txn:
  129. cur = _txn.cursor(db)
  130. yield cur
  131. cur.close()
  132. else:
  133. try:
  134. cur = txn.cursor(db)
  135. yield cur
  136. finally:
  137. if cur:
  138. cur.close()
  139. cur = None