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)
Databases( system: hat.event.backends.lmdb.systemdb.SystemDb, latest: hat.event.backends.lmdb.latestdb.LatestDb, timeseries: hat.event.backends.lmdb.timeseriesdb.TimeseriesDb, ref: hat.event.backends.lmdb.refdb.RefDb)
Create new instance of Databases(system, latest, timeseries, ref)
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)
Changes( system: hat.event.backends.lmdb.systemdb.Changes, latest: hat.event.backends.lmdb.latestdb.Changes, timeseries: dict[int, collections.deque[tuple[hat.event.common.Timestamp, hat.event.common.Event]]], ref: dict[int, hat.event.backends.lmdb.refdb.ServerChanges])
Create new instance of Changes(system, latest, timeseries, ref)
timeseries: dict[int, collections.deque[tuple[hat.event.common.Timestamp, hat.event.common.Event]]]
Alias for field number 2
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
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
async def
query( self, params: hat.event.common.QueryLatestParams | hat.event.common.QueryTimeseriesParams | hat.event.common.QueryServerParams) -> hat.event.common.QueryResult:
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