@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Ractorを試す

Ruby3 さみっと online - connpass を見ていました。 Ractorの発表を見て使ってみたくなったので環境を整えてみます。

ビルドする

https://github.com/ko1/ruby/tree/ractorソースコードをビルドします。

git clone git@github.com:ko1/ruby.git ractor
cd ractor
git checkout ractor
autconf
./configure
make
ractor.c:1105:13: error: implicit conversion loses integer precision: 'VALUE' (aka 'unsigned long') to 'uint32_t' (aka 'unsigned int') [-Werror,-Wshorten-64-to-32]
    r->id = ractor_next_id();

が出ます。

Mac で ruby-1.9.3-p385 がビルドできない問題を素早く解決する - Qiita を参考にして環境変数CFLAGS=-Wno-error=shorten-64-to-32を指定します。

CFLAGS=-Wno-error=shorten-64-to-32 ./configure
make

コンパイルは成功しました。 rubyコマンドを実行するとrubygems.rbが見つからずにエラーになります。

~ ./ruby
Traceback (most recent call last):
    1: from <internal:gem_prelude>:one:in `<internal:gem_prelude>'
<internal:gem_prelude>:one:in `require': cannot load such file -- rubygems.rb (LoadError)

make installも実行する必要があるようです。 PCの環境が壊れるかもと考えると億劫です。

Dockerイメージを使う

id:wakaba260yen さんがDockerイメージを用意していてくれました。 使ってみます。

~ docker run --rm  wakaba260/ruby-ractor-dev ruby -e 'r = Ractor.new(42) { p _1 }; r.take'
42

たとえば ring.rb を実行するには次のように実行します。

~ docker run --rm -v (pwd):/ractor wakaba260/ruby-ractor-dev ruby ractor/ring.rb
:setup_ok
1
:fin

参考資料

http://atdot.net/~ko1/activities/2020_ruby3summit.pdf

ruby/ractor.ja.md at ractor · ko1/ruby · GitHub

自宅作業環境の整備

新型コロナウイルスの影響で在宅勤務しています。 3月3日から、少しずつ作業環境を整備しています。

ノイズキャンセリングヘッドホン

オフィス作業時代に、作業集中用に使っていたものをそのまま使っています。

オンラインミーティングしている相手の声が聞き取りにくい時や、自宅内の騒音が大きいときに使っています。 騒音の音が自分には聞こえなくなるのですが、オンラインミーティングの相手には流れているかもしれないのが、気になる点です。

www.sony.jp

モバイルディスプレイ 15,000円

作業に使っているノートPCに外付けディスプレイが欲しかったので買いました。

決め手は値段です。 自宅作業を始めた初期に買ったので、なるべく安いものを選びました。 今考えると、もう少し大きな据置ディスプレイを買ってもよかったかもしれません。

モバイルディスプレイの利点は作業終了後に片付けやすい点です。

同梱のUSBケーブルが2, 3日で壊れたので、追加でUSBケーブルを買いました。

肘置き 4,500円

自宅作業だと右の肩と背中が痛くなるので、対策で買いました。 使ってみたら、机と同じ高さに固定なのは予想より気になります。 1,000円ちょっと追加して、高さが調整できるタイプにすればよかったです。

ないよりはマシですが、これだけで、肩、背中の疲労がなくなることはないです。 予算ができたら、机と椅子を整備しようと思います。

USBマイク 5,000円

PC搭載のマイクだとキーボードのタッチ音を拾いやすいのと、自宅内の雑音も拾ってしまいます。 ヘッドホンにもマイクがついていたのですがBluetooth接続のノイズが乗るので微妙でした。

外付けの単一指向性マイクを買いました。 比較的安くて、アームやポップガードまでついているので、最初の1個目として良さそうと思い選びました。

どれぐらいの効果があるかは、自分が聞く側でないので、わかりません。 録音して試した感じでは、PC搭載のマイクよりはマシだと思います。 ヘッドセットのマイクだと飲み物を飲む音も拾うので、ヘッドセットより外部マイクの方がいいのかなあ?と思っています。

コンデンサマイクよりダイナミックマイクの方が単一指向性が強いらしいので、ダイナミックマイクの方が良いのかもしれません。 よくわかりません。

天板 2,000円

天板の広さ78x29cm、高さ80cm本棚の上で作業していました。 もう少し広い作業スペースがほしかったので、アイリスオーヤマの化粧板を買いました。 本棚の上に載せて天板にしています。

送料込みで2,000円、安い!

item.rakuten.co.jp

新型コロナウイルスの患者数を予測する

東京都 新型コロナウイルス陽性患者発表詳細 - データセット - 東京都オープンデータカタログサイト から患者の情報を取ってきます。 グーグルスプレッドシートをつかってグラフを書きます。

f:id:ledsun:20200329151852p:plain
新型コロナウイルスの患者数推移

24〜28日がほぼ直線になっていることが読み取れます。 赤線の部分です。 この部分だけをグラフにします。

f:id:ledsun:20200329152050p:plain
 3/24-3/28の傾向から予測する患者数

このまま線形に推移すると仮定してトレンドラインを引きます。赤線の部分です。

  • 4/1には500人
  • 4/11には1000人
  • 5月には2000人

と、予測できます。

グラフ作成に使ったスプレッドシートです。 20200329の患者数推移予測 - Google スプレッドシート

都内の感染症指定医療機関で何が起こっているのか(忽那賢志) - 個人 - Yahoo!ニュース

無症状の患者、すでに改善した患者、軽症の患者については隔離を行わずに自宅療養とすべきと考えます。

すべての感染者を隔離し、封じ込めを狙うフェーズはすでに過ぎており、今は中等症〜重症例の医療を必要とする患者を医療機関で診療すべき段階に来ています。

は、本当にそうなのだろうと思います。 つまり、

  1. 感染者を隔離できない
  2. 感染者増加ペースはおさまらない
  3. 今後もいまの外出自粛が続く

のでしょう。気長に頑張りたいと思います。

新型コロナウイルスの患者数増加ペースが指数関数的増加を少し下回る

3月27日が40人増加で、指数関数的増加を少し下回っています。 指数関数的増加をしていたら累計で400人台前半、28日には400人を超えている見込みでした。 少し希望が見えて来ました。 なるべく家族以外との接触を控えて、増加ペースの鈍化に協力していきたいところです。

f:id:ledsun:20200328075259p:plain
東京都の新型コロナウイルス「COVID-19」感染者数推移

都内の最新感染動向 | 東京都 新型コロナウイルス感染症対策サイトより

日テレのグラフはちょっと過激に感じすぎる気がしたので、変えました。

新型コロナウイルスの勢い衰えず

感染者数は、3/22に勢いが衰えたかと思いきや、見事な指数関数的増加中。この先1〜2週間では収まらなさそうです。

f:id:ledsun:20200326101430p:plain
東京都の新型コロナウイルス「COVID-19」感染者数推移

データとグラフで見る「新型コロナウイルス」感染状況(国内版)|日テレNEWS24 より

RubyMineのターミナルが壊れた話

RubyMineのターミナルのシェルがおかしいです。

パスから/usr/local/binが消えてしまった。rbenvが動かせません

rbenvが動かせません。

シェルにはfish-shellを使っています。 fish-shellの起動時に、次のエラーメッセージが出ます。

~/.config/fish/config.fish (line 1): 
rbenv init -|psub
^
in command substitution
        called on line 39 of file ~/.config/fish/config.fish
from sourcing file ~/.config/fish/config.fish
        called on line 185 of file /usr/local/Cellar/fish/3.1.0/share/fish/config.fish
in function '.' with arguments '/Users/shigerunakajima/.config/fish/config.fish'
        called on line 20 of file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
from sourcing file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
        called during startup
~/.config/fish/config.fish: Unknown error while evaluating command substitution
from sourcing file ~/.config/fish/config.fish
        called on line 185 of file /usr/local/Cellar/fish/3.1.0/share/fish/config.fish
in function '.' with arguments '/Users/shigerunakajima/.config/fish/config.fish'
        called on line 20 of file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
from sourcing file /Applications/RubyMine.app/Contents/plugins/terminal/fish/config.fish
        called during startup
Welcome to fish, the friendly interactive shell

~/.config/fish/config.fish の39行目の status --is-interactive; and source (rbenv init -|psub) でエラーが出ています。

これはなんのためのコードでしょうか? rbenv init - は、つぎのようなrbenvのPATHなどを初期化するシェルスクリプトを出力します。

set -gx PATH '/Users/shigerunakajima/.rbenv/shims' $PATH
set -gx RBENV_SHELL fish
source '/usr/local/Cellar/rbenv/1.1.2/libexec/../completions/rbenv.fish'
command rbenv rehash 2>/dev/null
function rbenv
  set command $argv[1]
  set -e argv[1]

  switch "$command"
  case rehash shell
    source (rbenv "sh-$command" $argv|psub)
  case '*'
    command rbenv "$command" $argv
  end
end

psubというのはコマンドの実行結果を名前付きパイプ(仮想ファイルみたいなもの?)に書き込むfishのコマンドです。

たとえば、次のようにうごきます。

~ echo (rbenv init -|psub)
/var/folders/3m/82sq4tnn62d59h7h6wzn6w340000gn/T//.psub.s8MwzlfDwx

この出力された名前付きパイプをsourceすると、rbenvが初期化されます。

結局 /usr/local/bin/rbenv にパスが通ってないからエラーになるようです。

PATHに/usr/local/binを追加してみましょう。

fishらしく set -U fish_user_paths /usr/local/bin $fish_user_paths でユニバーサルなPATHに追加してみても、変わりません。

~/.config/fish/config.fish の39行目の直前で set -g fish_user_paths "/usr/local/bin /usr/local/opt/ruby/bin" $fish_user_paths してPATHに追加してみます。変わりません。

~/.config/fish/config.fish の39行目の直前で set PATH /usr/local/bin $PATH すると、うごきました!

config.fishなのにfish-shellのコマンドが効かない、エラーが出ないのに効果が出ないのは、不思議な感じがします。

蛇足 RubyMineのPATHどこからくるのか?(不明)

path_helperについて。Mac OSX 版 - それマグで! によると、 mac のパス設定は path_helper というのがあり、path_helper は /etc/paths のをPATHに反映するらしい。

/etc/pathsを確認してみます。

~ cat /etc/paths
/usr/local/bin
/usr/bin
/bin
/usr/local/sbin
/usr/sbin
/sbin

/usr/local/bin が入っている。RubyMineがpath_helperを使わないのかもしれません。

RubyMineの設定をみてみます。

f:id:ledsun:20200304043810p:plain
RubyMineの設定

システムの環境変数を使うって有るし、/usr/local/bin も入っています・・・

Rubyのコンパイルに挑戦した記録

OpenSSLがみつからない

GithubのREADMEに従って

git clone git@github.com:ruby/ruby.git

して

./configure
make
make check

すると、次のような警告メッセージがでました。

*** Following extensions are not compiled:
openssl:
    Could not be configured. It will not be installed.
    /Users/shigerunakajima/ruby/ext/openssl/extconf.rb:97: OpenSSL library could not be found. You might want to use --with-openssl-dir=<dir> option to specify the prefix where OpenSSL is installed.
    Check ext/openssl/mkmf.log for more details.
*** Fix the problems, then remove these directories and try again if you want.

OpenSSLが見つからないみたいです。

brew info opensslすると

For compilers to find openssl@1.1 you may need to set:
  set -gx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
  set -gx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"

と表示されたので、この設定をして

./configure
make clean
make

しました。 該当の警告メッセージは消えました。

テスト失敗

make check

すると、今度は次のテストが失敗しました。

1)
Socket.getnameinfo using IPv6 using a 3 element Array as the first argument using NI_NUMERICHOST as the flag returns an Array containing the numeric hostname and service name FAILED
Expected ["::ffff:127.0.0.1", "ftp"] == ["::1", "ftp"]
to be truthy but was false
/Users/shigerunakajima/ruby/spec/ruby/library/socket/socket/getnameinfo_spec.rb:111:in `block (6 levels) in <top (required)>'
/Users/shigerunakajima/ruby/spec/ruby/library/socket/socket/getnameinfo_spec.rb:65:in `<top (required)>'

該当のテストコードは https://github.com/ruby/ruby/blob/master/spec/ruby/library/socket/socket/getnameinfo_spec.rb#L109-L113 です。

describe 'using NI_NUMERICHOST as the flag' do
  it 'returns an Array containing the numeric hostname and service name' do
    Socket.getnameinfo(@addr, Socket::NI_NUMERICHOST).should == [ip_address, 'ftp']
  end
end

ソースコード追っかけて要約すると、このテストコードは

Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket::NI_NUMERICHOST)

[":1", "ftp"] を返すことを期待しています。

irbを使ってためしてみます。

irb(main):012:0> Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket::NI_NUMERICHOST)
=> ["::ffff:127.0.0.1", "ftp"]

なるほど["::ffff:127.0.0.1", "ftp"]が返ってきています。 それでテストが失敗しているようです。

ネットワークを変えると

ネットワークをLANからスマートフォンテザリングに変更して試してみます。

irb(main):003:0> Socket.getnameinfo([Socket::AF_INET6, 21, 'localhost'], Socket:
:NI_NUMERICHOST)
=> ["::1", "ftp"]

ネットワーク環境依存しているみたいです。

2019年のふりかえりと2020の目標

全体的に

私事に掛かるリソースが増えたので、残されたリソースでどうやりくりしたものか工夫した一年でした。

認知度

認知度に関しては、自分の能力以上に認知度が上がっても辛そうなので、一番最初にあきらめました。 その結果が、登壇数0です。

認知度を上げて、期待値に答えるように自分をストレッチするやり方もあると思います。 そのためのリソースを捻出しにくい状況だったので、やりませんでした。

技術力

業務で新しいツールを使って引き出しは増えました。

実感としては技術力が上がった感はありません。 今更使えるツールが増えてもそんなに技術力は上がらなさそうです。 ツールを使うこと自体は案件中にキャッチアップできるので、予め使えるようになっていることのメリットが感じられません。

理解しているレイヤーを増やすようなアプローチが必要かな?と思っています。 「作って理解するOS」を読み始めました。まだ途中です。

2020年には、実装するところまでやりきりたいです。 2017年に「RubyでつくるRuby 」を読んだときは、考え方を応用してできることがぐっと増えたので、同じような効果を期待しています。

影響力

javascriptingの運営で、自分以外の人がPullRequestを送りやすくするにはどうすればいいか?と工夫し始めました。

2020年は、リアルな人間関係、具体的に職場で「ミドルクラスのエンジニアをシニアエンジニアにする」方法にチャレンジしたいと思っています。

職業プログラマを初めて4,5年経つと一人で仕事できるようになります。 ここではこの辺をミドルクラスとします。 ミドルクラスのエンジニアが、学習をやめてしまうのか、学習の仕方を身につけられていないのか、伸び止まる現象をみることがあります。 たぶん「プログラマ35歳定年説」というのは、この現象のことかな?と思います。

2020年は、この壁を乗り越えるサポートの仕方を見つけようと思います。

本当はユーザーが多いOSSを作ったりとか、新しい開発思想をまとめたりとかできるといいと思うんですが、現状の僕が1番影響力を発揮できそうなのは、この辺をじゃないか?と思っています。

その他

会社員としてのもう一つの目標は「上司を上手く使う」です。 上司にどう動いてもらえると効果的なのかを上手く説明できるようになりたいです。

あとは健康。健康は大事です。2019年の本厄は無事乗り切りました。後厄も大過なくやり過ごしたいものです。

登壇

今年は0回です。 去年の5回に比べるだいぶ減りました。

同人誌執筆

techbookfest.org

Ruby並行並列大全」という同人誌を書きました。 Rubyの標準ライブラリでできる並行処理、並列処理を一通り網羅して説明した本です。 在庫はだいぶ残っているのですが、電子版も公開していないですし、委託販売もしていないというよくわからない事態になっています。 次の販売予定は決まっていません。

貰った感想では「concurrent-rubyeventmachineのようなライブラリの使いかた解説がほしい」というものがありました。 それらのライブラリをバリバリ使うようになったら書いてみたいと思います。

とはいえ、2020年は同人誌執筆を一旦お休みするつもりです。

OSS活動

javascriptingのメンテナンス

github.com

Nodeコミュニティのはworkshopperという、CLIJavaScriptプログラミング入門のCLIプログラムがあります。 そのうち一番入門よりのjavascriptingのメンテナンス権を5年位前から持っていました。 今年は、やる気を出してメンテナンスしはじめました。

いまさらソースコードに手を入れる必要は殆どないので、ソースコードの修正よりは、issueの整理に力を入れていました。 issueを閉じて1ページに収めました。 軽微な修正をしたり、議論が止まっているissueを閉じました。 また、残ったissueにはlabelをつけて、プロジェクトの状況をわかりやすくしました。

その結果、イタリア、ブラジル、ペルーあたりからPullRequestをもらいました。 javascriptingは問題文や回答文が多言語化されています。 機能への貢献より、文章を翻訳するために、その言語を使える人たちに協力してもらう必要があります。 協力してもらいやすい環境ができたのは良かったと思います。

workshopperは日本では完全に下火です。 英語圏でもあまり使われていないように思います。 Node.jsのブームが一巡したからではないかと思います。 一方で、ブラジル、ペルーでは、まだ需要があることがわかりました。 興味深いです。

いつかペルーのエンジニアと会ってみたいものです。

creekにPullRequest

github.com

xlsxファイルフォーマットでは、日本語の「ふりがな」が扱えます。 Excelで日本語を入力するとIMEの変換情報から「ふりがな」を取得して、自動的にxlsx内に埋め込みます。 便利なのだと思うのですが、creekというxlsxファイルのパーサーは、意図せずにセルの値に「ふりがな」をくっつけて返していました。 修正しました。

ふりがなに関するxlsxファイルフォーマットの機能に、英語圏の開発者に気づけというのは無茶な話でね。

ハック

autodocでOpenAPIドキュメント生成

github.com

autodocというAPIサーバーの使用例ドキュメントをRequest Specから生成するGemがあります。 これを元に、Request SpecからOpenAPIドキュメントを生成する、Railsアプリケーションのコントローラーファイルを生成する機能を追加しました。 APIの詳細をあとから書くために、コントローラーファイルを生成しています。 ファイルを修正してから、特定のコントローラーを呼び出すとOpenAPIドキュメントを生成します。

API定義の集め方やコントローラーを呼び出す使いかたがイマイチ気に入っていないのでPull Requestにしていません。

lhalibのRuby 2.0対応

github.com

RubyLZHファイルを解凍でるGemがあります。 C拡張ライブラリです。 Ruby 2.0でRubyのC拡張APIが変わりました。 その変更に対応していないため、Ruby 2.0以降ではGemのインストールに失敗します。 C拡張のAPIを修正して、Gemインストール可能にしました。

RubyGems.org | your community gem hostで公開されているgemはないので、Pull Requestは作成していません。

自作プログラム

github.com

「GemfileやGemfile.lockファイルを解析すれば、どんなGemが使われているのか調べられそう」と思って作ったRubyスクリプトです。

github.com

「bootstrap-select-railsというGemをTurbolinks 5に対応するハックが上手くうごかない」という情報を聞きつけて作成した サンプルアプリケーションです。

github.com

管理職業務を少し軽減するスクリプトです。

読書

エンジニアの知的生産術

学習モデルの考え方が参考になりました。

作って理解するOS

コンピューターサイエンス再入門として楽しく読んでいます。 大学の授業でやったけど理解があやふやなところを復習しています。 なんとなく理解が不安なところを理解しているかどうか確認できるので、満足感が高いです。

解説が丁寧なので、この本だけ読んでいればまあまあ理解が進むので楽です。 一方でページ数が多いので、読むのには時間がかかります。 5分の1ぐらい読みました。引き続き読んでいこうと思います。

「やさしさ」という技術

結論は当たり前の話なんですけど、どう論理的に說明するかという本です。 途中まで読んで、今すぐに役立つ本ではないかなあ?というところでした。

理論と事例でわかる自己肯定感

booth.pm

自己肯定感が低いときは「これでよい」だけではダメで、どっかでギャンブルして「とても良い」を感じる必要がある、といった記述があり、目から鱗でした。

英語

TOEICの結果です。

受験日 点数
2019/1/1 675
2019/3/10 755
2019/09/29 710

コンスタントに700点取れているのは良い点です。 もう少し目に見えて成果が欲しいのですが、英語の勉強を日常習慣に組み込まないと、なんともならない感じです。

Qiita

ledsun - Qiita

Tips的な内容はブログ読者より扱っている技術に興味がある人に届くと良さそうなので、 ブログでなくQiitaに書くことにしています。

12件。 2018年の16件に比べ少し減りました。

SameSite cookieについての勉強メモ

発端

Google Chromeの開発コンソールを有効にして、Webアプリケーションを開発しているときに次のような警告メッセージが表示されました。 これは一体どういう意味で、何に注意すればいいのでしょうか?

f:id:ledsun:20191202095526p:plain
Google Chromeの表示する警告

A cookie associated with a cross-site resource at http://pubannotation.org/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

Cookies default to SameSite=Lax - Chrome Platform Status を見ろと書いてあるので、みてみましょう。

Cookies default to SameSite=Lax

Treat cookies as SameSite=Lax by default if no SameSite attribute is specified.

CookiesをデフォルトでSameSite=Laxとして扱うと書いてあります。 SameSite=Lax とは何でしょうか?

SameSite=Lax

SameSite=Laxを検索するとCross-Site Request Forgery is dead!で紹介されています。 CSRF対策のようです。

日本語の解説もあります。

CSRF

対策したい問題がわからないと、対応策も理解できません。 CSRFはどのような攻撃でしたっけ?

安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構にあるPDF資料「安全なウェブサイトの作り方」のP30に「CSRF(クロスサイト・リクエスト・フォージェリ)」の解説があります。

あるサイトAへのログイン状態を維持したまま、悪意あるサイトB上のサイトAへのリンクをクリックした際に、サイトAがログインしたユーザーから意図したの操作だと誤認してしまう問題です。

よくある対応は、次のようなものです

  1. 変更が必要な処理はHTTPリクエストのメソッドをPOSTにする
  2. フォーム上にテンポラリーなトークンをhiddenパラメーターとして埋め込んでおく
  3. サーバーはHTTPリクエストに有効なトークンが含まれているか検証する

これによりユーザーのリクエストが、正規のフォームからのものであることが確認できます。

ふたたびSameSite=Lax

既知の対応策があるにも関わらずSameSite=Laxが道入されるのはなぜでしょうか? Cross-Site Request Forgery is dead! にProblemという章があります。

The trouble is though that these both put some kind of requirement on the site to implement and maintain the solution.

訳せば

問題は、これらの両方がソリューションを実装および維持するためにサイトに何らかの要件を課すことです。

そりゃまあ、Webサイト構築時に何らかの工夫をするよりは、ブラウザが対応してくれた方が嬉しいですよね。 で、具体的にはどのように使えばよいのでしょうか?

SameSite属性は次のように書きます。

Set-Cookie: sess=abc123; path=/; SameSite=lax

SameSite属性には次の2つの値があります。

  • Strict
  • LaxSameSite

SameSite=Strict

SameSite=Strict モードでは、オリジンが異なるサイトにクッキーを一切送信しません。他のサイトにあるリンクをクリックして対象の遷移してきた場合でも、送信しません。 つまり、ユーザーがログイン済みでも、クッキーを送信しないので、再度ログインを要求します。

このモードは、AmazonFacebookのように、認証クッキーが次の2段階に分かれているサイトでつかうことを想定しています。

  • ログイン用のクッキー
  • 決済や情報更新用のクッキー

後者のクッキーを SameSite=Strict モードにすることで、CSRF攻撃を確実に防ぎます。

SameSite=Lax

SameSite=Laxモードでは、 GET, HEAD, OPTIONS and TRACE メソッドのときはクッキーを送ります。 ログイン済みのユーザが、他サイトのリンクをクリックして遷移してきた場合は、クッキーが送信されるので、サーバーはログイン済みとして扱います。

この場合、サイトは変更を行うリクエストにはPOSTメソッドをつかうように実装します。 POSTメソッドをつかっている限り、CSRF攻撃を防ぎます。

Chromeの出す警告の意味

以上の知識を踏まえて、Chromeの警告の意味を考えてみましょう。

A cookie associated with a cross-site resource at http://pubannotation.org/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.

翻訳すると

http://pubannotation.org/のクロスサイトリソースに関連付けられたCookieは、 SameSite属性なしで設定されました。 Chromeの今後のリリースでは、「SameSite = None」と「Secure」が設定されている場合にのみ、クロスサイトリクエストでCookieを配信します。開発者ツールのCookieを[アプリケーション]> [ストレージ]> [Cookies]で確認し、https://www.chromestatus.com/feature/5088147346030592およびhttps://www.chromestatus.com/feature/5633521622188032で詳細を確認できます。

この警告がでている時、ブラウザはXMLHttpRequestを使って自サイト以外のサイト(http://pubannotation.org/)からデータを取得しています。 このリクエストに対して、「クロスサイトリソースに関連付けられたCookie」と言っています。 特に、Chrome 80からは明示的に SameSite=NoneSecure が設定されていないクッキーはクロスオリジンのサイトには送信しなくなるという警告です。

このアプリケーションの用途ではクッキーを送信する必要はありません。 しかし http://pubannotation.org/ へのHTTPリクエストのレスポンスには Set-Cookieヘッダーがついています。 それで、Google Chromeは親切に教えてくれました。

Secure フラグ

ついでに警告に一緒に出ているSecureフラグはTough Cookies によると、次の形式で指定します。

Set-Cookie: sess=123; path=/; Secure

その意味は

The Secure flag is another optional flag that can be included in a Set-Cookie header that instructs the browser that the cookie must only ever be sent over a secure connection.

翻訳すると

ブラウザはセキュア(https)でない接続ではクッキーの情報を送りません。

蛇足

Cookiesの仕様、クソむずい・・・

https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3 を見れば網羅できるのでしょうか?

RSpecでSharedExamplesを使ったときのエラー表示の現状

RSpecで、SharedExamplesを使うと、失敗したテストの行番号の代わりに [1:2:2:3:1] みたいなのが表示されて、どのテストを見ればいいのかわかりにくくなるのが辛いです。 2015年からInclude line numbers as well as index in output · Issue #2119 · rspec/rspec-coreがあります。

https://github.com/rspec/rspec-core/issues/2119#issuecomment-173664887 の辺りを ざっくり要約すると

SharedExampleを使ったテストが失敗した時に、見たい場所はspecを定義している場所とexampleを定義している場所の2箇所ある。 どっちを見たいかは人によって変わる。 specを定義している場所を表示すると、それはそれで不便。 Summary output of shared failed examples is useless when examples defined in external file · Issue #793 · rspec/rspec-coreで議論済み。 かと、言って両方を表示するのはSummaryとしては冗長すぎて、それも不便でしょ?

今週の作業メモ

JavaScripting

runコマンドのバグ

https://github.com/workshopper/javascripting/issues/218 javascripting run hogohoge.jsを実行するとたしかに[object Object]が表示されます。 javascriptingではrunコマンドは使っていません。

https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L264 workshopper-adventure 経由です。 アンドキュメントなコマンド発見して実行して上手く動かないと言われるのは心外だが、不親切なのも確か。 こういうときはどうするのがいいのかなあ? コマンドをworkshopperに投げる前に検査してUsage表示するとか?

helpコマンドでrunコマンドでの説明が出てくるのか・・・全然アンドキュメントじゃないな

~ bin/javascripting help

 # JAVASCRIPTING

 ## javascripting のワークショップに対して質問がありますか?

  エキスパートチームはあなたの質問を待っています。あなたもNode.jsのマスター
  になれるようにここに質問(New Issue)をしてみてください:

     https://github.com/nodeschool/discussions/issues

  どんな質問でもどうぞ!

  一般的なNode.jsのヘルプが必要であればNode.jsの日本のGoogleグループがありま
  す:

     https://groups.google.com/forum/#!forum/nodejs_jp

 ## javascriptingのバグが見つかった場合または貢献したい場合:

  javascriptingのリポジトリはここにあります:

     https://github.com/sethvincent/javascripting.git

  バグの報告やPullリクエストは日本語でいつでもどうぞ!

 ## 使い方

  javascripting .....................
  ワークショップを選択する対話的メニューを表示します。

  javascripting list ................
  登録されているすべてのワークショップの名称一覧を表示します。

  javascripting select NAME ......... ワークショップを選択します。

  javascripting current .............
  現在選択されているワークショップの名称を表示します。

  javascripting print ...............
  現在選択されているワークショップのガイドを表示します。

  javascripting next ................
  現在選択されているワークショップの次の未修了のワークショップのガイドを表示
  します。

  javascripting reset ...............
  ワークショップの進捗状況をリセットします。

  javascripting run program.js ......
  あなたが作成したプログラムを実行します。

  javascripting verify program.js ...
  あなたが作成したプログラムが正しいかを検証します。

  javascripting -l <language> .......
  システムで使用する言語を指定されたものに変更します。

ソースコードリーディング

javascriptingソースコードからjavascriptingworkshopperの役割分担を読みときます。 https://github.com/workshopper/javascripting/blob/master/index.jsworkshopper-adventureインスタンスを読み込んでいます。 サブコマンドによる分岐の全体的な制御フローは workshopper-adventure が担当します。

https://github.com/workshopper/javascripting/blob/master/index.js#L8 で、 menu.json に含まれる problem の一覧を読む。

https://github.com/workshopper/javascripting/blob/master/index.js#L12-L14problemごとに require('problems/introduction') な処理をする関数を作る。

https://github.com/workshopper/javascripting/blob/master/problems/introduction/index.jsrequire("../../lib/problem")(__dirname) なので 実質的に https://github.com/workshopper/javascripting/blob/master/lib/problem.js を実行している。

createProblem 関数で作った exercise を引数にして https://github.com/workshopper/workshopper-adventure/blob/master/index.js#L345 が呼ばれます。

この辺は VSCodeデバッグしながら追いかけています。 https://code.visualstudio.com/docs/nodejs/nodejs-debugging

{
  // IntelliSense を使用して利用可能な属性を学べます。
  // 既存の属性の説明をホバーして表示します。
  // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "program": "${workspaceFolder}/bin/javascripting",
      "args": ["verify", "index.js"]
    }
  ]
}

修正案

https://github.com/workshopper/javascripting/blob/master/lib/problem.js ではrunメソッドも呼んでいる。この辺をいじると、クリーンな修正ができるのだろうか?

デバッグするとたしかに run メソッドは呼ばれています。ただし、出力文字列はこで作っているわけでは無さそうです。 https://github.com/workshopper/javascripting/blob/master/lib/problem.js#L53-L55 workshopper-adventure が出力文字列は作っていそう、javascripting 側から上書きする方法があるのかな?

対処

単に run メソッドの定義を消せばよいだけだった。こんなに簡単な話だったとはね、ふふ

~ bin/javascripting run index.js
run is not required for this exercise.

github.com

今週の作業メモ

spring

Rails 5.2.3でrails consoleを実行すると、プロンプトが出る前で止まります。

~ bundle exec rails --version
Rails 5.2.3
~ bundle exec rails c
^CTraceback (most recent call last):
    15: from bin/rails:3:in `<main>'
    14: from bin/rails:3:in `load'
    13: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `<top (required)>'
    12: from /Users/shigerunakajima/circular_dependency_detected_job/bin/spring:15:in `require'
    11: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
    10: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
     9: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
     8: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
     7: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     6: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:24:in `call'
     5: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
     4: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:35:in `call'
     3: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:42:in `warm_run'
     2: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:62:in `run'
     1: from /Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `verify_server_version'
/Users/shigerunakajima/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/run.rb:117:in `gets': Interrupt

https://github.com/rails/spring/blob/v2.1.0/lib/spring/client/run.rb#L117 ここのserver(どうもUnixソケットらしい)の読み込みで止まっている。

試しに、Gemfileからspringを消してみると、期待通りにプロンプトが表示されます。

~ bundle exec rails c
Loading development environment (Rails 5.2.3)
irb(main):001:0> 

@p_ck_ 曰く

springが悪さをするのに疲れ果てて最近は DISABLE_SPRING=1 してます 似たような状況のときは bin/spring stopをしたら直っていたので毎回それをしていた記憶があります

Bingo!

bootsnap

ruby 2.7-preview1でRails 6.0.0.rc2の rails --version を実行すると、エラーが起きます。

~ rails --version
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Traceback (most recent call last):
        36: from bin/rails:3:in `<main>'
        35: from bin/rails:3:in `load'
        34: from /Users/shigerunakajima/my_first_rails_6/bin/spring:15:in `<top (required)>'
        33: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        32: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        31: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
        30: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
        29: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
        28: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
        27: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
        26: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `call'
        25: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `load'
        24: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `<top (required)>'
        23: from /Users/shigerunakajima/my_first_rails_6/bin/rails:8:in `require_relative'
        22: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `<top (required)>'
        21: from /Users/shigerunakajima/my_first_rails_6/config/boot.rb:4:in `require'
        20: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/setup.rb:30:in `<top (required)>'
        19: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap.rb:30:in `setup'
        18: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache.rb:9:in `setup'
        17: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:50:in `require_relative'
        16: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
        15: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
        14: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
        13: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
        12: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
        11: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
        10: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
         9: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require'
         8: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/compile_cache/iseq.rb:1:in `<top (required)>'
         7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `require'
         6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:288:in `load_dependency'
         5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.0.rc2/lib/active_support/dependencies.rb:322:in `block in require'
         4: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
         3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
         2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
         1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi'
/Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require': no implicit conversion of String into Integer (TypeError)

エラーが起きているのはこの辺 https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24

エラーが起きるときの path の値は "bootsnap/bootsnap"です。 require_without_bootsnap は require の alias_methodなので、 require "bootsnap/bootsnap"で再現します。

~ irb
irb(main):001:0> require "bootsnap/bootsnap"
Traceback (most recent call last):
        7: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `<main>'
        6: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/bin/irb:23:in `load'
        5: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/gems/2.7.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        4: from (irb):1
        3: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:34:in `require'
        2: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `rescue in require'
        1: from /Users/shigerunakajima/.rbenv/versions/2.7.0-preview1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:130:in `require'
TypeError (no implicit conversion of String into Integer)

これでRuby 2.7 + Rails 6ではなくRuby 2.7 + bootsnapの問題だと絞り込めました。bootsnapのissueを漁ると https://github.com/Shopify/bootsnap/issues/258 が 発見できます。 https://github.com/Shopify/bootsnap/issues/258#issuecomment-518726704 をみるとmasterブランチでは修正済みのようです。

https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 パッチの作者は須藤さんや!さすがや、俺の遥かに前を行っているッ!!! Ruby2.7からはRubyのリビジョン番号が数値からgitのSHAハッシュに変わったので、数値前提のソースコードが壊れたらしい。なるほど・・・そういう変更もあるのか。 bootsnap.c なので、bootsnapはC拡張なのねえ。

ていうかBootsnapって、なんなんだろう?名前しか知らないや

READMEによりますと、Rubyプログラムの起動を早くするためのgemです。大きなRailsアプリケーションだと、50%以上の高速化の実績があります。主な手法としては Kernel#require を上書きして、キャッシュファイルをつかうことで、読み込むrubyスクリプトの検索を速くします。 今回エラーが出ていた https://github.com/Shopify/bootsnap/blob/v1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb#L17-L24 は、まさにこのKernel#require を上書きしている部分!

もう一つの最適化手法にiSeq(コンパイル済みのRubyバイトコード)のキャッシュがあります。MessagePack形式でファイルに保存しておくことで、Ruby本体による「ファイルの読み込み〜コンパイル」を「ファイルの読み込み」だけにできます。iSeqはコンパイルしたRubyのバージョン(だけでなくリビジョン単位)によって変わるので、キャッシュが有効かどうか判断するには、Rubyのリビジョン番号が必要です。このリビジョン番号を取るところがRuby 2.7で動かなく成っていたのの修正が https://github.com/Shopify/bootsnap/commit/a163c7cddfccd68277f6ea43b1831378509817c9 なるほどー!

jQueryUI ダイアログ

jQueryUIダイアログには、閉じた時にその前に選択していた要素を再選択する機能があるようです。

Upon closing a dialog, focus is automatically returned to the element that had focus when the dialog was opened.

https://api.jqueryui.com/dialog/

なるほど・・・

closeするときに this.opener をfocusします。 https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L224

this.opnerは開いたときの document.activeElement なのかな? https://github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L273

$.ui.safeActiveElement は、IEのエラーを出なくした document.activeElement 取得関数です。 https://github.com/jquery/jquery-ui/blob/74f8a0ac952f6f45f773312292baef1c26d81300/ui/safe-active-element.js#L12

タスクの完了からはじめるプロジェクト進捗管理

何の話?

タスクの進捗状況をパーセンテージで管理することがあります。

例えば、進捗報告会議で現在着手中のタスクAの進捗を報告するとき「50%完了しています。」という報告の仕方です。 進捗を「タスクの進捗率」で測る方法には、ちょっとした問題があります。

「予定しているタスクのうち2つ完了しています。残りのタスクは3つです。」というように、完了したタスクの数と未完了のタスクの数で報告すると、プロジェクトの進捗管理が、すこし良くなります。

どんなメリット?

プロジェクトの進捗を「タスクの進捗率で測る」方法に比べて、「完了したタスクの個数を数える」方法は、早く正確な進捗情報が手に入ります。 早く正確な情報が手に入れば、プロジェクトが遅れたときにとれる対応策の選択肢が増えます。

  1. お客さん(ステークホルダー)とスケジュールの調整をする
  2. プロジェクト全体で確保してあるバッファを使う
  3. 予想より早く作業が進んでいる人を、遅れている作業のヘルプに割り当てる

プロジェクトの遅れの発見がおそいと、1のような大きな対応しか取れません。 遅れを早くみつけると、2, 3のような、会社内、チーム内のより小さな対応でカバーできるチャンスが増えます。

遅れを早く見つけ、速く小さな対応をする、プロジェクトマネージメントの腕の見せどころです。

f:id:ledsun:20190514170032p:plain
プロジェクトマネージメントの腕の見せどころ

進捗を進捗率で測るときの落とし穴

なぜ、「タスクの進捗率」で測るより、「完了したタスクの個数」で測る方が、早くタスクの遅れに気づけるのでしょうか?

「タスクの進捗率の報告」には次の性質があります。

進捗率10%のタスクは、それまで掛かった時間の10倍の時間を掛けても完了す(100%にな)るとは限らない

なぜでしょうか?

不確実性コーン

ソフトウェアの見積には不確実性コーンと呼ばれる性質があります*1。下の図をみてください。

不確実性コーン
不確実性コーン
*2

プロジェクトの初期段階では、見積もりが1/4倍〜4倍になる不確実性があります。プロジェクトが進むに連れて見積と実際に掛かる工数(実績)が離れる不確実性が減っていきます。プロジェクト完了時には不確実性が0に収束します。 プロジェクトの初期段階では、細かい設計や実装方針などが決めておらず、決定した設計・実装方針によって必要な工数が上下します。 必要な機能があとから発見され工数が増えることもあります。 プロジェクトが進むうちに、新たに発見される情報は減り、いつしかプロジェクトは収束します。 そんな経験はないでしょうか?

タスクの見積りの不確実性

「ソフトウェア見積り」の中で「不確実性コーン」は、プロジェクト単位の話として挙げられています。 個人的に、「不確実性コーン」はタスクにも適用できると考えています。 あるタスクを進めた時、タスク着手前に考えていた必要な時間と、タスク完了までに実際にかかった時間は、たいてい一致しません。 実際にタスク着手してみて、思っていたより簡単に済むことや、考えてもいなかった問題を発見して予想以上に時間がかかることがあります。

また、作業を進めるにしたがって、タスクの完了に必要なことがだんだんわかっていき、不明な部分が減り、見積と実績の差が減っていく点も一致しています。

報告する進捗率はどうやって決めてる?

タスクに着手した初期段階では、見積りと実際に掛かる時間には大きな乖離があります。 仮に、マコネルの例にように1/4倍〜4倍の不確実性があるとしましょう。 作業者が「タスクを10%進めた」と思っている時、実際に掛かる時間の2.5%しか進んでいない可能性もあれば、40%進んでいる可能性もあります。

10%の時点で40%進んでいる可能性があるのであれば、40%と報告しても嘘ではありません。 プロジェクト管理者が10%と報告したとき不機嫌になり、40%と報告したときご機嫌になるのであれば、プロジェクト管理者を喜ばせたい作業者は40%と報告するするかもしれません。 あるいは、自分がプロジェクト管理者の機嫌に左右されていると感じる作業者は、影響を差っ引いて低目に2.5%と報告するかもしれません。

作業者の実感する進捗率と報告される進捗率の数値は同じとは限りません。

プロジェクト管理者の感情

プロジェクト管理者がポーカーフェイスを貫ければ、作業者の実感通り10%と報告してもらえるかもしれません。 予想より遅い進捗を報告されたプロジェクト管理者は、なんらかの対策を考える必要があるので、不機嫌にならないとしても困りはするでしょう。 おそらく困っていることは顔に出るでしょう。

プロジェクトのあらゆる場面で感情を隠しつづけるのは難しいです。 プロジェクトが順調な時はいいです。 プロジェクトが遅れ、遅れに対して上司やお客様から苦情をもらっているときに、作業者からもっと遅れる報告をもらったら、感情的にならないことは難しいでしょう。

プロジェクト管理者が望んでも望まなくても、作業者に善意があっても悪意があっても、作業者はプロジェクト管理者のなんらかの意図を読み取って、都合の良い進捗率を勝手に報告します。

f:id:ledsun:20190514170215p:plain
プロジェクト管理者の機嫌を伺って進捗報告する作業者

90%まで進行して完了しないタスク

プロジェクト管理あるあるに「永遠の進捗率90%」があります。 あるタスクの進捗率が順調に推移して90%まで進んで、そこから進まなくなる現象です。

「不確実性コーン」は、直接的にはタスクの初期段階で見積と実績の乖離を生みます。 しかし、本来幅があるはずの進捗率を一つの数値に落とし込んで報告する作業は、間接的に、タスクの末期になってもその威力を発揮します。

特に、作業者が、自身で問題を解決したいと考えている場合に起こります。 あるいは、プロジェクト管理者が、作業者だけで遅れを解消させようとする場合に起こります。

進捗率では進捗率はわからない

以上のように「報告するタスクの進捗率」は作業者の個人の価値観や、プロジェクト管理者と作業者の関係によって変ります。 「進捗率の報告」では、進捗率はわかりません。

作業者のトレーニングや、作業者とプロジェクト管理者の関係構築によって、「進捗率」の認識を合わせて行くこともできるでしょう。 人数が多いと修正する「作業者の振る舞い」と「作業者とプロジェクト管理者の関係」は増えます。

もっと簡単な方法があります。 プロジェクトの進捗を「完了したタスクの個数を数え」て測りましょう。 「完了したタスクの数」は、プロジェクト管理者と作業者の間の誤解が少ないです。 良くも悪くも報告内容を誤魔化せません。

どうやって始める?

進捗を「完了したタスクの個数を数え」で測るためにはタスクのサイズを小さくします。

進捗報告のタイミングで、進行中のタスクが「完了したタスク」と「未完了のタスク」の半々になるようにしましょう。 割合は重要でありません。 進捗報告の際に「完了したタスク」と「未完了のタスク」が混ざることが重要です。

具体的には、タスクのサイズを「次の進捗報告までに、理想的な見積で完了する」サイズより小さくしましょう。 毎日進捗報告するのであれば、1日より小さくします。 毎週であれば5日より小さくします。

実績が見積を下回ったタスクは「完了したタスク」になります。 実績が見積を上回ったタスクは「未完了のタスク」になります。 次の進捗報告では「完了したタスク」と「未完了のタスク」が混ざるはずです。

「ごまかしのないプロジェクトの進捗率」が、「進捗報告のたびに」得られます🎉

直近のタスクだけを小さく見積る

プロジェクトのすべてのタスクを1日単位のタスクに分割するのは大変です。 「完了したタスクの個数を数える」のに必要な小さなサイズに見積もるのは、直近のタスクだけで十分です。

見積はプロジェクト全体の「不確実性コーン」の影響を受けます。 プロジェクトの初期に全てのタスクを見積もっても実績と乖離する確率が高いです。 不確実性が高いうちに多くのタスクを細かく見積もっても、労多くして功少なしです。 プロジェクトが進んでから、細かく見積もれば、不確実性が低くなった段階で見積もれるので、見積の精度が上がります。

細かく見積もるのは、直近の進捗報告 1サイクルか2サイクル分のタスクにしましょう。 直近でない、大きな単位のタスクは、荒く見積もって、プロジェクトの不確実性に応じて2〜4倍のバッファを積みましょう。

タスクの分け方

タスクを小さいサイズに分けるときにはコツがあります。

大きなタスクには、不明な部分が多く入っています。 不明な部分を明確する作業をタスクとして切り出しましょう。

  • 仕様の不明点を顧客(またはプロダクトマネージャー)にインタビューして解消する
  • UXの不明点を解消するためにデモ画面を作る
  • 使用ライブラリの不明点をサンプルプログラムを作って解消する

不明点が少ない作業は、やることがわかっています。 分担しやすい部分で、2つなり3つなりのタスクに分ければよいでしょう。

f:id:ledsun:20190515163031p:plain
わからないことをわかるようにする作業をタスクにしよう

どこから始める?

個人プロジェクト

一人プロジェクトの場合、タスクの進捗率を意識することはないと思います。

例えば、卒論の進捗率をページ数で測ることはあっても、「実験結果の考察が30%進んだ」と管理することはないと思います。 それは30%と数値化しても、3.3倍の時間で完了するとは限らないからです。 タスクの進捗は「全然わかっていない」や「あともうちょっと」で把握していれば十分です。

「タスクの進捗率」を求めるのは、複数人の作業進捗を集計できるように正規化するためです。 「Aさんが40%進んでいてBさんが50%進んでいるので、全体としては平均して50%」と全体像を把握するためです。

個人プロジェクトに「タスクの進捗率」は必要ありません。

少人数プロジェクト

3〜5人程度のプロジェクトを考えてみましょう。 進捗を「完了したタスクの個数を数え」て測るのを、始めやすい規模です。

人間には、タスクを分けるのが上手い人と下手な人がいます。 上手い人には、必要なタスクのサイズを伝えれば、それで十分です。 下手な人は、下手なので、タスクを分ける手伝いをしてあげましょう。

タスクを分けるのが下手な人の多くは、タスクの中の「何がわからないのかわからない」ことが多いです。 地道に「わかっていること」と「わかっていないこと」を聞いて、わかっていないことを「不明な部分を明確する作業」としてタスクに切り出しましょう。 手間はかかります。

タスクの進捗率で報告させれば、タスクを分けるのが下手な人でも、とりあえず作業に着手して進捗報告ができます。 たとえば「設計資料のファイルを開いたから10%」、「hoge.cファイルを作ったから10%」。 「何がわからないのかわからない」人でも、わかることははあります。わかることの作業ができます。 作業さえすれば進捗率は増やせます。90%まで。

プロジェクト管理者としてみたら、作業者のスキルが多少不安だと思っていても、進捗率が増えている間は安心できます。 でも、なぜか90%まで進んだら止まります。タスクはなかなか完了しません。 90%ということはスケジュール上は後半のはずです、リカバリ策が取りにくい段階で、遅れていることが判明します。 結局困りますよね? 最初から、タスクを分ける作業を手伝っておけばよかったです😥

もっと悪くすると、その作業者は複数のタスクを担当しているかもしれません。 10個のタスクが90%まで進むんです。 プロジェクトの進捗率は90%です。 その実態は・・・怖いですね、恐ろしいですね。さよなら、さよなら👋

大規模プロジェクト

20人程度のプロジェクトを考えてみましょう。 プロジェクトの参加人数が多いと、タスク分割が下手な人も増えます。

プロジェクト管理者が、タスク分割が下手な人全員の「タスクを分ける作業」を手伝うのは不可能です。 頑張って「タスク分割できる人」を集めましょう。 少なくとも参加人数の1/4ぐらいタスク分割できる人を集めて、残りの人の「タスク分割のお手伝い」は「タスク分割できる人」達に見てもらいましょう。

「タスク分割のお手伝い」の工数は余計に掛かります。 全体の見積に、タスク分割の工数も入れておきましょう。 「プロジェクト管理工数」というやつです。

f:id:ledsun:20190515163636p:plain
タスク分割が得意な人にチームを率いてもらおう

*1: Cone of Uncertainty - Wikipedia によると「不確実性コーン」の概念は1958年の化学業界には既にあり、1981年にベームがソフトウエア産業に持ち込み、1997年にマコネルが「不確実性コーン」という呼び名にしたそうです

*2:この図はプロジェクトの本質とはなにか | 日経 xTECH(クロステック) から引用しました。もともとは「ソフトウェア見積り」(スティーブ・マコネル/日経BPソフトプレス)に掲載されている図です。

Ruby2.6 に関数合成オペレータが追加されたのでFizzBuzzで遊ぶ その2

ledsun.hatenablog.com

の続きです。 もうちょっと関数合成しがいのある書き方を思いつきました。

fizz = -> x {[x, ('Fizz' if x % 3 == 0)]}
buzz = -> (x) {[x[0], x[0] % 5 == 0 ? "#{x[1]}Buzz" : x[1]]}
filter = -> (x) {x[1] || x[0]}
do_fizzbuzz = filter << buzz << fizz

(1..15).each {|n| p do_fizzbuzz.call(n)}

fizzに変換する関数とbuzzに変換する関数を組み合わせるとfizzbuzzになるのは、ちょっと関数を合成している感があります。

RubyKaigiに行く意味とは?

結論をいうと「RubyKaigiに行くと自分が世界最高のエンジニアになる可能性」が得られます。どういうことでしょうか?

目的地を知らないとどうがんばっても目的地にたどりつけません。当たり前の話です。エンジニアとしての成長も一緒です。自分がエンジニアとして成長する上限は「自分の心の中にある理想のエンジニア像」で決まります。自分がどうなりたいか知らないと、それ以上には成長できません。

普通に会社に来て仕事をしていると「自分の心の中にある理想のエンジニア像」は先輩社員です。それ以外の人と会わなければ、知っている人の中から自分に一番合いそうな人を選ぶのが自然です。社内の一番優秀なエンジニアは、日本最高のエンジニアかというと、残念ながらそうではありません。だから、自分の心中にある理想のエンジニア像は、日本全体でみると、「中の上」や「上の下」のエンジニアになります。

さっき言ったように「自分のエンジニアのとして成長する上限は、自分の心の中にある理想のエンジニア像」で決まります。真面目に仕事して、真面目に自習しても、成長の上限は「中の上」か「上の下」のエンジニアです。経験年数によりません。5年やろうが10年やろうが、上限以上に成長できません。

もっとすごいエンジニアに成長するにはどうしたらよいでしょうか?「自分の心の中にある理想のエンジニア像」のレベルを上げます。そのためには何をすればいいでしょうか? 最高のエンジニアに出会って交流すればいいのです。そうすれば「自分の心の中にある理想のエンジニア像」が出会った最高のエンジニアになります。

で、RubyKagiがどういうカンファレンスかというと、世界最高のRubyエンジニアが集まるカンファレンスです。だから、RubyKaigiに参加して、勇気を出して最高のエンジニアに話しかければ「自分の心の中にある理想のエンジニア像」が世界最高のエンジニアになります。

というわけで結論。 RubyKaigiに行くと自分が世界最高のエンジニアになる可能性が得られます。 参加しても必ず「世界最高のエンジニア」になれるわけではありません。 でも、参加すると、その可能性は0でなくなります。