@ledsun blog

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

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スクリプトファイルに書くのと同じです。

参考

Rubyでテトリスを実装する その1

WSL2上にRuby開発環境を構築してテトリスを作ってみた #Ruby - Qiita を見ながらテトリスを動かそうとしています。 読む分には簡単にできそうと思いました。 実際に、写経して動かそうとしてみたら結構大変でした。 特に、Cursesアプリケーションのデバッグがとても大変です。

一気に全部動かすのは諦めます。 マイルストーンを切ることにしました。 まずは、ゲームの盤面を表示します。

ゲームの盤面を表示したスクリーンショット

ここまでのソースコードです。 GitHub - ledsun/tetoris at draw_wall

次は、一種類のテトリミノが落ちてくるのを目指します。

wez.termのテーマを変えた

変更後のテーマのスクリーンショット

matrixという中2っぽいテーマにしました。

ついでに .wezterm.lua に次の設定を追加してフォントを変更しました。

config.font = wezterm.font '源ノ角ゴシック Code JP'
config.font_size = 14.0

もともとは補完途中の文字が暗い赤で表示されて見にくかったのでテーマを変えようとしました。 途中で、wez.termのテーマではなく、fish-shellのcolor-schemeの設定だと気がつきました。 fish-shellの設定をいじっているうちに直ってしまいました。 しかし、どの設定がきいたのかわかりません・・・。

明確にわかっているのは、以前、GitHub - microsoft/inshellisense: IDE style command line auto completeを試したときの設定が .config/fish/config.fishに残っているのを消しました。 でも、inshellisenseを試す前から、色が暗い赤にだったので影響するとは思えないんですよね・・・。

相対URLの解決

URL: URL() コンストラクター - Web API | MDN を使うと、基準になるURLからの相対パスを解決したURLが得られます。 例えば、次のように使います。

// ベース URL:
let baseUrl = "https://developer.mozilla.org";

new URL("ja/docs", baseUrl);
// => 'https://developer.mozilla.org/ja/docs'

このコンストラクターの挙動を試しているときに、次の例を考えました。

new URL('a.rb', 'http://exapmle.com/lib').toString()
// => 'http://exapmle.com/a.rb'

このとき http://exapmle.com/lib/a.rb となって、libディレクトリの中を参照して欲しいのではないでしょうか?

これはJavaScript特有の動作なのでしょうか?

Rubyでも試してみました。 RubyではURLの結合には URI.join を使います。

require 'uri'

URI.join(URI.parse('http://exapmle.com/lib'), 'a.rb')
# => #<URI::HTTP http://exapmle.com/a.rb>

やはり lib が消えます。 統一された動作です。 もしかしてこれはどこかで決まっているのでしょうか?

るりまに以下の説明がありました。

[RFC2396] の Section 5.2 の仕様に従って連結します。

というわけでRFCを見てます。 https://datatracker.ietf.org/doc/html/rfc2396#autoid-33

6) If this step is reached, then we are resolving a relative-path reference. The relative path needs to be merged with the base URI's path. Although there are many ways to do this, we will describe a simple method using a separate string buffer.

 a) All but the last segment of the base URI's path component is
    copied to the buffer.  In other words, any characters after the
    last (right-most) slash character, if any, are excluded.

base URIの最後のスラッシュ以降に何かあれば、その部分はバッファ(解決後のURLを結合するための場所)に入れないそうです。 なるほど、JavaScriptRubyもこの動きをしていそうです。

この記事を書いている途中で気がつきました。 URLは文字列で、ファイルシステムではありません。 http://exapmle.com/lib がファイルかディレクトリかという区別は、文字列から読み取るしかありません。 すると

  • /で終わるのが、ディレクト
  • /の後ろに文字列が続いていたらファイル

みたいな、単純な方法で区別するしかない気がしてきました。 libディレクトリに見えるのは、背景知識があるから人間に判別できているっぽいです。

RubyJavaScript、ファイルとURLの間を行ったり来たりしていると、自分がどこにいるのか、よく見失います。