Goのsingleflight packageの紹介

このエントリーは、[Go5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go5)の14日目のエントリーです。

今回、[singleflight Package](https://godoc.org/golang.org/x/sync/singleflight)の紹介をしたいと思います。 利用したことがない方は、一度、検討する材料となってもらえればと幸いです。

# What is singleflight?

singleflight とはなにかというと、次の通りになります。

> Package singleflight provides a duplicate function call suppression mechanism.

Package singleflightは、重複したfunction callをまとめるメカニズムを提供します。


私は、もしアプリケーションが同じリソースのための複数のリクエストを受けるようなことがあるのであれば、singleflightは、とても役に立つPackage、と考えています。例えば、RDB(マスタデータ)、画像ファイル、IPアドレスのLookup、 Client証明書 など、こういった普段変わることがないリソースの参照が必要となるケースが該当するケースです。

また、singleflightの手法では、[Thundering herd problem](https://en.wikipedia.org/wiki/Thundering_herd_problem) を回避することができます。


客観的な指標として、実際にどれくらいのPackageから参照されているかというと、現時点(2019/12/13)では、
[120 packages](https://godoc.org/golang.org/x/sync/singleflight?importers)から参照されています。

その中でも、どんなプロダクトで利用されているか参考までにあげてみると、HashiCorp社の[Consul](https://www.consul.io/)だったり、[Fabio](https://fabiolb.net)だったり、証明書の取得する処理で利用されているUse caseがあります。

https://github.com/hashicorp/consul/blob/master/agent/consul/acl.go
https://github.com/fabiolb/fabio/blob/master/cert/source.go

singleflightが提供しているfunctionは、次の３つになります。

- [func (g *Group) Do(key string, fn func() (interface{}, error)) (v -interface{}, err error, shared bool)](https://godoc.org/golang.org/x/sync/singleflight#Group.Do)
- [func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result](https://godoc.org/golang.org/x/sync/singleflight#Group.DoChan)
- [func (g *Group) Forget(key string)](https://godoc.org/golang.org/x/sync/singleflight#Group.Forget)

実際のソースファイルは、[singleflight.go](https://github.com/golang/sync/blob/master/singleflight/singleflight.go)が用意されています。

## a recent trend of singleflight

Goのソースの中身をみてみると、singleflightは、標準ライブラリとして使われていて、Goの[internal Package](https://github.com/golang/go/tree/master/src/internal)にもあります。Goの内部ではどういったUse caseでcallされてるかというと、[net/lookup.go](https://github.com/golang/go/blob/master/src/net/lookup.go) がその例としててあげられます。GoDocも2種類存在しています。

- [GoDoc - x/sync/singleflight](https://godoc.org/golang.org/x/sync/singleflight)
- [GoDoc - internal/singleflight](https://godoc.org/internal/singleflight)

[Go Sync](https://github.com/golang/sync)という名前の別のリポジトリでも管理されてあり、このリポジトリは、言語や `sync` や `sync/atomic` Packageで提供されるものに加えて、Goで並列実行の基本機構を提供しています。

Goの[Release dashboard](https://dev.golang.org/release)によると、 `internal/singleflight`は、`Go1.14`から削除するかも、`x/sync/singleflight`を利用することを推奨する予定だそうです。

詳しい背景や議論のポイントが知りたい場合はこちらのissuesとこのgo-reviewのリンクより確認できる。ただし、go-reviewでのstausは、2019/11/22に`Abandoned`へ変更されているため、変わらないかもしれません。

https://github.com/golang/go/issues/31697
https://go-review.googlesource.com/c/go/+/174080/


# Introduction to Use case

ここでは、singleflight Packageを利用したUse caseを2つ紹介しようと思います。

## Use case 1 - net/lookup

lookupGroupは、LookupIPAddr 呼び出しが同じhostをlookupするために、一緒にマージしています。そして、LookupIPAddrは、そのlookupGroupをlocal resolverを介して使い、hostを参照します。

以下に関連する処理のコードを引用しておきます。

```go
	// lookupGroup merges LookupIPAddr calls together for lookups for the same
	// host. The lookupGroup key is the LookupIPAddr.host argument.
	// The return values are ([]IPAddr, error).
	lookupGroup singleflight.Group
```
https://github.com/golang/go/blob/master/src/net/lookup.go#L151


```go
func (r *Resolver) getLookupGroup() *singleflight.Group {
	if r == nil {
		return &DefaultResolver.lookupGroup
	}
	return &r.lookupGroup
}
```

https://github.com/golang/go/blob/master/src/net/lookup.go#L160

```go
	// We don't want a cancellation of ctx to affect the
	// lookupGroup operation. Otherwise if our context gets
	// canceled it might cause an error to be returned to a lookup
	// using a completely different context. However we need to preserve
	// only the values in context. See Issue 28600.
	lookupGroupCtx, lookupGroupCancel := context.WithCancel(withUnexpiredValuesPreserved(ctx))

	lookupKey := network + "\000" + host
	dnsWaitGroup.Add(1)
	ch, called := r.getLookupGroup().DoChan(lookupKey, func() (interface{}, error) {
		defer dnsWaitGroup.Done()
		return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host)
	})
	if !called {
		dnsWaitGroup.Done()
	}

```

https://github.com/golang/go/blob/master/src/net/lookup.go#L257

## Use case 2 - DataLayerとBFFのMicroservices

Microservicesで開発していたときに、次のような仕様の場合、適用可能かと考えています。

### Client

- Microservice A に対して、GetProduct, ListProductsのAPIを呼び出します。
- APIから受け取った結果を、一覧として表示します。

### Microservice A  -  [BFF layer](https://microservices.io/patterns/apigateway.html)

- Microservice Aは、API:GetProductComponent を提供しています。
- API:GetProductComponentは、Microservice Bの2つのAPI（GeProduct, ListProducts)を呼び出します。その後に、受け取った情報を集約して、ProductComponentとして、結果を返します。

### Microservice B  -  [Database layer](https://microservices.io/patterns/data/database-per-service.html) 

- Microservice Bは、2つのAPI（GeProduct, ListRecommendedProduct)を提供しています。
- API: GeProductは、IDを利用して、Productのマスタ情報を結果として返します。
- API: ListProductsは、Productのマスタに登録してから1週間以内のProduct情報を結果として返します。

図示すると以下のような関連になります。

![Screen Shot 2019-12-14 at 0.47.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14680/f1ff9427-c4ab-db63-563b-a3ece871b3bb.png)


以上のような形で、このUse caseでは、Microservice Bが提供しているAPIで参照されるマスタ情報があまり変わらないリソースであるととらえて、呼び出し元であるMicroservice A側にsingleflightを使うようにすることで重複したAPI呼び出しをまとめることが可能となります。


# Conclusion

- singleflight packageは、重複したfunction callをまとめるメカニズムを提供してくれます。もし同じリソースを要求するAPIをチューニングする必要があるときは、とても役に立つかと思います。
- Microservicesの[BFF pattern](https://microservices.io/patterns/apigateway.html)を使用している場合には、簡単に適用させるUse Caseが見つかるかと思います。
- もしsingleflightを興味を持ったようでしたら、他のプロダクトでどのように使っているかを調べてみるとより理解が深まるかと思います。 
REF: https://godoc.org/golang.org/x/sync/singleflight?importers

以上になります。このエントリーをもとにsingleflightを使ってみようと思える機会になったのであれば、うれしく思います。