hat.event.backends.lmdb.backend

  1from collections.abc import Collection
  2from pathlib import Path
  3import asyncio
  4import collections
  5import contextlib
  6import logging
  7import typing
  8
  9from hat import aio
 10from hat import json
 11
 12from hat.event.backends.lmdb import common
 13from hat.event.backends.lmdb import environment
 14from hat.event.backends.lmdb import latestdb
 15from hat.event.backends.lmdb import refdb
 16from hat.event.backends.lmdb import systemdb
 17from hat.event.backends.lmdb import timeseriesdb
 18from hat.event.backends.lmdb.conditions import Conditions
 19
 20
 21mlog = logging.getLogger(__name__)
 22
 23cleanup_max_results = 1024
 24
 25flush_queue_size = 4096
 26
 27max_registered_count = 256 * 1024
 28
 29default_timeseries_max_results = 4096
 30
 31default_timeseries_event_type_cache_size = 256 * 1024
 32
 33version = '0.9'
 34
 35
 36class Databases(typing.NamedTuple):
 37    system: systemdb.SystemDb
 38    latest: latestdb.LatestDb
 39    timeseries: timeseriesdb.TimeseriesDb
 40    ref: refdb.RefDb
 41
 42
 43class Changes(typing.NamedTuple):
 44    system: systemdb.Changes
 45    latest: latestdb.Changes
 46    timeseries: timeseriesdb.Changes
 47    ref: refdb.Changes
 48
 49
 50async def create(conf: json.Data,
 51                 registered_events_cb: common.BackendRegisteredEventsCb | None,
 52                 flushed_events_cb: common.BackendFlushedEventsCb | None
 53                 ) -> 'LmdbBackend':
 54    backend = LmdbBackend()
 55    backend._registered_events_cb = registered_events_cb
 56    backend._flushed_events_cb = flushed_events_cb
 57    backend._conditions = Conditions(conf['conditions'])
 58    backend._loop = asyncio.get_running_loop()
 59    backend._flush_queue = aio.Queue(flush_queue_size)
 60    backend._registered_count = 0
 61    backend._registered_queue = collections.deque()
 62    backend._async_group = aio.Group()
 63
 64    backend._env = await environment.create(Path(conf['db_path']))
 65    backend.async_group.spawn(aio.call_on_done, backend._env.wait_closing(),
 66                              backend.close)
 67
 68    try:
 69        latest_subscription = common.create_subscription(
 70            tuple(i) for i in conf['latest']['subscriptions'])
 71
 72        timeseries_partitions = (
 73            timeseriesdb.Partition(
 74                order_by=common.OrderBy[i['order_by']],
 75                subscription=common.create_subscription(
 76                    tuple(event_type) for event_type in i['subscriptions']),
 77                limit=(
 78                    timeseriesdb.Limit(
 79                        min_entries=i['limit'].get('min_entries'),
 80                        max_entries=i['limit'].get('max_entries'),
 81                        duration=i['limit'].get('duration'),
 82                        size=i['limit'].get('size'))
 83                    if 'limit' in i else None))
 84            for i in conf['timeseries'])
 85
 86        timeseries_max_results = conf.get('timeseries_max_results',
 87                                          default_timeseries_max_results)
 88        timeseries_event_type_cache_size = conf.get(
 89            'timeseries_event_type_cache_size',
 90            default_timeseries_event_type_cache_size)
 91
 92        backend._dbs = await backend._env.execute(
 93            _ext_create_dbs, backend._env, conf['identifier'],
 94            backend._conditions, latest_subscription, timeseries_partitions,
 95            timeseries_max_results, timeseries_event_type_cache_size,
 96            backend.flush)
 97
 98        backend.async_group.spawn(backend._flush_loop, conf['flush_period'])
 99        backend.async_group.spawn(backend._cleanup_loop,
100                                  conf['cleanup_period'])
101
102    except BaseException:
103        await aio.uncancellable(backend._env.async_close())
104        raise
105
106    return backend
107
108
109def _ext_create_dbs(env, identifier, conditions, latest_subscription,
110                    timeseries_partitions, timeseries_max_results,
111                    timeseries_event_type_cache_size, flush_cb):
112    with env.ext_begin(write=True) as txn:
113        system_db = systemdb.ext_create(env, txn, version, identifier)
114        latest_db = latestdb.ext_create(env, txn, conditions,
115                                        latest_subscription)
116        timeseries_db = timeseriesdb.ext_create(
117            env, txn, conditions, timeseries_partitions,
118            timeseries_max_results, timeseries_event_type_cache_size,
119            flush_cb)
120        ref_db = refdb.RefDb(env)
121
122    return Databases(system=system_db,
123                     latest=latest_db,
124                     timeseries=timeseries_db,
125                     ref=ref_db)
126
127
128class LmdbBackend(common.Backend):
129
130    @property
131    def async_group(self) -> aio.Group:
132        return self._async_group
133
134    async def get_last_event_id(self,
135                                server_id: int
136                                ) -> common.EventId:
137        if not self.is_open:
138            raise common.BackendClosedError()
139
140        return self._dbs.system.get_last_event_id(server_id)
141
142    async def register(self,
143                       events: Collection[common.Event]
144                       ) -> Collection[common.Event] | None:
145        if not self.is_open:
146            raise common.BackendClosedError()
147
148        for event in events:
149            server_id = event.id.server
150
151            last_event_id = self._dbs.system.get_last_event_id(server_id)
152            last_timestamp = self._dbs.system.get_last_timestamp(server_id)
153
154            if last_event_id >= event.id:
155                mlog.warning("event registration skipped: invalid event id")
156                continue
157
158            if last_timestamp > event.timestamp:
159                mlog.warning("event registration skipped: invalid timestamp")
160                continue
161
162            if not self._conditions.matches(event):
163                mlog.warning(
164                    "event %s registration skipped: invalid conditions",
165                    event.type)
166                continue
167
168            refs = collections.deque()
169
170            latest_result = self._dbs.latest.add(event)
171
172            if latest_result.added_ref:
173                refs.append(latest_result.added_ref)
174
175            if latest_result.removed_ref:
176                self._dbs.ref.remove(*latest_result.removed_ref)
177
178            refs.extend(self._dbs.timeseries.add(event))
179
180            if not refs:
181                continue
182
183            self._dbs.ref.add(event, refs)
184            self._dbs.system.set_last_event_id(event.id)
185            self._dbs.system.set_last_timestamp(server_id, event.timestamp)
186
187        self._registered_queue.append(events)
188        self._registered_count += len(events)
189
190        if self._registered_count > max_registered_count:
191            await self._flush_queue.put(None)
192
193        if self._registered_events_cb:
194            await aio.call(self._registered_events_cb, events)
195
196        return events
197
198    async def query(self,
199                    params: common.QueryParams
200                    ) -> common.QueryResult:
201        if not self.is_open:
202            raise common.BackendClosedError()
203
204        if isinstance(params, common.QueryLatestParams):
205            return self._dbs.latest.query(params)
206
207        if isinstance(params, common.QueryTimeseriesParams):
208            return await self._dbs.timeseries.query(params)
209
210        if isinstance(params, common.QueryServerParams):
211            return await self._dbs.ref.query(params)
212
213        raise ValueError('unsupported params type')
214
215    async def flush(self):
216        try:
217            future = self._loop.create_future()
218            await self._flush_queue.put(future)
219            await future
220
221        except aio.QueueClosedError:
222            raise common.BackendClosedError()
223
224    async def _flush_loop(self, flush_period):
225        futures = collections.deque()
226
227        async def cleanup():
228            with contextlib.suppress(Exception):
229                await self._flush()
230
231            await self._env.async_close()
232
233        try:
234            while True:
235                try:
236                    future = await aio.wait_for(self._flush_queue.get(),
237                                                flush_period)
238                    futures.append(future)
239
240                except asyncio.TimeoutError:
241                    pass
242
243                except aio.CancelledWithResultError as e:
244                    if e.result:
245                        futures.append(e.result)
246
247                    raise
248
249                while not self._flush_queue.empty():
250                    futures.append(self._flush_queue.get_nowait())
251
252                await aio.uncancellable(self._flush())
253
254                while futures:
255                    future = futures.popleft()
256                    if future and not future.done():
257                        future.set_result(None)
258
259        except Exception as e:
260            mlog.error('backend flush error: %s', e, exc_info=e)
261
262        finally:
263            self.close()
264            self._flush_queue.close()
265
266            while not self._flush_queue.empty():
267                futures.append(self._flush_queue.get_nowait())
268
269            for future in futures:
270                if future and not future.done():
271                    future.set_exception(common.BackendClosedError())
272
273            await aio.uncancellable(cleanup())
274
275    async def _cleanup_loop(self, cleanup_period):
276        try:
277            while True:
278                await asyncio.sleep(0)
279
280                repeat = await self._env.execute(_ext_cleanup, self._env,
281                                                 self._dbs, common.now())
282                if repeat:
283                    continue
284
285                await asyncio.sleep(cleanup_period)
286
287        except Exception as e:
288            mlog.error('backend cleanup error: %s', e, exc_info=e)
289
290        finally:
291            self.close()
292
293    async def _flush(self):
294        if not self._env.is_open:
295            return
296
297        self._registered_count = 0
298        registered_queue, self._registered_queue = (self._registered_queue,
299                                                    collections.deque())
300
301        changes = Changes(system=self._dbs.system.create_changes(),
302                          latest=self._dbs.latest.create_changes(),
303                          timeseries=self._dbs.timeseries.create_changes(),
304                          ref=self._dbs.ref.create_changes())
305
306        # TODO lock period between create_changes and locking executor
307        #      (timeseries and ref must write changes before new queries are
308        #      allowed)
309
310        await self._env.execute(_ext_flush, self._env, self._dbs, changes)
311
312        if not self._flushed_events_cb:
313            return
314
315        while registered_queue:
316            events = registered_queue.popleft()
317            await aio.call(self._flushed_events_cb, events)
318
319
320def _ext_flush(env, dbs, changes):
321    with env.ext_begin(write=True) as txn:
322        dbs.system.ext_write(txn, changes.system)
323        dbs.latest.ext_write(txn, changes.latest)
324        dbs.timeseries.ext_write(txn, changes.timeseries)
325        dbs.ref.ext_write(txn, changes.ref)
326
327
328def _ext_cleanup(env, dbs, now):
329    with env.ext_begin(write=True) as txn:
330        result = dbs.timeseries.ext_cleanup(txn, now, cleanup_max_results)
331        if not result:
332            return False
333
334        dbs.ref.ext_cleanup(txn, result)
335
336    return len(result) >= cleanup_max_results
mlog = <Logger hat.event.backends.lmdb.backend (WARNING)>
cleanup_max_results = 1024
flush_queue_size = 4096
max_registered_count = 262144
default_timeseries_max_results = 4096
default_timeseries_event_type_cache_size = 262144
version = '0.9'
class Databases(typing.NamedTuple):
37class Databases(typing.NamedTuple):
38    system: systemdb.SystemDb
39    latest: latestdb.LatestDb
40    timeseries: timeseriesdb.TimeseriesDb
41    ref: refdb.RefDb

Databases(system, latest, timeseries, ref)

Create new instance of Databases(system, latest, timeseries, ref)

Alias for field number 0

Alias for field number 1

Alias for field number 2

Alias for field number 3

class Changes(typing.NamedTuple):
44class Changes(typing.NamedTuple):
45    system: systemdb.Changes
46    latest: latestdb.Changes
47    timeseries: timeseriesdb.Changes
48    ref: refdb.Changes

Changes(system, latest, timeseries, ref)

Create new instance of Changes(system, latest, timeseries, ref)

Alias for field number 0

Alias for field number 1

timeseries: dict[int, collections.deque[tuple[hat.event.common.Timestamp, hat.event.common.Event]]]

Alias for field number 2

Alias for field number 3

async def create( conf: None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')], registered_events_cb: Callable[[Collection[hat.event.common.Event]], None | Awaitable[None]] | None, flushed_events_cb: Callable[[Collection[hat.event.common.Event]], None | Awaitable[None]] | None) -> LmdbBackend:
 51async def create(conf: json.Data,
 52                 registered_events_cb: common.BackendRegisteredEventsCb | None,
 53                 flushed_events_cb: common.BackendFlushedEventsCb | None
 54                 ) -> 'LmdbBackend':
 55    backend = LmdbBackend()
 56    backend._registered_events_cb = registered_events_cb
 57    backend._flushed_events_cb = flushed_events_cb
 58    backend._conditions = Conditions(conf['conditions'])
 59    backend._loop = asyncio.get_running_loop()
 60    backend._flush_queue = aio.Queue(flush_queue_size)
 61    backend._registered_count = 0
 62    backend._registered_queue = collections.deque()
 63    backend._async_group = aio.Group()
 64
 65    backend._env = await environment.create(Path(conf['db_path']))
 66    backend.async_group.spawn(aio.call_on_done, backend._env.wait_closing(),
 67                              backend.close)
 68
 69    try:
 70        latest_subscription = common.create_subscription(
 71            tuple(i) for i in conf['latest']['subscriptions'])
 72
 73        timeseries_partitions = (
 74            timeseriesdb.Partition(
 75                order_by=common.OrderBy[i['order_by']],
 76                subscription=common.create_subscription(
 77                    tuple(event_type) for event_type in i['subscriptions']),
 78                limit=(
 79                    timeseriesdb.Limit(
 80                        min_entries=i['limit'].get('min_entries'),
 81                        max_entries=i['limit'].get('max_entries'),
 82                        duration=i['limit'].get('duration'),
 83                        size=i['limit'].get('size'))
 84                    if 'limit' in i else None))
 85            for i in conf['timeseries'])
 86
 87        timeseries_max_results = conf.get('timeseries_max_results',
 88                                          default_timeseries_max_results)
 89        timeseries_event_type_cache_size = conf.get(
 90            'timeseries_event_type_cache_size',
 91            default_timeseries_event_type_cache_size)
 92
 93        backend._dbs = await backend._env.execute(
 94            _ext_create_dbs, backend._env, conf['identifier'],
 95            backend._conditions, latest_subscription, timeseries_partitions,
 96            timeseries_max_results, timeseries_event_type_cache_size,
 97            backend.flush)
 98
 99        backend.async_group.spawn(backend._flush_loop, conf['flush_period'])
100        backend.async_group.spawn(backend._cleanup_loop,
101                                  conf['cleanup_period'])
102
103    except BaseException:
104        await aio.uncancellable(backend._env.async_close())
105        raise
106
107    return backend
class LmdbBackend(hat.event.common.backend.Backend):
129class LmdbBackend(common.Backend):
130
131    @property
132    def async_group(self) -> aio.Group:
133        return self._async_group
134
135    async def get_last_event_id(self,
136                                server_id: int
137                                ) -> common.EventId:
138        if not self.is_open:
139            raise common.BackendClosedError()
140
141        return self._dbs.system.get_last_event_id(server_id)
142
143    async def register(self,
144                       events: Collection[common.Event]
145                       ) -> Collection[common.Event] | None:
146        if not self.is_open:
147            raise common.BackendClosedError()
148
149        for event in events:
150            server_id = event.id.server
151
152            last_event_id = self._dbs.system.get_last_event_id(server_id)
153            last_timestamp = self._dbs.system.get_last_timestamp(server_id)
154
155            if last_event_id >= event.id:
156                mlog.warning("event registration skipped: invalid event id")
157                continue
158
159            if last_timestamp > event.timestamp:
160                mlog.warning("event registration skipped: invalid timestamp")
161                continue
162
163            if not self._conditions.matches(event):
164                mlog.warning(
165                    "event %s registration skipped: invalid conditions",
166                    event.type)
167                continue
168
169            refs = collections.deque()
170
171            latest_result = self._dbs.latest.add(event)
172
173            if latest_result.added_ref:
174                refs.append(latest_result.added_ref)
175
176            if latest_result.removed_ref:
177                self._dbs.ref.remove(*latest_result.removed_ref)
178
179            refs.extend(self._dbs.timeseries.add(event))
180
181            if not refs:
182                continue
183
184            self._dbs.ref.add(event, refs)
185            self._dbs.system.set_last_event_id(event.id)
186            self._dbs.system.set_last_timestamp(server_id, event.timestamp)
187
188        self._registered_queue.append(events)
189        self._registered_count += len(events)
190
191        if self._registered_count > max_registered_count:
192            await self._flush_queue.put(None)
193
194        if self._registered_events_cb:
195            await aio.call(self._registered_events_cb, events)
196
197        return events
198
199    async def query(self,
200                    params: common.QueryParams
201                    ) -> common.QueryResult:
202        if not self.is_open:
203            raise common.BackendClosedError()
204
205        if isinstance(params, common.QueryLatestParams):
206            return self._dbs.latest.query(params)
207
208        if isinstance(params, common.QueryTimeseriesParams):
209            return await self._dbs.timeseries.query(params)
210
211        if isinstance(params, common.QueryServerParams):
212            return await self._dbs.ref.query(params)
213
214        raise ValueError('unsupported params type')
215
216    async def flush(self):
217        try:
218            future = self._loop.create_future()
219            await self._flush_queue.put(future)
220            await future
221
222        except aio.QueueClosedError:
223            raise common.BackendClosedError()
224
225    async def _flush_loop(self, flush_period):
226        futures = collections.deque()
227
228        async def cleanup():
229            with contextlib.suppress(Exception):
230                await self._flush()
231
232            await self._env.async_close()
233
234        try:
235            while True:
236                try:
237                    future = await aio.wait_for(self._flush_queue.get(),
238                                                flush_period)
239                    futures.append(future)
240
241                except asyncio.TimeoutError:
242                    pass
243
244                except aio.CancelledWithResultError as e:
245                    if e.result:
246                        futures.append(e.result)
247
248                    raise
249
250                while not self._flush_queue.empty():
251                    futures.append(self._flush_queue.get_nowait())
252
253                await aio.uncancellable(self._flush())
254
255                while futures:
256                    future = futures.popleft()
257                    if future and not future.done():
258                        future.set_result(None)
259
260        except Exception as e:
261            mlog.error('backend flush error: %s', e, exc_info=e)
262
263        finally:
264            self.close()
265            self._flush_queue.close()
266
267            while not self._flush_queue.empty():
268                futures.append(self._flush_queue.get_nowait())
269
270            for future in futures:
271                if future and not future.done():
272                    future.set_exception(common.BackendClosedError())
273
274            await aio.uncancellable(cleanup())
275
276    async def _cleanup_loop(self, cleanup_period):
277        try:
278            while True:
279                await asyncio.sleep(0)
280
281                repeat = await self._env.execute(_ext_cleanup, self._env,
282                                                 self._dbs, common.now())
283                if repeat:
284                    continue
285
286                await asyncio.sleep(cleanup_period)
287
288        except Exception as e:
289            mlog.error('backend cleanup error: %s', e, exc_info=e)
290
291        finally:
292            self.close()
293
294    async def _flush(self):
295        if not self._env.is_open:
296            return
297
298        self._registered_count = 0
299        registered_queue, self._registered_queue = (self._registered_queue,
300                                                    collections.deque())
301
302        changes = Changes(system=self._dbs.system.create_changes(),
303                          latest=self._dbs.latest.create_changes(),
304                          timeseries=self._dbs.timeseries.create_changes(),
305                          ref=self._dbs.ref.create_changes())
306
307        # TODO lock period between create_changes and locking executor
308        #      (timeseries and ref must write changes before new queries are
309        #      allowed)
310
311        await self._env.execute(_ext_flush, self._env, self._dbs, changes)
312
313        if not self._flushed_events_cb:
314            return
315
316        while registered_queue:
317            events = registered_queue.popleft()
318            await aio.call(self._flushed_events_cb, events)

Backend ABC

async_group: hat.aio.group.Group
131    @property
132    def async_group(self) -> aio.Group:
133        return self._async_group

Group controlling resource's lifetime.

async def get_last_event_id(self, server_id: int) -> hat.event.common.EventId:
135    async def get_last_event_id(self,
136                                server_id: int
137                                ) -> common.EventId:
138        if not self.is_open:
139            raise common.BackendClosedError()
140
141        return self._dbs.system.get_last_event_id(server_id)

Get last registered event id associated with server id

async def register( self, events: Collection[hat.event.common.Event]) -> Collection[hat.event.common.Event] | None:
143    async def register(self,
144                       events: Collection[common.Event]
145                       ) -> Collection[common.Event] | None:
146        if not self.is_open:
147            raise common.BackendClosedError()
148
149        for event in events:
150            server_id = event.id.server
151
152            last_event_id = self._dbs.system.get_last_event_id(server_id)
153            last_timestamp = self._dbs.system.get_last_timestamp(server_id)
154
155            if last_event_id >= event.id:
156                mlog.warning("event registration skipped: invalid event id")
157                continue
158
159            if last_timestamp > event.timestamp:
160                mlog.warning("event registration skipped: invalid timestamp")
161                continue
162
163            if not self._conditions.matches(event):
164                mlog.warning(
165                    "event %s registration skipped: invalid conditions",
166                    event.type)
167                continue
168
169            refs = collections.deque()
170
171            latest_result = self._dbs.latest.add(event)
172
173            if latest_result.added_ref:
174                refs.append(latest_result.added_ref)
175
176            if latest_result.removed_ref:
177                self._dbs.ref.remove(*latest_result.removed_ref)
178
179            refs.extend(self._dbs.timeseries.add(event))
180
181            if not refs:
182                continue
183
184            self._dbs.ref.add(event, refs)
185            self._dbs.system.set_last_event_id(event.id)
186            self._dbs.system.set_last_timestamp(server_id, event.timestamp)
187
188        self._registered_queue.append(events)
189        self._registered_count += len(events)
190
191        if self._registered_count > max_registered_count:
192            await self._flush_queue.put(None)
193
194        if self._registered_events_cb:
195            await aio.call(self._registered_events_cb, events)
196
197        return events

Register events

199    async def query(self,
200                    params: common.QueryParams
201                    ) -> common.QueryResult:
202        if not self.is_open:
203            raise common.BackendClosedError()
204
205        if isinstance(params, common.QueryLatestParams):
206            return self._dbs.latest.query(params)
207
208        if isinstance(params, common.QueryTimeseriesParams):
209            return await self._dbs.timeseries.query(params)
210
211        if isinstance(params, common.QueryServerParams):
212            return await self._dbs.ref.query(params)
213
214        raise ValueError('unsupported params type')

Query events

async def flush(self):
216    async def flush(self):
217        try:
218            future = self._loop.create_future()
219            await self._flush_queue.put(future)
220            await future
221
222        except aio.QueueClosedError:
223            raise common.BackendClosedError()

Flush internal buffers and permanently persist events