base_lmdb_store.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. """
  17. path = None
  18. """
  19. Filesystem path where the database environment is stored.
  20. This is a mandatory value for implementations.
  21. :rtype: str
  22. """
  23. db_labels = None
  24. """
  25. List of databases in the DB environment by label.
  26. If the environment has only one database, do not override this value (i.e.
  27. leave it to ``None``).
  28. :rtype: tuple(str)
  29. """
  30. options = {}
  31. """
  32. LMDB environment option overrides. Setting this is not required.
  33. See `LMDB documentation
  34. <http://lmdb.readthedocs.io/en/release/#environment-class`_ for details
  35. on available options.
  36. Default values are available for the following options:
  37. - ``map_size``: 1 Gib
  38. - ``max_dbs``: dependent on the number of DBs defined in
  39. :py:meth:``db_labels``. Only override if necessary.
  40. - ``max_spare_txns``: dependent on the number of threads, if accessed via
  41. WSGI, or ``1`` otherwise. Only override if necessary.
  42. :rtype: dict
  43. """
  44. def __init__(self, create=True):
  45. """
  46. Initialize DB environment and databases.
  47. """
  48. if not path.exists(self.path) and create is True:
  49. try:
  50. makedirs(self.path)
  51. except Exception as e:
  52. raise IOError(
  53. 'Could not create the database at {}. Error: {}'.format(
  54. self.path, e))
  55. options = self.options
  56. if not options.get('max_dbs'):
  57. options['max_dbs'] = len(self.db_labels)
  58. if options.get('max_spare_txns', False):
  59. options['max_spare_txns'] = (
  60. env.wsgi_options['workers']
  61. if getattr(env, 'wsgi_options', False)
  62. else 1)
  63. logger.info('Max LMDB readers: {}'.format(
  64. options['max_spare_txns']))
  65. self._dbenv = lmdb.open(self.path, **options)
  66. if self.db_labels is not None:
  67. self._dbs = {
  68. label: self._dbenv.open_db(
  69. label.encode('ascii'), create=create)
  70. for label in self.db_labels}
  71. @property
  72. def dbenv(self):
  73. """
  74. LMDB environment handler.
  75. :rtype: :py:class:`lmdb.Environment`
  76. """
  77. return self._dbenv
  78. @property
  79. def dbs(self):
  80. """
  81. List of databases in the environment, as LMDB handles.
  82. These handles can be used to begin transactions.
  83. :rtype: tuple
  84. """
  85. return self._dbs
  86. @contextmanager
  87. def txn(self, write=False):
  88. """
  89. Transaction context manager.
  90. :param bool write: Whether a write transaction is to be opened.
  91. """
  92. try:
  93. txn = self.dbenv.begin(write=write)
  94. yield txn
  95. txn.commit()
  96. except:
  97. txn.abort()
  98. raise
  99. finally:
  100. txn = None
  101. @contextmanager
  102. def cur(self, index=None, txn=None, write=False):
  103. """
  104. Handle a cursor on a database by its index as a context manager.
  105. An existing transaction can be used, otherwise a new one will be
  106. automatically opened and closed within the cursor context.
  107. :param str index: The database index. If not specified, a cursor is
  108. opened for the main database environment.
  109. :param lmdb.Transaction txn: Existing transaction to use. If not
  110. specified, a new transaction will be opened.
  111. :param bool write: Whether a write transaction is to be opened. Only
  112. meaningful if ``txn`` is ``None``.
  113. :rtype: lmdb.Cursor
  114. """
  115. if txn is None:
  116. txn = self.txn(write=write)
  117. db = None if index is None else self.dbs[index]
  118. with txn as _txn:
  119. try:
  120. cur = _txn.cursor(db)
  121. yield cur
  122. finally:
  123. cur.close()