Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Pythonによるイベントソーシングへの挑戦と現状に対する考察 / Challenging E...

nrs
September 28, 2024

Pythonによるイベントソーシングへの挑戦と現状に対する考察 / Challenging Event Sourcing with Python and Reflections on the Current State

PyConJP 2024 のセッション用スライドです
Python eventsourcing がメインとなっています

Proposal: https://2024.pycon.jp/ja/talk/DAH78A

本トークではイベントソーシングの概念や利点をお話するのと同時に、Pythonにおけるイベントソーシングに挑戦した結果を共有します。

私は普段、JVM でイベントソーシングをベースとしたマイクロサービス群の開発に取り組んでいます。 イベントソーシングはシステム上の出来事をイベントとして永続化することでシステムを構築する手法です。 システムをイベントでとらえることは多くの利点をもたらします。

ひとつ分かりやすい例を挙げるとすれば、システムの全ての状態変化を時間を追って追跡することが可能となることが挙げられます。 たとえば、何らかの問題が発生した場合には、発生したイベントのシーケンスをたどることで問題の原因を容易に特定できます。 ちょうどGitのログをたどるのと同じイメージです。 これはシステムの運用を確実に楽にします。

その他にもイベントデータはシステム間の連携を疎結合にしますし、永続化されたイベントデータを利用して、あとからBIツールと連携するといった芸当も可能です。

本トークではそのように便利なイベントソーシングの基本原理やメリットなどをお話しし、またイベントソーシングを実践する上で諸問題への回答となる各種機能について網羅して解説します。 また、それらの前提を共有した上で、現時点で Python における実装に挑戦した結果を紹介し、実践にむけて越えなくてはいけないハードル=「Python で現状実現できてないこと」について探ります。

# URL
YouTube: https://www.youtube.com/c/narusemi
HomePage: https://nrslib.com
Twitter: https://twitter.com/nrslib
Instagram: https://www.instagram.com/nrslib/

nrs

September 28, 2024
Tweet

More Decks by nrs

Other Decks in Programming

Transcript

  1. [tool.poetry] name = "pyes-fastapi" version = "0.1.0" description = ""

    authors = ["Your Name <[email protected]>"] readme = "README.md" [tool.poetry.dependencies] python = "^3.10" uvicorn = {extras = ["standard"], version = "^0.24.0.post1"} fastapi = "^0.109.1" sqlalchemy = "^2.0.0" fastapi-sqlalchemy = "^0.2.1" pydantic = "^1.10.12" # latest eventsourcing = { version = "~=9.3", extras = ["sqlite"] } eventsourcing_sqlalchemy = "^0.8" # with Axon #eventsourcing = { version = ">=9.2,<9.3", extras = ["sqlite"] } #eventsourcing-axonserver="^0.1.5" #eventsourcing_sqlalchemy = "^0.7" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"
  2. class DocService(Application): def create(self, body): doc = Doc(body) self.save(doc) return

    doc.id class Doc(Aggregate): body: str class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body :
  3. class DocService(Application): def create(self, body): doc = Doc(body) self.save(doc) return

    doc.id class Doc(Aggregate): body: str class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body :
  4. class DocService(Application): def create(self, body): doc = Doc(body) self.save(doc) return

    doc.id class Doc(Aggregate): body: str class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body :
  5. class DocService(Application): def create(self, body): doc = Doc(body) self.save(doc) return

    doc.id class Doc(Aggregate): body: str class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body :
  6. system = System( pipes=[ [DocService, DocumentDataModelProjection], [EffectiveService, DocumentDataModelProjection], ] )

    runner = MultiThreadedRunner # SingleThreadedRunner にすると同期的に処理される runner = runner(system, None) runner.start() doc_service = runner.get(DocService)
  7. system = System( pipes=[ [DocService, DocumentDataModelProjection], [EffectiveService, DocumentDataModelProjection], ] )

    runner = MultiThreadedRunner # SingleThreadedRunner にすると同期的に処理される runner = runner(system, None) runner.start() doc_service = runner.get(DocService)
  8. class DocumentDataModelProjection(ProcessApplication): @policy.register(Doc.DocCreated) def handle(self, event: Doc.DocCreated, process_event): document_create =

    DocumentCreate( document_id=event.originator_id, body=event.body, effective_count=0 ) with get_db() as db: create_document(db, document_create)
  9. class DocumentDataModelProjection(ProcessApplication): @policy.register(Doc.DocCreated) def handle(self, event: Doc.DocCreated, process_event): document_create =

    DocumentCreate( document_id=event.originator_id, body=event.body, effective_count=0 ) with get_db() as db: create_document(db, document_create)
  10. class DocumentDataModelProjection(ProcessApplication): @policy.register(Doc.DocCreated) def handle(self, event: Doc.DocCreated, process_event): document_create =

    DocumentCreate( document_id=event.originator_id, body=event.body, effective_count=0 ) with get_db() as db: create_document(db, document_create)
  11. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  12. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  13. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  14. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  15. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  16. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  17. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  18. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  19. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  20. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  21. class DocService(Application): is_snapshotting_enabled = True def modify(self, doc_id, body): doc

    = self.repository.get(doc_id) doc.modify(body) self.save(doc) self.take_snapshot(doc.id) class Doc(Aggregate): class DocCreated(Aggregate.Created): body: str @event(DocCreated) def __init__(self, body): self.body = body class DocModified(Aggregate.Event): body: str @event(DocModified) def modify(self, body): self.body = body
  22. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool # Event

    Classes class Created(Aggregate.Created): def apply(self, aggregate: Aggregate): aggregate.when(self) @classmethod def create(cls, doc_id: UUID, user_id: UUID): return cls._create( event_class=cls.Created, id=cls.create_id(doc_id, user_id), doc_id=doc_id, user_id=user_id, ) @staticmethod def create_id(doc_id: UUID, user_id: UUID) -> UUID: return uuid5(NAMESPACE_URL, f"/effective/{doc_id}|{user_id}") @event(EffectiveCreated) def __init__(self, doc_id: UUID, user_id: UUID): self.doc_id = doc_id self.user_id = user_id self.marked = True # <略>
  23. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool # Event

    Classes class Created(Aggregate.Created): def apply(self, aggregate: Aggregate): aggregate.when(self) @classmethod def create(cls, doc_id: UUID, user_id: UUID): return cls._create( event_class=cls.Created, id=cls.create_id(doc_id, user_id), doc_id=doc_id, user_id=user_id, ) @staticmethod def create_id(doc_id: UUID, user_id: UUID) -> UUID: return uuid5(NAMESPACE_URL, f"/effective/{doc_id}|{user_id}") @event(EffectiveCreated) def __init__(self, doc_id: UUID, user_id: UUID): self.doc_id = doc_id self.user_id = user_id self.marked = True # <略>
  24. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool # Event

    Classes class Created(Aggregate.Created): def apply(self, aggregate: Aggregate): aggregate.when(self) @classmethod def create(cls, doc_id: UUID, user_id: UUID): return cls._create( event_class=cls.Created, id=cls.create_id(doc_id, user_id), doc_id=doc_id, user_id=user_id, ) @staticmethod def create_id(doc_id: UUID, user_id: UUID) -> UUID: return uuid5(NAMESPACE_URL, f"/effective/{doc_id}|{user_id}") @event(EffectiveCreated) def __init__(self, doc_id: UUID, user_id: UUID): self.doc_id = doc_id self.user_id = user_id self.marked = True # <略>
  25. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool # Event

    Classes class Created(Aggregate.Created): def apply(self, aggregate: Aggregate): aggregate.when(self) @classmethod def create(cls, doc_id: UUID, user_id: UUID): return cls._create( event_class=cls.Created, id=cls.create_id(doc_id, user_id), doc_id=doc_id, user_id=user_id, ) @staticmethod def create_id(doc_id: UUID, user_id: UUID) -> UUID: return uuid5(NAMESPACE_URL, f"/effective/{doc_id}|{user_id}") @event(EffectiveCreated) def __init__(self, doc_id: UUID, user_id: UUID): self.doc_id = doc_id self.user_id = user_id self.marked = True # <略>
  26. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool # Event

    Classes class Created(Aggregate.Created): def apply(self, aggregate: Aggregate): aggregate.when(self) @classmethod def create(cls, doc_id: UUID, user_id: UUID): return cls._create( event_class=cls.Created, id=cls.create_id(doc_id, user_id), doc_id=doc_id, user_id=user_id, ) @staticmethod def create_id(doc_id: UUID, user_id: UUID) -> UUID: return uuid5(NAMESPACE_URL, f"/effective/{doc_id}|{user_id}") @event(EffectiveCreated) def __init__(self, doc_id: UUID, user_id: UUID): self.doc_id = doc_id self.user_id = user_id self.marked = True # <略>
  27. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  28. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  29. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  30. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  31. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  32. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def mark(self):

    if not self.marked: self._mark(self.doc_id) @event(EffectiveMarked) def _mark(self, doc_id: UUID): self.marked = True # <略>
  33. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def unmark(self):

    if self.marked: self._unmark() def _unmark(self): self.trigger_event(Effective.EffectiveUnmarked, doc_id=self.doc_id) @singledispatchmethod def when(self, event) -> None: raise NotImplementedError(f"Unsupported event type: {type(event)}") @when.register(EffectiveUnmarked) def _(self, event: EffectiveUnmarked) -> None: self.marked = False # <略>
  34. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def unmark(self):

    if self.marked: self._unmark() def _unmark(self): self.trigger_event(Effective.EffectiveUnmarked, doc_id=self.doc_id) @singledispatchmethod def when(self, event) -> None: raise NotImplementedError(f"Unsupported event type: {type(event)}") @when.register(EffectiveUnmarked) def _(self, event: EffectiveUnmarked) -> None: self.marked = False # <略>
  35. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def unmark(self):

    if self.marked: self._unmark() def _unmark(self): self.trigger_event(Effective.EffectiveUnmarked, doc_id=self.doc_id) @singledispatchmethod def when(self, event) -> None: raise NotImplementedError(f"Unsupported event type: {type(event)}") @when.register(EffectiveUnmarked) def _(self, event: EffectiveUnmarked) -> None: self.marked = False # <略>
  36. class Effective(Aggregate): doc_id: UUID user_id: UUID marked: bool def unmark(self):

    if self.marked: self._unmark() def _unmark(self): self.trigger_event(Effective.EffectiveUnmarked, doc_id=self.doc_id) @singledispatchmethod def when(self, event) -> None: raise NotImplementedError(f"Unsupported event type: {type(event)}") @when.register(EffectiveUnmarked) def _(self, event: EffectiveUnmarked) -> None: self.marked = False # <略>
  37. class DocumentDataModelProjection(ProcessApplication): @singledispatchmethod def policy(self, domain_event, process_event): """Default policy""" @policy.register(Effective.EffectiveCreated)

    def handle(self, event: Effective.EffectiveCreated, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count + 1)) @policy.register(Effective.EffectiveMarked) def handle(self, event: Effective.EffectiveMarked, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count + 1)) @policy.register(Effective.EffectiveUnmarked) def handle(self, event: Effective.EffectiveUnmarked, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count - 1))
  38. class DocumentDataModelProjection(ProcessApplication): @singledispatchmethod def policy(self, domain_event, process_event): """Default policy""" @policy.register(Effective.EffectiveCreated)

    def handle(self, event: Effective.EffectiveCreated, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count + 1)) @policy.register(Effective.EffectiveMarked) def handle(self, event: Effective.EffectiveMarked, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count + 1)) @policy.register(Effective.EffectiveUnmarked) def handle(self, event: Effective.EffectiveUnmarked, _): with get_db() as db: document = get_document_by_id(db, event.doc_id) update_document(db, event.doc_id, DocumentUpdate(effective_count=document.effective_count - 1))
  39. os.environ['PERSISTENCE_MODULE'] = 'eventsourcing_axonserver' os.environ['AXONSERVER_URI'] = '127.0.0.1:8124' system = System( pipes=[

    [DocService, DocumentDataModelProjection], [EffectiveService, DocumentDataModelProjection], ] ) runner = MultiThreadedRunner runner = runner(system, None) runner.start() doc_service = runner.get(DocService)