Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update users channels and messages via API #102

Closed
wants to merge 4 commits into from

Conversation

thinca
Copy link
Member

@thinca thinca commented May 10, 2020

  • API 経由で users.json を更新する update-user-list サブコマンド
  • API 経由で channels.json を更新する update-channel-list サブコマンド
  • API 経由で指定した日付1日分の全チャンネルのログを取得してチャンネル毎の日付のファイルに保存する fetch-messages サブコマンド

を追加しました。

正直なところ設計とか命名とか全体的にイケてない感じがするので、アドバイスよろしくお願いします!

@thinca
Copy link
Member Author

thinca commented May 10, 2020

そう言えば messages に関しては並列化の余地がありますが

  • めちゃくちゃ遅いわけではない
  • 50+ per minute の API rate limit がある
    • 現状すでに 81 チャンネルあるので普通にやったら引っかかりそうだけど手元で試した時はひっかからなかった…よくわからない…
  • ぶっちゃけめんどい

ので保留にしました。

Copy link
Member

@koron koron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DownloadEntitiesToFile は Go 的な書き方からはほど遠い。APIRequest にどのような値を設定すべきかそれを呼ぶ人が意識しなければいけない(さらにドキュメントもない)のは論外といってよい。この設計は書く手間を減らして読む手間を犠牲にしている感がある。

Go的に考えるなら以下のような構成になる。

  1. まず conversations.history を正しく1ページ読める関数(メソッド)を作る
  2. 次にそれを1つのファイルに保存する仕組みを作る
  3. それを必要な回数イテレーション(=ページネーション)する仕組みを作る
  4. 必要なら複数ファイルに渡ったデータを1つのファイルにまとめる仕組みを作る
    このとき既存のgeneratorの入力フォーマットも再考してよい

conversations.listusers.list は多少書く手間がかかっても別の入り口になってるべき。
DownloadConversationHistory, DownloadConversationList, DownloadUsersList みたいに。
それぞれ最小限の引数を受け取るのが好ましい。

いっぺんに3つ作るのではなく1つを正しく作る。それを3回繰り返す。そのうえでまとめられるところはまとめる、というのが正着ではないでしょうか。


以上、かなり辛目なレビューでした。

"path/filepath"
)

type APIRequest struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

公開型はかならずコメントを付けましょう。

golint 入れておいて make lint で警告がでるんで
それが増えないようにしてください。

}

func reverse(slice []interface{}) {
for i := 0; i < len(slice)/2; i++ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

毎回 len()/2 はあんま良くないですね。

こんな感じでどうでしょか?

Suggested change
for i := 0; i < len(slice)/2; i++ {
for i, j := 0, len(slice)-1; i < j; i++, j-- {

}
}

func DownloadEntitiesToFile(request APIRequest, destJSONFilePath string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

公開関数は…以下略

if !ok {
return ""
}
metadata := metadataValue.(map[string]interface{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

キャスト(type assertion)に失敗したケースについて考慮が抜けてます。

他にも何か所かあるみたいですが指摘は省略しますので留意してください。

Suggested change
metadata := metadataValue.(map[string]interface{})
metadata, ok := metadataValue.(map[string]interface{})
if !ok {
return ""
}

for true {
var json map[string]interface{}

fmt.Printf("GET %v page %d of %s ... ", request.ExtraParams, pageNum, request.APIMethod)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ログは log パッケージ使ったほうが良いです。

Suggested change
fmt.Printf("GET %v page %d of %s ... ", request.ExtraParams, pageNum, request.APIMethod)
log.Printf("GET %v page %d of %s ... ", request.ExtraParams, pageNum, request.APIMethod)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ、今のところ場所によってまちまちなので統一したいですね > log or fmt
場所によっては fmt.Fprintln()os.Stderr に出してるとこもあります

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうなんですよね。ほんとは脳死で log.Printf 使っておいてほしいんです。
そうすれば後から logging ライブラリを別のやつに差し替えるとかも簡単なんで。

list := json[request.JSONKey].([]interface{})
fmt.Printf("fetched %s count: %d\n", request.JSONKey, len(list))

results = append(results, list...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patination が必要だと考えてるものを、メモリ上で全部合成するのはあんま嬉しくなさげ。
以下みたいに都度ファイルに出力してしまったほうが良いのでは?

もしくはドキュメントコメントにあんま大きなものは取らないで、みたいに書くべきか?

[
{"entity": "123456"},
{"entity": "123456"},
{"entity": "123456"},

{"entity": "123456"},
{"entity": "123456"},
{"entity": "123456"},

]

reverse との兼ね合いはあるが…そもそもJSONに書き出す段階で reverse する=順序を気にする必要ってなんなんだろう?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どうせ読み込む時は全部メモリに読みますが…しかも全期間分のものを同時に。ダウンロード時にだけそこを気にする必要ありますかね?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いまここから手を付けておけばその方向性が示せますよね。
generatorも今の全部メモリに乗せる方式は破綻が見えてるので、直さなきゃいけないですから。

downloaderは SQLiteとかのローカルのDBに逐次乗せる。
generatorはそれを切り出して1ページずつHTML生成
みたいなのが目指すべきゴールかなと。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generator 側はともかく、こちらは1チャンネル/1日分です。1チャンネルの1日分のログのサイズはたかが知れていると思っているんですが(実際に 400 件以上の発言があった場合でも JSON で 300KB に満たない、メモリに展開しても 1MB も取ることはなさそう)、そこを今頑張る必要あるでしょうか?
この部分は過去の全ログを処理する generator と違って、常に1日分なので今後ログが蓄積していっても処理する量が増えて行ったりはしません。
メモリも基本的に潤沢な今の時代にそこを気にするのは早すぎる最適化を感じてしまいます。

return nil, fmt.Errorf("Error response: %v", json)
}

list := json[request.JSONKey].([]interface{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

キーがみつからないケース。型アサーションに失敗したケースの考慮もれ。

Comment on lines 12 to 18
type APIRequest struct {
SlackToken string
APIMethod string
ExtraParams map[string]string
JSONKey string
Reverse bool
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

名前と設計があってない。

APIRequest だと将来的にすべてのAPIを叩けるようになりそうで、そのパラメータをすべて内包しそう。
slackclient.Do(APIRequest) みたいな使い方が想定される。

また DownloadEntitiesToFile() (Pagination) 専用だとしても必須パラメータとオプショナルがまざってて、使う法としては困惑する。

仮に自分がやるならこうなる

type DownloadEntitiesOptions struct {
    ExtraParams map[string]string
    Reverse bool
}

func DownloadEntitiesToFile(token, method, itemKey, dstName string, opts DownloadEntitiesOptions) error {

もしくは token は必須だとしてメソッド生やす形かな。

type SlackClient struct {
    token string
    c *http.Client
}

type DownloadEntitiesOptions struct {
    ExtraParams map[string]string
    Reverse bool
}

func (sc *StackClient) DownloadEntitiesToFile(method, itemKey, dstName string, opts DownloadEntitiesOptions) error {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いい名前が思い付かなくて諦めたやつです…。
愚直に引数を10個でも20個でも並べた方がいい感じなんですね。構造体使うのやめてみます。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必須パラメータはなるべく引数に倒したほうが使うときにわかりやすいです。
で省略しても問題ないオプショナルパラメーターは構造体に突っ込むと。

もしくは全部の引数を構造体に入れるなら、全フィールドに
「これは必須」
「こっちはオプショナルでこういう時に使ってね」
みたいなドキュメント書くと良いです。

return nextCursorValue.(string)
}

func httpGetJSON(rawurl string, params map[string]string, dst interface{}) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIの汎用性を考えると paramsmap[string][]string のが良いかもしれない。
同じ名前の query string ってなんだよって思うけれど url.Values はそれを考慮した設計になってるし、
まれにそういうAPIもある。

当然 values.Set ではなくて values.Add で地道に構築する形になる。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そのケースは使わないだろうと判断して省略しました。

"limit": "200",
},
JSONKey: "messages",
Reverse: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この順序を保証しなきゃいけないのは、実は generator の処理の仕方に問題があるからで、それをココで吸収するのは好ましくない。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本 PR とは独立した問題ですね。Issue 立てておいてもらえると…。 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#109

説明はあとから

var nextCursor string

pageNum := 1
for true {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここ、for { でよかったりします

@tennashi
Copy link

個人的には Downloader に寄せれそうに思いますが、それは後でやればいいので、どこでどの処理を担保するのかが定まってれば良いと思っています // というお気持ちだけ忘れないよう書いておきます

Copy link
Member Author

@thinca thinca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビューありがとうございます。勉強になります。

まず conversations.history を正しく1ページ読める関数(メソッド)を作る
次にそれを1つのファイルに保存する仕組みを作る

これをやってしまうと1日1ファイルで保存すると言う目的が達成できなくなってします。

全体としてどう直すのがいいのか、指摘からだとちょっとわからず…。
とりあえず細かい指摘の部分を直してみます。

list := json[request.JSONKey].([]interface{})
fmt.Printf("fetched %s count: %d\n", request.JSONKey, len(list))

results = append(results, list...)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どうせ読み込む時は全部メモリに読みますが…しかも全期間分のものを同時に。ダウンロード時にだけそこを気にする必要ありますかね?

return nextCursorValue.(string)
}

func httpGetJSON(rawurl string, params map[string]string, dst interface{}) error {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そのケースは使わないだろうと判断して省略しました。

"limit": "200",
},
JSONKey: "messages",
Reverse: true,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本 PR とは独立した問題ですね。Issue 立てておいてもらえると…。 🙏

Comment on lines 12 to 18
type APIRequest struct {
SlackToken string
APIMethod string
ExtraParams map[string]string
JSONKey string
Reverse bool
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いい名前が思い付かなくて諦めたやつです…。
愚直に引数を10個でも20個でも並べた方がいい感じなんですね。構造体使うのやめてみます。

@thinca thinca force-pushed the update-users-channels-and-messages-via-api branch from a1f3b12 to 84780c6 Compare May 10, 2020 19:14
@thinca thinca force-pushed the update-users-channels-and-messages-via-api branch from 84780c6 to ea295a7 Compare May 10, 2020 19:22
@thinca
Copy link
Member Author

thinca commented May 11, 2020

#116 を見ましたが、理解しづらく、今の私ではレベルが足りずに足を引っ張りそうなのでこの件からは手を引きます。お手数をおかけしますが後は任せました。

@thinca thinca closed this May 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants