DRYな備忘録

Don't Repeat Yourself.

rustc、cargo、rustup、rustfmtなどのインストール

前回のエントリでは横着してbrew install rustとしたけど、それだといろいろ無いっぽいので、toolchain一式入るように入れ直す。

まずはお掃除

% brew uninstall rust

無いことを確認。

% which rustc
% which cargo

公式ドキュメントに従ってインストール

www.rust-lang.org

% curl https://sh.rustup.rs -sSf | sh

以下、その出力全文

info: downloading installer

Welcome to Rust!

This will download and install the official compiler for the Rust programming
language, and its package manager, Cargo.

It will add the cargo, rustc, rustup and other commands to Cargo's bin
directory, located at:

  /Users/otiai10/.cargo/bin

This path will then be added to your PATH environment variable by modifying the
profile files located at:

  /Users/otiai10/.profile
  /Users/otiai10/.zprofile

You can uninstall at any time with rustup self uninstall and these changes will
be reverted.

Current installation options:

   default host triple: x86_64-apple-darwin
     default toolchain: stable
  modify PATH variable: yes

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
1

info: syncing channel updates for 'stable-x86_64-apple-darwin'
info: latest update on 2018-02-15, rust version 1.24.0 (4d90ac38c 2018-02-12)
info: downloading component 'rustc'
 38.5 MiB /  38.5 MiB (100 %)  27.1 MiB/s ETA:   0 s
info: downloading component 'rust-std'
 54.6 MiB /  54.6 MiB (100 %)  29.1 MiB/s ETA:   0 s
info: downloading component 'cargo'
info: downloading component 'rust-docs'
info: installing component 'rustc'
info: installing component 'rust-std'
info: installing component 'cargo'
info: installing component 'rust-docs'
info: default toolchain set to 'stable'

  stable installed - rustc 1.24.0 (4d90ac38c 2018-02-12)


Rust is installed now. Great!

To get started you need Cargo's bin directory ($HOME/.cargo/bin) in your PATH
environment variable. Next time you log in this will be done automatically.

To configure your current shell run source $HOME/.cargo/env

ということで、言われたとおり、$HOME/.cargo/binPATHに追加する。

% ls $HOME/.cargo/bin
cargo     cargo-fmt rls       rust-gdb  rust-lldb rustc     rustdoc   rustfmt   rustup
% echo 'export PATH=${HOME}/.cargo/bin:${PATH}' >> ~/.zshrc
% source ~/.zshrc
% which rustc
/Users/otiai10/.cargo/bin/rustc
% rustc --version
rustc 1.24.0 (4d90ac38c 2018-02-12)

いいですね。

cargo new してみる

% cd ~/proj/rust
% cargo new --help
% cargo new --bin
% cd hello
% cargo run
   Compiling hello v0.1.0 (file:///Users/otiai10/proj/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39 secs
     Running `target/debug/hello`
Hello, world!
%

いい感じに、前回記事の状態を再現できた。

DRY


以下追記 2018/02/27

error: toolchain 'beta-x86_64-apple-darwin' does not have the binary rustfmt

rustfmtが動かんかったので

% rustfmt --version
error: toolchain 'beta-x86_64-apple-darwin' does not have the binary `rustfmt`
% rustup update
# ↓ toolchain install は要りません。
# rustup toolchain install stable-x86_64-apple-darwin
% rustup component add rustfmt-preview
% rustfmt --version
0.3.8-nightly (346238f 2018-02-04)

はい。

【Go言語】複数並列goroutineの中でのSIGINTの捕捉

やりたいこと

  • 複数立ち上がっているgoroutineの中で、それぞれ独立にSIGINT捕捉できるだろうか?という素朴な疑問

ソースコード

  • 期待されるアウトプット「各goroutineのidとinterruptedというログが出る」
package main

import (
    "fmt"
    "os"
    "os/signal"
    "sync"
)

func main() {
    wg := new(sync.WaitGroup)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go spawn(i, wg)
    }
    wg.Wait()
    fmt.Println("hoge")
}

func spawn(id int, wg *sync.WaitGroup) {
    c := make(chan os.Signal, 1)
    defer close(c)
    signal.Notify(c, os.Interrupt)
    sig := <-c
    // ↑ここでwg.Doneをblockしているので、
    // Ctrl+cを押さないとDoneに至らず、
    // この親プロセスは終わらないです。
    fmt.Println(id, sig)
    wg.Done()
}

実行結果

% go run main.go
^C3 interrupt
1 interrupt
2 interrupt
5 interrupt
7 interrupt
0 interrupt
9 interrupt
8 interrupt
4 interrupt
6 interrupt
hoge
%
  • 理解した

雑感

  • ねむい

DRYな備忘録として

【Go言語】Ctrl+cなどによるSIGINTの捕捉とdeferの実行

問題

deferを使って後処理をしたい場合に、プロセスがCtrl+cなどSIGINTで中断されるとdeferしたものが発火しない。プロセス自体が中断されるのであたりまえなんだけども。

問題の再現

package main

import (
    "fmt"
    "time"
)

func main() {

    defer teardown()

    hoge()

    fmt.Println("終了")

}

func hoge() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

上記の実行

% go run main.go
0
1
2
3
4
終了
データのあとかたづけ
%

SIGINTの場合

% go run main.go
0
1
2
3
^Csignal: interrupt
%
# 「データのあとかたづけ」が発火しない

SIGINTの捕捉

package main

import (
    "fmt"
    "os"
    "os/signal"
    "time"
)

func main() {

    defer teardown()

    // {{{ ここから
    // SIGINTを待ち受けるchanを作成
    c := make(chan os.Signal, 1)
    // それを登録
    signal.Notify(c, os.Interrupt)
    // chanからの通知を受けるgoroutineを起動
    go func() {
        for sig := range c {
            fmt.Println("シグナル来た", sig)
            close(c)
            // SIGINTをchanで吸収しちゃってるので、
            // 明示的にExitする必要がある。
            os.Exit(130)
        }
    }()
    // }}} ここまで追加

    hoge()

    fmt.Println("終了")

}

func hoge() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

os.Exitを使い明示的にプロセスを終了しており、このままではdeferしたteardownは呼ばれないので、以下の行を追加した。

                for sig := range c {
                        fmt.Println("シグナル来た", sig)
                        close(c)
+                       teardown()
                        // SIGINTをchanで吸収しちゃってるので、
                        // 明示的にExitする必要がある。
                        os.Exit(130)

実行してCtrl+cすると、

% go run main.go
0
1
2
^Cシグナル来た interrupt
データのあとかたづけ
exit status 130
%

となっていい感じ。

deferのためのリファクタリング

teardownの呼び出しが2箇所に書かなきゃいけないのはあまり筋がよくないので、defer一発でどうにかしたほうがシュッとすると思う。「hogeが終わった」というメッセージを伝達するdoneというようなチャンネルを経由して、SIGINTを受けるチャンネルと並列で扱うのがいいんではないか。終了コードが0になってしまうが、まあいっか。

package main

import (
    "fmt"
    "os"
    "os/signal"
    "time"
)

func main() {

    defer teardown()

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)

    done := make(chan error, 1)
    go hoge(done)

    select {
    case sig := <-c:
        fmt.Println("シグナル来た:", sig)
        /*
         teardown中に再度SIGINTが来る場合を考慮し、
         send on closed channelのpanicを避ける。
       */
        // close(c)
        return
    case err := <-done:
        fmt.Println("hogeの終了:", err)
    }

    fmt.Println("終了")

}

func hoge(done chan<- error) {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
    done <- nil
    close(done)
}

func teardown() {
    fmt.Println("データのあとかたづけ")
}

非常にGoっぽいコードになった。

参考

DRYな備忘録として

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

【メモ】aws-sdk-goを使ったSecurityGroupの作成

tl;dr

docs.aws.amazon.com

動くコード

package main

import (
    "fmt"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/ec2"
)

func main() {

    sess := session.Must(session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
        Config:            aws.Config{Region: aws.String("us-west-1")},
    }))
    client := ec2.New(sess)

    // 名前がコンフリクトするので同名のものがあれば削除しておく
    _, err := client.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
        GroupName: aws.String("test-sdk-go"),
    })
    fmt.Println("DELETE:", err)

    // SGの作成
    group, err := client.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{
        GroupName:   aws.String("test-sdk-go"),
        Description: aws.String("Foo Bar Baz"),
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("GROUP: %v\n", group)

    // ルールの追加
    _, err = client.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
        GroupId: group.GroupId,
        IpPermissions: []*ec2.IpPermission{
            &ec2.IpPermission{
                IpRanges:   []*ec2.IpRange{&ec2.IpRange{CidrIp: aws.String("0.0.0.0/0")}},
                IpProtocol: aws.String("tcp"),
                FromPort:   aws.Int64(22),
                ToPort:     aws.Int64(22),
            },
        },
    })
    fmt.Println(err)
}

確認

このSGを持ったEC2インスタンスを立ち上げ、sshの疎通が確認できた。

ストレージサービスのBucket存在確認メモ

S3

s3://hoge

aws s3api wait bucket-exists --bucket hoge
echo $? # 255
  • 存在するが権限が無い
    • Waiter BucketExists failed: Forbidden
  • 謎ケース(要調査: このへん?
    • Waiter BucketExists failed: Bad Request
    • 再現
      • aws s3api wait bucket-exists --bucket xxxyyyzzz
  • 存在しない
    • Waiter BucketExists failed: Max attempts exceeded ← 遅い!

Cloud Storage

gs://hoge

gsutil ls hoge
echo $? # 1
  • 存在するが権限が無い
    • AccessDeniedException: 403 otiai10@gmail.com does not have storage.objects.list access to xxxyyyzzz.
  • 存在しない
    • BucketNotFoundException: 404 gs://wwwxxxyyyzzz bucket does not exist. 早い

// TODO

ほかの

【Go言語】bufio.Scannerの静かな突然死に3営業日ハマった

tl;dr

  • bufio.ScannerScanが終わってもErrはちゃんと見ましょう。以上。

問題

継続的に書き込みのあるTCP接続からの読み込みを、bufio.Scannerをつかって華麗に逐次読み込みしていた。が、どうやらそれが静かに切断され、クライアント(こちら)側のローカルでの処理が先走っているように見える挙動があった。

res, err := ExecRemoteLongScript()
defer res.Body.Close()

scanner := bufio.NewScanner(res.Body)
for scanner.Scan() {
    // do something for scanner.Bytes()
}

fmt.Println("done")

みたいなので、RemoteScript(概念)が完全に終了していないのに、クライアント(こちら)側でfmt.Println("done")に到達してしまっている。

再現と原因

すごく色々なレイヤーのある話だったので、色々調査したんですが、最終的に再現できたのはこれでした。

package main

import (
    "bufio"
    "fmt"
    "io"
)

func dummyExecRemoteScript() io.Reader {
    r, w := io.Pipe()
    text := "Test"
    go func() {
        for i := 0; i < 20; i++ {
            text += text
            w.Write([]byte(text + "\n"))
        }
        r.Close()
    }()
    return r
}

func main() {

    body := dummyExecRemoteScript()
    scanner := bufio.NewScanner(body)

    i := 0
    for scanner.Scan() {
        fmt.Println(i, len(scanner.Bytes()))
        i++
    }

    fmt.Println("done")
}

0から19までナンバリングされたレスポンス断片が出力されるはずなんですが、なぜか12番までしか出力されません https://play.golang.org/p/DDeu8kfrU5D。なんでやろなーと思って( ゚д゚)ハッ!っと気づいたのが、bufio.Scanner.Errです。

ということで、

   for scanner.Scan() {
        fmt.Println(i, len(scanner.Bytes()))
        i++
    }
+   fmt.Println("Err", scanner.Err())

    fmt.Println("done")

としたら、これがビンゴでした。 https://play.golang.org/p/HlR7SpCTSkc

0 8
1 16
2 32
3 64
4 128
5 256
6 512
7 1024
8 2048
9 4096
10 8192
11 16384
12 32768
Err bufio.Scanner: token too long
done

Scanning stops unrecoverably at EOF, the first I/O error, or a token too large to fit in the buffer. When a scan stops, the reader may have advanced arbitrarily far past the last token. Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.
https://golang.org/pkg/bufio/#Scanner

解決

上記の通り、bufio.Scannerではなく、bufio.Readerを使って同じ内容を書き直します。

func main() {

    body := dummyExecRemoteScript()
    reader := bufio.NewReader(body)

    i := 0
    for {
        b, err := reader.ReadBytes('\n')
        if err != nil {
            if err == io.EOF || err == io.ErrClosedPipe {
                break
            }
            panic(err)
        }
        fmt.Println(i, len(b))
        i++
    }

    fmt.Println("done")
}

https://play.golang.org/p/vmctCOfK9nM

まとめ

  • bufio.Scannerは便利だけど、Scanが終わってもErrは必ず見るようにしましょう

自戒

DRYな備忘録として

追記 ⛳

コードゴルフできるところみつけたんでちょっと変えた

- i := 0
-  for {
+   for i := 0; ; i++ {
        b, err := reader.ReadBytes('\n')
        if err != nil {
            if err == io.EOF || err == io.ErrClosedPipe {
                break
            }
            panic(err)
        }
        fmt.Println(i, len(b))
-      i++
    }

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]

Webプログラマから見た「CWL」の功績と罪過

このエントリは、CWL Advent Calendar 2017 - Qiitaの25日目です。

背景

f:id:otiai10:20171225123808p:plain

誰がエモ芸人や

CWL is 何?

異なる処理系の間で同一のワークフローの実行を可能にするために、ワークフローの内容を記述し、実行系の間で交換するための標準形式の策定を目的として Common Workflow Language (CWL) プロジェクトが発足した。

すげーざっくり言うと、バイオインフォのゲノム解析の手順書を統一規格で書こうぜ、という感じのブツです。

実際どうやって使うねん

% cwltool 1st-tool.cwl echo-job.yml

サンプルで、1st-tool.cwlと書かれているものが、いわゆるワークフローの定義であり、echo-job.ymlというのはパラメータとかサンプルとか呼ばれたりするらしいです。これ、なんで分割されているかというと、

世界のどこかで、とっても良い感じのワークフローを提唱したひとがいたとして、

f:id:otiai10:20171225131640p:plain

同じワークフローで別のサンプルで試してみたいと思うひとが出てきます。

f:id:otiai10:20171225132547p:plain

ということで、ワークフローの記述実際のパラメータは分割されている必要があります。

CWLの初期衝動

ところが、論文に載っている手順に従っても、それが「再現」できないという問題が起きます。

f:id:otiai10:20171225133953p:plain

この点を解決しようと立ち上がったのが、CWLです(雑)。Repeat/Reproduce/Replicate/Reuseについては先述のinutanoさんのエントリが詳しいです。

CWLの実行エンジン

入力としてワークフロー(.cwl)とパラメータ(.jsonなど)を受取り、記載されたワークフローを実行するソフトウェアが必要です。これを公式では「implementation」、日本のコミュニティでは「エンジン」と呼んだりしています。

私もいちおうソフトウェアデベロッパーの端くれとして、manabuishii先輩と一緒に、Go言語でCWLのデコーダとimplementationを実装中です。

CWLの競合

同様の問題意識を持ち始まったプロジェクトは他にも複数あります。

// TODO: 競合のリストがここにくる
- WDLとか?
- nextflowとか?

CWLのここがよい

第1に、バイオインフォが抱える問題(とは言っても僕自身この分野の新参者なのであまり歴史的な背景とか精通してるわけじゃないんですけど)にスポットライトを当て、それを解決しようと立ち上がったのは、素晴らしいことかと思われます。

第2に、国際学会やカンファレンスで精力的にディスカッションやハッカソンをしたり、GitHubで仕様を管理したり、コミュニティ形成に力を入れており、提言やPull-Reqにたいしてもかなり柔軟に取り入れたりしているのは普及したポイントかなと思います。実際、我々が作成中のGo言語実装:yacleも実装例のひとつとして早くもリストされています。

第3に、実際多くの場面で使われていること。国際的に、ワークフローの共有といったらCWLファイルつくっとくかぁ〜みたいな雰囲気がすでに漂っており、また公開されたCWLで記述されたワークフローをクラウド上で動かすウェブサービスなども出てきております。

以上が、CWLもなかなかいいじゃん、というProsになります。

.....

....

...

..

.













f:id:otiai10:20091121210556j:plain

CWLのここが嫌い

そういう上っ面じゃねえんだ、アドベントカレンダーはよぉ!こっからが本題だ。

Webプログラマから見たCWLの罪過、罪過?むずかしい言葉使ってんじゃねえよ!嫌いなところについて!うらみつらみを列挙して行こうと思います。

  1. まず名前が嫌い
    • CommonWorkflowLanguageって言うからにはDSLかな?って思うじゃないですか?違うんですよこれ単なるYAML or JSONなんですよ
    • あと何「Common」って!すべてをかいけつするソフトウェアはなにもかいけつしませんよ?
  2. syntaxが嫌い
  3. Data Modelが嫌い
    • みてくださいこの型array<CommandInputParameter> | map<CommandInputParameter.id, CommandInputParameter.type> | map<CommandInputParameter.id, CommandInputParameter> って!
      • つまり[]Model|map[string]string|map[string]Modelってことです
        • これCとかGoでどうやってstructにデコードしますかね
  4. 表現方法が嫌い
    • 百歩譲って、ある言語でデコードがたいへんなのはその言語のせいだとしましょう。しかしながら、ある1つのことを表現するのに、多くの表現方法があることは、規格として正しいあり方なんでしょうか。
  5. 表現方法が嫌い・その2
    • なんやねん!その2て!!!!!
    • Exrepssionsというclauseがあるんですけど、これがマジやっかい
    • これねーJavaScriptなんですよ!!(しかもES5!)
      • ワークフロー定義の内部がJS要求するっていうのならまだ分かるんですが、ワークフローの記述の表現にある特定のランタイムを要求するのって筋悪すぎじゃないですか
      • しかも、あたりまえだけどめっちゃくちゃいろんなこと出来るので、ワークフロー記述規格としての治安が乱れまくる
  6. 哲学が嫌い
    • 上にも書いたように、かなりいろいろディスカッションして、だいたいのものは取り入れちゃってるので、仕様がどんどん肥大化していって、エンジンの実装者が「どの機能をサポートしないべきか」とかまず考えなきゃいけないレベルになっている
    • "All is nothing" を地で行ってて最高にクール
  7. そして、すでにけっこう使われているのが何よりも嫌
    • プラットフォームつくりました!という話しても「それ、CWL食える?」みたいな質問が普通に飛んでくるぐらいには普及してしまっている
    • 過渡期に、パイ取った感じある
    • もう避けては通れない






f:id:otiai10:20091009172516j:plain

提言と提案

  • ワークフローを記述する規格は
    • DSLではなく、一般的なデータ構造フォーマットであるべきだ
    • 静的型付けの厳しい言語であっても、多くの一般的な言語でデコードが容易であるべきだ
      • 多少冗長であっても、厳格な構造をしているべきで、型ゆるゆるでは嫌だ
    • それ自身の解釈に特定のランタイムを要求するべきではない
    • 表現を絞り、 具体的に解決したい問題 をメッセージとして発信し続ける
  • 我々はバイオインフォマティシャンは*1
    • CWL+cwltoolでもいいし、何かほかの方法でもいいが、自分が直面している問題を解決することにフォーカスしたワークフローの記述&実行を心がけるべき
    • そういった「ぼくのかんがえたさいきょうのわーくふろー大会」をやりましょう!
      • painを同じくする仲間が見つかれば、たのしいですね

f:id:otiai10:20171225182717j:plain

雑感

  • ちょっと遅くなりましたが、とびきりエモいやつを書きました
  • 大口を叩きましたが、今後ともご指導ご鞭撻のほど、よろしくお願い致します

DRY

*1:や、俺はただのデベロッパであってバイオインフォマティシャンではないが