@ledsun blog

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

wasmtime-dotnetを使ってruby.wasmを実行する

前提条件

手順

プロジェクトを作成

dotnet new console -n wasmintro
cd wasmintro
dotnet add package Wasmtime

wasmtime向けruby.wasmバイナリを用意する

# 1. ダウンロード
Invoke-WebRequest `
  -Uri https://github.com/ruby/ruby.wasm/releases/latest/download/ruby-3.4-wasm32-unknown-wasip1-full.tar.gz `
  -OutFile ruby-3.4-wasm32-unknown-wasip1-full.tar.gz

# 2. 展開
tar -xvzf ruby-3.4-wasm32-unknown-wasip1-full.tar.gz

# 3. ruby.wasm を移動(元の ruby 実行ファイルをリネーム)
Move-Item `
  -Path "ruby-3.4-wasm32-unknown-wasip1-full/usr/local/bin/ruby" `
  -Destination "ruby.wasm"

https://github.com/ruby/ruby.wasm/?tab=readme-ov-file#quick-example-how-to-package-your-ruby-application-as-a-wasi-application に記載されているwasmtimeで動かす手順を参考にしてます。

ruby.wasmでは何種類かのwasmバイナリを作ります。 例えば rake npm:ruby-head-wasm-wasi で作る packages/npm-packages/ruby-head-wasm-wasi/dist/ruby.wasm を使うとうまく行きません。 このwasmバイナリにはJavaScriptでホストするための関数が定義されています。 このwasmバイナリをインスタンス化するには、これらの関数にC#側のスタブ関数を紐付ける必要があります。 紐付けないと次のようなエラーメッセージ表示されます。

Unhandled exception. Wasmtime.WasmtimeException: unknown import: `rb-js-abi-host::rb_wasm_throw_prohibit_rewind_exception` has not been defined
   at Wasmtime.Linker.Instantiate(Store store, Module module)
   at Program.<Main>$(String[] args) in C:\Users\led_l\wasmintro\Program.cs:line 18

wasmtimeで動かしやすいwasmバイナリを選ぶ必要があります。

ソースコード

using System;
using Wasmtime;

var engine  = new Engine();
var module  = Module.FromFile(engine, "ruby.wasm");

using var linker = new Linker(engine);
linker.DefineWasi();

using var store  = new Store(engine);
store.SetWasiConfiguration(
    new WasiConfiguration()
        .WithArgs("ruby", "--version")   // Rubyのバージョンを表示
        .WithInheritedStandardOutput()
        .WithInheritedStandardError()
);

var instance = linker.Instantiate(store, module);
instance.GetFunction("_start")?.Invoke();

実行する

dotnet run
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [wasm32-wasi]

良い戦略、悪い戦略

の参考図書にあったんですよ。せっかくだし買って読むかーと思ってアマゾン開いたら

さすが俺。 すでに購入済みです。 さっぱり記憶にありません。 新たな気持ちで読みます。

今のところの感想またはメモ

戦略とは有限のリソースを集中するためにある。戦略がないと組織のメンバーは思い思いの行動をする。それぞれの行動が逆の効果を生み打ち消し合ってしまう。

良い戦略には「診断」が必要。「診断」とは課題の構造を説明すること。構造の説明がないと、課題の解決策は多様になり過ぎる。例えば課題「利益が低い」は「売上を増やす」「経費を減らす」という矛盾した解決策を導く。

「診断」は的外れではいけない。しかし、完璧でなくてもよい。構造の中に推測が含まれていても問題ない。多少ずれていても、そこから導かれる行動が一貫していればよい。100の推進力がなくても80の推進力があれば競争力として十分機能する。

EM(エンジニアリングマネージャー)として気をつけていること

EM(エンジニアリングマネージャー)という役割にはさまざまな形態があります。ここでは、私が経験している「複数(おおよそ3つ)のソフトウェア開発チームを束ねる役割」を前提に、日々気をつけていることです。

1. 情報は早く、未確定でも共有する

EMの役割でまず大切にしているのは「情報の展開の速さ」です。 確定情報だけを共有していると、「決まるまで思考停止」となりメンバーが受け身になります。 その結果、自分で考えないようになりまいます。

未確定の情報も「これはまだ決まっていませんが、こういう議論があります」と伝えることで、チームの思考を促し、状況への参加意識を高められます。 情報を早く、正確に、そして“未確定は未確定として”伝える。 これだけで価値観や姿勢が伝わることもあります。

情報共有はできるだけ対面(またはそれに近い場)で行い、その場で質問を受け付けます。 丁寧に説明しているつもりでも予想外に誤解される事があります。 即座に訂正・補足できます。

2. 不満や誤解には1on1で向き合う

チーム内で不平や不満が見えるときは、放置せず1on1の時間をとって丁寧に聞きます。 背景を聞いて、誤解があれば説明し直します。

不満の「ぶつけ方」も一緒に整理します。 感情を人にぶつけるのではなく、問題に向けるように促します。 リフレーミング(見方の切り替え)によって、問題を前向きに捉えられるよう手助けします。

3. コミュニケーションは自然に発生しない、だから設計する

人と人の相談ごとは、自然発生的には起きません。 EMはコミュニケーションの“マネージャー”でもあります。 意図的に相談できる雰囲気や場をつくる必要があります。

チーム内で相談が生まれないと思ったら、相談する時間をとってファシリテーターをやります。 メンバー同士だけでも相談すれば問題が解決できると知れば、自発的に相談するようになります。

4. 「知っている人」に引き上げる

「グルーミングコミュニケーション(軽い雑談やちょっとした気遣いの会話)」をメンバー任せにすると、仲のいい人だけが話す“仲良しグループ”が生まれます。 仲良しグループに入らない人が生まれます。 チームメンバーに「同じ会社に所属しているけど、よく知らない人」がいる状態は危険です。 「あの人、なんかよくわかんないんだよね」という距離感は、チームの一体感を損ないます。 職場として望ましくありません。

仕事である以上、全メンバーがグルーミングコミュニケーションをとれる場を設計し、強制的にでも接点を持たせます。 「あの人の名前は知ってる」「話したことがある」くらいの関係をメンバー全員がお互いに築けるよう引き上げます。

5. 無言の分断を予防する

チームのどこか一箇所でも壊れると、チーム全体がギスギスしてきます。 対立があればEMが仲裁すれば済むことです。 “無言の分断”や“自然発生的な孤立”が起きると、「もうちょっと仲良くしてよ」と言うくらいしかできません。 あとから解消するのは困難です。 予防を心がけます。


技術や進捗のようなハードスキルのサポートはチームリーダーに任せます。 「人と人との関係」のようなチームリーダーの手からこぼれる問題を積極的に拾います。

RubyでWebSocketサーバー

RubyでEchoサーバー - @ledsun blog の経験を踏まえまして、改めて RubyでシンプルなWebSocketサーバーをゼロからつくってみたに取りくみます。 TCPサーバーの部分の理解が進みます。 HTTPで使ったTCPコネクションをそのままつかって、送受信データをWebSocketフレームに変えたことがわかります。 よしこれならできそう!

require 'socket'
require 'digest/sha1'

server = TCPServer.new 'localhost',
                       2345

loop do
  # ここら辺はecho_server.rbと同じ。純粋なTCP通信
  socket = server.accept

  # HTTPリクエストを読み込む
  http_request = ""
  while (line = socket.gets) && (line != "\r\n")
    http_request += line
  end
  puts http_request

  # WebSocketリクエストかどうかを判定する
  unless match = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
    puts "Not a websocket request"
    socket.close
    next
  end

  ws_key = match[1]
  puts "ws_key: #{ws_key}"

  response_key = Digest::SHA1.base64digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
  puts "response_key: #{response_key}"

  handshake_response = <<~EOS
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: #{response_key}
    \r\n
  EOS

  # ソケットそのものはHTTP通信で使われているものと同じ
  socket.write handshake_response
  puts 'Handshake response sent'

  # ここからはWebSocket通信
  first_byte = socket.getbyte
  fin = first_byte & 0b10000000
  opcode = first_byte & 0b00001111

  raise 'fin bit is not set' unless fin
  raise 'opcode is not a text' unless opcode == 0x1

  second_byte = socket.getbyte
  is_masked = second_byte & 0b10000000
  payload_size = second_byte & 0b01111111

  raise 'mask bit is not set' unless is_masked
  raise 'payload size > 125 is not supported' unless payload_size <= 125

  puts "Payload size: #{payload_size}"

  mask = 4.times.map { socket.getbyte }
  puts "Mask: #{mask}"

  data = payload_size.times.map.with_index { socket.getbyte ^ mask[_2 % 4] }
  puts "Data: #{data.pack('C*')}"

  # クライアントにデータを返す
  response_message = "Loud and clear!"
  response = [0b10000001,
              response_message.size,
              response_message
            ].pack("CCA#{response_message.size}")
  puts "Response: #{response.unpack('C*')}"
  socket.write response


  socket.close
end

というわけで

ブラウザでInvalid Frame Headerが出ているスクリーンショット

動きませんでした。 なんで・・・。

コネクション周りはわかりました。 ハンドシェイクの必要性がよくわかりません。 この辺は RFC 6455 - The WebSocket Protocol を読むと良さそうです。

20241103 追記

動かない原因はハンドシェイクのレスポンスでした。

  handshake_response = <<~EOS
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: #{response_key}
    \r\n
  EOS

最後に\r\nというゴミがついています。 これを消せば動きます。

ゴミがあったためクライアントはハンドシェイクレスポンスの終わりを上手く検知できていなかったようです。

大人数のデイリーミーティング

今、チームのデイリーミーティングの参加者が14人です。 アジャイルソフトウェア開発の文脈の朝会としては人数が多めに感じます。 それもそのはず、4つの開発チームで合同でやっています。 このチームは発足当初は1開発チームでした。 売上の都合で、あれよあれよと分裂しました。 現在は4開発チームです。

開発チームが分かれたとき、デイリーミーティングをやめる選択もありました。 やめませんでした。 その理由は「咳さんのチームは10人越えててもデイリーミーティングが上手く回っている」と聞いていたからです。 実際、人数が多いので「ちょっと発言しにくいかな?」と感じるときもありますが、おおむね上手く回っているように感じます。

その後もずっと続けています。 なんで続けているのか上手く言語化出来ていませんでした。 リードについて - @m_seki の を見て、なぜなのか思い出しました。

メンバーの誰かがチームを変化させることがあります。いつもの 1 日 のちょっとした場面で、チームの安定を壊すような「言いにくいこと」を言うのです。 

たまに大きく変化するより、毎日ちょっとずつ変化してる方が安定します。 「人間が直立しているとき、動いていないつもりで揺れている」のに似ています。 開発チームがわかれていると、僕からはチームと開発チームの隙間にある何が見えません。 リーダー役の一部の代表者だけで「言いにくいこと」に、すばやく気がつくのは難しいです。 リーダーが「言いにくいこと」気がついてから変化させようと変化が大きくなります。 直立でなくて、ステップを踏みだします。

あらためて、僕のチーム運営のロールモデルは「咳さんのチーム」なのでした。

docker composeコマンドのcompose spec準拠はcompose-goモジュールを使って実現されている

WARN[0000] /home/ledsun/pubdictionaries/docker-compose.yml: version is obsolete

というワーニングを追いかけたら 「docker composeコマンドのcompose spec準拠はcompose-goモジュールを使って実現されている」ことに気がつきました。 その記録です。

警告に出会う

docker compose コマンドを実行したら次のように警告がでました。

docker composeコマンドで警告が出ているスクリーンショット

WARN[0000] /home/ledsun/pubdictionaries/docker-compose.yml: version is obsolete

言っていることは簡単です。 対応は docker-compose .yml ファイルの version フィールドを消せばよいだけです。 でも、一次情報を確認しておきたいです。

長いissueコメントの謎

github.com

docker compose コマンドの v2.25.0 から出るようになったみたいです。 コメントのやりとりをみると

How will docker-compose.yml files for versions 2.x and 3.x be distinguished without the version tag?

Version 2と3を区別しなくていいの?

The current file format is more of a "descendant" of the 2.x series and the 3.x series is, again, only relevant for docker stack, where 2.x had no ... relevance.

docker stackしか関係なくて、docker stackはvesion 3しか扱わない。

なるほど、納得できます。 が、なんかコメントがすごく長く続いています。 飛ばし読みすると「急にログに警告が出てきてびっくりした」人がたくさん居るみたいです。 なぜでしょう?

リリースノートには記載がない

確かに、Release v2.25.0 · docker/compose · GitHub には、この警告がでることについて書いてありません。 それはびっくりする人もいそうです。 僕も、リリースノートに関連情報が書かれていないと「本当に消して良いのか」不安を感じます。

リリースノートには記載がない理由

前提として

Docker Compose V2で変わったdocker-compose.ymlの書き方

Docker Compose V2はCompose Spec[1]に準拠している

そうです。

2024年4月の version-patch by aevesdocker · Pull Request #489 · compose-spec/compose-spec · GitHub で、Composer Spec上で version フィールドが obsolete になりました。 docker comopse v2.25.0 は、3月にリリースされています。 未来の情報はリリースノートに書けません。

なぜdocker comopse v2.25.0に version フィールドの obsolete が反映されているのか?

https://github.com/docker/compose/compare/v2.24.7...v2.25.0#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6L9 を見ると github.com/compose-spec/compose-go/v2 v2.0.0-rc.8.0 を参照していたのが、github.com/compose-spec/compose-go/v2 v2.0.0に変わっています。 rcが外れています。

Release v2.0.0 · compose-spec/compose-go · GitHub を見ると

warn user version is obsolete by @ndeloof in #575

があります。 warn user `version` is obsolete by ndeloof · Pull Request #575 · compose-spec/compose-go · GitHub です。 この変更とリリースは3月に行われています。 面白いことに、compose-specより先にcompose-goが修正されていたようです。

まとめ

docker composeから見るとcopomse-goのRCを取っただけです。 以前からある「compose specに準拠する」ポリシーは変わっていません。 docker composeにとっての大きな変更ではなさそうです。

また、copomse-goの変更を全部みてリリースノートに書くのも不毛に感じます。

なるほど「compose specに準拠する」の実現方法が「copomse-goを使う」であることを知らないと、混乱しそうです。 僕の中のメンタルモデル(docker compose の理解)が更新されました。 1つ賢くなれたようです。

参考

RubyKaigiで心の洗濯

RubyKaigiで自分よりすごい技術者、技術に真剣に取り組んで深い洞察を得ている技術者を、目の当たりにすると、自分の慢心に気がつきます。

会社では、それなりに上位の技術者です。色々な技術を少しずつ取り組んでは素早く判断をしている日々です。それはそれで必要です。ですが、この行為は、心のどこかに自分の技術領域の境界線を作っているようです。

RubyKaigiでは、自らの技術領域に制限を設けず、新しい領域へ切り込んでいく技術者の人たちが見れます。直接しゃべるとさらに深い洞察が聞けます。その姿を見て言葉を聞いて、自分の心の中の技術領域の境界線が幻想だったと気がつきます。

同じ技術領域に取り組む必要はありませんが、自分も自分なりに技術領域の境界線を取り払って挑戦していきたいな、そしてあの技術者たちの仲間入りをしたいなと思います。

一年に一度こういうイベントがあるの良いですね。日本の、いや、世界の技術者の成長を生む畑なのだと感じます。

一番の下手くそでいよう

情熱プログラマーにあるエピソードです。 僕はこの話はあまり好きではありません。 しかし、一理あるところもあるようです。

一つの会社の中で一番上手いプログラマーになると、どうも手癖が抜けなくなるようです。 手癖というのは、環境に最適化されているので、その環境では効率的です。 初めて見た人にはとっつきにくいです。

また、この手の手癖プログラマーはドキュメントをあまり書きません。書くんですけど、コンセプトの説明に背景情報が抜けてたり、イマイチもの足りないことがあります。 多くの場合、一番腕が立つプログラマーにはドキュメントよりコードを書かせた方がよいのはそうです。

手癖から抜け出るにはOSS活動が良さそうです。仕事で出会わないコンテキストでコードを書く経験が得られます。OSSではドキュメントでの説明は不可欠です。ドキュメントをわかりやすくするにはAPIデザインもわかりやすくする工夫が必要です。

なるほど、情熱プログラマーの言うように、会社の中で上位の強いプログラマーになったら、もっと腕を磨きたくなったら、OSS活動をするのが良さそうです。

設計作業は見積もれない

設計作業はふたつの作業から成り立っています。

  1. 調査作業
  2. アウトプットをまとめる作業

後半の「アウトプットをまとめる作業」はまあまあよい精度で作業時間を見積もれます。前半の「調査作業」は見積もりが大きく外れることがあります。

「設計作業」とひとまとめに呼ぶと見積もれる気がしてきます。意識して、調査がどれくらい必要そうかに気を掛けましょう。

また、見積もりが難しい作業はタイムボックス方式が進捗管理しやすいです。作業完了を待たずに一定期間ごとに作業状況をヒアリングするのがおすすめです。

MySQLのロックに関する調査メモ その4

MySQLのロックに関する調査メモ その3 - @ledsun blog の続きです。

SHOW ENGINE INNODB STATUS\Gで出力されるロック情報を眺めていたら意味がわかってきました。

まずテーブルに格納されているレコードの情報です。

 select * from mysqlcasual;
+----+------+------+
| id | col1 | col2 |
+----+------+------+
|  1 |    2 |    0 |
|  2 |    4 |    0 |
|  3 |    6 |    0 |
|  4 |    8 |    0 |
|  5 |   10 |    0 |
|  6 |   12 |    0 |
|  7 |   14 |    0 |
|  8 |   16 |    0 |
+----+------+------+

1つ目のレコードを選択

begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 2 FOR UPDATE;

三つのロック情報が出てきます。 一つ目から見ていきます。

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7731 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000001; asc     ;;

index idx_col1 of table はインデックス idx_col1 に対するロックであることを表しています。 lock_mode Xネクスキーロックであることを表しています。無印がネクスキーロックです。

 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000001; asc     ;;

はロックしているレコードを表してます。 80000002はcol1カラムの値が2、80000001はidカラムの値です。

選択するレコードを変えてみます。

2つ目のレコードを選択

begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 4; hex 80000002; asc     ;;

8000000480000002になりました。

次のロック情報を見てみましょう。

2つ目のロック情報

RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

index PRIMARY of tableとあるので、プライマリーインデックスへのロックです。セカンダリーインデックスをロックするとプライマリーインデックスもロックされるやつですね。 lock_mode X locks rec but not gapは、レコードロックを表してます。ギャップロックでないものがレコードロックです。

 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

800000028000000480000000はレコードの値を表していそうです。 00000000162aaa0000011e011dは謎です。プライマリーインデックスとidex_col1を表しているのかもしれません。 上は2つ目のレコードです。1つ目のレコードは下になります。

 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e0110; asc        ;;
 3: len 4; hex 80000002; asc     ;;
 4: len 4; hex 80000000; asc     ;;

00000000162aは変わりません。 aa0000011e011daa0000011e0110になりました。 うーん、これはちょっとよくわかりません。

3つ目のロック情報

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

index idx_col1 of tableなのでidx_col1へのロックです。 lock_mode X locks gap before recなので、ギャップロックです。

 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

は、3つ目のレコードを表しています。 ギャップロックなので、3つ目のレコードの手前までをロックしているようです。

もしかして、1つ目のネクスキーロックを、2つ目のレコードロックと3つ目のギャップロックで実現していることを表現しているのでしょうか?

MySQLのロックに関する調査メモ その3

MySQLのロックに関する調査メモ その2 - @ledsun blog の続きです。 概念だけ勉強しても理解に限度があります。 実際にロックを起こして観察してみました。

InnoDBのロックの範囲とネクストキーロックの話 - かみぽわーる を参考にしました。 MySQL Shellを起動しMySQLに接続します。

\connect --mysql --user root
\use sandbox
\sql

sandboxスキーマは練習用に作ってあったものを使い回しています。 テーブルを作成します。

CREATE TABLE `mysqlcasual` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `col1` int(11) NOT NULL,
  `col2` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_col1` (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `mysqlcasual`(`col1`) VALUES (2),(4),(6),(8),(10),(12),(14),(16);

MySQL 5.7 以降では、ここからの手順が変わっています*1

CREATE TABLE innodb_lock_monitor(a int) ENGINE=InnoDB;

の代わりに

SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

します。 また、共有ロックではロックが表示されませんでした。

BEGIN;
SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 LOCK IN SHARE MODE;

の代わりに

BEGIN;
SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;

を試しました。

SHOW ENGINE INNODB STATUS\G

を実行するとTRANSACTIONSセクションにロック情報が表示されました。

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000004; asc     ;;
 1: len 4; hex 80000002; asc     ;;

ネクスキーロック

RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000000162a; asc      *;;
 2: len 7; hex aa0000011e011d; asc        ;;
 3: len 4; hex 80000004; asc     ;;
 4: len 4; hex 80000000; asc     ;;

インデックスレコードロック?

RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000006; asc     ;;
 1: len 4; hex 80000003; asc     ;;

ギャップロック?

出力された情報の読み方が謎です。

MySQLのロックに関する調査メモ その2

MySQLのロックに関する調査メモ その1 - @ledsun blog の続きです。

MySQLのロックについて - SH2の日記 のPDFを読みました。 面白かったです。

MySQL(というかInnoDB)の最初のスタート地点が「絶対にファントムリードさせたくない」なようです。 つまり一度トランザクションをはじめたら、他のトランザクションでの操作を見たくありません。 例えば、自トランザクションで見ている行と行の間に、他トランザクションからInsertされたくありません。 ということは行単位のロックでは足りなくて、自トランザクションで見ている範囲をロックしたいです。 これが、ギャップロックとネクスキーロックのようです。

今の段階ではギャップロックとネクスキーロックの違いはわかっていません。 特に、なぜネクスキーロックをしなければいけないのかよくわかりません。

どうもこの辺の思想は「トランザクション処理 概念と技法」のでしょうか?

もう一つ、MySQLの面白い特徴は、プライマリーインデックスのリーフに行があることです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.6.2.1 クラスタインデックスとセカンダリインデックス

すべての InnoDB テーブルは、行のデータが格納されているクラスタ化されたインデックスと呼ばれる特別なインデックスを持っています。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に次のように書いてあります。

レコードロックは、インデックスレコードのロックです。

どうもこういうことを説明しているみたいです。

また、セカンダリーインデックスをロックするときは、セカンダリーインデックスとプライマリーインデックスのインデックスレコードを両方ロックするそうです。

MySQLのロックに関する調査メモ その1

MySQLのロックのついては MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に書いてあるはずです。 読んでもいまいちよくわかりません。

MySQL/MariaDBとTransactdのInnoDBロック制御詳細 その1 - BizStationブログを合わせて読むとわかりやすいです。 どうやら公式ドキュメントは内部実装の立場で説明しているようです。 たとえば、後者には次のような記述があります。

2つのトランザクションが同じレコードのロックを取得しようとしたときのロックの可否を表したのがInnoDBソースコードの以下の部分です。

このソースコーコメントをマトリックスにしたものが公式ドキュメントに載っています。 ですので、公式ドキュメントは実装者視点で読む必要があります。 利用者視点で読むと理解が難しいようです。

後者の文章には、実装者視点の解説があるので理解の手引きになります。

Windows Formの同期イベントハンドラーから入れ子になった非同期関数を呼ぶとデッドロックする

await と Task.Result によるデッドロックによるとWindows FormでTaskを使ったときにデッドロックするケースがあるようです。 検証してみます。

デッドロックするソースコード

試しに次のようなコードを書いてみました。

namespace AwaitDeadLockEight
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            label1.Text = "";
            string str = TimeCosumingMethod().Result;
            label1.Text = str;
        }

        private async Task<string> TimeCosumingMethod()
        {
            await Task.Delay(3000);
            return "TimeCosumingMethod の戻り値";
        }
    }
}

ボタンを押すと確かに固まります。 await Task.Delay(3000);コメントアウトすると動きます。

なので、非同期関数を呼ぶだけでは問題ないです。 非同期関数のなかで非同期関数を呼ぶと固まります。

Task.Delay(3000).Wait();でも固まります。 awaitを使うかどうかは関係ないようです。

デッドロックしないパターン

次のように呼び出すイベントハンドラーを非同期関数にすると期待通りに動きます。

private async void button1_Click(object sender, EventArgs e)
{
    label1.Text = "";
    string str = await TimeCosumingMethod();
    label1.Text = str;
}

private async Task<string> TimeCosumingMethod()
{
    await Task.Delay(3000);
    return "TimeCosumingMethod の戻り値";
}

不思議な挙動です。何が起きているのでしょうか?

今のところの仮説

ここからさきは、今のところの仮説です。 イマイチ上手く説明できていませんが、現在の理解を書いておきます。

  1. .NETにはSynchronizationContextというものがあり、非同期関数の処理結果をUIスレッドのコンテキストに戻している
  2. コンテキストを戻すとは、UIスレッドのイベントループのタスクキュー(?)にタスクを積んでいる
  3. スレッドは自タスクキューをLIFOで処理する
  4. 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
  5. 「TimeCosumingMethodの結果を処理する」タスクBがUIスレッドのタスクキューに積まれる
  6. LIFOでタスクBから処理しようとするが、タスクAが完了していないので、デッドロックする

でも、この説明だとイベントハンドラーを非同期関数にしたときに上手く動く理由が説明できません。 そもそも 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる は本当でしょうか? ワーかスレッドのキューに積まれるないのでしょうか? 謎です。

参考

オートローダー設計調査

ruby.wasmでオートロードする - @ledsun blogModule#const_missingをつかってオートローダーを書いてみました。 Module#const_missingAPIで、汎用的なオートローダーを書くのは少し難しそうなことがわかりました。 難しそうですが、どれくらい難しいのかよくわかっていません。 そこで、先人の知恵に頼ります。

具体的には、Rails 5.2 から6.0でオートローダーをclassicからzeitwerkに変更した出来事を調べます。 Rails 5.2までのオートローダーはModule#const_missingを使って実装されています。 Rails 6.0以降のオートローダーzeitwerkはKernel.#autoloadを使って実装されています。 なぜでしょうか?

ClassicローダーからみるModule#const_missingを使ったオートローダーの問題点

classicローダーのModule#const_missingを使った実装は、おおむね動いていましたが細かい問題がありました。 わかりやすい例は Understanding Zeitwerk in Rails 6 | by Marcelo Casiraghi | Cedarcode | Medium で、説明されています。

次のような例が挙げられています。

# app/models/user.rb
class User < ApplicationRecord
end

# app/models/admin/user.rb
module Admin
  class User < ApplicationRecord
  end
end

# app/models/admin/user_manager.rb
module Admin
  class UserManager
    def self.all
      User.all # Want to load all admin users
    end
  end
end

UserManager内でUserを呼び出したときに、Module#const_missingでは、呼び出された物がUserかAdmin::Userかわかりません。 Module#const_missingの引数には、どちらの場合であってもシンボル:Userが渡されます。

その他の多くの問題がRails 5.2のRailsガイドで列挙されています。 Module#const_missingでオートローダーを実装するには、とても多くの問題があるようです。

Zeitwerkのとった解決方法

これらの問題を解決するためにzeitwerkはKernel.#autoloadを使って再実装されました。 Kernel.#autoloadはモジュールを読み込むパスを指定できます。 モジュールが必要になったときに指定したパスをつかってKernel.#autoloadします。 つまり、オートロードです。 正確にはオートロードに使うヒントをRuby VMに渡します。

zeitwerkでは、起動時にsetupメソッドを使ってルートディレクトリからモジュールのファイルパスを収集し、Kernel.#autoloadを呼び出します。 Kernel.#autoloadを使うには、モジュール呼び出しより前に、モジュールの実体ファイルのパスを調べます。

ruby.wasmへの応用を考える

ruby.wasmでは、この方式は厳しいです。 サーバーに対してパス探索をかけたくありません。 自動的にモジュールのパスを集めるのは難しいです。

さらに前の段階でモジュールパスを調査しておいて、import mapsのようにブラウザに渡せばできるでしょう。 ですが、オートロードがうれしいのは設定が要らない点です。設定より規約(Convention over Configuration、CoC)です。 マップを作ってブラウザに渡すのは設定です。 Kernel#require_relativeを各Rubyスクリプトファイルに書くのと同じです。

参考