Go の context パッケージとは何か

この記事はフラー Advent Calendar 2020 - Adventar の17日目の記事です。 16日目は shmokmt さん で 「MySQL の Generated Columnsについて」でした。


私がGoを勉強し始めたときに独特な概念だなと思ったのがcontextパッケージでした。

今日はそのcontextパッケージについてまとめようと思います。

※ この記事は先日、社内勉強会で発表した資料の簡単な書き直しになります。

context を使うと何が嬉しいのか

  • リクエストスコープなデータにアクセスしやすくなります
  • goroutineのリソースの開放漏れやタイムアウト処理が実装しやすくなります

context の定義

type Context interface {
   // コンテキストがキャンセルされたらチャネルをクローズ
   Done() <- chan struct{}
   // コンテキストがキャンセルされた理由
   Err() error
   // 時間が設定されている場合は時間を教えてくれる
   Deadline() (deadline time.Time, ok bool)
   // Value(key interface{}) interface{}
}
  • Context はgorotine safeで任意の数のgoroutineにContextを渡すことができます
  • Context の親は子をキャンセルできるが、子は親をキャンセルすることができます

タイムアウトの処理(例)

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithTimeOut(ctx, 3 * time.Second)
    defer cancel()

    go doHeavyProcess(ctx context.Context) {
        for {
                fmt.Println(ctx.Deadline())
                time.Sleep(1 * time.Second)
         }
}

リクエストスコープなデータへのアクセス

WithValue(parent Context, key, val interface{}) Context
Value(key interface{}) interface{}

context はキャンセル/タイムアウト処理だけでなく、リクエストスコープなデータにアクセスする手段も提供します。

ここでいうリクエストスコープなデータというのは、ユーザID, 認証情報(token), トレースID

などのことを指しています。

API Client, DB driver, Loggerなどのリクエストスコープでないことが自明であるものは含めないようにしましょう。

値の取得と設定は色んなで実装してしまうと混乱してしまうので、set/get用の関数を定義して呼ぶことが推奨されています。

type firebaseTokenKeyType struct{}
var firebaseTokenKey firebaseTokenKeyType

func WithFirebaseToken(ctx context.Context, token string) context.Context {
   return context.WithValue(ctx, firebaseTokenKey, token)
}

func GetFirebaseToken(ctx context.Context string {
    // inteface{} が返ってくるので、型アサーションしてあげる
    v, ok := ctx.Value(firebaseTokenKey).(string)
    if !ok {
            return ""
    }
    return v
}

所感

  • ctx は関数/メソッドの第一引数に書く
  • 活用することでgoroutineの開放漏れを防ぐことができる
  • タイムアウト処理はtimeパッケージではなく、contextパッケージを活用するとGoっぽさが出る
  • ctxに保存する情報は認証情報やユーザーIDなどのcredentialsなど(リクエストスコープなデータに限る)
  • ctxのget/set は関数を定義して、それを呼び出して利用するとコードが荒れない
  • setする関数を呼ぶ場所は HTTP Middlewareあたりでやると良さそう

合わせて読みたい

Junpei Tsuji さんのブログの解説が丁寧で非常に参考になりました。

ありがとうございます!

明日は @yudai_watanabe さんで 「リモートワーク下のスクラム開発で気をつけたこと」です。お楽しみに!

MySQL の Generated Columnsについて

この記事はフラー Advent Calendar 2020 - Adventar の16日目の記事です。 15日目は sohei さん で 「オンボーディングHACKS」でした。


MySQL 5.7.6以降ではGenerated Columns という機能を使うことができます。 これは実カラムの値を計算した結果を格納するためのカラムを作る機能です。

CREATE TABLE `member` (
    `id` INT AUTO_INCREMENT NOT NULL,
    `first_name` VARCHAR(10) NOT NULL,
    `last_name` VARCHAR(10) NOT NULL,
    `full_name` VARCHAR(255) AS (CONCAT(last_name, ' ', first_name)),
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

Generated Columnsをデフォルトの設定で使う場合は上記のfull_nameAS expr で計算したい式を追加するだけで大丈夫です。

INSERT INTO `member` (first_name, last_name) VALUES ('shoma', 'okamoto');

厳密なsyntaxは以下のようになっています

col_name data_type [GENERATED ALWAYS] AS (expr)
  [VIRTUAL | STORED] [NOT NULL | NULL]
  [UNIQUE [KEY]] [[PRIMARY] KEY]
  [COMMENT 'string']

実際にデータを投入してみて、SELECTしてみます。

mysql> SELECT * FROM `member`;
+----+------------+-----------+---------------+
| id | first_name | last_name | full_name     |
+----+------------+-----------+---------------+
|  1 | shoma      | okamoto   | okamoto shoma |
+----+------------+-----------+---------------+
1 row in set (0.00 sec)

取得できました。簡単! ヤッタネ!

Types

STOREVIRTUAL という格納タイプが存在します。

デフォルトでは VIRTUAL になります。 それぞれの意味は以下の通りです。

STORED: 挿入/更新時に列の値を計算してストレージに保持します。インデックスを貼ることが可能です。

VIRTUAL: 取得時に都度、仮想列の値を計算します。ストレージには保持されません。

InnoDBのみ仮想列にセカンダリインデックスを貼ることが可能です。

制約

  • サブクエリで実行することができない
  • 生成列を AUTO_INCREMENT で使えない
  • 計算元の指標に AUTO_INCREMENT が使えない
  • 外部キーが仮想列を参照することができない

等の細かい制約があるため使うときにはしっかりと公式ドキュメントを読んだ方が良いでしょう。 おそらくシンプルな使い方しかできないんじゃないかなと思います。

どんなときに使うのか

  • 複雑な条件文をシンプルにしたいとき

  • 生成カラム(関数インデックス)にインデックスを貼りたいとき

関数を使った条件文は基本的にインデックスが効きませんが、Generated Columnsでは関数に対してインデックスを貼ることができます。 yoku0825 さんのブログに書いてある具体例がわかりやすかったです。

日々の覚書: MySQL 5.7.6のgenerated columnは関数インデックスの夢を見るか

気になった箇所

For stored generated columns, the disadvantage of this approach is that values are stored twice; once as the value of the generated column and once in the index.

STORED 生成列にインデックスを貼る場合は生成列を計算するときとインデックスを貼るときで2回計算が走るようなので、注意が必要です。

When a secondary index is created on a virtual generated column, generated column values are materialized in the records of the index. If the index is a covering index (one that includes all the columns retrieved by a query), generated column values are retrieved from materialized values in the index structure instead of computed “on the fly”.

理由は仮想生成列にセカンダリインデックスが作成されると、生成された列の値をインデックスレコードが持つからだそうです。したがって、インデックスを貼りたい場合はデフォルトの VIRTUAL を使えば良いのかなと思っています。(間違っていたら、指摘してもらえると嬉しいです)

終わりに

ISUCON10の予選で先輩にGenerated Columnsという機能があるんだよと教えていただき、今回調べてみました。 明日は shmokmtさんで 「Go の context パッケージについて」です。

参考

MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.7 CREATE TABLE and Generated Columns

MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.8 Secondary Indexes and Generated Columns

ISUCON10 の振り返りと MySQL の generated columns - Techtouch Developers Blog

MySQLのGenerated Columnsまとめ with Rails - Qiita

日々の覚書: MySQL 5.7.6のgenerated columnは関数インデックスの夢を見るか

自分なりの集中を継続する方法

実務でどうやったら集中できるかの自分なりの考察とその対策 

ADHD傾向があるので、どういう状態のときに作業しやすいか書いてみる。

どうやったら集中を継続できるか

  • リズミカルな音楽がある 
  • 取り組む1つあたりのタスクの量が適切である

多すぎると見積もりの粒度がブレるし、コンスタントにPRをマージできないのは、士気が下がる気がする

  • 取り組むタスクの範囲が明確である

エンジニアが決めても大丈夫な問題でもあれば、ステークホルダーに確認を取っちゃう。

  • 取り組むタスクの難易度が自分にとって少し難しいレベルである

何がわからないのかわからないというパニックゾーンに陥りそうなものはあらかじめアラートを出す。またはすぐアドバイスがもらえる状態にしておくなどの工夫をする。

  • 作業を進めるにあたって何かしらのフィードバックがある

エディタやCIのLinter / レビュワーのコメント / プロダクトオーナーからのフィードバックなど

どんなときに集中が切れるか

  • 考察する際に経験がない深い知識を求められるとき

要は興味はあるがわからないことが多すぎて、何がわからないのかわからないみたいなとき。 難しいアルゴリズムコンピュータサイエンスの専門的な理論や知識を求められるときはすぐに自分のものにできないので、 GitHubで素振りのリポジトリを作る等して分割しながら少しずつ理解を進めていくほうが良さそう。

  • トイレ(椅子から離れる)

  • 細切れでミーティングが入るとき

いい集中力の切れ方がある

新しい知識を獲得しようとしているときは要は筋トレみたいなもので、いい集中力の切れ方。 でも、切れにくいように対処することはできる気がしていて、大体以下のようなことをしてるっぽい。

  • 自分にとってちょっと負荷がかかる程度のブログやスライド、本を漁る

難しすぎるものを読んでもパニックになって認知負荷が上がり、学習効率が悪く、疲れるだけな気がする派です。

  • 取り組む範囲を資料ベースで決める

自分に合う資料をベースにそれを理解するようにググって、暗黙知をためていく。

※ 自分でも読破できそうという自信がモチベーションを下げないために大事なので、 公式ドキュメントが難しすぎるようであれば、最初は信頼できる記事を読んでもいいと思う派。 でもそのあと必ず公式ドキュメントと照らし合わせたりするのは大事だと思う。

  • 素振りをする

手を動かす

獲得したい知識を使用した小さなコードをリポジトリにアップロードして、限りなくノイズが入っていない状態で動作を確認する。 まだ新しい技術やライブラリであれば、それが本番環境に適用できるレベルのものか確認する。

  • 歴史を知る

その技術が使われているのは、それ以前の技術に改善の余地があったから。 ルーツを辿ることで記憶できるし、腹落ちした状態で取り組むことができる。 その技術の本質を見失うことがないと思う。

集中力切れたとき

  • 風呂
  • ランニング
  • 睡眠

大体上記のどれかで解決する

まとめ

定期的に素振りをして、なんでそれを使うのか背景から説明できると、自分の中で意義が見出だせて モチベーションが下がらない気がする。

結果的に過集中モードに入りやすい。

合わせて読みたい

スイッチの入れ方 - mizchi's blog

プログラミングを学ぶにあたって詰まったことと、そこから学んだこと - mizchi's blog

ISUCON10予選参加しました

先日、会社の先輩方と一緒にISUCONに参加しました。(自分だけ初参加) アプリケーションだけでなく、Nginx、MySQLSQLの知識も全体的に欠落していることが再確認できた良い機会でした。 自分は横でずっとアドバイスをもらいながらGoを8時間ぐらい眺めていたら終わっちゃいました。スコアは1000ちょっとぐらいでした。 来年は今年よりもチームに貢献できるように頑張ります!

リポジトリ

mitamae でプロビジョニングしていたので、そのレシピ集

github.com

予選のコード

github.com

合わせて読みたい

cli/cli をシェルでちょっといい感じにする

はじめに

GitHub製のCLIツールである GitHub - cli/cli: GitHub’s official command line tool の小ネタです。

ちなみに clihttps://github.com/github/hub というツールの後継に当たります。

zsh と peco でシュッっとブラウザに飛ぶをやってみる

チーム開発でよくしたくなるのは、手元でissueやPRの一覧を確認して、シュッっとブラウザに飛ぶことではないでしょうか。

ghコマンドで実際にissueとPRをブラウザで開くコマンドは以下のような形式を取ります。 ですが、issueやPRの番号とかいちいち覚えてないと思います。

gh issue view <number> --web
gh pr view <number> --web

なので、

gh issue list
gh pr list

などの一覧を取得するコマンドから番号だけ抜き取るようにします。

❯ gh pr list                          

Showing 4 of 4 pull requests in kayac/ddl-maker

#23  Support for FULL TEXT index  shmokmt:full-text-index
#22  add LF to EOF                shogo82148:add-lf-to-eof
#21  Add new nullable types       shogo82148:add-new-nullable-types
#20  add go.mod                   shogo82148:add-go-mod

空白で列を分割できそうなので、awk で分割してあげます。

gh issue list | peco | awk -F" " '{print $1}' | read num; gh issue view $num --web
gh pr list | peco | awk -F" " '{print $1}' | read num; gh pr view $num --web

毎回これらのコマンドを実行するのは面倒なので、function として ~/.zshrc にでも登録してあげると良いと思います。

function issue() {
  line=$(gh issue list | peco)
  num=$(echo "${line}" | awk -F" " '{print $1}')
  gh issue view "${num}" --web
}

function pr() {
  line=$(gh pr list | peco)
  num=$(echo "${line}" | awk -F" " '{print $1}')
  gh pr view "${num}" --web
}

これでコマンドを実行するカレントディレクトリがGitHubリポジトリ配下だった場合に対象のページをシュッっと開くことができます。

合わせて読みたい

Goでforkしたpackageに参照を向ける方法

はじめに

  • Go Modules を使っている
  • 使用しているthird-partyのpackageのメンテナンスが滞っているため一時的に参照をfork先に向けたい

replace directive を使う

ソースコードのimportパッケージをfork先のものに書き換えるのかなと思っていましたが、Go Modules の replace directive を使うと少ない変更で参照を変えることができます。

eg)

github.com/hoge/package の参照を github.com/foo/package に変えたい

require (

    github.com/hoge/package {version}
)


replace github.com/hoge/package => github.com/foo/package master

でfork先のmasterブランチに対して参照を向けることができます。

簡単ですね!

合わせて読みたい

Exponential Backoff とは何か

Exponential Backoff という言葉を最近覚えたので、まとめておきます。

これはなにか

ネットワークを通してサーバーと通信失敗時にリトライ間隔をクライアント側で調節する手法です。 Exponential という名前の通り、リトライするときに失敗回数に応じて待ち時間を指数関数的に増やします。 1, 2, 4, 8秒・・・みたいな感じでどんどん次のリクエストを送るまでの時間が伸びていくわけですね。

ロードバランサを経由したリクエストを受けたけど、その先のWeb APIサーバーがダウンしてしまっている。。。みたいなときに有効です。

AWSの橋本さんの記事 にも実例が書かれています。

ジッターはリクエストの集中を防ぐ

システム内にクライアントが複数ある場合は、このまま特定のタイミングにリクエストが集中してしまう可能性があります。 そこで、ランダムに少しだけ時刻をずらすジッター(ゆらぎ)を加えることでリクエストの集中を防ぐのが定石です。

アルゴリズム

Google Cloud Storage の Truncate Exponential backoff を参考に説明すると、

  1. リクエストを送る
  2. 失敗したら、1秒 + ジッター秒分だけずらす
  3. 失敗したら、2秒 + ジッター秒分だけずらす
  4. 失敗したら、4秒 + ジッター秒分だけずらす
  5. 待ち時間の上限を超えるまで増やし続ける
  6. リトライ上限回数を超えるまでリトライする(※ただし、待ち時間はこれ以上増やさない)

という風になります。

実装例

Go

Go も色々な実装があります。shogo82148さんのgo-retryはcontextがサポートされており、Goっぽい実装です。

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/shogo82148/go-retry"
)

type Result int

func DoSomething(ctx context.Context) (Result, error) {
    // do something here that should to do exponential backoff https://en.wikipedia.org/wiki/Exponential_backoff
    return 0, errors.New("fails")
}

var policy = retry.Policy{
    MinDelay: 100 * time.Millisecond,
    MaxDelay: time.Second,
    MaxCount: 10,
}

func DoSomethingWithRetry(ctx context.Context) (Result, error) {
    retrier := policy.Start(ctx)
    for retrier.Continue() {
        if res, err := DoSomething(ctx); err == nil {
            return res, nil
        }
    }
    return 0, errors.New("tried very hard, but no luck")
}

func main() {
    fmt.Println(DoSomethingWithRetry(context.Background()))
}

Python

invlさんのretryPython実装でデコレーターを使うAPIになっていて、Pythonicな実装だなーと思いました。

@retry()
def make_trouble():
    '''Retry until succeed''

参考