Slack で動いてるボットの処理が長い場合、フィードバックとしてインジケーターを出すということをやった
いま golang の練習用に作成しているボットには、URL をわたすと、そのサイトのキャプチャを撮影する機能があります。諸事情からボットのいるマシンとは別の場所、Heroku に設置していますが、起動が遅かったり、キャプチャ自体が遅かったりするので、ちゃんとやっていってるのかわかりません。そこで、ローディングインジケーターを出すことにしました。
できあがり
「こはる」とは社の Slack にいるボットですが、対抗して golang でつくっているのが「ごはる」です。かわいいですね。
アイコンの元ネタには以下を使わせてもらっております。
インジケーター
まずインジケーターを作成しました。実際はベタッと書いてから切りだしましたが、とにかくまずインジケーターを用意します。できあがりを想像してワクワクするのが原動力です。
type CaptureLoadingIndicator struct { indicators []string length int step int } func (c *CaptureLoadingIndicator) initialize(indicators []string) { c.indicators = indicators c.length = len(c.indicators) c.step = c.length - 1 } func (c *CaptureLoadingIndicator) next() string { c.step = ((c.step + 1) % c.length) return c.indicators[c.step] } func createCaptureLoadingIndicator() *CaptureLoadingIndicator { c := (&CaptureLoadingIndicator{}) c.initialize([]string{"▖", "▘", "▝", "▗"}) return c }
goroutine
キャプチャ機能では、キャプチャされた画像は Heroku からダイレクトに Slack に投稿されますが、キャプチャできたかどうかは Response で受けとれます。よって、Heroku へのリクエスト開始をインジケーターの開始、Response が帰ってくれば終了するとします。
細かい処理ははぶいて、goroutine 部です。http.PostForm
は同期的に処理されますから、基本的に以降の処理をブロックします。これを goroutine に追いだして本プロセスとは並行に行いつつ、終わるまでの間はインジケーターを表示します。
Big Sky :: golang の channel を使ったテクニックあれこれを参考に、処理が終わるまで待つ処理を書きます。
// インジケーターを表示するメッセージの TS を取得する var ind slack.ChatPostMessagesResponse slack.ChatPostMessage.Strike(&ind, string(m.Channel), "starting...") // 以下で goroutine ポーリングする q := make(chan CaptureServerResult, 2) go func() { // Heroku へリクエスト開始 resp, err := http.PostForm(captureServer, values) // リクエスト終わり q <- CaptureServerResult{resp, err} }() indicator := createCaptureLoadingIndicator() for { if len(q) > 0 { break } // 上記で取得した TS をターゲットとして、chat.update をかける slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, fmt.Sprintf("loading `%v`", indicator.next())) // update 間隔は加減しましょう time.Sleep(200 * time.Millisecond) } slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, "loaded") resp := <- q // response をもとにした処理が続く
<- q
は処理をブロックしてしまうので、なんの動きもない重い処理、待ちの時に他の処理をさせておきたい場合はチェッカーとしては使えないんですね。
しかし、Channel をうまく利用することにより、簡単に実現することが出来ました (先達の知識に感謝)。こういうのは大体、開始や interval 処理ではなく、終了がめんどくさかったりするのですが、それが特に問題にならずよかったです。
あと func(){}()
ができるのが色々便利ですね。