Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
PHPでアクターモデルを活用したSagaパターンの実践法 / php-saga-pattern...
Search
yuuki takezawa
March 21, 2025
Technology
3.1k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PHPでアクターモデルを活用したSagaパターンの実践法 / php-saga-pattern-with-actor-model
phperkaigi2025
yuuki takezawa
March 21, 2025
More Decks by yuuki takezawa
See All by yuuki takezawa
なぜAI時代に 「イベント」を中心に考えるのか? / Why focus on "events" in the age of AI?
ytake
4
2.2k
PHP ステートレス VS ステートフル 状態管理と並行性 / php-stateless-stateful
ytake
0
380
PHPでアクターモデルを理解・体験しよう / Understand and experience the actor model in PHP
ytake
2
1k
再考 アクターモデル/ reconsider actor model
ytake
0
1.7k
GoとアクターモデルでES+CQRSを実践! / proto_actor_es_cqrs
ytake
1
730
Phluxorでアクターモデルを 理解・体験しよう / toolkit-for-flexible-actor-models-in-php-phluxor
ytake
1
400
オブジェクトのおしゃべり大失敗 メッセージングアンチパターン集 / messaging anti-pattern collection
ytake
2
1.4k
DRE/SREのプラクティス融合によるクラウドネイティブなデータ基盤作り / dre_sre
ytake
0
1.1k
技術的負債と向き合う取り組みでよかったもの / positive_efforts_to_tackle_technical_debt
ytake
10
4.1k
Other Decks in Technology
See All in Technology
Futexes the good, the bad, the ugly
ennael
PRO
0
110
1人アドミンな私はAWSアカウント申請をSlackで完結したい!
ysuzuki
0
110
Lambda MicroVMsが分からなすぎたので使い所を1から考えてみた
tsukuboshi
2
340
雪かき部 #7 もう怖くない!SELECT文!
foursue
0
280
FinTech 1-2 : Overview of FinTech
ks91
PRO
0
140
Apache Iceberg が拓く AI 時代のオープンレイクハウス
tomtanaka
0
220
[2026 Oracle Technical Deep Dive] OCI AI Resilience -OCIのセキュリティ対策機能をきちんと使いこなす- (2026年9月17日開催)
oracle4engineer
PRO
0
110
Google Cloud Next Tokyo 26登壇時のスクリプト
recruitengineers
PRO
0
170
おそらく日本で唯一のDevRelインターン生として
husengs7
0
160
IR Today: Theory, Practice, and Agents
dtunkelang
0
340
【データ横丁主催】AI Agentがコンテキストを使って仕事をした後、何が残るのか― 組織の経験を次の判断に引き継ぐ「Agent Memory」
shisyu_gaku
2
320
生成AIで高い生産性を求められて、 悩み、乗り越えた話
mot_techtalk
2
110
Featured
See All Featured
Optimizing for Happiness
mojombo
378
71k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.8k
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
350
Build your cross-platform service in a week with App Engine
jlugia
234
19k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
340
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
600
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.4k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.9k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
1k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
720
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
63
46k
Transcript
PHPͰΞΫλʔϞσϧΛ ׆༻ͨ͠ Sagaύλʔϯͷ࣮ફ๏ PHPerKaigi 2025 ytake
Pro fi le • ᖒ ༗و a.k.a ytake • ઍגࣜձࣾ
CTO גࣜձࣾACESɺגࣜձࣾωοτϓϩςΫγϣϯζٕज़ސ • Go / Scala / Kotlin
https://github.com/ytake/ phluxor-saga-example
SagaύλʔϯͬͯͳΜͩʁ
Sagaύλʔϯͱ • ෳͷαʔϏεʢཧతʹ͔Ε͍ͯΔͷʣʹ·͕ͨΔॲཧͰ Ұͭʹ·ͱ·ͬͨτϥϯβΫγϣϯͷΑ͏ʹѻ͏ͨΊͷύλʔϯ • ్தͷॲཧ͕ࣦഊͨ͠߹ɺ γεςϜશମΛໃ६ͷͳ͍ঢ়ଶʹͨ͢Ίͷ ʮิঈτϥϯβΫγϣϯʯΛ࣮ߦ
None
None
None
X
X
ฦ • ϨγʔτΛͱʹɺฦɾฦۚͯ͠Β͏ ߪೖͷࣄ࣮͕͍ͬͯΔ • ͓ళʹΓɺ͓ۚࣗͷखݩʹฦͬͯ͘Δ
࣮ફ͢Δʹɾɾɾʁ
ࣦഊ͔Βͷճ෮ઓུ • ࣦഊͨ͠߹ʹલͷঢ়ଶʹͤΔ͜ͱ • ్தͷॲཧ͕ࣦഊͨ͠߹ɺϦτϥΠ͢Δ • ଞͷϥϯλΠϜͰॲཧ͕ߦΘΕͯໃ६͕ൃੜ͠ͳ͍Α͏ʹ͢Δ
࣮͢Δ߹ • ΠϕϯτιʔγϯάͰલͷঢ়ଶʹͤΔΑ͏ʹ • Πϕϯτঢ়ଶͷεφοϓγϣοτʹ • ෭߹ͤΛߦͬͯঢ়ଶ෮ݩ͢Δܗʹ͠ͳ͍ʂ
None
None
None
ؾΛ͚ͭΑ͏ • ରͷͷҎ֎௨ৗʹՔಇ • ଞͷͷΛελοΫͤ͞ͳ͍Α͏ʹ • ࣌ܥྻ͕લޙ͠ͳ͍Α͏ʹ
ૹۚͰߟ͑ͯΈΑ͏
γφϦΦ
ۚમৼସγφϦΦ • Account1 ͔Β Account2 10ԁΛૹۚ͢ΔγφϦΦ • ޱ࠲͕ϦϞʔταʔϏεͱͯ͠ಈ࡞͢Δ߹ʹى͜Γ͏Δ ϦΫΤετڋ൱ɺΫϥογϡɺϏδʔঢ়ଶɺλΠϜΞτͳͲΛ
ߟྀ
None
ޭ • Account1 ͷߴ 0 ԁɺAccount2 ͷߴ 20 ԁ •
ظ௨Γʹ͓ޓ͍ͷॲཧ͕ਖ਼ৗʹऴΘΔ
ࣦഊ • Account1 ͱ Account2 ͷߴͦΕͧΕ 10 ԁ (Ұ؏ੑͷ͋Δঢ়ଶ) •
ظ௨Γʹ͓ޓ͍ͷঢ়ଶ͕ͱʹΔ
ࣦഊγφϦΦͷରԠ:ϦτϥΠ • Ұ࣌తͳΤϥʔʢϏδʔঢ়ଶɺλΠϜΞτʣʹରͯ͠ɺ ҰఆճϦτϥΠΛࢼΈΔ
ิঈॲཧ (ϩʔϧόοΫ) • Debit ͕ޭ͕ͨ͠ Credit ͕ࣦഊͨ͠߹ɺ Debit ΛऔΓফͨ͢Ίʹ Credit
Λ Account1 ʹ͢
ΤεΧϨʔγϣϯ • ϦτϥΠิঈॲཧ͕͏·͍͔͘ͳ͍߹ɺ खಈհೖ͕ඞཁͱͳΔՄೳੑ͕͋ΔͨΊɺ ΤεΧϨʔγϣϯઓུΛઃ͚Δ
ႈੑ • Account αʔϏε͕ϦτϥΠ࣌ʹಉ͡ϝοηʔδΛ ෳճड͚औͬͯಉ݁͡Ռ͕ಘΒΕΔΑ͏ʹɺ ႈͰ͋Δඞཁ͕͋Γ • ॲཧࡁΈͷϝοηʔδΛอ͠ɺ ॏෳͨ͠ϝοηʔδʹରͯ͠ಉ͡ԠΛฦ͢
ΦʔέετϨʔγϣϯͷSaga࣮
ΞΫλʔϞσϧͰ࣮ફʂ
Phluxor
Similar to akka, pekko and Proto Actor
ɹPHPΛ௨ͯ͡ ΞΫλʔϞσϧ͕ ମݧͰ͖ΔπʔϧΩοτ *༻࣮ફଞݴޠΛ͓નΊ͠·͢
ΞΫλʔϞσϧͷ͓͞Β͍
None
None
None
ߏ • Runner - సૹϓϩηεΛ։࢝͠ɺ݁ՌΛऩू͢ΔओཁͳΞΫλʔ • Account - ߴΛ࣋ͪɺೖۚ/ग़ۚૢ࡞Λॲཧ͢ΔΞΫλʔ •
TransferProcess - ΞΧϯτؒͷૹۚΛ੍ޚ͢ΔΞΫλʔ • AccountProxy - TransferProcessͱAccountΞΫλʔؒͷ௨৴Λհ
None
None
TransferProcessΞΫλʔ
None
TransferProcess ΞΫλʔ • సૹΛཧ͢ΔΛ࣋ͭ • Process Manager • ྃͨ͠εςοϓΛ͠ɺࣦഊ࣌ʹิঈાஔΛద༻ •
BehaviorɺPersistenceɺSupervisionར༻ʂ
Βͳ͍༻ޠ͔Γɾɾ
࣮ફ͍ͨ͠ํͷͨΊʹղઆʂ
Process Manager
None
ΑΓཧղ͢ΔͨΊʹ
Behavior
Behavior • ΞΫλʔ͕ड৴͢Δϝοηʔδʹର͢Δॲཧͷఆٛ • ΞΫλʔ ঢ়ଶΛ࣋ͨͣʢεςʔτϨεʣͰͳ͍ • ΞΫλʔঢ়ଶΛ࣋ͪಈతʹ ৼΔ͍Λมߋ ͢Δ
Become / Unbecome • Become ৽͍͠ৼΔ͍ʹʮมԽʯ͢Δʢྫ: ঢ়ଶભҠΛ͏ಈ࡞ʣ มԽҎ߱ͷϝοηʔδ ৽͍͠ৼΔ͍Ͱॲཧ •
Unbecome ҎલͷৼΔ͍ʹʮΔʯɺ·ͨσϑΥϧτͷಈ࡞ʹϦηοτ
None
None
class TransferProcess implements ActorInterface, PersistentInterface { use Mixin; private bool
$processCompleted = false; private bool $restarting = false; private bool $stopping = false; public function __construct( private readonly Ref $from, private readonly Ref $to, private readonly float $amount, private readonly float $availability, private readonly Behavior $behavior = new Behavior() ) { $this->behavior->become( new ReceiveFunction( fn($context) => $this->starting($context) ) ); }
class TransferProcess implements ActorInterface, PersistentInterface { use Mixin; private bool
$processCompleted = false; private bool $restarting = false; private bool $stopping = false; public function __construct( private readonly Ref $from, private readonly Ref $to, private readonly float $amount, private readonly float $availability, private readonly Behavior $behavior = new Behavior() ) { $this->behavior->become( new ReceiveFunction( fn($context) => $this->starting($context) ) ); } 1FSTJTUFODFΛ͏͜ͱͰɺ ΞΫλʔ͕ࣗͲ͏ͳ͔ͬͨΛอ ͭ·Γ&WFOU4PVSDJOH
class TransferProcess implements ActorInterface, PersistentInterface { use Mixin; private bool
$processCompleted = false; private bool $restarting = false; private bool $stopping = false; public function __construct( private readonly Ref $from, private readonly Ref $to, private readonly float $amount, private readonly float $availability, private readonly Behavior $behavior = new Behavior() ) { $this->behavior->become( new ReceiveFunction( fn($context) => $this->starting($context) ) ); } 1IMVYPSͰ.JYJO5SBJUʹجຊ࣮͋Γ ར༻͢Δͱঢ়ଶ෮ݩͳͲΛࣗಈͰߦ͏
class TransferProcess implements ActorInterface, PersistentInterface { use Mixin; private bool
$processCompleted = false; private bool $restarting = false; private bool $stopping = false; public function __construct( private readonly Ref $from, private readonly Ref $to, private readonly float $amount, private readonly float $availability, private readonly Behavior $behavior = new Behavior() ) { $this->behavior->become( new ReceiveFunction( fn($context) => $this->starting($context) ) ); } ΞΫλʔͷৼΔ͍Λมߋ͢Δ #FIBJWPS
class TransferProcess implements ActorInterface, PersistentInterface { use Mixin; private bool
$processCompleted = false; private bool $restarting = false; private bool $stopping = false; public function __construct( private readonly Ref $from, private readonly Ref $to, private readonly float $amount, private readonly float $availability, private readonly Behavior $behavior = new Behavior() ) { $this->behavior->become( new ReceiveFunction( fn($context) => $this->starting($context) ) ); } ͜ͷΞΫλʔͷॳظঢ়ଶͱͯ͠ TUBSUJOHϝιου͕ಈ͘
None
private function starting(ContextInterface $context): void { if ($context->message() instanceof Started)
{ $context->spawnNamed($this->tryDebit($this->from, -$this->amount), 'DebitAttempt'); $this->persistEvent(new ProtoBuf\TransferStarted()); } } private function tryDebit(Ref $targetActor, float $amount): Props { return Props::fromProducer( fn() => new AccountProxy( $targetActor, fn($sender) => new Debit($amount, $sender) ) ); }
private function starting(ContextInterface $context): void { if ($context->message() instanceof Started)
{ $context->spawnNamed($this->tryDebit($this->from, -$this->amount), 'DebitAttempt'); $this->persistEvent(new ProtoBuf\TransferStarted()); } } private function tryDebit(Ref $targetActor, float $amount): Props { return Props::fromProducer( fn() => new AccountProxy( $targetActor, fn($sender) => new Debit($amount, $sender) ) ); } %FCJUΞΫλʔΛੜ
private function starting(ContextInterface $context): void { if ($context->message() instanceof Started)
{ $context->spawnNamed($this->tryDebit($this->from, -$this->amount), 'DebitAttempt'); $this->persistEvent(new ProtoBuf\TransferStarted()); } } private function tryDebit(Ref $targetActor, float $amount): Props { return Props::fromProducer( fn() => new AccountProxy( $targetActor, fn($sender) => new Debit($amount, $sender) ) ); } సૹ։࢝ΠϕϯτΛอ
private function starting(ContextInterface $context): void { if ($context->message() instanceof Started)
{ $context->spawnNamed($this->tryDebit($this->from, -$this->amount), 'DebitAttempt'); $this->persistEvent(new ProtoBuf\TransferStarted()); } } private function tryDebit(Ref $targetActor, float $amount): Props { return Props::fromProducer( fn() => new AccountProxy( $targetActor, fn($sender) => new Debit($amount, $sender) ) ); } "DDPVOU1SPYZΛੜ͠ɺ ग़ۚϝοηʔδΛૹ৴
Persistence
Persistence • ΞΫλʔͷӬଓԽΛߦ͏ • ΞΫλʔͷϥΠϑαΠΫϧͱରԠ͓ͯ͠Γɺ ىಈ࣌ʹӬଓԽ͞Εͨঢ়ଶʹΞΫλʔΛ෮ݩ͢Δ • δϟʔφϧͱεφοϓγϣοτΛΈ߹Θͤͯ࠷Ͱ෮ݩ͞ΕΔ • ࠷৽ঢ়ଶΛ͚࣋ͭͩɺͰͳ͍ͷ͕ϙΠϯτ
PersistEvent
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } }
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } } ͍͔ͭ͘ͷΠϕϯτΛӬଓԽͨ͠ޙʹ ࣗΞΫλʔͷৼΔ͍Λมߋ͢Δ
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } } ૹ͕ۚ։࢝͢Δͱɺ Ҿ͖མͱ͠ʹؔ͢ΔৼΔ͍ʹมԽ
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } } ೖۚʹؔ͢ΔৼΔ͍ʹมԽ
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } } ೖ͕ࣦۚഊ͢Δͱ ϩʔϧόοΫͷৼΔ͍ʹ
private function applyEvent(Message $event): void { switch (true) { case
$event instanceof ProtoBuf\TransferStarted: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingDebitConfirmation($context) ) ); break; case $event instanceof ProtoBuf\AccountDebited: $this->behavior->become( new ReceiveFunction( fn($context) => $this->awaitingCreditConfirmation($context) ) ); break; case $event instanceof ProtoBuf\CreditRefused: $this->behavior->become( new ReceiveFunction( fn($context) => $this->rollingBackDebit($context) ) ); break; case $event instanceof ProtoBuf\AccountCredited: case $event instanceof ProtoBuf\DebitRolledBack: case $event instanceof ProtoBuf\TransferFailed: $this->processCompleted = true; break; } } ਖ਼ৗʹೖग़͕ۚྃɺ ϩʔϧόοΫ͕ྃɺ ࢦఆճҎ্ૹۚʹࣦഊ͢Δͱऴྃ
None
Supervision
Supervision • ΞΫλʔࢠؔΛ࣋ͪɺࢠΛࢹ͢ΔΛ࣋ͭ • ࢠΞΫλʔͷΤϥʔൃੜ࣌ɺΞΫλʔ͕ઓུʹج͖ͮ ࢠΞΫλʔͷ࠶ىಈ෮چΛཧ • ͕μϯ͢ΔͱԼͷࢠΞΫλʔμϯ
Supervision Strategies • OneForOneStrategy • AllForOneStrategy • ExponentialBackoffStrategy • RestartStrategy
None
None
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } }
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } } ΞΫλʔੜ࣌ʹ োઓུΛࢦࣔ
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } } 0OF'PS0OF ࢠΞΫλʔͷ͏ͪରͷҰ͚ͭͩ
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } } ඵؒͷ͏ͪࢦఆճ ճ ֘ͷΞΫλʔΛ࠶ىಈ͢Δ
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } } ϝοηʔδΛอͯ͠ ΞΫλʔΛ෮ݩ͢Δ
readonly class TransferFactory { public function __construct( private ContextInterface $context,
private float $availability, private int $retryAttempts, private ProviderInterface $provider, ) { } public function createTransfer( string $actorName, Ref $fromAccount, Ref $toAccount, float $amount, ): SpawnResult { $props = Props::fromProducer( fn() => new TransferProcess($fromAccount, $toAccount, $amount, $this->availability), Props::withReceiverMiddleware( new EventSourcedFactory($this->provider) ), Props::withSupervisor( new OneForOneStrategy( $this->retryAttempts, new DateInterval('PT10S'), new DefaultDecider(), ) ) ); return $this->context->spawnNamed($props, $actorName); } } ࢠΞΫλʔΛ࠶ىಈ͢Δͱɺ ϝοηʔδʹରԠͨ͠ॲཧΛ෦Ͱߦ͍ ࠷৽ঢ়ଶʹ෮ݩ োൃੜ࣌ͷঢ়ଶʹΔʂ
TransferProcess + State Machine + Persistence
TransferProcess ฒߦͰଘࡏ • ֤ग़ۚ͝ͱʹಠཱͯ͠ੜɾঢ়ଶڞ༗͞Εͳ͍ • ࢦఆ͞Εͨϝοηʔδ͕དྷͳ͍ݶΓৼΔ͍มΘΒͳ͍ɾΒͳ͍ • ӬଓԽ͞Ε͍ͯΔͨΊɺ࠶ىಈ͢ΔͱͦΕͧΕͷ࠷৽ঢ়ଶ෮ݩ • Error
Kernel Patternར༻ͰোΛΓ͠
Error Kernel Pattern?
None
AccountΞΫλʔ
AccountΞΫλʔ • ग़ۚͱೖ͕ۚͦΕͧΕݸͷΞΫλʔͰɺ FromAccount{*} / ToAccount{*} ͱಠཱͯ͠ੜ͞ΕΔ • ͦΕͧΕcoroutineͰ੍ޚ͞ΕΔΞΫλʔͰ ϝοηʔδͷΓऔΓΛߦ͏
class Account implements ActorInterface { private float $balance = 10.0;
private array $processedMessages = []; public function __construct( private readonly float $serviceUptime, private readonly float $refusalProbability, private readonly float $busyProbability ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Message\Credit: case $message instanceof Message\Debit: $this->handleBalanceChange($context, $message); break; case $message instanceof Message\GetBalance: $context->respond($this->balance); break; } }
class Account implements ActorInterface { private float $balance = 10.0;
private array $processedMessages = []; public function __construct( private readonly float $serviceUptime, private readonly float $refusalProbability, private readonly float $busyProbability ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Message\Credit: case $message instanceof Message\Debit: $this->handleBalanceChange($context, $message); break; case $message instanceof Message\GetBalance: $context->respond($this->balance); break; } } ΞΫλʔʹϝοηʔδ͕౸ୡ͢Δͱ ࣮ߦ͞ΕΔSFDFJWFϝιου
class Account implements ActorInterface { private float $balance = 10.0;
private array $processedMessages = []; public function __construct( private readonly float $serviceUptime, private readonly float $refusalProbability, private readonly float $busyProbability ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Message\Credit: case $message instanceof Message\Debit: $this->handleBalanceChange($context, $message); break; case $message instanceof Message\GetBalance: $context->respond($this->balance); break; } } ೖग़ۚϝοηʔδʹରԠ
class Account implements ActorInterface { private float $balance = 10.0;
private array $processedMessages = []; public function __construct( private readonly float $serviceUptime, private readonly float $refusalProbability, private readonly float $busyProbability ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Message\Credit: case $message instanceof Message\Debit: $this->handleBalanceChange($context, $message); break; case $message instanceof Message\GetBalance: $context->respond($this->balance); break; } } ߴௐʹؔ͢Δॲཧ
class Account implements ActorInterface { private float $balance = 10.0;
private array $processedMessages = []; public function __construct( private readonly float $serviceUptime, private readonly float $refusalProbability, private readonly float $busyProbability ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Message\Credit: case $message instanceof Message\Debit: $this->handleBalanceChange($context, $message); break; case $message instanceof Message\GetBalance: $context->respond($this->balance); break; } } ߴ֬ೝͷϝοηʔδ͕དྷΔͱ ૹ৴ݩʹฦ٫
private function handleBalanceChange( ContextInterface $context, Message\ChangeBalance $message ): void {
if ($this->alreadyProcessed($message->replyTo)) { $context->send($message->replyTo, $this->processedMessages[(string) $message->replyTo]); return; } if ($message instanceof Message\Debit && ($message->amount + $this->balance) < 0) { $context->send($message->replyTo, new Message\InsufficientFunds()); return; } if ($this->refusePermanently()) { $this->processedMessages[(string) $message->replyTo] = new Message\Refused(); $context->send($message->replyTo, new Message\Refused()); return; } if ($this->isBusy()) { $context->send($message->replyTo, new Message\ServiceUnavailable()); return; } if ($this->shouldFailBeforeProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } usleep(random_int(0, 150) * 1000); $this->balance += $message->amount; $this->processedMessages[(string) $message->replyTo] = new Message\Ok(); if ($this->shouldFailAfterProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } $context->send($message->replyTo, new Message\Ok()); }
private function handleBalanceChange( ContextInterface $context, Message\ChangeBalance $message ): void {
if ($this->alreadyProcessed($message->replyTo)) { $context->send($message->replyTo, $this->processedMessages[(string) $message->replyTo]); return; } if ($message instanceof Message\Debit && ($message->amount + $this->balance) < 0) { $context->send($message->replyTo, new Message\InsufficientFunds()); return; } if ($this->refusePermanently()) { $this->processedMessages[(string) $message->replyTo] = new Message\Refused(); $context->send($message->replyTo, new Message\Refused()); return; } if ($this->isBusy()) { $context->send($message->replyTo, new Message\ServiceUnavailable()); return; } if ($this->shouldFailBeforeProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } usleep(random_int(0, 150) * 1000); $this->balance += $message->amount; $this->processedMessages[(string) $message->replyTo] = new Message\Ok(); if ($this->shouldFailAfterProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } $context->send($message->replyTo, new Message\Ok()); } ߴௐʹؔ͢Δॲཧ
private function handleBalanceChange( ContextInterface $context, Message\ChangeBalance $message ): void {
if ($this->alreadyProcessed($message->replyTo)) { $context->send($message->replyTo, $this->processedMessages[(string) $message->replyTo]); return; } if ($message instanceof Message\Debit && ($message->amount + $this->balance) < 0) { $context->send($message->replyTo, new Message\InsufficientFunds()); return; } if ($this->refusePermanently()) { $this->processedMessages[(string) $message->replyTo] = new Message\Refused(); $context->send($message->replyTo, new Message\Refused()); return; } if ($this->isBusy()) { $context->send($message->replyTo, new Message\ServiceUnavailable()); return; } if ($this->shouldFailBeforeProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } usleep(random_int(0, 150) * 1000); $this->balance += $message->amount; $this->processedMessages[(string) $message->replyTo] = new Message\Ok(); if ($this->shouldFailAfterProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } $context->send($message->replyTo, new Message\Ok()); } ҾͰͦΕͧΕͷঢ়ଶΛ γϛϡϨʔτ
private function handleBalanceChange( ContextInterface $context, Message\ChangeBalance $message ): void {
if ($this->alreadyProcessed($message->replyTo)) { $context->send($message->replyTo, $this->processedMessages[(string) $message->replyTo]); return; } if ($message instanceof Message\Debit && ($message->amount + $this->balance) < 0) { $context->send($message->replyTo, new Message\InsufficientFunds()); return; } if ($this->refusePermanently()) { $this->processedMessages[(string) $message->replyTo] = new Message\Refused(); $context->send($message->replyTo, new Message\Refused()); return; } if ($this->isBusy()) { $context->send($message->replyTo, new Message\ServiceUnavailable()); return; } if ($this->shouldFailBeforeProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } usleep(random_int(0, 150) * 1000); $this->balance += $message->amount; $this->processedMessages[(string) $message->replyTo] = new Message\Ok(); if ($this->shouldFailAfterProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } $context->send($message->replyTo, new Message\Ok()); } ϝοηʔδʹؚ·ΕΔૹ৴ઌʹ ฦ৴
private function handleBalanceChange( ContextInterface $context, Message\ChangeBalance $message ): void {
if ($this->alreadyProcessed($message->replyTo)) { $context->send($message->replyTo, $this->processedMessages[(string) $message->replyTo]); return; } if ($message instanceof Message\Debit && ($message->amount + $this->balance) < 0) { $context->send($message->replyTo, new Message\InsufficientFunds()); return; } if ($this->refusePermanently()) { $this->processedMessages[(string) $message->replyTo] = new Message\Refused(); $context->send($message->replyTo, new Message\Refused()); return; } if ($this->isBusy()) { $context->send($message->replyTo, new Message\ServiceUnavailable()); return; } if ($this->shouldFailBeforeProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } usleep(random_int(0, 150) * 1000); $this->balance += $message->amount; $this->processedMessages[(string) $message->replyTo] = new Message\Ok(); if ($this->shouldFailAfterProcessing()) { $context->send($message->replyTo, new Message\InternalServerError()); return; } $context->send($message->replyTo, new Message\Ok()); } ΞΫλʔ͕ࣗঢ়ଶཧ
AccountProxy ΞΫλʔ • TransferProcessΞΫλʔʹੜ͞ΕΔΞΫλʔ • AccountΞΫλʔͱͷ௨৴Λ୲͢ΔΞΫλʔ • AccountΞΫλʔʹԿ͔͕ى͖ͨ߹ʹͰ͋Δ TransferProcessʹରԠΛҕৡ͢Δʢ࠶ىಈͳͲΛߦ͏ʣ
Error Kernel Pattern • Reactive Design Patterns • োͱ৴པੑͷཁ͕͍݅ࠞͬͯ͟Δ߹ʹ ར༻Ͱ͖Δύλʔϯ(Akka,OTPͳͲͰ͓ͳ͡Έ)
• ఀࢭ͍͚ͯ͠ͳ͍ͷ͕͍ࠞͬͯ͟Δ߹ʹִ͢Δ
None
4BHB࣮ߦ
5SBOTGFS1SPDFTT͕
"DDPVOU1SPYZͱ"DDPVOU ࢠؔͰͳ͍ "DDPVOU1SPYZ͕μϯͯ͠ "DDPVOUӨڹͳ͠
"DDPVOUΞΫλʔͷ ࣮ߦͰ͖ͳ͍ཧ༝Λड৴ ࣗΛμϯͤ͞Δ
ࢠΞΫλʔͷμϯΛݕ ઓུʹج͍ͮͯ "DDPVOU1SPYZΞΫλʔ࠶ੜ ࠶ॲཧ࣮ߦ
௨৴ͱೖग़ۚΛ͍ͯ͠Δύλʔϯ ΞΫλʔϞσϧͳΒͰͷͷ
readonly class AccountProxy implements ActorInterface { public function __construct( private
Ref $target, private Closure $createMessage ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Started: $context->send($this->target, ($this->createMessage)($context->self())); $context->setReceiveTimeout(new DateInterval('PT2S')); break; case $message instanceof Refused: case $message instanceof Ok: $context->cancelReceiveTimeout(); $context->send($context->parent(), $message); break; case $message instanceof InsufficientFunds: case $message instanceof InternalServerError: case $message instanceof ReceiveTimeout: case $message instanceof ServiceUnavailable: throw new RuntimeException('Unexpected message'); } } }
readonly class AccountProxy implements ActorInterface { public function __construct( private
Ref $target, private Closure $createMessage ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Started: $context->send($this->target, ($this->createMessage)($context->self())); $context->setReceiveTimeout(new DateInterval('PT2S')); break; case $message instanceof Refused: case $message instanceof Ok: $context->cancelReceiveTimeout(); $context->send($context->parent(), $message); break; case $message instanceof InsufficientFunds: case $message instanceof InternalServerError: case $message instanceof ReceiveTimeout: case $message instanceof ServiceUnavailable: throw new RuntimeException('Unexpected message'); } } } "DUPS͕ىಈ͢ΔͱྲྀΕͯ͘Δ ෦ϝοηʔδ 4UBSUFE
readonly class AccountProxy implements ActorInterface { public function __construct( private
Ref $target, private Closure $createMessage ) { } public function receive(ContextInterface $context): void { $message = $context->message(); switch (true) { case $message instanceof Started: $context->send($this->target, ($this->createMessage)($context->self())); $context->setReceiveTimeout(new DateInterval('PT2S')); break; case $message instanceof Refused: case $message instanceof Ok: $context->cancelReceiveTimeout(); $context->send($context->parent(), $message); break; case $message instanceof InsufficientFunds: case $message instanceof InternalServerError: case $message instanceof ReceiveTimeout: case $message instanceof ServiceUnavailable: throw new RuntimeException('Unexpected message'); } } } -FU*U$SBTI ͜Ε·Ͱͷ1)1ͱҟͳΔߟ͑ํ
None
None
RunnerΞΫλʔ
None
RunnerΞΫλʔ • SagaΛ࣮ߦ͢Δׂ • Scatter Gather PatternͰ͕ͯ͢ऴΘΔ·Ͱͭ • ࢦఆAccountΞΫλʔͱTransferProcessΞΫλʔੜ •
TransferProcessΞΫλʔͷϦϑΝϨϯεΛRunner͕อ࣋
Scatter Gatherʁ
None
Scatter Gather • ෳʹϒϩʔυΩϟετޙɺฦ৴ΛҰͭʹू͢Δ • ΞΫλʔϞσϧͳΒͰͷύλʔϯ • ϩʔΧϧϦϞʔτͱͷΈ߹ΘͤͰར༻
$inMemoryProvider = $this->inMemoryProvider(); (new ForWithProgress( $this->numberOfIterations, $this->intervalBetweenConsoleUpdates, true, false ))->everyNth(
fn($i) => print("Started {$i}/{$this->numberOfIterations} processes\n"), function ($i, $nth) use ($context, $inMemoryProvider) { $fromAccount = $this->createAccount($context, "FromAccount{$i}"); $toAccount = $this->createAccount($context, "ToAccount{$i}"); $actorName = "Transfer Process {$i}"; $factory = new TransferFactory( $context, $this->uptime, $this->retryAttempts, $inMemoryProvider ); $transfer = $factory->createTransfer($actorName, $fromAccount, $toAccount, 10); $this->transfers[] = (string) $transfer->getRef(); if ($i === $this->numberOfIterations && !$nth) { print("Started {$i}/{$this->numberOfIterations} processes\n"); } } );
$inMemoryProvider = $this->inMemoryProvider(); (new ForWithProgress( $this->numberOfIterations, $this->intervalBetweenConsoleUpdates, true, false ))->everyNth(
fn($i) => print("Started {$i}/{$this->numberOfIterations} processes\n"), function ($i, $nth) use ($context, $inMemoryProvider) { $fromAccount = $this->createAccount($context, "FromAccount{$i}"); $toAccount = $this->createAccount($context, "ToAccount{$i}"); $actorName = "Transfer Process {$i}"; $factory = new TransferFactory( $context, $this->uptime, $this->retryAttempts, $inMemoryProvider ); $transfer = $factory->createTransfer($actorName, $fromAccount, $toAccount, 10); $this->transfers[] = (string) $transfer->getRef(); if ($i === $this->numberOfIterations && !$nth) { print("Started {$i}/{$this->numberOfIterations} processes\n"); } } ); 'SPN"DDPVOU9ͱ5P"DDPVOU9ͱ͍͏໊લͰ "DDPVOUΞΫλʔΛੜ
$inMemoryProvider = $this->inMemoryProvider(); (new ForWithProgress( $this->numberOfIterations, $this->intervalBetweenConsoleUpdates, true, false ))->everyNth(
fn($i) => print("Started {$i}/{$this->numberOfIterations} processes\n"), function ($i, $nth) use ($context, $inMemoryProvider) { $fromAccount = $this->createAccount($context, "FromAccount{$i}"); $toAccount = $this->createAccount($context, "ToAccount{$i}"); $actorName = "Transfer Process {$i}"; $factory = new TransferFactory( $context, $this->uptime, $this->retryAttempts, $inMemoryProvider ); $transfer = $factory->createTransfer($actorName, $fromAccount, $toAccount, 10); $this->transfers[] = (string) $transfer->getRef(); if ($i === $this->numberOfIterations && !$nth) { print("Started {$i}/{$this->numberOfIterations} processes\n"); } } ); 5SBOTGFS1SPDFTTΞΫλʔʹ ̎ͭͷ"DDPVOUΞΫλʔϦϑΝϨϯε "DDPVOUΞΫλʔؒͰ૬ޓॲཧ
None
ΤϯτϦϙΠϯτ • ΞΫλʔγεςϜͷىಈ • RunnerΞΫλʔͷੜͱࢹɺ Runnerͷ࠶ࢼߦʹؔ͢ΔΛ࣋ͭ • ͦΕҎ֎Կͳ͠ʂ
go(function () { $system = ActorSystem::create(); $system->getLogger()->info('Starting'); $numberOfTransfers = 10;
$intervalBetweenConsoleUpdates = 1; $uptime = 99.99; $retryAttempts = 3; $refusalProbability = 0.01; $busyProbability = 0.01; $props = ActorSystem\Props::fromProducer( fn() => new Runner( $numberOfTransfers, $intervalBetweenConsoleUpdates, $uptime, $refusalProbability, $busyProbability, $retryAttempts ), ActorSystem\Props::withSupervisor( new ActorSystem\Strategy\OneForOneStrategy( $retryAttempts, new DateInterval('PT10S'), new ActorSystem\Supervision\DefaultDecider(), ) ), ); $system->root()->spawnNamed($props, 'runner'); });
go(function () { $system = ActorSystem::create(); $system->getLogger()->info('Starting'); $numberOfTransfers = 10;
$intervalBetweenConsoleUpdates = 1; $uptime = 99.99; $retryAttempts = 3; $refusalProbability = 0.01; $busyProbability = 0.01; $props = ActorSystem\Props::fromProducer( fn() => new Runner( $numberOfTransfers, $intervalBetweenConsoleUpdates, $uptime, $refusalProbability, $busyProbability, $retryAttempts ), ActorSystem\Props::withSupervisor( new ActorSystem\Strategy\OneForOneStrategy( $retryAttempts, new DateInterval('PT10S'), new ActorSystem\Supervision\DefaultDecider(), ) ), ); $system->root()->spawnNamed($props, 'runner'); }); 3VOOFSΞΫλʔੜ
go(function () { $system = ActorSystem::create(); $system->getLogger()->info('Starting'); $numberOfTransfers = 10;
$intervalBetweenConsoleUpdates = 1; $uptime = 99.99; $retryAttempts = 3; $refusalProbability = 0.01; $busyProbability = 0.01; $props = ActorSystem\Props::fromProducer( fn() => new Runner( $numberOfTransfers, $intervalBetweenConsoleUpdates, $uptime, $refusalProbability, $busyProbability, $retryAttempts ), ActorSystem\Props::withSupervisor( new ActorSystem\Strategy\OneForOneStrategy( $retryAttempts, new DateInterval('PT10S'), new ActorSystem\Supervision\DefaultDecider(), ) ), ); $system->root()->spawnNamed($props, 'runner'); }); 3VOOFSΞΫλʔʹର͢Δઓུ
go(function () { $system = ActorSystem::create(); $system->getLogger()->info('Starting'); $numberOfTransfers = 10;
$intervalBetweenConsoleUpdates = 1; $uptime = 99.99; $retryAttempts = 3; $refusalProbability = 0.01; $busyProbability = 0.01; $props = ActorSystem\Props::fromProducer( fn() => new Runner( $numberOfTransfers, $intervalBetweenConsoleUpdates, $uptime, $refusalProbability, $busyProbability, $retryAttempts ), ActorSystem\Props::withSupervisor( new ActorSystem\Strategy\OneForOneStrategy( $retryAttempts, new DateInterval('PT10S'), new ActorSystem\Supervision\DefaultDecider(), ) ), ); $system->root()->spawnNamed($props, 'runner'); }); نఆ݅Ҏ্ࣦഊ͢Δͱ ଞͷγεςϜʹΤεΧϨʔτ͢ΔͳͲ͕Մೳ
None
·ͱΊ • ΞΫλʔϞσϧΛ׆༻ͨ͠ิঈτϥϯβΫγϣϯ • ϦτϥΠઓུ • ΤϥʔΧʔωϧύλʔϯͱಜ • BehaviorʹΑΔState Machine
• Persistenceͷ׆༻
Let It Crash