memory.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. # -*- coding: utf-8 -*-
  2. """
  3. In-Memory Event Store.
  4. Speichert Events im Arbeitsspeicher.
  5. """
  6. from __future__ import annotations
  7. import asyncio
  8. from datetime import datetime
  9. from typing import TYPE_CHECKING, Any
  10. from trixy_core.events.persistence.store import (
  11. EventStore,
  12. StoredEvent,
  13. EventQuery,
  14. EventQueryResult,
  15. )
  16. if TYPE_CHECKING:
  17. from trixy_core.events.event_data.base import EventData
  18. class InMemoryEventStore(EventStore):
  19. """
  20. Event Store im Arbeitsspeicher.
  21. Schnell, aber nicht persistent. Ideal für Tests
  22. und kurzfristiges Event-Tracking.
  23. Example:
  24. store = InMemoryEventStore(max_events=10000)
  25. # Events speichern
  26. await store.append("click", event_data)
  27. # Abfragen
  28. result = await store.query(EventQuery(limit=100))
  29. """
  30. def __init__(
  31. self,
  32. max_events: int = 100000,
  33. auto_cleanup: bool = True,
  34. cleanup_threshold: float = 0.9,
  35. ) -> None:
  36. """
  37. Initialisiert den Store.
  38. Args:
  39. max_events: Maximale Anzahl Events.
  40. auto_cleanup: Automatisches Löschen alter Events.
  41. cleanup_threshold: Schwelle für Cleanup (0.0-1.0).
  42. """
  43. self._events: dict[str, StoredEvent] = {}
  44. self._sequence_index: list[str] = [] # IDs in Reihenfolge
  45. self._event_name_index: dict[str, list[str]] = {} # Event-Name -> IDs
  46. self._source_index: dict[str, list[str]] = {} # Source -> IDs
  47. self._max_events = max_events
  48. self._auto_cleanup = auto_cleanup
  49. self._cleanup_threshold = cleanup_threshold
  50. self._current_sequence = 0
  51. self._lock = asyncio.Lock()
  52. @property
  53. def event_count(self) -> int:
  54. """Anzahl gespeicherter Events."""
  55. return len(self._events)
  56. async def append(
  57. self,
  58. event_name: str,
  59. event_data: "EventData",
  60. ) -> StoredEvent:
  61. """
  62. Speichert ein Event.
  63. Args:
  64. event_name: Name des Events.
  65. event_data: Die Event-Daten.
  66. Returns:
  67. Das gespeicherte Event.
  68. """
  69. async with self._lock:
  70. # Cleanup wenn nötig
  71. if self._auto_cleanup and self.event_count >= int(self._max_events * self._cleanup_threshold):
  72. await self._cleanup()
  73. self._current_sequence += 1
  74. stored = StoredEvent.from_event(
  75. event_name=event_name,
  76. event_data=event_data,
  77. sequence=self._current_sequence,
  78. )
  79. # In Hauptindex speichern
  80. self._events[stored.id] = stored
  81. # In Sequence-Index
  82. self._sequence_index.append(stored.id)
  83. # In Event-Name-Index
  84. if event_name not in self._event_name_index:
  85. self._event_name_index[event_name] = []
  86. self._event_name_index[event_name].append(stored.id)
  87. # In Source-Index
  88. if stored.source:
  89. if stored.source not in self._source_index:
  90. self._source_index[stored.source] = []
  91. self._source_index[stored.source].append(stored.id)
  92. return stored
  93. async def get(self, event_id: str) -> StoredEvent | None:
  94. """
  95. Gibt ein Event nach ID zurück.
  96. Args:
  97. event_id: Die Event-ID.
  98. Returns:
  99. StoredEvent oder None.
  100. """
  101. return self._events.get(event_id)
  102. async def query(self, query: EventQuery) -> EventQueryResult:
  103. """
  104. Führt eine Abfrage aus.
  105. Args:
  106. query: Die Abfrage.
  107. Returns:
  108. EventQueryResult.
  109. """
  110. async with self._lock:
  111. # Kandidaten ermitteln
  112. if query.event_names:
  113. candidate_ids: set[str] = set()
  114. for name in query.event_names:
  115. candidate_ids.update(self._event_name_index.get(name, []))
  116. else:
  117. candidate_ids = set(self._events.keys())
  118. # Source-Filter
  119. if query.source:
  120. source_ids = set(self._source_index.get(query.source, []))
  121. candidate_ids &= source_ids
  122. # Events filtern
  123. filtered: list[StoredEvent] = []
  124. for event_id in candidate_ids:
  125. event = self._events.get(event_id)
  126. if not event:
  127. continue
  128. # Zeit-Filter
  129. if query.start_time and event.timestamp < query.start_time:
  130. continue
  131. if query.end_time and event.timestamp > query.end_time:
  132. continue
  133. # Sequenz-Filter
  134. if query.after_sequence is not None and event.sequence <= query.after_sequence:
  135. continue
  136. if query.before_sequence is not None and event.sequence >= query.before_sequence:
  137. continue
  138. # Metadata-Filter
  139. if query.metadata_filter:
  140. match = True
  141. for key, value in query.metadata_filter.items():
  142. if event.metadata.get(key) != value:
  143. match = False
  144. break
  145. if not match:
  146. continue
  147. filtered.append(event)
  148. # Sortieren
  149. filtered.sort(
  150. key=lambda e: e.sequence,
  151. reverse=not query.order_ascending,
  152. )
  153. total_count = len(filtered)
  154. # Paginierung
  155. start = query.offset
  156. end = start + query.limit
  157. paginated = filtered[start:end]
  158. return EventQueryResult(
  159. events=paginated,
  160. total_count=total_count,
  161. has_more=end < total_count,
  162. first_sequence=paginated[0].sequence if paginated else None,
  163. last_sequence=paginated[-1].sequence if paginated else None,
  164. )
  165. async def count(self, query: EventQuery | None = None) -> int:
  166. """
  167. Zählt Events.
  168. Args:
  169. query: Optionale Abfrage.
  170. Returns:
  171. Anzahl der Events.
  172. """
  173. if query is None:
  174. return self.event_count
  175. result = await self.query(EventQuery(
  176. event_names=query.event_names,
  177. start_time=query.start_time,
  178. end_time=query.end_time,
  179. source=query.source,
  180. metadata_filter=query.metadata_filter,
  181. limit=1000000, # Großes Limit für Zählung
  182. ))
  183. return result.total_count
  184. async def delete(self, event_id: str) -> bool:
  185. """
  186. Löscht ein Event.
  187. Args:
  188. event_id: Die Event-ID.
  189. Returns:
  190. True wenn gelöscht.
  191. """
  192. async with self._lock:
  193. event = self._events.pop(event_id, None)
  194. if not event:
  195. return False
  196. # Aus Indices entfernen
  197. if event_id in self._sequence_index:
  198. self._sequence_index.remove(event_id)
  199. if event.event_name in self._event_name_index:
  200. ids = self._event_name_index[event.event_name]
  201. if event_id in ids:
  202. ids.remove(event_id)
  203. if event.source and event.source in self._source_index:
  204. ids = self._source_index[event.source]
  205. if event_id in ids:
  206. ids.remove(event_id)
  207. return True
  208. async def clear(self) -> int:
  209. """
  210. Löscht alle Events.
  211. Returns:
  212. Anzahl gelöschter Events.
  213. """
  214. async with self._lock:
  215. count = len(self._events)
  216. self._events.clear()
  217. self._sequence_index.clear()
  218. self._event_name_index.clear()
  219. self._source_index.clear()
  220. self._current_sequence = 0
  221. return count
  222. async def _cleanup(self) -> int:
  223. """
  224. Entfernt älteste Events.
  225. Returns:
  226. Anzahl entfernter Events.
  227. """
  228. # Älteste 10% entfernen
  229. to_remove = int(self._max_events * 0.1)
  230. removed = 0
  231. for _ in range(to_remove):
  232. if not self._sequence_index:
  233. break
  234. event_id = self._sequence_index[0]
  235. if await self.delete(event_id):
  236. removed += 1
  237. return removed
  238. def get_event_names(self) -> list[str]:
  239. """
  240. Gibt alle Event-Namen zurück.
  241. Returns:
  242. Liste der Event-Namen.
  243. """
  244. return list(self._event_name_index.keys())
  245. def get_sources(self) -> list[str]:
  246. """
  247. Gibt alle Quellen zurück.
  248. Returns:
  249. Liste der Quellen.
  250. """
  251. return list(self._source_index.keys())
  252. def get_stats(self) -> dict[str, Any]:
  253. """
  254. Gibt Statistiken zurück.
  255. Returns:
  256. Statistik-Dictionary.
  257. """
  258. return {
  259. "total_events": self.event_count,
  260. "max_events": self._max_events,
  261. "fill_rate": self.event_count / self._max_events if self._max_events > 0 else 0,
  262. "current_sequence": self._current_sequence,
  263. "event_types": len(self._event_name_index),
  264. "sources": len(self._source_index),
  265. "events_per_type": {
  266. name: len(ids)
  267. for name, ids in self._event_name_index.items()
  268. },
  269. }