CircleCIで変更があった箇所だけに限定してビルドする

CircleCi が公式で path-filtering という orb を提供しているのでそれを使う。現在のバージョンは 0.1.1。

詳しくは公式のドキュメントにも書いてある。https://circleci.com/docs/2.0/configuration-cookbook/#execute-specific-workflows-or-steps-based-on-which-files-are-modified

以下、自分がやったこと

  1. Project ページ (自分の場合は GitHub を使っているので? https://app.circleci.com/pipelines/github/$org_name/$project_name に行って、Project Settings > Advanced > Enable dynamic config using setup workflows にチェックをつける
  2. 設定ファイルを追加

例えば以下のようなディレクトリ構成を想定する。Gradle を使ったマルチプロジェクト構成で、project1 と project 2 がサブプロジェクトとなっているような場合。project1 と project2 がお互いに依存関係がない場合、project1 のファイルを変えた時は project 1に関する CI だけ走らせたいという時(project2 も同様)。

.
├── .circleci
│   ├── config.yml
│   └── continue_config.yml
├── build.gradle.kts
├── project1
│   └── ...
└── project2
    └── ...

ポイントは

たとえば project1/Main.kt を変更したとすると、project1/.* run-build-project1 true のルールによりrun-build-project1true がセットされる 。project1 workflow は pipeline.parameters.run-build-project1true なので実行される。この時 project2 は pipeline.parameters.run-build-basepipeline.parameters.run-build-project1 が両方 false (default 値)なので実行されない。

.circleci/config.yaml

version: 2.1

setup: true

orbs:
  path-filtering: circleci/path-filtering@0.1.1

workflows:
  always-run:
    jobs:
      - path-filtering/filter:
          name: check-updated-files
          mapping: |
            build.gradle.kts run-build-base true
            project1/.* run-build-project1 true
            project2/.* run-build-project2 true
          base-revision: master
          config-path: .circleci/continue_config.yml

.circleci/continue_config.yml

version: 2.1

jobs:
  test_project1:
      - checkout
      - build:
          name: project1 test
          pwd: project1
          command: |
            ./gradlew test
  test_project2:
      - checkout
      - build:
          name: project2 test
          pwd: project2
          command: |
            ./gradlew test

parameters:
  run-build-base:
    type: boolean
    default: false
  run-build-project1:
    type: boolean
    default: false
  run-build-project2:
    type: boolean
    default: false

workflows:
  version: 2
  project1:
    jobs:
      - test_project1
    when:
      or:
        - << pipeline.parameters.run-build-base >>
        - << pipeline.parameters.run-build-project1 >>
  project2:
    jobs:
      - test_project2
    when:
      or:
        - << pipeline.parameters.run-build-base >>
        - << pipeline.parameters.run-build-project2 >>

Docs for Developers 読んだ

Docs for Developers 読んだメモ。

ドキュメント書き方だけにとどまらず、どのようにドキュメントを改善していくか、誰に向けて書くかなどが書いてあり、非常に面白かった。e34.fm で話していたのを聞いて買った気がする。

特に印象に残ったのは

  • ドキュメントを書く前に誰が読者で、その人はどんなゴールがあるかを考える
  • ドキュメントをいくつかの種類に分けて、伝えたい内容によって変える
  • ドキュメントは公開して終わりではなく、内容についてのフィードバックをもらったり、計測したり、内容が古けれ適宜修正したりする

誰のためのドキュメントか

有用なドキュメントを書くためには読者が誰であり、何を達成したいかを知ることが重要だ。例えば読者が開発者の場合にはコードを含めた説明でも多分通じるだろう。しかし読者がプロダクトマネージャーの場合は通じないかもしれない。また、開発者であっても、シニアレベルかジュニアレベルかで内容違った方が良いかもしれないし、SRE か Application Engineer かで求めている情報が違うかもしれない。これらの違いを理解するために本書では direct interview (対面でのインタビュー)や、survey (アンケートみたいなもの) を薦めている。それらの情報を元にユーザのペルソナを作り、ユーザストーリを作り、ユーザジャーニーを作りと、まるでサービス開発のような手法でドキュメントを作り始めている。自分の中ではこの部分が一番印象的だった。

伝わりやすいドキュメントの種類を選ぶ

コンテンツタイプを変えることによって、伝えたい内容をより効率的に伝えることができる。その文書がチュートリアルなのか、API reference なのかで章立てや、書く内容が変わっている方がよい。本書では大きく分けてコンテンツタイプを以下の6つに分けている。例えば Procedural documentation というは、チュートリアルや アプリケーションのインストール方法の説明などの文書。読者は自分の問題(アプリケーションを使い始めたいがどうすればいいかわからない。など)を素早く、しかもなるべく簡単に解決するためにこのタイプのドキュメントを読むだろう。ということで、この文書は長々とした説明は避けて、簡潔な手順書のような構成が望ましい。自分はドキュメンを書くときにどんなタイプの文章かというのを明示的に考えたことがなかった。この部分が言語化されていて感動した。

  • Code comment
  • READMEs
  • Getting started documentation
  • Conceptual documentation
  • Procedural documentation
    • Tutorial
    • How-to guides
  • Reference documentation
    • API reference
    • Glossary (用語集)
    • Troubleshooting documentation
    • Change documentation

ドキュメントを最新に維持する

ドキュメントは公開したら終わりではなく改訂が必要だ。プロダクトチームは機能を追加したり、削除をしたりするだろうし、API reference に書いてあるメソッドは書き換えられかもしれない。ドキュメントもこれらの変更と共に変更されていく必要がある。本書では機能を設計/変更する段階で、それがユーザに及ぼす機能などと同等に、それが”ドキュメント”にどのように影響があるかも考えるべきだと言っている。また、それぞれのドキュメントのオーナーを決めて、そのオーナーが責任を持って文書のアップデートやレビューをした方がよい。

Redis のプロトコルの理解のために Redis client を書いてみた

reimpl/redis_client at main · ganmacs/reimpl · GitHub

Redis のプロトコルがシンプルでだというのを聞きつけた1のでいくつかのコマンドをサポートした Redis client を書いた。 Redis は普段仕事で使ってる割にはどういうプロトコルを使っているか知らなかったのでいい勉強になった。

使い方

ping, get, set, incr, decr, subscribe, publish をサポートしている。それ以外のコマンドはほぼ同じようなことの繰り返していてやる気が無くなった。 使い方は以下。https://github.com/ganmacs/reimpl/tree/main/redis_client/examples 以下に他のコマンドを使った例もある。

#[tokio::main]
async fn main() -> Result<(), RErr> {
    let mut client = client::connect("127.0.0.1:6379").await?;
    client.ping().await?;

    client.set("key1", "value".into()).await?;

    let r = client.get("key").await?;
    dbg!(r); // => "value"
}

Redis のプロトコル

仕様はこれ https://redis.io/topics/protocol 。聞いていたとおり使用は非常にシンプルだった。データ型は全部で5つ。

  • Simple String
  • Error
  • Integer
  • Bulk String
  • Array

クラアントはコマンドを Bulk Stirngs の Array な形でサーバに送り、サーバ側は何かしらのデータを返す。 サーバにデータを送るとき、データはシリアライズされて送られるがそれも非常にシンプル。 Simple String のときは 1byte 目が +、 Error の場合は 1byte 目が -、というという感じで 1byte 目を見ればデータ型がわかるようにシリアライズされる。 Simple String と Array だけはシリアライズされたとき長さを持ってないとデシリアライズできないので長さを持つ必要がある2。 あとは、区切り文字として \r\n が使われている。

例えば get コマンドをシリアライズして送る場合はだいたいこんなイメージになる。

["get", "ky"]

これをシリアライズすると以下になる。

*2\r\n$3\r\nGET\r\n$3\r\nky\r\n

読み方はこんな感じ。

*2\r\n         | $3\r\nGET\r\n                  | $2\r\nky\r\n
Array(size=2)  | Bulk String(size=3) (1st elem) | Bulk String(size=2) (2nd elem)

これをサーバに送ると結果が帰ってくる。 どんな結果が帰ってくるかは https://redis.io/commands/get の Return value を読むとわかる。

Bulk string reply: the value of key, or nil when key does not exist.

と書いてあるので、もし key が存在するならその値が返ってくる。なければ nil を返す。 例えば、値が value で キーが ky のデータが保存されているとすると、キーky で get コマンドを発行すると$5\r\nvalue\r\nを返す。

感想

久しぶりに Rust を書いて良かった。やはりこういうタイプのソフトウェアに向いた言語だなと思った。 Redis のプロトコルはどんなバイナリフォーマットが来るのかと思っていたら非常にわかりやすく簡単に実装できた。 次は Rebuilding Redis in Ruby をやって、このクライアントを使って話をさせたい。


  1. Rebuild.fm の episode 321 で話していたの気になっていた。

  2. https://redis.io/topics/protocol の RESP Bulk Strings のあたりに詳しく書いてある

UDPでメッセージを送り合う時に一つにパケットに他のパケットを相乗りさせる

SWIM ではそれぞれのノード間の変化(メンバーの追加または離脱)を障害検知のために送り合っている ping / ack メッセージに相乗り (piggyback) して送ることでより強固に効率よく他のノードに変化を知らせている。 SWIM について詳しくは論文1を読むか日本語の記事2があるのでそっちをアレしてください。

SWIM では ping メッセージをランダムに選んだノードに一定間隔で送っている。 この ping メッセージに対して、join メッセージという「新たなノードを追加した時に、他のノードに送るメッセージ」を相乗りさせて送る部分の実装の話 (実際にはどんなメッセージでもいい)。 SWIM に若干の変更を加えた実装した memberlist という surf の内部で使用されているライブラリのコードみて書いている。 ただ、memberlist 自体は SWIM に若干の手を加えているし、複雑なので自分で相乗りする部分だけを書いたのが以下のリポジトリにあり、それを元に説明は書いている。

github.com

まずは ping と join それぞれのバイト単位での表現の仕方。 ping のような一つのメッセージを送る場合は、1 バイト目にどのメッセージであるかを、それ以降にそのデータの構造体を msgpack でエンコードしたデータが入っている。

例えば、以下が ping メッセージの中身。 1 バイト目に ping メッセージであることを表す値 (ここでは、0x01) を、それ以降に ping の構造体をエンコードしたものが入る。

+--------+========+
|  0x01  |  ping  |
+--------+========+

次が join メッセージの中身 (0x02 が join メッセージを表す)。

+--------+========+
|  0x02  |  join  |
+--------+========+

ping に相乗りさせるには、新たに compound メッセージというメッセージのタイプを作る。 このメッセージの中身に、含まれているメッセージの数、それぞれのメッセージのサイズの、実際のメッセージを載せることで複数のメッセージを同時に送れる。 以下が実際のメッセージの中身。 1 バイト目に coumpund メッセージであることを表す値 (0x03)、2 バイト目にはいくつのメッセージが詰まっているかを表す値 (0x02、つまり 2 つのメッセージが入っている)、 3 から 4 (5 から 6 も同様)の2バイトを使って 1 つめのメッセージの大きさ、それ以降には実際のメッセージが乗るという感じになっている。

+--------+--------+--------+--------+--------+--------+========+========+
|  0x03  |  0x02  |XXXXXXXX|XXXXXXXX|YYYYYYYY|YYYYYYYY|  ping  |  join  |
+--------+--------+--------+--------+--------+--------+========+========+

go で書くと以下のような関数で実現することができる。 msgs にエンコード済みのメッセージを渡している。

func ComposeCompoundMessage(msgs [][]byte) *bytes.Buffer {
    log.Println("Composing compound message")
    buf := bytes.NewBuffer(nil)

    // message type is 1 byte
    buf.WriteByte(uint8(CompoundMsg))

    // message count is 1 byte
    buf.WriteByte(uint8(len(msgs)))

    for _, msg := range msgs {
        // message size is 2bytes
        binary.Write(buf, binary.BigEndian, uint16(len(msg)))
    }

    for _, msg := range msgs {
        buf.Write(msg)
    }

    return buf
}

まとめ

何が言いたかったのかよくわからなくなったけど、とりあえず書いた。

ディレクトリの移動も peco を使うことにしてみた

最近ディレクトリ構造が複雑な(単純に長い)アプリケーションを触っていて、移動したい先のディレクトリまでスッと飛べないことが多かったのにイライラして書いた。 自分の場所より下のディレクトリをすべて出して peco でインクリメンタルサーチして飛ぶ感じ。 雑に .git を検索範囲に入れないようにした。とりあえず Ctrl-z で運用しているけどもっといいキーバンドがありそう。

function peco-change-dir() {
    selected_dir=$(find . -type d | grep -v .git | peco --prompt "[Change dir]")
    if [ -n "$selected_dir" ]; then
        BUFFER="cd ${selected_dir}"
        zle accept-line
    fi
    zle redisplay
}
zle -N peco-change-dir
bindkey '^z' peco-change-dir

unicorn の hook が呼ばれるタイミングを調べてみた

unicorn には特定のタイミングで発火する hook を仕掛ける仕組みがあるが、それらの hook が呼ばれるタイミングを正確にわかっていなかったのでメモ。 いちおう公式のドキュメントはこちらにある。ここで言及する unicorn のバージョンは v5.3.0

それぞれの hook の説明

after_fork, after_worker_exit, after_worker_ready, before_exec, before_fork の 5 つある。 以下はそれぞれの説明&コードの場所。

after_fork

この hook は fork した後にワーカープロセスから呼ばれる ( init_worker_process )。 init_worker_process は fork したワーカープロセスの中で呼ばれる worker_loop で呼ばれるメソッド。

after_worker_exit

この hook はワーカープロセスが exit する際にマスタープロセスから呼ばれる。 ワーカーが死んだ後、ワーカーの socket などを閉じた後に呼ばれる ( reap_all_worker )。

after_worker_ready

この hook はワーカープロセスがレスポンスを受けられる状態になったところでワーカープロセスから呼ばれる。 fork したワーカープロセスの中で呼ばれる ( worker_loop )。

before_exec

この hook は新しい unicorn のコマンドを exec する直前にマスタプロセスによって呼ばれる。 unicorn は graceful restart などをする際に、呼び出されたときと同じコマンド (例えば unicorn -c unicorn.ru など) を Kernel#exec する。 その直前に呼ばれる ( reexec )。

before_fork

それぞれのワーカプロセスを fork する前にマスタープロセスから呼ばれる。 Unicorn::Worker オブジェクトを作成したらすぐ呼ばれている ( spawn_missing_workers )。

呼び出しタイミングの確認

ワーカーが 2 つの設定で幾つか実験を行って呼び出しタイミングを見てみた。今回確認する際に使用したソースコードは以下。 もちろんオプションによっては呼び出し順序が変わるので注意。

github.com

起動時

  1. before_fork
  2. after_fork
  3. after_worker_ready

マスタープロセスが起動してから 2 つのワーカーに対してそれぞれ before_fork, after_fork, affter_worker_ready が呼ばれている。 マスタープロセスの pid が 46056 でワーカープロセスがそれぞれ、46090 と 46089 になる。 before_fork はマスタープロセスからよばれるので pid は 46056 になる。それ以外はワーカープロセスで呼ばれるのでそれぞれの pid になる。

$ be unicorn -c unicorn.ru
I, [2017-05-21T16:26:53.009330 #46056]  INFO -- : listening on addr=0.0.0.0:8080 fd=9
I, [2017-05-21T16:26:53.009448 #46056]  INFO -- : [before_fork] worker=0 pid=46056
I, [2017-05-21T16:26:53.010518 #46056]  INFO -- : [before_fork] worker=1 pid=46056
I, [2017-05-21T16:26:53.012068 #46056]  INFO -- : master process ready
I, [2017-05-21T16:26:53.012010 #46089]  INFO -- : [after_fork] worker=0 pid=46089
I, [2017-05-21T16:26:53.012433 #46089]  INFO -- : Refreshing Gem list
I, [2017-05-21T16:26:53.013825 #46090]  INFO -- : [after_fork] worker=1 pid=46090
I, [2017-05-21T16:26:53.014232 #46090]  INFO -- : Refreshing Gem list
I, [2017-05-21T16:26:53.121563 #46090]  INFO -- : [after_worker_ready] worker=1 pid=46090
I, [2017-05-21T16:26:53.125676 #46089]  INFO -- : [after_worker_ready] worker=0 pid=46089

マスタープロセスに SIGINT

  1. after_worker_exit

すぐにワーカープロセスを殺して after_worker_exit が呼ばれ、その後マスタープロセスも死ぬ。 after_worker_exit はマスタープロセスで呼ばれるので pid は 46056 となる。

I, [2017-05-21T16:29:04.026190 #46056]  INFO -- : [after_worker_exit] worker=1 pid=46056 status=pid 46090 exit 0
I, [2017-05-21T16:29:04.026515 #46056]  INFO -- : [after_worker_exit] worker=0 pid=46056 status=pid 46089 exit 0
I, [2017-05-21T16:29:04.026615 #46056]  INFO -- : master complete

ワーカープロセスに SIGINT

  1. after_worker_exit
  2. before_fork
  3. after_fork
  4. after_worker_ready

kill -s INT 46651 を実行した後のログ。 マスタープロセスの pid は 46622 なので、after_worker_exit はマスタープロセス内で呼ばれていることがわかる。 そのあと、新しいワーカープロセスを作成するので起動時と同じログになる(ただしワーカーは 1 つ)。

I, [2017-05-21T16:35:10.707660 #46622]  INFO -- : [after_worker_exit] worker=1 pid=46622 status=pid 46651 exit 0
I, [2017-05-21T16:35:10.707795 #46622]  INFO -- : [before_fork] worker=1 pid=46622
I, [2017-05-21T16:35:10.709798 #46653]  INFO -- : [after_fork] worker=1 pid=46653
I, [2017-05-21T16:35:10.710304 #46653]  INFO -- : Refreshing Gem list
I, [2017-05-21T16:35:10.828349 #46653]  INFO -- : [after_worker_ready] worker=1 pid=46653

マスターに SIGUSER2 (graceful restart)

  1. before_exec
  2. before_fork
  3. after_fork
  4. after_worker_ready

ログとしては Kernel#exec した (1行目の executing) 後に before_exec がきているが、これはコードでなぜかそういう順番になっているだけで本当の順番は逆 ( https://github.com/defunkt/unicorn/blob/v5.3.0/lib/unicorn/http_server.rb#L455 )。 あとは起動時のときと同じログになる。

I, [2017-05-21T15:45:51.255441 #43971]  INFO -- : executing ["/Users/yuta-iwama/src/github.com/ganmacs/playground/ruby/unicorn-hook/vendor/bundle/ruby/2.4.0/bin/unicorn", "-c", "unicorn.ru", {9=>#<Unicorn::TCPSrv:fd 9>}] (in /Users/yuta-iwama/src/github.com/ganmacs/playground/ruby/unicorn-hook)
I, [2017-05-21T15:45:51.255758 #43971]  INFO -- : [before_exec]
I, [2017-05-21T15:45:51.497657 #43971]  INFO -- : inherited addr=0.0.0.0:8080 fd=9
I, [2017-05-21T15:45:51.497814 #43971]  INFO -- : [before_fork] worker=#<Unicorn::Worker:0x007ffc888f3018>
I, [2017-05-21T15:45:51.498835 #43971]  INFO -- : [before_fork] worker=#<Unicorn::Worker:0x007ffc888f2ca8>
I, [2017-05-21T15:45:51.499294 #43971]  INFO -- : master process ready
I, [2017-05-21T15:45:51.500538 #43982]  INFO -- : [after_fork] worker=#<Unicorn::Worker:0x007ffc888f3018>
I, [2017-05-21T15:45:51.500818 #43983]  INFO -- : [after_fork] worker=#<Unicorn::Worker:0x007ffc888f2ca8>
I, [2017-05-21T15:45:51.501025 #43982]  INFO -- : Refreshing Gem list
I, [2017-05-21T15:45:51.501152 #43983]  INFO -- : Refreshing Gem list
I, [2017-05-21T15:45:51.602740 #43983]  INFO -- : [after_worker_ready] worker=#<Unicorn::Worker:0x007ffc888f2ca8>
I, [2017-05-21T15:45:51.604692 #43982]  INFO -- : [after_worker_ready] worker=#<Unicorn::Worker:0x007ffc888f3018>

その他

自分は before_exec と before_fork の呼ばれるタイミングの違いがよくわかっていなかった。 before_exec は restart などをする時に unicorn コマンドを内部で叩く前、before_fork は ワーカープロセスを fork する直前呼ばれるみたい。

簡単な UDP client / server in golang

UDP server と client の覚書。Ping を送って Pong と帰ってくるだけの雑なやつ。 特に難しいところもなくこの辺 みながらやると簡単にできる。

server 側

サーバ側では 127.0.0.1:8080 をlisten するようにしている。 何か送られてきたらそれを読んで、送ってきた相手に Pong と返すサーバ。

package main

import (
    "log"
    "net"
    "os"
)

func main() {
    udpAddr := &net.UDPAddr{
        IP:   net.ParseIP("127.0.0.1"),
        Port: 8080,
    }
    updLn, err := net.ListenUDP("udp", udpAddr)

    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }

    buf := make([]byte, 1024)
    log.Println("Starting udp server...")

    for {
        n, addr, err := updLn.ReadFromUDP(buf)
        if err != nil {
            log.Fatalln(err)
            os.Exit(1)
        }

        go func() {
            log.Printf("Reciving data: %s from %s", string(buf[:n]), addr.String())

            log.Printf("Sending data..")
            updLn.WriteTo([]byte("Pong"), addr)
            log.Printf("Complete Sending data..")
        }()
    }
}

client 側

サーバに対して Pong と送信して、その返信をlogに出力する。

package main

import (
    "log"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }
    defer conn.Close()

    n, err := conn.Write([]byte("Ping"))
    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }

    if len(buf) != n {
        log.Printf("data size is %d, but sent data size is %d", len(buf), n)
    }

    recvBuf := make([]byte, 1024)

    n, err = conn.Read(recvBuf)
    if err != nil {
        log.Fatalln(err)
        os.Exit(1)
    }

    log.Printf("Received data: %s", string(recvBuf[:n]))
}

参考

net - The Go Programming Language