@ledsun blog

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

Ractor間のメッセージ送受信 Pull型とPush型

やることは前回と同じです。Ractorの構成を組み替えてみました。

Pull型とPush型

Ractorのメッセージ送受信にはPull型とPush型があります。 前回、メインスレッド(?)でRactorインスタンスをselectしてイベントを待ちました。Pull型です。

f:id:ledsun:20200422213039p:plain
Pull型

今回、描画用のRactorインスタンスrendererを用意して、他のRactorインスタンスからイベントをsendします。Push型です。

f:id:ledsun:20200422213126p:plain
Push型

完成形

require 'io/console'

# 結果を描画するRactor。inputとclockの存在を知りません。
renderer = Ractor.new do
  val = 0
  loop do
    # メッセージを待ちます
    msg = Ractor.recv

    # メッセージがユーザー入力だったら値をリセットします。
    val = 0 if msg == :reset

    # カウントアップします。
    val += 1
    # 行をクリア
    print "\e[2K"
    # 行頭へ移動
    print "\e[0G"
    # 出力
    print val
  end
end

# ユーザー入力を待つRactor。引数でrendererを受け取ります。
input = Ractor.new renderer do |renderer|
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    # メッセージを送ります
    renderer << :reset
  end
end

# クロックイベントを発生するRactor。引数でrendererを受け取ります。
Ractor.new renderer do |renderer|
  loop do
    # メッセージを送ります
    renderer << nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# Ractorの終了を待つ?
Ractor.recv

はまったところ

Ractor.recvを書かずに実行したら、何も出力されずにプログラムが終了しました。 Ractorインスタンスが動き出す前に、メインスレッド(?)が終了したようです。

感想

Pull型とPush型では、Ractor間の依存関係が逆転しました。 この規模では、ふーんて感じです。

Ractorを使ったTUIプログラムの一歩

前回のユーザー入力とクロックを両方待つプログラムを少し改造します。

  • Enter以外の入力を受け取る
  • カーソルを移動せずに書き換える

後者はRactorは関係ないです。

Enter以外の入力を受け取る

任意の1文字を受け取るためにSTDIN.getchを使いたいです。 STDINは共有不可能オブジェクトです。 次のようなRactorを書くとcan not access non-sharable objects in constant STDIN by non-main Ractors (NameError)が発生します。

# ユーザー入力を待つRactor
input = Ractor.new do
  while  "\C-c" != STDIN.getch
    Ractor.yield :reset
  end
end

共有不可能オブジェクトをRactorに渡すときはmoveします。

# ユーザー入力を待つRactor
input = Ractor.new do
  io = Ractor.recv
  while  "\C-c" != io.getch
    Ractor.yield :reset
  end
end

input.send STDIN, move: true

カーソルを移動せずに書き換える

エスケープシーケンスを使ってカーソルを操作します。

  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val

完成形

require 'io/console'

# ユーザー入力を待つRactor
input = Ractor.new do
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val
end

次のように動きます。一定のペースでカウントアップし、何か入力すると1に戻ります。

f:id:ledsun:20200421225620g:plain
プログラム実行時のスクリーンショット

参考

Ractorを試す 改訂版

macOShttps://github.com/ko1/ruby/tree/ractorソースコードをビルドする方法を改善しました。

インストール先

コンパイルしたRubyをmake installしたら「PCの環境が壊れるかも」とビビっていたところ、 id:hanachin さんに ./configure --prefix="$HOME/.rbenv/versions/ractor" するとrbenvの1実行環境として扱えると教えてもらいました。

OpenSSL

makeのログを見てたら、次のようなエラーも出ていました。

*** Following extensions are not compiled:
openssl:
    Could not be configured. It will not be installed.
    /Users/shigerunakajima/ractor/ext/openssl/extconf.rb:99: 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.

--with-openssl-dir=/usr/local/opt/openssl@1.1/オプションをつけて、HomebrewでインストールしたOpenSSLのディレクトリを指定します。

完成形

CFLAGS=-Wno-error=shorten-64-to-32 ./configure --prefix="$HOME/.rbenv/versions/ractor" --with-openssl-dir=/usr/local/opt/openssl@1.1/
make
make install

まだ、次のようなエラーが表示されます。

installing bundled gems:            /Users/shigerunakajima/.rbenv/versions/ractor/lib/ruby/gems/2.8.0
Traceback (most recent call last):
    6: from ./tool/rbinstall.rb:957:in `<main>'
    5: from ./tool/rbinstall.rb:957:in `each'
    4: from ./tool/rbinstall.rb:960:in `block in <main>'
    3: from ./tool/rbinstall.rb:889:in `block in <main>'
    2: from ./tool/rbinstall.rb:889:in `foreach'
    1: from ./tool/rbinstall.rb:894:in `block (2 levels) in <main>'
./tool/rbinstall.rb:818:in `load_gemspec': [/Users/shigerunakajima/ractor/.bundle/gems/minitest-5.14.0/minitest.gemspec] isn't a Gem::Specification (NilClass instead). (TypeError)
make: *** [do-install-all] Error 1

Rubyコマンドのインストールはできているみたいです。

~ ls ~/.rbenv/versions/ractor/bin/ruby
/Users/shigerunakajima/.rbenv/versions/ractor/bin/ruby*

プログラムの実行だけなら、minitestはなくてもよさそうなので、先に進みます。

動作確認

システムのRubyではRactorを使ったRubyスクリプトの実行に失敗します。

~ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
Traceback (most recent call last):
sum_1_to_10.rb:2:in `<main>': uninitialized constant Ractor (NameError)

ractor版を使うと実行できました。

~ rbenv shell ractor
~ ruby --version
ruby 2.8.0dev (2020-04-08T16:31:17Z ractor 2658fc668d) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
55

Ractorをつかうプログラムの練習 2つのイベントを待つ

Ractorをつかって、ユーザー入力とクロックを両方待つプログラムを書いてみます。 練習中であり、Ractorを使ったイケているソースコードではありません。

次のプログラムを実装します。 1秒毎にカウントアップします。 ユーザーがEnterキーを押したらカウンターをリセットします。

完成形

# ユーザー入力を待つRactor
input = Ractor.new do
  loop do
    gets
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 1
  end
end

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  p val
end

失敗作

処女作

ユーザー入力があったときだけ、カウンターから帰ってくる値を0に戻さそうと思って書きました。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  loop do
    Ractor.yield Ractor.recv + 1
    sleep 1
  end
end

counter << 0
loop do
  _, v = Ractor.select input, counter
  p v
  counter << v
end

実際はこんな感じにうごきます。

~ docker run --rm -it -v (pwd):/ractor wakaba260/ruby-ractor-dev ruby ractor/input_and_timer.rb
1
2

0
3
1
4

リセットされた値とされていない値が両方出力されます。 Ractorはキューを持っているので、counter << vした値は上書きされることなく保存されています。

debounceを実装

最新の値だけがほしいので、debounceが実装できれば良さそうです。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  v = 0
  loop do

    v = Ractor.recv
    Ractor.yield v + 1
    sleep 1
  end
end

debounce = Ractor.new do
  prev = Time.now.to_i
  loop do
    v = Ractor.recv

    now = Time.now.to_i
    if now - prev > 0.1
      p v
      prev = now
    end
  end
end

counter << 0
debounce << 0
loop do
  _, v = Ractor.select input, counter
  counter << v
  debounce << v
end

実際には、これも期待通りには動きません。 毎回counter << vしているので、counterのキューに全部の値が貯まります。

どうやらcounterで1秒に1加算しているのがよくなさそうです。 counterを1秒毎にイベントを発火するclockにし、mainで加算しました。

感想

Ractorインスタンスからグローバルな値を変更できないので、Ractor.yieldを使って値を親に返す必要があります。 その代わり、レールに乗りさせすれば、Ractor.selectを使って、イベントの待ち合わせを簡単に書けます。

スレッドで書くと次のようになります。

queue = Queue.new

input = Thread.new do
  loop do
    gets
    queue << :reset
  end
end

clock = Thread.new do
  loop do
    queue << nil
    sleep 1
  end
end

val = 0
loop do
  flag = queue.pop
  val = 0 if flag == :reset
  val += 1
  p val
end

今回の使い方からは「最初からキューを持っているスレッド」というイメージを持ちました。

Ractorを使ってプログラムを書く練習    

注意:Ractorならではとか、計算効率の良さとか考えていません。

1から10までを足す

# 最終結果送り先のRactor
CR = Ractor.current
r = Ractor.new { CR << Ractor.recv }

1.upto(10) do |i|
  # 数を足すRactorを作ってポインタを置き換える
  r = Ractor.new(r, i) do |next_r, i|
    # 担当の整数を足して次のRactorに送る
    next_r << Ractor.recv + i
  end
end

r << 0 # 計算開始。初期値を送る
p Ractor.recv # 結果を出力

<<でRactorインスタンスに値を送ります。 Ractor.recvで値を受け取ります。 並列に動いていません。

10個のRactorを並列に動かす

# 10個のRactorを作る
ractor_list = (1..10).map do |i|
  Ractor.new i do |i|
    i
  end
end

# 10個のRactorの終わりを待つ
until ractor_list.empty?
  # いずれかのRactorの結果を待つ
  r, val = Ractor.select(*ractor_list)
  p val

  # 実行の終わったRactorは、もう待たない
  ractor_list.delete r
end

Ractor.selectでRactorインスタンスの終了を待ちます。 Ractor.newに渡したブロックの戻り値が得られます。

参考

https://github.com/ko1/ruby/blob/ractor/ractor.ja.md

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としては冗長すぎて、それも不便でしょ?