daprのActorをつかってみる

## はじめに

この記事は、 [富士通クラウドテクノロジーズ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fjct) の12日目の記事です。
11日目は @tanopanta さんの [【Conftest】YAMLを目でレビューするのはもうヤメル【Policy as Code】](https://qiita.com/tanopanta/private/75c32fe1e6fecf47b710) でした！　

YAMLのレビューで疲弊している、みなさん！ぜひ試してみてください:sunglasses: 

さて、今年も Dapr について書いていこうとおもいます。

**我々、富士通クラウドテクノロジーズ株式会社（の一部のサービス）では Dapr を取り入れた開発を行っています** :sunglasses:  

Daprは、 2021/02/17 に [v1.0.0](https://github.com/dapr/dapr/releases/tag/v1.0.0) がリリースされ、Production ready となりました[^0]。 その頃、我々のチームでは導入にチャレンジしており、「コアロジックへフォーカスする」という開発における Simplicity としての1つの解となったと感じている今日のこの頃です。

今回はまだまだ注目していきたい Dapr ですが、 Virtual Actors の機能が提供されており、 アクターモデルを利用することができます。 2021/11/10 に [github.com/dapr/go-sdk/issues/21](https://github.com/dapr/go-sdk/issues/21) がクローズし、go-sdkでも実装されたので記念に使ってみようとおもいます。

## daprとは

Dapr については以前紹介としてまとめたものがあるので下記をご覧ください :hugging: 

https://qiita.com/kzmake/items/4136a11fa301cdec4ccb

## daprのActor

dapr は Virtual Actors を提供してくれます。
Virtual Actors　は４つの特徴[^1]として、

1. Perpetual existence
1. Automatic instantiation
1. Location transparency
1. Automatic scale out

があるようです。

また、明示的に　`Mailboxes` を持っておらず、 **ターンベース(またはシングル スレッド)のアクセスモデル**として提供されているようです。dapr が提供するアクターモデルでは、アクター間のメッセージ送信だけでなく、タイマーとリマインダーを使用したスケジュールもサポートされています[^2]。

## 使ってみよう

今回は豚の `貯金箱` をモデリングし、最近 dapr の Actor 機能がサポートされた [github.com/dapr/go-sdk](https://github.com/dapr/go-sdk) を使ってモデルの振る舞いを実装＆実行してみます。下記を実装してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/e50d5eb8-33d7-9dd8-044c-e129fea9a3f4.png)

まず、貯金箱とコインのモデルを定義していきます。 この後 Actor で状態として管理したい `PiggyBank`　は json　として serde できるようにしておきます。

```golang:domain/domain.go
// ...

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 を登録できないようです。

```golang:actor/main.go
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"` を指定しています。

```golang:actor/main.go
// ...

func (a *PiggyBankActor) Type() string {
	return "PiggyBank"
}
```

後は　`PiggyBank`　で期待する振る舞いを　`PiggyBankActor` として実装します。

```golang:actor/main.go
// ...

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)` を実行することで、サービスの実装が呼び出せるようになるといった感じですね。

```golang:client/main.go
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](https://github.com/kzmake/dapr-actor) を参考にいただければとおもいます。

## 実行してみる

kind + skaffold で準備していますので、 [github.com/kzmake/dapr-actor](https://github.com/kzmake/dapr-actor) をクローンしていただければすぐに試せます！

```bash
$ 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
```

```bash
$ 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 の呼び出し、起動＆実行できました :tada: 　

## まとめ

いかがだったでしょうか？

実際に使った所感としては、

- dapr を利用することでどの言語でも Virtual Actors を利用できる可能性がある
- ターンベースのアクセスモデルを持つ Actor として実装されているので同時実行の管理がシンプル
- Akka などメールボックスがある Actor に馴染んでいる人はちょっと癖がある

があると感じています。[Support pubsub for Actors #501](https://github.com/dapr/dapr/issues/501) もあるので、個人的には今後に期待(たぶん v1.6.+ あたり？)といったところかと思います！

## さいごに

今回は、dapr の Actor 機能を使って、モデリングした「貯金箱」を

- クライアント側: 貯金箱の定義と振る舞いの呼び出し
- アクター側: 貯金箱の振る舞いを実装

とためしてみました。Virtual Actors のアイデアには嬉しい特徴がたくさんあるので、すでに多機能な　dapr　ですが、これからの Actor 機能エンハンスも楽しみだな〜と感じています。

さて、明日は @tunakyonn さんが「CDK for Terraform をニフクラ provider で試してみた」について書いてくれるようです！お楽しみに :yum:

[^0]: [Announcing Dapr v1.0](https://blog.dapr.io/posts/2021/02/17/announcing-dapr-v1.0/)

[^1]: [Orleans: Distributed Virtual Actors for Programmability and Scalability](http://research.microsoft.com/apps/pubs/default.aspx?id=210931)

[^2]: [Dapr アクター構成ブロック | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/architecture/dapr-for-net-developers/actors)