作者別: Jun Mukai

GoでどれぐらいDIするか

https://recruit-tech.co.jp/blog/2017/12/11/go_dependency_injection/

この記事をみた。ここからは個人的な雑感だけど、結論からいえばGoではここまでのDIは必要ないことがおおいとおもう。

DIがなぜ必要なのかというと、テストをしたいからだ。しかもたいていはユニットテストだとおもう。UserServiceとUserRepositoryみたいなのがあったとして、UserServiceの挙動をテストするために、UserRepositoryがある特定のふるまいをしたときのことをテストしたい、みたいなやつ。

Goのばあい、ユニットテストは元のコードとおなじパッケージに存在していて、テストコードは内部構造もしっているし直接さわれる。だからこんなに頑張ってビルダをそとからわたさなくてもいいことがおおい。

具体的には、たとえば

package user

type Service struct {
  repo Repo
}

func NewService(opts... ServiceOptions) *Service {
  return &Service{repo: initRepo(opts...)}
}

みたいなservice.goファイルがあるとすると、service_test.goファイルでは、

package user

func TestService(t *testing.T) {
  s := &Service{repo: mockRepo}
  ...
}

などのようにNewServiceをつかわずに初期化していい。

もちろんこのとき、初期化関数(NewServiceなど)じたいのテストができないという難点がある。でもこうした初期化関数はほんとうにインスタンスをつくるだけのような自明なものにすれば、テストする必要性はそれほどない。

もちろん、ほかにいろんなパターンがあるので、DIする必要がまったくないということはない。たとえば、インテグレーションテストの環境のために外部サービスをきりかえる、ということはある。ほんとうは外部のストレージサービスに接続するが、テスト用にはオンメモリな実装をつくって、テストデータでうごかしたいときとか。そういうばあいには、インタフェースをパラメータとしてうけとるような構造をつくってDIすることにはなるとおもう(ただ、ビルダが必要になるかはわからない。インスタンスをつくって渡すのでいいんじゃないかなあ)。ほかにも実際の実装モジュールがいろいろあって設定できりかえたいとか、DIしたいパターンはおおいとはおもう。

とはいえ、そのようにきりわけられるべきレイヤはそこまで大量にはならないので、こういうJavaみたいな壮大なDIフレームワークがほしいことは、あまりないんじゃないか。ふつうにmainでわたすとか、カスタマイズしたいときだけオプショナルなパラメータを指定するとか、そういうのであまりこまらないと、個人的にはおもう。といった雑感です。

豆腐づくり

料理ネタ。にがりと大豆があれば豆腐は作れるらしい、という話をきいて、つくってみた。

比較的簡易なのは、いわゆる寄せ豆腐(汲み取り豆腐)というやつで、これは雑に言うと、

  1. 豆乳をあたためる(75-80℃程度)
  2. にがりをまぜ、少しだけ撹拌する
  3. 20-30分ほど放置してから、布巾をしたザルの上にあげ、水分とにがりが流れ出るのを待つ

というだけでできる。にがりは日本食スーパーで入手できる……という話だったが、Marukaiになかったので、アマゾンから購入した。

IMG_20170917_194233.jpg

これはスーパーで買ってきた豆乳で作ったもの。豆乳は成分無調整がよいとされるが、成分調整って砂糖とかで甘くしているだけという話らしいので、成分調整豆乳でもできるとは思う(味は変になるかもだけど)。

次は豆からに挑戦、ということで、大豆から作る場合は、まず豆乳を作ってから上の行程を経ることになる。

IMG_20170924_013457.jpg

大豆。中国語では黄豆というらしい。これを水につけて一晩とかおく。水を吸ってふくらむ。ふくらんだら水と大豆をあわせ、ミキサーで粉砕。

IMG_20170924_192142.jpg

これは粉砕後。大豆はサポニンが豊富なのでめちゃくちゃ泡だらけになる。これを火にかけて加熱。沸騰後弱火で10分とか。で、熱々のところを布巾にとり、がんばって絞る。とっても熱い。なんでこんなことやってるのかなと思う。

IMG_20170924_194306.jpg

しぼった豆乳は少し冷まして、75-80℃程度になるまでふたたび加熱。

IMG_20170924_200953.jpg

にがりをいれて放置。

IMG_20170924_201010.jpg

IMG_20170924_204417.jpg

放置のあと、布巾にふたたび汲み取り。

IMG_20170924_214032.jpg

できました。これはちょっとにがりが残っていてあまりうまくなかった。

豆乳をしぼる行程がひたすらめんどう&熱いなど難点があり、ぶっちゃけ買ってきた豆乳でもいいのではないか、という疑念は拭えないものの、科学実験ぽくてそれなりにおもしろさがあります。味はよいです。しかし買ってくる場合とのコスパを考えると……や、考えたら負けか。ベイエリアはサンノゼ豆腐など地元の豆腐屋があるので、買ってくる豆腐には事欠かないのでした。

そういうわけで豆腐でした。

Googleに入社して10年たった

2007年10月1日に入社したので、10年が経過したことになる。10年! ずいぶんな長さだなあ。すっかりグーグラーとして慣れてしまったような気がする。渋谷ではたらいていたころ、まさか自分がシリコンバレーに移住するとは微塵も思ってもいなかった。

それなりにいろんなチームでの仕事もしてきた。はじめはモバイル検索のことをやってて(モバイルといってもフィーチャーフォン向けの検索ですが)、Google日本語入力、ChromeOS。一瞬のAndroid業を経て、いまはクラウド。

ひとくちにグーグルといってもそれぞれ仕事の内容や扱うテクノロジは違っていて、検索のころはmapreduceを書いてデータパイプラインをうんぬんしていたりウェブページや自然言語を相手にしていたわけだし、Google日本語入力は多少の自然言語処理と動作プラットフォームの知識、ChromeOSのころはおもにUIでウィンドウマネージャやウィンドウシステムやモダンなGUIシステムの話になったし、クラウドではAPIとかサービス管理。むだにいろんなことをやってる。

いろいろやってきたけど個別の成果は大したことなくて、まあたいして出世もしていないけれど、いちおう今のところなんとかかんとか生き延びている。

2012年9月にアメリカに移住してきたので、そこからすでに5年。ていうかグーグルの東京オフィスでの勤務期間よりアメリカのほうが長かったりして、これもあまり実感もないものだ。英語がうまくなった実感もまったくないな……。

そういえばメインの仕事がオープンソースになって久しい。ChromeもOSSプライマリだし、今のプロジェクトもgithubで仕事してる。数えてみれば、自分のグーグル内キャリアのうち半分以上はオープンソースで給料をもらっているわけで、自分がそういう立場になりえるとは学生時代とかは思ってもみなかったなと感慨にふける。これはもちろん時代の変化もある。いまどき、企業が自社の製品や成果の一部をOSSとしてリリースするなんて、ふつうのことだし。

まあ一種の記念カキコなのでオチとかはないですが、そんなかんじでぼちぼちやっております。

GoとBazel

最近、メインの仕事ではGoで書かれたサーバをいじっていて、ビルドするのにBazelを使っている。使っているとBazelなかなかいいやつだな、と好感を持つのだけど、でもGoの場合、標準ツールとの相性というのがなあ、という微妙さもある。

その辺の個人的な雑感です。

Bazelとは

Googleが公開しているオープンソースのビルドシステム。もともと社内に存在していたビルドシステムのオープンソース版という位置づけ。社内版と何がどう違うのかはよく知らない。BUILDという名前のファイルにビルドレシピを書く。構文はそこそこ簡潔で、わりと宣言的に書ける。ちなみに構文はPythonのサブセットなのだけど、Bazel自体はJavaで書かれている。なぜ……いやまあそれはべつにいい。

社内版ビルドシステムと構文が同じなので、社内版プロダクトがわりとそのまま(もしくは構文解析後のノードの簡単な書き換えによって)オープンソース化できる、とか、両者の行き来が楽になってうれしい、といったあたりがオープンソース化した存在意義かと思われる。

Bazelはサーバプロセスとコマンドラインツールに分かれていて、サーバプロセスは適宜自動的に起動され、依存グラフの情報などを保存してくれる構成になっている。サーバからはイベントデータが取ってこれて、これをフックして各種のバックエンド拡張を行えているんじゃないかなとおもう。Bazelそれ自体には、グーグルの巨大な社内レポジトリを扱うほどの機能はビルトインされていないと思うけれど、そうやって社内依存度の高い部分は切り離す設計になっているとおもわれる。

GoでBazelを使う意味

ところで、Goは標準のツールチェインがよくできていて、ビルドとかの面倒を見てくれる。go getとかgo buildとかすればいいわけで、なぜBazelを使うのか?というのはあまり自明ではない。実際、チームのみんなもBazelが好きだから使いたいというのでもなく、実際には必要だから使っている、といったところではないかと思う。

WORKSPACEと依存関係の固定

Bazelにはワークスペースという概念があって、このワークスペースに依存する外部ライブラリのレポジトリを指定できる。Bazelはビルドレシピを解析して、依存する先が外部ライブラリになっていると、当該のgitレポジトリを自分でpullしてリビジョンを維持してくれる。リビジョンを指定しておけばそのリビジョンに固定される。

ビルドシステムが外部の依存ライブラリのリビジョンも管理している、というのは一見するとちょっとキモい構造なのだけど、いい面もある。というのは、新しいリビジョンへの同期などがビルドの作業の途中の、依存性として表現されるからだ。ようは、誰かがワークスペースを編集したり、あるいは自分のローカルな作業ブランチの一部が古いワークスペースになっていても、開発者はあまり気にせず、bazel buildするだけで勝手に現在の環境に更新しつつビルドしてくれて、問題が顕在化しない。

実際、repoやgclientを使っていると、git bisectがそこそこ手間なのだけど、ビルドプロセスと外部依存ライブラリの管理が統合されていると、そこの手間が全然ない。

Goの場合、いまはvendorディレクトリもあるし、godepみたいなツールもある。でもまあ、なんだかんだでビルドプロセスと一緒になっているのは便利だなと思う。

ちなみにbazelの場合は外部ライブラリにもBUILDファイルが必要になるので、下手をするとぜんぶ手書きする必要があるのだが、bazel用にgazelleというツールがあって、Goプログラムを解析してBUILDファイルを自動生成してくれるので、ふつうはそういう手間は存在しない。

コード生成

Bazelは一般的なビルドツールなので、Go以外のコンパイルもできるし、コード生成もできる。仕事のプロダクトではProtocol Buffers / gRPCを多用しているので、そういうコード生成が大量発生しているし、自前のコード生成ツールも使ったりしている。

Goでコード生成というとgo generateコマンドがあるのだけど、このコマンドはさすがにちょっとシンプルすぎると思う。というか、用途と想定されるワークフローが違うということだろうか。

go generateは自動的には動作しない。レポジトリには生成されたGoのファイルをチェックインすることが前提になっていて、開発者は必要に応じて手動でgo generateを走らせて再生成してチェックインしてね、という仕組みだ。なのでgo getなどは.goファイルだけを相手にすればいいし、コードジェネレータに対する依存関係とか、ややこしい問題が発生しない。

しかし正直に言って、ジェネレータが生成したコードとかチェックインしたくない。生成物に依存する入力ファイルや生成ツールの依存関係もわかっていることなので、どういうときにどのファイルを生成するか、というのはビルドプロセスの一部であってほしい。

Bazelはごくふつうの汎用ビルドツールなので、この辺は(ルールをちゃんと書けば)きちんとやってくれる。当たり前のことですがね……。とくにprotobufまわりのサポートは充実しているので、まあ、楽。

GoでBazelを使うと困るところ

そういうわけでなかなかいいのだけれど、困っている面もあって、具体的にいうと、ふつうのgoコマンドはまともに動かない。

わたしがかかわっているプロダクトは、最終的にバイナリをつくってdockerイメージとかを作るので、まあいいのかもしれない。でもこれがライブラリとか外部ツールだった場合、ふつうみんなはgo getしたら使える、というのを想定するだろう。go getだけじゃ動かない、なぜならコード生成が……とか言われたら、常識的にいって何らかの罵声が飛んでくることであろう。

それだけじゃない。いろんな外部ツールが、goコマンドの動作やその想定するgopath環境というものを前提につくられている。ようするにリントチェッカやコード書き換えツール(gofmt、gosimpleなど)が動かない。テスト、raceチェッカ、ベンチマーク、などは動くが、なぜかBazelのテストはコードカバレッジの情報を取得できないというバグがあるので、現状ではカバレッジを取りたかったらgo testする必要がある。

自分たちのプロジェクトでは、Bazelがとってきた依存ライブラリへのシンボリックリンクをvendorの下に展開するスクリプトがあるけれど、まあこんなのはハックであって、何もしなくてもうまいこと動いてくれたらいいのにな、とは思う。

まとめ

あくまでも雑感なので、とくに有意義な結論はないのだが、BazelとGoがうまくまとまるような、いい方法はないものかな、と思っている。

ひとつにはBazelがワークスペース(~/.cache下の領域)にGOPATHと同じような環境を構築できたら良いのかもしれない。そうすればもっと簡単にリントツールとか外部ツールを動かしたりできる。

ライブラリなどの用途を考えた場合とコード生成の成果物はチェックインしたくない、というのを両立させるには、コード生成等の処理を済ませたあとのソースコードを配布する場所がやっぱりあるとうれしい。CPANとかgemsとかnpmとか……いや、それならgopkg.inでいいのかも。ビルドツール側がコード生成やvendorディレクトリの作成、さらにはパッケージ参照なども書き換えた上で別ブランチにマージでき、そのブランチをgopkg.inか、あるいは別のURLから参照できるようにすればいいのかなと思う。kubernetesはそういうことをやってそうな気配がある。この辺はCI/CDまわりで頑張るべきか。

Goのツールチェインから歩み寄る方向性はあるだろうか? go buildやgo testにフックがあるといろいろ捗るだろうな、とは思う。ただ、どういうフックがどう設定できたらいいのか?go getとかでどういうフックがどう動くべきで、レポジトリにどう設定できるのか?とか、考え出すときりがない。

ローカルなワークスペースについては、vendorディレクトリが解決してくれた感はあるが、vendorディレクトリ以下の管理はそんなに自明ではない。godepが標準ツールと統合されたりするとうれしいだろうか?


なんだかんだ書いたが、BazelとGoはちゃんと動くような整備がかなり整っている方だと思う。cgoがあってもコンパイルできるし、go_proto_libraryとかは全然心配しなくていいし、カバレッジがとれないのは困るけどrace testingが動くのはうれしい。総じてなかなかよくできている。このへん見てるとyuguiさんが尽力した後が垣間見えて、お世話になってるなあと思ったりしている。

ネットで話題になってた影絵アートを見てきた

(同じ内容で投稿してたつもりが間違えて site page として公開してたみたいなので、ブログエントリとして再公開します。以下もとと同文)

少し前にツイッターかなにかで見たと思うんだけど、街角のいろんなブツ(街灯とかベンチとか柵とか)の下に影っぽいかわいい絵をつけたやつ、ってのがあったんだけど、

IMG_20170910_161121

こういうやつ。

これよくみたらレッドウッドシティなのね。ご近所さんじゃん!てことで行ってみてきた。

そのまえに軽く検索してみたところ、レッドウッドシティ(グーグル本社等のあるマウンテンビュー市とサンフランシスコ空港の中間あたり)のダウンタウンのあちこちにこういうshadow artができたらしい。公式サイトに詳細な場所の情報とかがあるけど、できたのは2015年末というから1年半ほど前。そこそこ前なので、いまごろになってわざわざ写真をとってるのは自分たちだけだった。

ダウンタウンについてから地図をみながらあっちこっちを歩いて回って、shadow artを探すのはなかなか楽しくて、1時間か1時間半ぐらいダウンタウンをうろうろしてコンプしました。ちょっと暑かったけど。

写真はこちらのリンクにまとめておきましたが、いくつかピックアップ。

IMG_20170910_163603
ハートの出てるロボット。かわいい
IMG_20170910_165129
box社ヘッドクォーター前のやつ。地図と微妙に位置がずれてて戸惑った
IMG_20170910_164251
ベンチの犬。ベンチにカップルが座ってて、写真を撮ろうとしたらよけてくれた(が足が写っている)
IMG_20170910_164605
位置がズレてるように見えて、その後レイアウトが変更になったのかな?と思ったが、もともとこういうものらしい。自転車を誰かが留めてくれて完成するってことかも

まあそんだけなんですが、近隣住民の皆さんとか行ってみるとなかなか楽しいと思います。

牛タン料理

唐突に料理ブログ。

牛タンの塊を買った。

このへんだとアジア系スーパーに行けば普通にあるんじゃねえの?と思ったんだけど、意外とそうでもなかった。韓国系スーパーに行ったら薄切り牛タンがあったので、精肉コーナーの人にお願いして切る前の塊をゲット。巨大な塊で$50ぐらい。

IMG_20170624_201309
会計のときレジのおねえさんが一瞬ヒッてなってた

でまあよくわからないのだけど、塊なのでひとまず解凍するか、とその辺に放置(なお皮を剥いたりするには半解凍状態のほうがいいらしい、とあとで知った)。

小林銅蟲のブログに「ふつうの家の設備では無理」と書いてあることに気づいてびびったけど、じっさい舌の表面は想像以上にざらついていて硬いため、素手だとつらそう。ゴム手袋があれば、あとはまあふつうの包丁でなんとかなった。動画検索して見よう見まね。

IMG_20170625_132808

IMG_20170625_133116

IMG_20170625_141031

とりあえず「下のビロビロ」「根元」「中間」「舌先」の4パーツに分解。根元の部分を低温調理で。63度で約4時間。

IMG_20170625_142203

IMG_20170625_190505オーブンで焼き1分といった風情。そこそこの硬さは残りつつ、しかしかんたんに噛み切れるといったちょうどいい塩梅でうまい。

IMG_20170625_194407

IMG_20170625_195140

中間部分は低温調理がめんどうだったので適当に切って焼いた。焼肉用のように薄切りはできないので、雑な切り方で適当に焼いただけ。

IMG_20170627_192952

肉厚で食いづらいけど、まあ焼肉程度にうまい。つまり、うまい。

残りの部分はシチューにして煮込むといいという話だったので、圧力鍋でタンシチュー。調べて出てきたレシピにより、まず表面を焼き固めた(油がすんごいはねて死にそうになった)。

IMG_20170629_190840

んで水と赤ワイン(400円ぐらいの超安いやつ)で圧力をかけて20分。そこにbeef brothのもとを投入。シャバシャバでなんか違うな……と小麦粉を混ぜてみたが、見た目としてはけっこう間違ったものができた。

IMG_20170629_204152

味はうまかった。タン肉、圧力かけて煮るとフワフワな食感になる。肉塊はもっと大きいほうがいい感じであったかな。

個人用のインスタンスへ安全につなぐ

自分個人の作業用にも、GCEのインスタンスを作ってそこにアクセスして作業している。で、作業用の物体がウェブサーバであるということがよくある。たとえばjupyter notebookとか、R Studioとか。いま自分はeditbookというウェブベースのエディタで遊んでいるが、これもそういうやつ。開発環境的な操作をするもの以外でも、ダッシュボードの表示とかで一時的にウェブサーバを上げるってことはあると思う。

こういう連中は作業環境がローカルホストであることが前提になってるので、アクセス制限はたいていとくにかかってない(127.0.0.1にbindすることはよくあるが、それだと外部から繋げないのでアクセスを全開放することが多いだろう)。で、そうなると世界中の誰でもアクセスできて嫌だな、とちょっと思う。

実際にはephemeral IPのインスタンスなのでIPアドレスがピンポイントにバレることは少ないし、それだけで不安なら簡単なパスワード認証でも入れればいっか、ということはよくある。でもまあもうちょっとなんとかしたほうがいいのかな、という気分もある。

もちろん真面目に頑張るための方法はいくらでもあり、企業などきちんとできる環境ならきちんとすべきだろう。でもいまの自分のは個人用途なので、適当なサーバで頑張らずにそこそこマシにしたい。どうしたらいいだろう。

oauth2_proxy

oauth2_proxyはoauth2による認証だけを行うプロキシだ(openidじゃなくていいのかな? まあいいのかな……)。これによりアクセスしたユーザのメールアドレスがあらかじめ指定したやつだったときだけアクセスを許可できる。まあ似たようなのはほかにもあると思うが、こいつを自分用のサーバの手前に置いておくことで気軽にアクセスを制限できる。

ただeditbookはwebsocketを使っていて、oauth2_proxyはwebsocketがちゃんと動かなかったので困ったが、手元で適当なパッチを書いて動かしてみたら動いたのでなんとかなっている。たぶんそのうちPRを送ると思う。

gcloud dns

自分の使っているインスタンスは立ち上げるとIPアドレスが毎回変わるのでちょっと困る、という問題がある。第一に、Googleではoauth2用のコールバックアドレスはあらかじめ登録しておく必要があり、IPアドレスではうまくいかない。後述するようにhttpsでアクセスするためにもホスト名は持っておきたい。

一つの方法としては、固定IPのインスタンスにするという手がある。あるいはephemeralなIPでも、インスタンスを上げっぱなしにしておけばだいたいオッケーだったりするだろう。プロキシ用途に小さいインスタンスを上げておいてバックエンド設定をいじる手もある。

今回はもうちょっと別な技を使うことにした。Google Cloudにはgcloudというコマンドラインツールがあり(Amazonにもawsコマンドというのがありますが似たようなものです)、これを使ってDNS APIにアクセスできる。

またGCEインスタンスの情報(インスタンス名とか、external IPとか)というのはmetadataのAPIを使うと比較的簡単に取得できる。

なのでこの辺を組み合わせた適当なシェルスクリプトを書いて、gcloud dnsコマンドを発行して外部IPとホスト名を結びつけてみた。さらにこれを起動スクリプト化してみたので、インスタンスの起動時にホスト登録して終了時に登録解除もできるようになった。

Let’s Encrypt

ホスト名が定まったので、https接続にして通信経路を暗号化しておく。oauth2_proxyはTLSをサポートしているので認証鍵を作ればよく、いまではLet’s Encryptを使えば簡単に、かつ無料で暗号化ができる。いい時代になりましたね。

関連技術

Google CloudだとIdentity Aware Proxy(IAP)というものがあって、どうもこいつを設定したらよろしくやってくれる気配が感じられた。ただ金額的にもいろいろある機能的にも、ちょっと個人が雑に使うというものではない雰囲気が出てきている。企業とかが開発者向けの環境を考える場合には検討するといいと思う。

DNSまわりはGoogle Cloud限定の話だが、AmazonもRoute53があるしawsコマンドもあるので似たようなことができるはず。まあこんな変なことをしなくても楽にやる手は他にもあるはず。

プロキシについてはもっと便利で使いやすいやつがあるのかもしれないですがよく知りません。

まとめ

個人が自分一人で使う目的として、雑に立てたクラウド上のインスタンスへのウェブアクセスをどうマシにするか、というのをいくつかやってみた。まあまあのものはできたと思っています。

How to buy a Nintendo Switch in US

とある情弱の記録である。


そもそもSwitchは事前予約の日程すら日本と海外では違っていた。Switch発表会は2017年1月13日、日本での予約開始は同21日とかだった。が、なぜか海外ではこの日本の予約開始に先立ち、というか発表直後ぐらいから予約を受け付けていたようなのだ。えええーマジすか。

しかもUSの予約分は数時間ではけた、という報道であり、日本予約開始日というのは遅いという表現では足りないレベル。何もできない。

でもこの時点での僕は正直あまく見ていて、任天堂信者というやつらがヤバイわけだが小売店における当日販売分を買えるだろうとは思っていた。でもそんなに情報収集してなかった。ちと本業が忙しかったし、舐めてたし、英語の情報サイトを巡回するのめんどくさいし……いやそんな言い訳はいい。

さて発売日の3月3日……ではなく戦いは2日に始まっていたのだった。bestbuy(家電量販店チェーン)など、小売店のなかには深夜イベントを開催し、3日になった瞬間0時、ようするに2日の深夜に売り始めるところもあったのだったが、あまりその辺も把握してないまま0時がすぎる。全米各地のbestbuyの行列の様子などもツイッターで見えてくる。えっみんなそんなにSwitch欲しいの? 一部の人だけじゃなく?

Targetなど、深夜イベントをやらずに朝から売り始める小売店もあったが、この状況からして、深夜から並ぶ必要はないとしても、開店後にのんびり行っても無駄だろうということがわかってきた。とりあえず目覚ましをセットして……としたのだが目覚まし設定ミスという痛恨の凡ミスが発生、開店直前には来店できたものの整理券配布は終了していた。……嗚呼。


その後、何度か各種の店を巡ってみたが、おそらく再入荷はしておらず、入手できる気配はない。ううーんまさか北米でもここまで人気のある商品だったのか……としみじみ愕然としていたところ、友達から「amazon.co.ukではin stock in March 13になっている!」と教えてもらう。

行ってみたところ、確かにそのような表記が! でもamazon.co.ukなんて前に利用したの何年前だろう……てな状態にて、ログインに手間取っているうちに在庫表示が不安定に。注文はできたのだけど、発送日未定で注文確定されてしまった。うーん参った、だがこれは13日に海外で新規入荷があるということではないか? その辺で再調査すればいいのでは? などと思う。

そんなこんなでいると9日、出荷したよ!というメールがamazon.co.ukから届く。到着予定日は27日。うーんこれ、出荷用の伝票が作成されただけだと思うんだけども、おかげでキャンセルできなくなってしまった。しかも27日ではスプラトゥーン2試射会に間に合わないんだけど。でもまあこっちで買えたら返送すればいいだけか、などと思う。

その後、13日近辺にも軽く見て回るものの、とても入荷とかある感じじゃない。なんだったんだろう? よくわからない。ただ、荷物のトラッキングページを眺めていると、どうやら本当にNintendo Switchが英国から出荷されたようだった。欧州と北米ではニーズや在庫も違うのかもなあ。

それからはトラッキングページをなんどもリロードしながらやきもきしていたものの、けっきょく当初の到着予定日である27日より大幅に早いアメリカ西海岸時間の夜となる今、ぶじ本体を入手できました。はーよかった。

さてなんとか入手できたわけですがこれからぼくはどうするかというと、まずはやりかけのHorizon Zero Dawnを進めないといけないのでしばらくは死蔵かな……。

SHA-1衝突とsubversionもう少し

昨日の記事については特に誰も教えてくれなかったが、subversionのメールスレッドは自力で発見した。

http://mail-archives.apache.org/mod_mbox/subversion-dev/201702.mbox/%3C20170223200227.ynjumirjnmlesy5m%40sunbase.org%3E

これによると、Subversionのバックエンドでrep-sharingという機能がオンかオフかで判断されていて、デフォルトはオン、オフだと大丈夫、ということっぽい?

ただ、試してみると、shattered-1.pdfとshattered-2.pdfの内容は完全に一致してしまうため、変なバグは残っているような気がする。なんでこうなってるんだろうな。内容はshattered-1.pdfになっている。が、エラーにはならない、という意味ではマシ。


さて、rep-sharingがどういう機能なのかというと、 http://svnbook.red-bean.com/en/1.6/svn.reposadmin.maint.html によれば、

Repositories created with Subversion 1.6 or later further enjoy the disk space savings afforded by representation sharing, a feature which allows multiple files or file revisions with identical file content to refer to a single shared instance of that data rather than each having their own distinct copy thereof.

ということなので、コンテントのハッシュを計算し、ハッシュ値が一致した場合は片方だけを保存しておくということなのだろう。一方、コミット時のMD5およびSHA1の値は、メタデータとして別個に保存しておき、svn co時に使われる、ということなのだろうか?

ところで、昨日の記事には間違いがあった(訂正済み)。エラーメッセージを再掲すると、

$ svn co file:///tmp/svntest/repo2
A repo2/shattered-1.pdf
svn: E200014: Checksum mismatch for '/tmp/svntest/c2/repo2/shattered-2.pdf':
 expected: 5bd9d8cabc46041579a311230539b8d1
 actual: ee4aa52b139d925f8d8884402b0a750c

となっているのだけど、expectedはshattered-2.pdfの正しいMD5値、actualはshattered-1.pdfのMD5値になっている。つまり、チェックアウト時にコンテントのMD5値も検証していて、この不一致がエラーになっている。

コードはちゃんと読めていないけれど、ここから推測するに

  • svn coの際、コンテントのハッシュを検証する。SHA1とMD5の両方で検証されるか、MD5のみ検証するかどっちか
  • rep-sharingが有効な場合、レポジトリ内にはshattered-1.pdfの内容だけが保存されている。svn coではこの内容が取り出される
  • 一方、rep-sharingが有効な場合、メタデータのMD5とSHA1はキャッシュされているので、この内容を使って検証される
  • MD5値は一致しないのでエラー

といった流れになると思われる。

rep-sharingが有効でない場合でも、チェックアウトするとなぜか両方ともshattered-1.pdfの内容になってしまうのだけど、これはプロトコルが何らかの最適化により、同じSHA1値のものを取得するのを避けているからだろうか? よくわからないな……。

なおsvnadmin dumpの内容をよく見ると、

  • rep-sharingが有効な場合、dump内容ではshattered-1.pdfとshattered-2.pdfの内容は一致している(が、メタデータのMD5が異なる)
  • rep-sharingが無効な場合、dump時にはファイルの中身は異なる

となっていて、rep-sharingの機能が窺い知れる。

なおダンプ内容にも元々のMD5とSHA1の値が記録されるけれど、rep-sharingによってコンテントのハッシュは一致しないので、このダンプ結果をsvnadmin loadすると同じ検証エラーにより失敗することになる。

SHA1衝突とバージョン管理システム

SHA1については https://www.slideshare.net/herumi/googlesha1 を読んでフムフムーと勉強していた。わかりやすい。

ところでwebkitがテスト用に入れてみたところ(ブラウザキャッシュをハッシュで管理しているから衝突時のテストをしたかったようだ)、subversion自体が発狂するという珍事件が発生したらしい: https://bugs.webkit.org/show_bug.cgi?id=168774&comment=c27#c27

これはひどいwって思うわけだけど、実際なんでこんなことが起こるんですかね? 誰か詳しい人に教えて欲しい。実際、

$ cd /tmp
$ mkdir svntest
$ cd svntest
$ mkdir repo
$ cd repo
$ svnadmin create .
$ cd ..
$ mkdir co1
$ cd co1
$ svn co file:///tmp/svntest/repo
Checked out revision 0.
$ cd repo
$ cp ~/shattered-* .
$ svn add shattered-*
A (bin) shattered-1.pdf
A (bin) shattered-2.pdf
$ svn commit -m 'test'
Adding (bin) shattered-1.pdf
Adding (bin) shattered-2.pdf
Transmitting file data ..done
Committing transaction...
Committed revision 1.
$ cd ../..
$ mkdir co2
$ cd co2
$ svn co file:///tmp/svntest/repo
A repo/shattered-1.pdf
svn: E200014: Checksum mismatch for '/tmp/svntest/co2/repo/shattered-2.pdf':
 expected: 5bd9d8cabc46041579a311230539b8d1
 actual: ee4aa52b139d925f8d8884402b0a750c
$ svn --version
svn, version 1.9.3 (r1718519)
 compiled Mar 14 2016, 07:39:01 on x86_64-pc-linux-gnu

などとなる。ここで”actual”は正しいSHA1ハッシュを計算できているので、なぜかサーバが提供するexpectedなハッシュが狂っているということになる。でもsvnadmin dumpすると正しいハッシュ値なんだよな。謎だ。(訂正:expectedは正しいMD5値、actualはshattered-1.pdfのMD5値だった)

git

gitにおいてファイルコミットはgitオブジェクトを作ることに相当し、gitオブジェクトはざっくりいうとSHA1ハッシュ値で識別される。ので、同じような理由により衝突の懸念がある。

ただ実際には https://git-scm.com/book/en/v2/Git-Internals-Git-Objects で詳しく解説されているようにヘッダ情報(ファイル長などの情報)とファイルの中身で構成されている。のでファイルの中身のSHA1ハッシュが一緒でもヘッダが先頭についているため、オブジェクトのハッシュ値は異なったものになるはず。

もっとも↑の解説スライドを読む限り、同じ手法により同じ長さの2つの相異なるPDFファイルをコミットすることにより、同じハッシュ値を持つgitオブジェクトが生成することは可能という気がする。その場合、その2つのファイル自体の中身は今回と全然異なるものになる。ファイルの中身自体のSHA1ハッシュも同じでかつgitのハッシュ値も同じになるというのは、今回の話とは関係なく、遥かに難しい問題であるように思う。

そもそもgitではハッシュは識別にしか使っていないだろうから、その場合でもcloneしたら2つのファイルの中身が同じになってしまうという意味では異常だけども、エラーを引き起こすようなことはない気がするけれど。

Mercurial

Mercurialではハッシュ値はグローバルな名前空間ではない。ファイルごとにそれぞれ履歴データを持っていて履歴をハッシュで管理するスタイルになっている。したがってそもそも違うファイルで同じハッシュ値となっても根本的に問題ないはず。

じゃあ同じファイル内の変更履歴でハッシュ値が衝突するとどうなるかというと、これは確かに問題になるかもしれない。ただし、ハッシュ値の計算には親のハッシュ値等を含むヘッダ情報が使われるため、このアプローチが正しいとすると通用しない(ヘッダ部分が共通でないため)。ただこれがどのような問題を引き起こすのかはわからない。ともあれこんなに簡単に壊れるような構造にはなっていないように思われる。

他のバージョン管理システム

知らんし調べてないけど問題ないんじゃないかなあ。