- 投稿日:2021-01-21T19:50:34+09:00
[追記]Go言語でチャンネル書込がブロックされるのを無理矢理解決した話
概要
Go言語のチャンネルは非常に便利なんですが、正直複雑すぎてあまり使いこなせていません。
この記事は、チャンネルの受信側のgoroutineが先に死んだ場合に、送信側のチャンネル書き込みが永久にブロックされるのを無理矢理解決した事例です。
正直こんな書き方がGo言語的に良いのかどうかわからないので、ご意見等いただけると嬉しいです。
バッドノウハウかもしれないのでご注意を!!
[追記]もしかしてBind呼ぶたびに終わらないgoroutineが増えるかも?参考
Go言語のチャンネル書き込みの時に…
条件 結果 受信ルーチンが生きていてチャンネルが満杯でない ブロックしない 受信ルーチンが生きていてチャンネルが満杯 チャンネルが空くまでブロック 受信ルーチンが死んでいる 永久にブロック チャンネルにnilが代入されている 永久にブロック チャンネルがcloseされている panicする panicするコード
panic.gopackage main import ( "fmt" "sync" "time" ) func main() { fmt.Println("Hello, playground") ch := make(chan string) var wg sync.WaitGroup wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Writer Finished") }() ch <- "AAA" time.Sleep(time.Second * 6) ch <- "BBB" }() wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Reader Finished") }() for i := 0; i < 5; i++ { select { case v := <-ch: fmt.Printf("OK:%v\n", v) default: fmt.Println("NG") } time.Sleep(time.Second) } }() wg.Wait() }受信側のルーチンが5秒で死んで、送信側が6秒後に新しい値を書こうとしてpanicします。
無理矢理解決したコード
muriyari.gopackage main import ( "fmt" "sync" "time" ) var m sync.Map func main() { fmt.Println("Hello, playground") var wg sync.WaitGroup wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Writer Finished") }() m.Store("AAA", "BBB") m.Store("CCC", "DDD") time.Sleep(time.Second * 6) m.Store("EEE", "FFF") }() wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Reader Finished") }() ch := make(chan interface{}) Bind(ch, "CCC") for i := 0; i < 5; i++ { select { case v := <-ch: fmt.Printf("OK:%v\n", v) default: fmt.Println("NG") } time.Sleep(time.Second) } }() wg.Wait() fmt.Println("Main Finished") } func Bind(ch chan interface{}, key string) { go func() { for { if v, ok := m.LoadAndDelete(key); ok { ch <- v } time.Sleep(time.Millisecond * 200) } }() }チャンネルに直接書き込むのではなく、sync.Mapに書き込んでるのでブロックしません。sync.Mapに書き込むところをきちんと型を書いた新たなfuncを定義してやれば、型チェックは働きます。上で定義しているBind関数も同様です。
ちなみに最初はBind関数内でチャンネルを作ろうとしてたんですが、関数を抜けた時点で変数が解放されるのでpanic起こしてダメでした。
パフォーマンスは計測してませんが、生のチャンネルを扱うよりは確実に悪いでしょう。sleepの時間調整である程度パフォーマンス調整が出来るかもしれません。
余談
チャンネルの容量を増やすことで満杯になるまではブロックしない書き込みになるんですが、無限に増やすわけにもいかないのでこんな方法を考えてみました。
設計が悪いのは明らかなんですが、一応なぜこんな設計を考えているのかの事情は説明しておきます。
それは
Ctrl+C
やSIGTERMなどOSのシステムコール時に全てのGoroutineが後始末をしながら終了をして欲しいからで、その際にcontextを使ってルーチンを終了させているのですがルーチンの終了の順番が不定だからです。なので送信側ルーチンが先に死んで正常終了するケースと、受信ルーチンが先に死んで永久にプロセスが動き続ける場合がありました。これはあくまで本当に無理矢理やった例ですので、もっと洗練されたやり方があったり、ライブラリがあればご紹介いただければ嬉しいです。
- 投稿日:2021-01-21T19:50:34+09:00
Go言語でチャンネル書込がブロックされるのを無理矢理解決した話
概要
Go言語のチャンネルは非常に便利なんですが、正直複雑すぎてあまり使いこなせていません。
この記事は、チャンネルの受信側のgoroutineが先に死んだ場合に、送信側のチャンネル書き込みが永久にブロックされるのを無理矢理解決した事例です。
正直こんな書き方がGo言語的に良いのかどうかわからないので、ご意見等いただけると嬉しいです。
バッドノウハウかもしれないのでご注意を!!
参考
Go言語のチャンネル書き込みの時に…
条件 結果 受信ルーチンが生きていてチャンネルが満杯でない ブロックしない 受信ルーチンが生きていてチャンネルが満杯 チャンネルが空くまでブロック 受信ルーチンが死んでいる 永久にブロック チャンネルにnilが代入されている 永久にブロック チャンネルがcloseされている panicする panicするコード
panic.gopackage main import ( "fmt" "sync" "time" ) func main() { fmt.Println("Hello, playground") ch := make(chan string) var wg sync.WaitGroup wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Writer Finished") }() ch <- "AAA" time.Sleep(time.Second * 6) ch <- "BBB" }() wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Reader Finished") }() for i := 0; i < 5; i++ { select { case v := <-ch: fmt.Printf("OK:%v\n", v) default: fmt.Println("NG") } time.Sleep(time.Second) } }() wg.Wait() }受信側のルーチンが5秒で死んで、送信側が6秒後に新しい値を書こうとしてpanicします。
無理矢理解決したコード
muriyari.gopackage main import ( "fmt" "sync" "time" ) var m sync.Map func main() { fmt.Println("Hello, playground") var wg sync.WaitGroup wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Writer Finished") }() m.Store("AAA", "BBB") m.Store("CCC", "DDD") time.Sleep(time.Second * 6) m.Store("EEE", "FFF") }() wg.Add(1) go func() { defer func() { wg.Done() fmt.Println("Reader Finished") }() ch := make(chan interface{}) Bind(ch, "CCC") for i := 0; i < 5; i++ { select { case v := <-ch: fmt.Printf("OK:%v\n", v) default: fmt.Println("NG") } time.Sleep(time.Second) } }() wg.Wait() fmt.Println("Main Finished") } func Bind(ch chan interface{}, key string) { go func() { for { if v, ok := m.LoadAndDelete(key); ok { ch <- v } time.Sleep(time.Millisecond * 200) } }() }チャンネルに直接書き込むのではなく、sync.Mapに書き込んでるのでブロックしません。sync.Mapに書き込むところをきちんと型を書いた新たなfuncを定義してやれば、型チェックは働きます。上で定義しているBind関数も同様です。
ちなみに最初はBind関数内でチャンネルを作ろうとしてたんですが、関数を抜けた時点で変数が解放されるのでpanic起こしてダメでした。
パフォーマンスは計測してませんが、生のチャンネルを扱うよりは確実に悪いでしょう。sleepの時間調整である程度パフォーマンス調整が出来るかもしれません。
余談
チャンネルの容量を増やすことで満杯になるまではブロックしない書き込みになるんですが、無限に増やすわけにもいかないのでこんな方法を考えてみました。
設計が悪いのは明らかなんですが、一応なぜこんな設計を考えているのかの事情は説明しておきます。
それは
Ctrl+C
やSIGTERMなどOSのシステムコール時に全てのGoroutineが後始末をしながら終了をして欲しいからで、その際にcontextを使ってルーチンを終了させているのですがルーチンの終了の順番が不定だからです。なので送信側ルーチンが先に死んで正常終了するケースと、受信ルーチンが先に死んで永久にプロセスが動き続ける場合がありました。これはあくまで本当に無理矢理やった例ですので、もっと洗練されたやり方があったり、ライブラリがあればご紹介いただければ嬉しいです。
- 投稿日:2021-01-21T19:10:12+09:00
sdk-goでdynamoDBの複雑なstructの値をupdateする
やりたいこと
GoでのdynamoDBに対しての操作は割とめんどかったりします。単純なStringやIntのものであればまだ良いのですが、ListのMapのなかのListとか、値の構造が複雑になってくると気が滅入ります。
// シンプルなパターン &dynamodb.UpdateItemInput{ TableName: aws.String("sampleTable"), Key: // 省略, ExpressionAttributeValues: map[string]*dynamodb.Attribute{ ":ssv": { S: aws.String("sampleStringValue") }, ":siv": { N: aws.String(strconv.Itoa(0)) // ここもintをstringに型変換してそれをaws.String()してkeyにはNを指定って感じでわかりづらい。 }, }, UpdateExpression: aws.String("set sampleStringValue=:ssv and sampleIntValue=:siv") } // 複雑なパターン &dynamodb.UpdateItemInput{ TableName: aws.String("sampleTable"), Key: //省略, ExpressionAttributeValues: map[string]*dynamodb.Attribute{ ":slv": { L: [ { M: { ":smp": { aws.String("sample") }.... } } ] }, } }これを楽にやれるのがsdk-goのexpressionってやつです。
expressionの使い方
update := expression.UpdateBuilder{}.Set( expression.Name("sampleComplexValue"), expression.Value(sampleCompexValue) ) expr, err := expression.NewBuilder().WithUpdate(update).Build() &dynamodb.UpdateItemInput{ TableName: aws.String("sampleTable"), Key: //省略, ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), UpdateExpression: expr.Update() }上の例よりかなりスマートにかけますね。可読性も格段に上がります。
expression.Valuesの引数に複雑な(もちろん単純なものでもなんの問題もありませんが)値を渡すだけで、ここまでできるのは結構ありがたい。もちろんexpressionは上記の例のような更新処理だけでなく、Query処理等にも使えるので積極的に使っていきたいところですね。
- 投稿日:2021-01-21T13:15:54+09:00
Azure FunctionsとGoでZoom会議参加者を取得する
はじめに
リモートワークによりZoom会議が増え、参加者を記録して残すことが増えてきました。
今までは会議にAcceptしたりした人をカウントしたり、会議参加者リストをスクリーンショットしたりして参加者を取得していましたが、書き漏らしたり、スクリーンショットを忘れていたりといったことが起きていました。Zoom自体に会議参加者をエクスポートする機能はあるのですが、せっかくなのでAPIを叩いて取得できるツールを作ってみようと思い、作り始めました。
構成
ツールを作るに当たり、必要な/やってみたい技術、どんな形にしたいかをまとめてみました。
完成形のツールイメージ
→ Meeting UUIDをチャットツールから送信するだけで参加者一覧の名前が返ってくる使用する技術と理由
- Slack(チャットツールなら何でも良かったのですが、手元にSlackがあったので)
- Azure Functions(会社でAzureを使っているため個人でも使ってみたかった)
- Go(未体験の言語学習)
- Zoom(参加者取得)
この技術を使ってイメージ図を作成しました。SlackからAzure Functionsへリクエストを飛ばし、カスタムハンドラーのGoへリクエストを渡します。カスタムハンドラー側でZoom APIにMeeting UUIDをつけたリクエストを飛ばし、レスポンスから参加者を取得してSlackに返します。
Azure FunctionsでGoを使う
去年の12月に、Azure Functionsがアップデートされ、 どんな言語でもCustom handlerで使用できるようになりました。これまではAzure Functionsで使用できる言語は限られていたので、このアップデートはすごく助かりました。
Azure Functions in Any Language with Custom Handlersまた、別ブログにて、このCustom HandlerにGoを使ったサンプルも投稿されていました。この記事が私のやりたいことと似ていたので、この記事を基にしてZoomを組み合わせてみることにしました。
How to build a serverless app using Go and Azure Functions開発環境
Go: go1.15.2 darwin/amd64
Visual Studio Code: 1.52.1ツールの制限事項
今回作成したツールでは、過去の会議参加者のAPI(GET
/past_meetings/{meetingUUID}/participants)を使用しています。このAPIは有料プランのPro以上でしか呼べないので、本ツールを使う場合、Zoomの料金プラン(Choose a plan)で登録しておく必要があります。作成手順
ZoomのAPI使用設定
ZoomのApp Marketplaceで、JWT認証によるアプリを作成します。このアプリ作成には以下記事を基に作成しました。
Zoom App Marketplace
Zoom APIの使い方:OAuth・JWT による認証方法・Postman での実行方法
Zoom APIを試してみる JWT編作成した結果がこちらです。今回はここに記載されているAPI KeyとAPI Secretを、後述する環境変数にセットして使用します。
コード
コードを書く準備として、ローカル環境構築をしておきます。この構築では、MicrosoftがGoのクイックスタートガイドを公開しているので、それに沿って構築してください。
Quickstart: Create a Go or Rust function in Azure using Visual Studio Code今回作成したコードはMeetingParticipantsExporterです。以降、このコードについて説明します。
HTTPServerの起動
mainでHTTP Serverを起動させておきます。これでFunctions HostからCustom Handlerに対して、
/api/zoom
でリクエストを送信できるようになります。func main() { port, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT") if !exists { port = "7071" } http.HandleFunc("/api/zoom", function.ExportParticipants) fmt.Println("Go server listening on port", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }Slackからのリクエストを検証
Verifying requests from Slackにて、Slackからのリクエスト検証方法が記載されています。この検証法と、参考にしたHow to build a serverless app using Go and Azure Functionsを基に、ヘッダのシグネチャやタイムスタンプを使用して、Slackからのリクエストかどうかを判定します。
Zoom APIの使用
JWT認証用のトークン作成
Zoom APIを使用するにはJWT認証用のトークンを作成する必要があります。そのトークンをを作成するために、goで書かれているjwtを使用することにしました。このjwtでは、API KeyやSecret Keyが必要だったので、
「ZoomのAPI使用設定」項目で取得したAPI Key、API Secretを、それぞれAPIKEY、SECRETKEYとして環境変数にセットして使用しています。
以下、トークン作成コードです。import ( ・・・ "github.com/cristalhq/jwt" ・・・ ) type Env struct { SecretKey string `required:"true"` ApiKey string `required:"true"` } ・・・ func ExportParticipants(w http.ResponseWriter, r *http.Request) { ・・・ key := []byte(goenv.SecretKey) signer, _ := jwt.NewSignerHS(jwt.HS256, key) // tokenは作成後1分間有効 now := time.Now() after := now.Add(time.Minute) claims := jwt.RegisteredClaims{ Issuer: goenv.ApiKey, ExpiresAt: jwt.NewNumericDate(after), } builder := jwt.NewBuilder(signer) token, _ := builder.Build(claims)これでZoom APIに使用するトークンを作成できました。
会議参加者のAPIへリクエスト・レスポンス
会議参加者のAPIへリクエストを送ります。使用するAPIの仕様(GET /past_meetings/{meetingUUID}/participants)を見ると、
URL:https://api.zoom.us/v2/past_meetings/{meetingUUID}/participants
Header:Authorization: "Bearer " + JWTアクセストークン
で送る必要があります。
また、レスポンスは以下のような形式となっているため、participants
内のname
を取得するようにします。{ "page_count": 1, "page_size": 30, "total_records": 1, "next_page_token": "aliqua", "participants": [ { "id": "8b29rgg4bb", "name": "Ram Shekhar", "user_email": "ram.shekhar.123@fkdngfjg.fdghdfgj" } ] }このURL生成、リクエスト、レスポンスからの参加者取得は以下のとおりです。
var tokenStr string tokenStr = token.String() meetingParticipantsUrl := "https://api.zoom.us/v2/past_meetings/" url := meetingParticipantsUrl + meetingUUID + "/participants" req, _ := http.NewRequest("GET", url, nil) bearerAccessToken := "Bearer " + tokenStr req.Header.Set("Authorization", bearerAccessToken) client := new(http.Client) resp, _ := client.Do(req) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) fmt.Printf("Response body: %s", body) if err != nil { fmt.Printf("Error ocurred while executing ReadAll. %s", err) log.Fatal(err) return } //Unmarshall json var meetingParticipantsResponseBody MeetingParticipantsResponseBody if err := json.Unmarshal(body, &meetingParticipantsResponseBody); err != nil { fmt.Printf("Error ocurred while unmarshal body. %s", err) log.Fatal(err) return } fmt.Println("Participant") participantNames := "" for _, participant := range meetingParticipantsResponseBody.Participants { fmt.Printf("%s\n", participant.Name) participantNames += participant.Name participantNames += "\n" }Slackへレスポンス
Slackへは参加者のテキストのみを返すようにします。
type SlackResponse struct { Text string `json:"text"` } ・・・ slackResponse := SlackResponse{Text: participantNames} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(slackResponse)Azure上での動作
Azure Functionsでアプリ作成
Azure Portal上で関数アプリを選択し作成していきます。
リソースグループ、関数アプリ名には適当な名前をセットします。ランタイムスタックやバージョンでは、Custom Handlerを使うようにしておきます。
OSはLinuxを使うようにしておき、プランにはサーバーレスを選択します。
ここまでの設定に問題がなければ、エラーメッセージが出ず、作成ボタンを押すことができます。
Azure Functionsにコードをアップロード
ローカル環境のコードをLinux/x64環境用にビルドしておきます。
> GOOS=linux GOARCH=amd64 go build -o go_main cmd/main.goこのコードをVSCodeからアップする方法もありますが、ここではコマンドでアップする方法を実行します。MicrosoftのAzure Functions Core Tools の操作を参照して、Azure Functions Core Toolsをインストールします。
インストール完了後、func azure functionapp publish アプリ名 --custom
を実行することでAzure Functionsにコードをアップロードできます。
ここでのアプリ名はAzure Functionsの関数アプリで作成したアプリ名となります。❯ func azure functionapp publish MeetingParticipantsExporter --custom Getting site publishing info... Uploading package... Uploading 7.72 MB [###########################################################] Upload completed successfully. Deployment completed successfully. Syncing triggers... Functions in MeetingParticipantsExporter: zoom - [httpTrigger] Invoke url: https://meetingparticipantsexporter.azurewebsites.net/api/zoom?code=xxxxxこれでAzure Functionsへのコードアップロードは完了です。
アップロード後のInvoke url: https://meetingparticipantsexporter.azurewebsites.net/api/zoom?code=xxxxx
は、後述するSlackのAPI使用設定で使用するため、控えておいてください。
SlackのAPI使用設定
slack apiのサイトから、Create New Appsを選択し、任意のアプリ名を入力します。
アプリ作成後、Slash Commandsを選択し、新しいコマンドを作成します。
コマンドの作成ではSlackからどのように呼び出すかを入力していきます。
/zoom meetinguuid
と呼び出したいので、コマンドには/zoom
とします。またRequest URLは、先程控えていたURLを入力します。
コマンドの作成が終わったら、Install to Workspaceボタンを押して、作成したアプリをワークスペースにインストールします。
インストールが終わったら、App Credentialsにある、Sigining Secretを控えておきます。
環境変数設定
Azure Functionsの構成項目で、ZoomのAPI Key、Secret Key、SlackのSigining SecretをそれぞれAPIKEY、SECRETKEY、SLACK_SIGNING_SECRETの環境変数としてセットします。
環境変数セット後、Azure Functionsのアプリを再起動しておきます。
これで準備は整いました。結果
Slackから/zoom meetinguuidを送ると、参加者のリストが返されます。
おわりに
各技術を使って、Zoomの参加者を取得するツールを作成しました。触ったことのない技術を独学で使っていくのは大変でしたが、一歩ずつ前に進める経験は結構楽しいですね。
ツールを作るにあたり、本記事に掲載した記事の作者の方々、Twitterでアドバイスを頂いた、@nthonyChuさん、@u_phyさん、ありがとうございました。この記事が誰かのお役に立てれば幸いです。