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
370
PHPでアクターモデルを理解・体験しよう / Understand and experience the actor model in PHP
ytake
2
990
再考 アクターモデル/ reconsider actor model
ytake
0
1.7k
GoとアクターモデルでES+CQRSを実践! / proto_actor_es_cqrs
ytake
1
680
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
AIオーケストレーションを活用した 開発ワークフローの設計と実践
bqnq
0
140
AI 駆動 Terraform 開発/SRE_BizReach_MIXI_2
visional_engineering_and_design
4
2.1k
AI時代のAPI品質を支えるガードレール / API Guardrails for API quality in the AI era
yokawasa
1
220
HRC_Frontend_Conference_Fukuoka_2026.pdf
ts020
0
540
『止めない』を設計する — 制約の中で、事業の根幹を支える判断
hiroyaterui
0
270
Snowflakeで実現する全社横断の顧客の声(VOC)分析・活用基盤@Snowflake World Tour Tokyo 2026
yuto16
0
190
20260912_スクフェス三河
kgnkhkr
0
300
Omarchy Quattro の日本語設定周り
simosako
2
150
DEFCON34-Write-up_HYCu-MYCu
daikiokazaki
0
150
20260906 「AWS運用入門」著者が教える、運用業務への生成AI活用入門
masaruogura
0
260
あるけみー式LTスライド作成術
alchemy1115
1
190
Deploying a Full-Stack Bun-Native Framework on Cloudflare Workers
7nohe
0
140
Featured
See All Featured
The World Runs on Bad Software
bkeepers
PRO
72
12k
Skip the Path - Find Your Career Trail
mkilby
1
220
Marketing to machines
jonoalderson
1
5.8k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
530
How to Get Subject Matter Experts Bought In and Actively Contributing to SEO & PR Initiatives.
livdayseo
0
190
The Cult of Friendly URLs
andyhume
79
7k
Design in an AI World
tapps
1
310
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
320
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
46
3k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
74
42k
Technical Leadership for Architectural Decision Making
baasie
3
560
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