base_lmdb_store.pyx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. import logging
  2. import os
  3. import threading
  4. import multiprocessing
  5. from contextlib import contextmanager
  6. from os import makedirs, path
  7. from shutil import rmtree
  8. from lakesuperior import env
  9. from lakesuperior.cy_include cimport cylmdb as lmdb
  10. from libc cimport errno
  11. from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free
  12. from cython.parallel import parallel, prange
  13. logger = logging.getLogger(__name__)
  14. cdef void _check(int rc, str message='') except *:
  15. """
  16. Check return code.
  17. """
  18. if rc == lmdb.MDB_NOTFOUND:
  19. raise KeyNotFoundError()
  20. if rc == lmdb.MDB_KEYEXIST:
  21. raise KeyExistsError()
  22. if rc == errno.EINVAL:
  23. raise InvalidParamError(
  24. 'Invalid LMDB parameter error.\n'
  25. 'Please verify that a transaction is open and valid for the '
  26. 'current operation.'
  27. )
  28. if rc != lmdb.MDB_SUCCESS:
  29. out_msg = (
  30. message + '\nInternal error ({}): '.format(rc)
  31. if len(message) else 'LMDB Error ({}): '.format(rc))
  32. out_msg += lmdb.mdb_strerror(rc).decode()
  33. raise LmdbError(out_msg)
  34. class LmdbError(Exception):
  35. pass
  36. class KeyNotFoundError(LmdbError):
  37. pass
  38. class KeyExistsError(LmdbError):
  39. pass
  40. class InvalidParamError(LmdbError):
  41. pass
  42. cdef class BaseLmdbStore:
  43. """
  44. Generic LMDB store abstract class.
  45. This class contains convenience method to create an LMDB store for any
  46. purpose and provides some convenience methods to wrap cursors and
  47. transactions into contexts.
  48. Example usage::
  49. >>> class MyStore(BaseLmdbStore):
  50. ... path = '/base/store/path'
  51. ... dbi_flags = ('db1', 'db2')
  52. ...
  53. >>> ms = MyStore()
  54. >>> # "with" wraps the operation in a transaction.
  55. >>> with ms.cur(index='db1', write=True):
  56. ... cur.put(b'key1', b'val1')
  57. True
  58. """
  59. dbi_labels = []
  60. dbi_flags = {}
  61. """
  62. Configuration of databases in the environment.
  63. This is an OderedDict whose keys are the database labels and whose values
  64. are LMDB flags for creating and opening the databases as per
  65. `http://www.lmdb.tech/doc/group__mdb.html#gac08cad5b096925642ca359a6d6f0562a`_
  66. .
  67. If the environment has only one database, do not override this value (i.e.
  68. leave it to ``None``).
  69. :rtype: dict or None
  70. """
  71. env_flags = 0
  72. """
  73. LMDB environment flags.
  74. See `mdb_env_open
  75. <http://www.lmdb.tech/doc/group__mdb.html#ga32a193c6bf4d7d5c5d579e71f22e9340>`_
  76. """
  77. env_perms = 0o640
  78. """
  79. The UNIX permissions to set on created environment files and semaphores.
  80. See `mdb_env_open
  81. <http://www.lmdb.tech/doc/group__mdb.html#ga32a193c6bf4d7d5c5d579e71f22e9340>`_
  82. """
  83. options = {}
  84. """
  85. LMDB environment option overrides. Setting this is not required.
  86. See `LMDB documentation
  87. <http://lmdb.readthedocs.io/en/release/#environment-class`>_ for details
  88. on available options.
  89. Default values are available for the following options:
  90. - ``map_size``: 1 Gib
  91. - ``max_dbs``: dependent on the number of DBs defined in
  92. :py:meth:``dbi_flags``. Only override if necessary.
  93. - ``max_spare_txns``: dependent on the number of threads, if accessed via
  94. WSGI, or ``1`` otherwise. Only override if necessary.
  95. :rtype: dict
  96. """
  97. ### INIT & TEARDOWN ###
  98. def __init__(self, env_path, open_env=True, create=True):
  99. """
  100. Initialize DB environment and databases.
  101. :param str env_path: The file path of the store.
  102. :param bool open: Whether to open the store immediately. If ``False``
  103. the store can be manually opened with :py:meth:`opn_env`.
  104. :param bool create: Whether the file and directory structure should
  105. be created if the store is opened immediately.
  106. """
  107. self._open = False
  108. self.is_txn_open = False
  109. self.env_path = env_path
  110. if open_env:
  111. self.open_env(create)
  112. #logger.info('Init DB with path: {}'.format(self.env_path))
  113. def __dealloc__(self):
  114. self.close_env()
  115. @property
  116. def is_open(self):
  117. return self._open
  118. @property
  119. def readers(self):
  120. return self._readers
  121. def open_env(self, create):
  122. """
  123. Open, and optionally create, store environment.
  124. """
  125. if self.is_open:
  126. logger.warning('Environment already open.')
  127. return
  128. logger.debug('Opening environment at {}.'.format(self.env_path))
  129. if create:
  130. #logger.info('Creating db env at {}'.format(self.env_path))
  131. parent_path = (
  132. path.dirname(self.env_path)
  133. if lmdb.MDB_NOSUBDIR & self.flags
  134. else self.env_path)
  135. if not path.exists(parent_path):
  136. #logger.info(
  137. # 'Creating store directory at {}'.format(parent_path))
  138. try:
  139. makedirs(parent_path, mode=0o750, exist_ok=True)
  140. except Exception as e:
  141. raise LmdbError(
  142. 'Could not create store at {}. Error: {}'.format(
  143. self.env_path, e))
  144. # Create environment handle.
  145. _check(
  146. lmdb.mdb_env_create(&self.dbenv),
  147. 'Error creating DB environment handle: {}')
  148. logger.debug('Created DBenv @ {:x}'.format(<unsigned long>self.dbenv))
  149. # Set map size.
  150. _check(
  151. lmdb.mdb_env_set_mapsize(self.dbenv, self.options.get(
  152. 'map_size', 1024 ** 3)),
  153. 'Error setting map size: {}')
  154. # Set max databases.
  155. max_dbs = self.options.get('max_dbs', len(self.dbi_labels))
  156. _check(
  157. lmdb.mdb_env_set_maxdbs(self.dbenv, max_dbs),
  158. 'Error setting max. databases: {}')
  159. # Clear stale readers.
  160. self._clear_stale_readers()
  161. # Open DB environment.
  162. logger.debug('DBenv address: {:x}'.format(<unsigned long>self.dbenv))
  163. _check(
  164. lmdb.mdb_env_open(
  165. self.dbenv, self.env_path.encode(),
  166. self.env_flags, self.env_perms),
  167. f'Error opening the database environment: {self.env_path}.')
  168. self._init_dbis(create)
  169. self._open = True
  170. cdef void _clear_stale_readers(self) except *:
  171. """
  172. Clear stale readers.
  173. """
  174. cdef int stale_readers
  175. _check(lmdb.mdb_reader_check(self.dbenv, &stale_readers))
  176. if stale_readers > 0:
  177. logger.debug('Cleared {} stale readers.'.format(stale_readers))
  178. cdef void _init_dbis(self, create=True) except *:
  179. """
  180. Initialize databases and cursors.
  181. """
  182. cdef:
  183. size_t i
  184. lmdb.MDB_txn *txn
  185. lmdb.MDB_dbi dbi
  186. # At least one slot (for environments without a database)
  187. self.dbis = <lmdb.MDB_dbi *>PyMem_Malloc(
  188. max(len(self.dbi_labels), 1) * sizeof(lmdb.MDB_dbi))
  189. if not self.dbis:
  190. raise MemoryError()
  191. # DBIs seem to start from 2. We want to map cursor pointers in the
  192. # array to DBIs, so we need an extra slot.
  193. self.curs = <lmdb.MDB_cursor **>PyMem_Malloc(
  194. (len(self.dbi_labels) + 2) * sizeof(lmdb.MDB_cursor*))
  195. if not self.curs:
  196. raise MemoryError()
  197. create_flag = lmdb.MDB_CREATE if create is True else 0
  198. txn_flags = 0 if create else lmdb.MDB_RDONLY
  199. rc = lmdb.mdb_txn_begin(self.dbenv, NULL, txn_flags, &txn)
  200. logger.info(f'Creating DBs.')
  201. try:
  202. if len(self.dbi_labels):
  203. for i, dblabel in enumerate(self.dbi_labels):
  204. flags = self.dbi_flags.get(dblabel, 0) | create_flag
  205. _check(lmdb.mdb_dbi_open(
  206. txn, dblabel, flags, self.dbis + i))
  207. dbi = self.dbis[i]
  208. logger.debug(f'Created DB {dblabel}: {dbi}')
  209. # Open and close cursor to initialize the memory slot.
  210. _check(lmdb.mdb_cursor_open(
  211. txn, dbi, self.curs + dbi))
  212. #lmdb.mdb_cursor_close(self.curs[dbi])
  213. else:
  214. _check(lmdb.mdb_dbi_open(txn, NULL, 0, self.dbis))
  215. _check(lmdb.mdb_cursor_open(txn, self.dbis[0], self.curs))
  216. #lmdb.mdb_cursor_close(self.curs[self.dbis[0]])
  217. _check(lmdb.mdb_txn_commit(txn))
  218. except:
  219. lmdb.mdb_txn_abort(txn)
  220. raise
  221. cpdef void close_env(self, bint commit_pending_transaction=False) except *:
  222. #logger.debug('Cleaning up store env.')
  223. if self.is_open:
  224. #logger.debug('Closing store env.')
  225. if self.is_txn_open is True:
  226. if commit_pending_transaction:
  227. self._txn_commit()
  228. else:
  229. self._txn_abort()
  230. self._clear_stale_readers()
  231. PyMem_Free(self.dbis)
  232. PyMem_Free(self.curs)
  233. lmdb.mdb_env_close(self.dbenv)
  234. self._open = False
  235. cpdef void destroy(self, _path='') except *:
  236. """
  237. Destroy the store.
  238. https://www.youtube.com/watch?v=lIVq7FJnPwg
  239. :param str _path: unused. Left for RDFLib API compatibility. (actually
  240. quite dangerous if it were used: it could turn into a
  241. general-purpose recursive file and folder delete method!)
  242. """
  243. if path.exists(self.env_path):
  244. if lmdb.MDB_NOSUBDIR & self.flags:
  245. try:
  246. os.unlink(self.env_path)
  247. except FileNotFoundError:
  248. pass
  249. try:
  250. os.unlink(self.env_path + '-lock')
  251. except FileNotFoundError:
  252. pass
  253. else:
  254. rmtree(self.env_path)
  255. ### PYTHON-ACCESSIBLE METHODS ###
  256. @contextmanager
  257. def txn_ctx(self, write=False):
  258. """
  259. Transaction context manager.
  260. Open and close a transaction for the duration of the functions in the
  261. context. If a transaction has already been opened in the store, a new
  262. one is opened only if the current transaction is read-only and the new
  263. requested transaction is read-write.
  264. If a new write transaction is opened, the old one is kept on hold until
  265. the new transaction is closed, then restored. All cursors are
  266. invalidated and must be restored as well if one needs to reuse them.
  267. :param bool write: Whether a write transaction is to be opened.
  268. :rtype: lmdb.Transaction
  269. """
  270. cdef lmdb.MDB_txn* hold_txn
  271. will_open = False
  272. if not self.is_open:
  273. raise LmdbError('Store is not open.')
  274. # If another transaction is open, only open the new transaction if
  275. # the current one is RO and the new one RW.
  276. if self.is_txn_open:
  277. if write:
  278. will_open = not self.is_txn_rw
  279. else:
  280. will_open = True
  281. # If a new transaction needs to be opened and replace the old one,
  282. # the old one must be put on hold and swapped out when the new txn
  283. # is closed.
  284. if will_open:
  285. will_reset = self.is_txn_open
  286. if will_open:
  287. #logger.debug('Beginning {} transaction.'.format(
  288. # 'RW' if write else 'RO'))
  289. if will_reset:
  290. hold_txn = self.txn
  291. try:
  292. self._txn_begin(write=write)
  293. self.is_txn_rw = write
  294. #logger.debug('In txn_ctx, before yield')
  295. yield
  296. #logger.debug('In txn_ctx, after yield')
  297. self._txn_commit()
  298. #logger.debug('after _txn_commit')
  299. if will_reset:
  300. lmdb.mdb_txn_reset(hold_txn)
  301. self.txn = hold_txn
  302. _check(lmdb.mdb_txn_renew(self.txn))
  303. self.is_txn_rw = False
  304. except:
  305. self._txn_abort()
  306. raise
  307. else:
  308. logger.info(
  309. 'Transaction is already active. Not opening another one.'
  310. )
  311. #logger.debug('before yield')
  312. yield
  313. #logger.debug('after yield')
  314. def begin(self, write=False):
  315. """
  316. Begin a transaction manually if not already in a txn context.
  317. The :py:meth:`txn_ctx` context manager should be used whenever
  318. possible rather than this method.
  319. """
  320. if not self.is_open:
  321. raise RuntimeError('Store must be opened first.')
  322. #logger.debug('Beginning a {} transaction.'.format(
  323. # 'read/write' if write else 'read-only'))
  324. self._txn_begin(write=write)
  325. def commit(self):
  326. """Commit main transaction."""
  327. #logger.debug('Committing transaction.')
  328. self._txn_commit()
  329. def abort(self):
  330. """Abort main transaction."""
  331. #logger.debug('Rolling back transaction.')
  332. self._txn_abort()
  333. def rollback(self):
  334. """Alias for :py:meth:`abort`"""
  335. self.abort()
  336. def key_exists(self, key, dblabel='', new_txn=True):
  337. """
  338. Return whether a key exists in a database (Python-facing method).
  339. Wrap in a new transaction. Only use this if a transaction has not been
  340. opened.
  341. """
  342. if new_txn is True:
  343. with self.txn_ctx():
  344. return self._key_exists(key, len(key), dblabel=dblabel)
  345. else:
  346. return self._key_exists(key, len(key), dblabel=dblabel)
  347. cdef inline bint _key_exists(
  348. self, unsigned char *key, unsigned char klen,
  349. DbLabel dblabel=b'') except -1:
  350. """
  351. Return whether a key exists in a database.
  352. To be used within an existing transaction.
  353. """
  354. cdef lmdb.MDB_val key_v, data_v
  355. key_v.mv_data = key
  356. key_v.mv_size = klen
  357. #logger.debug(
  358. # 'Checking if key {} with size {} exists...'.format(key, klen))
  359. try:
  360. _check(lmdb.mdb_get(
  361. self.txn, self.get_dbi(dblabel), &key_v, &data_v))
  362. except KeyNotFoundError:
  363. #logger.debug('...no.')
  364. return False
  365. #logger.debug('...yes.')
  366. return True
  367. def put(self, key, data, dblabel='', flags=0):
  368. """
  369. Put one key/value pair (Python-facing method).
  370. """
  371. self._put(
  372. key, len(key), data, len(data), dblabel=dblabel,
  373. txn=self.txn, flags=flags
  374. )
  375. cdef void _put(
  376. self, unsigned char *key, size_t key_size, unsigned char *data,
  377. size_t data_size, DbLabel dblabel='',
  378. lmdb.MDB_txn *txn=NULL, unsigned int flags=0) except *:
  379. """
  380. Put one key/value pair.
  381. """
  382. if txn is NULL:
  383. txn = self.txn
  384. key_v.mv_data = key
  385. key_v.mv_size = key_size
  386. data_v.mv_data = data
  387. data_v.mv_size = data_size
  388. #logger.debug('Putting: {}, {} into DB {}'.format(key[: key_size],
  389. # data[: data_size], dblabel))
  390. rc = lmdb.mdb_put(txn, self.get_dbi(dblabel), &key_v, &data_v, flags)
  391. _check(rc, 'Error putting data: {}, {}'.format(
  392. key[: key_size], data[: data_size]))
  393. cpdef bytes get_data(self, key, DbLabel dblabel=''):
  394. """
  395. Get a single value (non-dup) for a key (Python-facing method).
  396. """
  397. cdef lmdb.MDB_val rv
  398. try:
  399. self._get_data(key, len(key), &rv, dblabel=dblabel)
  400. return (<unsigned char *>rv.mv_data)[: rv.mv_size]
  401. except KeyNotFoundError:
  402. return None
  403. cdef void _get_data(
  404. self, unsigned char *key, size_t klen, lmdb.MDB_val *rv,
  405. DbLabel dblabel='') except *:
  406. """
  407. Get a single value (non-dup) for a key.
  408. """
  409. cdef:
  410. unsigned char *ret
  411. key_v.mv_data = key
  412. key_v.mv_size = len(key)
  413. _check(
  414. lmdb.mdb_get(self.txn, self.get_dbi(dblabel), &key_v, rv),
  415. 'Error getting data for key \'{}\'.'.format(key.decode()))
  416. def delete(self, key, dblabel=''):
  417. """
  418. Delete one single value by key. Python-facing method.
  419. """
  420. self._delete(key, len(key), dblabel)
  421. cdef void _delete(
  422. self, unsigned char *key, size_t klen,
  423. DbLabel dblabel=b'') except *:
  424. """
  425. Delete one single value by key from a non-dup database.
  426. TODO Allow deleting duplicate keys.
  427. """
  428. key_v.mv_data = key
  429. key_v.mv_size = klen
  430. try:
  431. _check(lmdb.mdb_del(self.txn, self.get_dbi(dblabel), &key_v, NULL))
  432. except KeyNotFoundError:
  433. pass
  434. cpdef dict stats(self):
  435. """Gather statistics about the database."""
  436. return self._stats()
  437. cdef dict _stats(self):
  438. """
  439. Gather statistics about the database.
  440. Cython-only, non-transaction-aware method.
  441. """
  442. cdef:
  443. lmdb.MDB_stat stat
  444. size_t entries
  445. lmdb.mdb_env_stat(self.dbenv, &stat)
  446. env_stats = <dict>stat
  447. db_stats = {}
  448. for i, dblabel in enumerate(self.dbi_labels):
  449. _check(
  450. lmdb.mdb_stat(self.txn, self.dbis[i], &stat),
  451. 'Error getting datbase stats: {}')
  452. entries = stat.ms_entries
  453. db_stats[dblabel] = <dict>stat
  454. return {
  455. 'env_stats': env_stats,
  456. 'env_size': os.stat(self.env_path).st_size,
  457. 'db_stats': {
  458. db_label: db_stats[db_label]
  459. for db_label in self.dbi_labels
  460. },
  461. }
  462. # UNFINISHED
  463. #cdef int _reader_list_callback(self, const unsigned char *msg, void *ctx):
  464. # """
  465. # Callback for reader info function.
  466. # Example from py-lmdb:
  467. # static int env_readers_callback(const char *msg, void *str_)
  468. # {
  469. # PyObject **str = str_;
  470. # PyObject *s = PyUnicode_FromString(msg);
  471. # PyObject *new;
  472. # if(! s) {
  473. # return -1;
  474. # }
  475. # new = PyUnicode_Concat(*str, s);
  476. # Py_CLEAR(*str);
  477. # *str = new;
  478. # if(! new) {
  479. # return -1;
  480. # }
  481. # return 0;
  482. # }
  483. # """
  484. # cdef:
  485. # unicode str = ctx[0].decode('utf-8')
  486. # unicode s = msg.decode('utf-8')
  487. # if not len(s):
  488. # return -1
  489. # str += s
  490. # logger.info('message: {}'.format(msg))
  491. # if not len(str):
  492. # return -1
  493. # ctx = &str
  494. #cpdef str reader_list(self):
  495. # """
  496. # Information about the reader lock table.
  497. # """
  498. # cdef unsigned char *ctx
  499. # lmdb.mdb_reader_list(self.dbenv, <lmdb.MDB_msg_func *>self._reader_list_callback, &ctx)
  500. # logger.info('Reader info: {}'.format(ctx))
  501. # return (ctx).decode('ascii')
  502. ### CYTHON METHODS ###
  503. cdef void _txn_begin(self, write=True, lmdb.MDB_txn *parent=NULL) except *:
  504. if not self.is_open:
  505. raise LmdbError('Store is not open.')
  506. cdef:
  507. unsigned int flags
  508. flags = 0 if write else lmdb.MDB_RDONLY
  509. logger.debug('Opening {} transaction in PID {}, thread {}'.format(
  510. 'RW' if write else 'RO',
  511. multiprocessing.current_process().pid,
  512. threading.currentThread().getName()))
  513. #logger.debug('Readers: {}'.format(self.reader_list()))
  514. rc = lmdb.mdb_txn_begin(self.dbenv, parent, flags, &self.txn)
  515. _check(rc, 'Error opening transaction.')
  516. logger.debug('Opened transaction @ {:x}'.format(<unsigned long>self.txn))
  517. self.is_txn_open = True
  518. self.is_txn_rw = write
  519. logger.debug('txn is open: {}'.format(self.is_txn_open))
  520. cdef void _txn_commit(self) except *:
  521. txid = '{:x}'.format(<unsigned long>self.txn)
  522. try:
  523. _check(lmdb.mdb_txn_commit(self.txn))
  524. logger.debug('Transaction @ {} committed.'.format(txid))
  525. self.is_txn_open = False
  526. self.is_txn_rw = False
  527. except:
  528. self._txn_abort()
  529. raise
  530. cdef void _txn_abort(self) except *:
  531. txid = '{:x}'.format(<unsigned long>self.txn)
  532. lmdb.mdb_txn_abort(self.txn)
  533. self.is_txn_open = False
  534. self.is_txn_rw = False
  535. logger.info('Transaction @ {} aborted.'.format(txid))
  536. cpdef int txn_id(self):
  537. return self._txn_id()
  538. cdef size_t _txn_id(self) except -1:
  539. return lmdb.mdb_txn_id(self.txn)
  540. cdef lmdb.MDB_dbi get_dbi(
  541. self, DbLabel dblabel=NULL, lmdb.MDB_txn *txn=NULL):
  542. """
  543. Return a DB handle by database name.
  544. """
  545. cdef size_t dbidx
  546. if txn is NULL:
  547. txn = self.txn
  548. if dblabel is NULL:
  549. logger.debug('Getting DBI without label.')
  550. dbidx = (
  551. 0 if dblabel is NULL
  552. else self.dbi_labels.index(dblabel)
  553. )
  554. #logger.debug(
  555. # f'Got DBI {self.dbis[dbidx]} with label {dblabel} '
  556. # f'and index #{dbidx}')
  557. return self.dbis[dbidx]
  558. cdef lmdb.MDB_cursor *_cur_open(
  559. self, DbLabel dblabel=NULL, lmdb.MDB_txn *txn=NULL) except *:
  560. cdef:
  561. lmdb.MDB_dbi dbi
  562. if txn is NULL:
  563. txn = self.txn
  564. dbi = self.get_dbi(dblabel, txn=txn)
  565. #logger.debug(f'Opening cursor for DB {dblabel} (DBI {dbi})...')
  566. #try:
  567. # # FIXME Either reuse the cursor, if it works, or remove this code.
  568. # _check(lmdb.mdb_cursor_renew(txn, self.curs[dbi]))
  569. # logger.debug(f'Repurposed existing cursor for DBI {dbi}.')
  570. #except LmdbError as e:
  571. # _check(
  572. # lmdb.mdb_cursor_open(txn, dbi, self.curs + dbi),
  573. # f'Error opening cursor: {dblabel}')
  574. # logger.debug(f'Created brand new cursor for DBI {dbi}.')
  575. _check(
  576. lmdb.mdb_cursor_open(txn, dbi, self.curs + dbi),
  577. f'Error opening cursor: {dblabel}')
  578. #logger.debug('...opened @ {:x}.'.format(<unsigned long>self.curs[dbi]))
  579. return self.curs[dbi]
  580. cdef void _cur_close(self, lmdb.MDB_cursor *cur) except *:
  581. """Close a cursor."""
  582. #logger.info('Closing cursor @ {:x} for DBI {}...'.format(
  583. # <unsigned long>cur, lmdb.mdb_cursor_dbi(cur) ))
  584. lmdb.mdb_cursor_close(cur)
  585. #logger.info('...closed.')