はじめに
この記事は、 富士通クラウドテクノロジーズ Advent Calendar 2021 の12日目の記事です。
11日目は @tanopanta さんの 【Conftest】YAMLを目でレビューするのはもうヤメル【Policy as Code】 でした!
YAMLのレビューで疲弊している、みなさん!ぜひ試してみてください
さて、今年も Dapr について書いていこうとおもいます。
我々、富士通クラウドテクノロジーズ株式会社(の一部のサービス)では Dapr を取り入れた開発を行っています
Daprは、 2021/02/17 に v1.0.0 がリリースされ、Production ready となりました1。 その頃、我々のチームでは導入にチャレンジしており、「コアロジックへフォーカスする」という開発における Simplicity としての1つの解となったと感じている今日のこの頃です。
今回はまだまだ注目していきたい Dapr ですが、 Virtual Actors の機能が提供されており、 アクターモデルを利用することができます。 2021/11/10 に github.com/dapr/go-sdk/issues/21 がクローズし、go-sdkでも実装されたので記念に使ってみようとおもいます。
daprとは
Dapr については以前紹介としてまとめたものがあるので下記をご覧ください
daprのActor
dapr は Virtual Actors を提供してくれます。
Virtual Actors は4つの特徴2として、
- Perpetual existence
- Automatic instantiation
- Location transparency
- Automatic scale out
があるようです。
また、明示的に Mailboxes
を持っておらず、 ターンベース(またはシングル スレッド)のアクセスモデルとして提供されているようです。dapr が提供するアクターモデルでは、アクター間のメッセージ送信だけでなく、タイマーとリマインダーを使用したスケジュールもサポートされています3。
使ってみよう
今回は豚の 貯金箱
をモデリングし、最近 dapr の Actor 機能がサポートされた github.com/dapr/go-sdk を使ってモデルの振る舞いを実装&実行してみます。下記を実装してみます。
まず、貯金箱とコインのモデルを定義していきます。 この後 Actor で状態として管理したい PiggyBank
は json として serde できるようにしておきます。
// ...
type PiggyBankID string
type PiggyBankState uint
const (
Healthy PiggyBankState = iota + 1
Broken
)
type Coin uint
const (
Yen1 Coin = 1
Yen5 Coin = 5
Yen10 Coin = 10
Yen50 Coin = 50
Yen100 Coin = 100
Yen500 Coin = 500
)
type PiggyBank struct {
ID PiggyBankID `json:"id"`
State PiggyBankState `json:"state"`
Coins []Coin `json:"coins"`
}
func NewPiggyBank() *PiggyBank {
return &PiggyBank{
ID: PiggyBankID(ulid.MustNew(ulid.Timestamp(time.Now()), crand.Reader).String()),
State: Healthy,
Coins: []Coin{},
}
}
続いて、Actor で PiggyBank
の振る舞いを実装してみます。Actor を起動できる dapr サービスとして作成します。
実装側の PiggyBankActor
は actor.ServerImplBase を埋め込んで、dapr(http) サービスに登録しておきます。
ちなみに、現在(2021/12/09)では、 github.com/dapr/go-sdk/service/grpc
には Actor を登録できないようです。
package main
import (
"github.com/dapr/go-sdk/actor"
daprd "github.com/dapr/go-sdk/service/http"
)
type PiggyBankActor struct {
actor.ServerImplBase
}
func NewPiggyBankActor() func() actor.Server {
return func() actor.Server {
return &PiggyBankActor{}
}
}
// ... そのうち PiggyBankActor の振る舞いを実装
func main() {
s := daprd.NewService(":8080")
s.RegisterActorImplFactory(NewPiggyBankActor())
if err := s.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error listenning: %v", err)
}
}
また、 func Type() string
は Actor の種類を特定するための要素なので一意の文字列を指定します。今回は "PiggyBank"
を指定しています。
// ...
func (a *PiggyBankActor) Type() string {
return "PiggyBank"
}
後は PiggyBank
で期待する振る舞いを PiggyBankActor
として実装します。
// ...
func (a *PiggyBankActor) Put(ctx context.Context, coin domain.Coin) error {
log.Println("Actor: ", a.Type(), "/", a.ID(), " call Put: ", coin)
pg, _ := a.get()
// ... something
a.set(pg)
return nil
}
func (a *PiggyBankActor) Break(context.Context) ([]domain.Coin, error) {
log.Println("Actor: ", a.Type(), "/", a.ID(), " call Break")
pg, _ := a.get()
// ... something
return pg.Coins, nil
}
func (a *PiggyBankActor) Jingle(context.Context) (string, error) {
log.Println("Actor: ", a.Type(), "/", a.ID(), " call Jingle")
pg, _ := a.get()
// ... something
return strings.Repeat("じゃら", len(pg.Coins)), nil
}
func (a *PiggyBankActor) get() (*domain.PiggyBank, error) {
pg := &domain.PiggyBank{
ID: domain.PiggyBankID(a.ID()),
State: domain.Healthy,
Coins: []domain.Coin{},
}
if found, err := a.GetStateManager().Contains("self"); err != nil {
return nil, err
} else if found {
if err := a.GetStateManager().Get("self", pg); err != nil {
return nil, err
}
}
return pg, nil
}
func (a *PiggyBankActor) set(pg *domain.PiggyBank) error {
if err := a.GetStateManager().Set("self", pg); err != nil {
return err
}
return nil
}
と、Actor を起動するサービスは以上で作成完了です。
ここの実装で Actor の状態を Get/Set している
a.GetStateManager().Contains(key)
a.GetStateManager().Get(key, pg)
a.GetStateManager().Set(key, pg)
は、 Appの ID と Actor の Type / ID / 指定した Key 毎に保存されるようです。
ex: {{ app_id }}||PiggyBank||{{ actor_id}}||{{ key }}
続いて、Actor の呼び出し元となる Client を実装していきます。先程実装したActorを起動させるために、
Type() string
ID() string
をクライアント側で実装します。 ここで、Actor は PiggyBank
毎に起動したいので PiggyBankActor.ID
を設定します。
クライアントで指定した actor に対して、 c.ImplActorClientStub(actor)
を実行することで、サービスの実装が呼び出せるようになるといった感じですね。
package main
import (
"context"
"log"
dapr "github.com/dapr/go-sdk/client"
"github.com/kzmake/dapr-actor/domain"
)
type PiggyBankActor struct {
Id string `json:"id"`
Put func(context.Context, domain.Coin) error
Break func(context.Context) ([]domain.Coin, error)
Jingle func(context.Context) (string, error)
}
func NewPiggyBankActor(pb *domain.PiggyBank) *PiggyBankActor {
return &PiggyBankActor{Id: string(pb.ID)}
}
func (a *PiggyBankActor) Type() string {
return "PiggyBank"
}
func (a *PiggyBankActor) ID() string {
return a.Id
}
func main() {
c, err := dapr.NewClient()
if err != nil {
panic(err)
}
defer c.Close()
ctx := context.Background()
pb := domain.NewPiggyBank()
actor := NewPiggyBankActor(pb)
c.ImplActorClientStub(actor)
actor.Put(ctx, domain.Yen100)
actor.Put(ctx, domain.Yen500)
s, _ = actor.Jingle(ctx)
log.Printf("actor(pb: %s).Jingle: %+v, %+v", string(pb.ID), s, err)
coins, _ := actor.Break(ctx)
log.Printf("actor(pb: %s).Break: %+v, %+v", string(pb.ID), coins, err)
}
※細かい部分は端折りました。詳細は github.com/kzmake/dapr-actor を参考にいただければとおもいます。
実行してみる
kind + skaffold で準備していますので、 github.com/kzmake/dapr-actor をクローンしていただければすぐに試せます!
$ kind create cluster --config kind.yaml
$ skaffold run -f skaffold.dapr.yaml
$ skaffold dev -f skaffold.actor.yaml
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEDBTG2TFBRBARFTS7 call Break
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Jingle
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Put: 10
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Jingle
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Put: 100
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Put: 500
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Jingle
[actor] 2021/12/09 13:19:23 Actor: PiggyBank / 01FPFNJDFEKHHT3GAF0961MXME call Break
$ skaffold run --tail
[client] dapr client initializing for: 127.0.0.1:50001
[client] 2021/12/09 13:19:23 actor(pb: 01FPFNDREFVB39EJHYQ3DRG1WB).Break: [], <nil>
[client] 2021/12/09 13:19:23 actor(pb: 01FPFNDREFZM3N3PE72P5SNXXC).Jingle: , <nil>
[client] 2021/12/09 13:19:23 actor(pb: 01FPFNDREFZM3N3PE72P5SNXXC).Jingle: じゃら, <nil>
[client] 2021/12/09 13:19:23 actor(pb: 01FPFNDREFZM3N3PE72P5SNXXC).Jingle: じゃらじゃらじゃら, <nil>
[client] 2021/12/09 13:19:23 actor(pb: 01FPFNDREFZM3N3PE72P5SNXXC).Break: [10 100 500], <nil>
クライアント側から Actor の呼び出し、起動&実行できました
まとめ
いかがだったでしょうか?
実際に使った所感としては、
- dapr を利用することでどの言語でも Virtual Actors を利用できる可能性がある
- ターンベースのアクセスモデルを持つ Actor として実装されているので同時実行の管理がシンプル
- Akka などメールボックスがある Actor に馴染んでいる人はちょっと癖がある
があると感じています。Support pubsub for Actors #501 もあるので、個人的には今後に期待(たぶん v1.6.+ あたり?)といったところかと思います!
さいごに
今回は、dapr の Actor 機能を使って、モデリングした「貯金箱」を
- クライアント側: 貯金箱の定義と振る舞いの呼び出し
- アクター側: 貯金箱の振る舞いを実装
とためしてみました。Virtual Actors のアイデアには嬉しい特徴がたくさんあるので、すでに多機能な dapr ですが、これからの Actor 機能エンハンスも楽しみだな〜と感じています。
さて、明日は @tunakyonn さんが「CDK for Terraform をニフクラ provider で試してみた」について書いてくれるようです!お楽しみに