Ruby フルタイムコミッタの仕事報告 2022年12月

こんにちは、hsbt です。先日、Podcast を収録した際に紹介した PS5 のウィッチャー3がまだ終わらずに引き続き時間を費やしています。いつになったら終わるのでしょう。

今日は前回の Ruby フルタイムコミッタになってからやったこと、の定期シリーズということで 2022年12月にフルタイムコミッタとして行った仕事の一部をご紹介します。

ソフトウェア開発におけるアーカイブの重要性

Ruby に限らずソフトウェア開発において、コードの Why と Why not を把握できる状況を維持することは、自分だけではなくソフトウェアに関わるチームメンバー全員にとって、継続的な開発を行うための重要な取り組みです。

t_wada さんが記載しているように、コミットログやコードへのコメントによってそれらが補完されていることが、コードのリポジトリからのみ取得することができるため理想的ではありますが、ソフトウェア開発はコードのリポジトリだけではなく、 GitHub や Redmine のような issue tracker や関係者とやりとりを行ったメール、昨今だと Twitter や Reddit など非公式の場においても、ソフトウェアの Why や Why not について言及がなされることがあります。

プログラミング言語の Ruby はまつもとさんによって開発され、 2023/2 に 30 周年を迎えるプログラミング言語です。30年前といえば筆者はまだ中学生になったかならないか、という時代で Windows 95 の発売前かつ家庭用のインターネットも普及していない時代でした。当時は GitHub のようなソフトウェア開発向けの SaaS などはなく、ソフトウェアとしての Ruby の仕様についての議論は主にメールと複数の参加者に同時にメールを配送するメーリングリストを中心に行われていました。

初期の Ruby はメールにコードの断片であるパッチを添付し、まつもとさんがパッチを手元のマシンにダウンロードし、パッチを取り込み、新しいバージョンをリリースするというタイミングで tar.gz のアーカイブをメールに添付するということが行われていました。Git や Subversion のようなバージョンコントロールシステムも導入されておらず、どのように開発をしていたのかの詳細は現代からはとても想像できないような状況です。

つまり Ruby の仕様についてのディスカッションや意思決定を、GitHub や Redmine の導入前のものを改めて全て把握するためには、メールのアーカイブを探るということが唯一残されている手段となります。

メーリングリストアーカイブの消失

さて、そのようなメールとメーリングリストのアーカイブですが、Ruby では blade.nagaokaut.ac.jp、通称 blade と呼ばれるサーバーに構築されていました。これは長岡技術科学大学の原先生が作成したプログラムとともに構築されており、英語と日本語、Ruby の開発と利用者それぞれのメーリングリストのすべてのデータを保持して、ビューアを提供しているものでした。この blade は Ruby の開発では度々重要な資料として用いられており、Redmine や GitHub でも「ここでまつもとさんがこう言っているから、こうしよう」というような使い方で大活躍していました。

ところが、この blade はサーバー障害により、2022 年の 6 月に閉鎖してしまいました。これは大変なことです。Redmine を導入した 2008 年より以前に書かれたコードの Why や Why not を把握するためにはコミットログとコードからわかることだけを読み、補えないところは推測または開発者の記憶頼りになります。また、Redmine の導入後もメーリングリストによる議論も活発に行われていたため、それらの情報も失われてしまうことは大きな損失です。

幸か不幸か、blade の移設を hsbt が 2021 年から計画していたこともあり、原先生から 2021年4月の時点の blade のデータを保持していたため、フルタイムコミッタの仕事の第二弾として新しい blade の構築に取り組むことにしました。

新 blade の構築

新しい blade を構築するにあたって、利用できるインフラリソースや今後メンテナンスに関われる人の数などを考慮し、以下のようなアーキテクチャをエイっと決めてしまいました。

  • メールデータは S3 の buckets へ `#{メーリングリスト名}/#{post_id} という名前の plain text オブジェクトで保存する
  • S3 を static site で公開し、Fastly を経由して blade.ruby-lang.org というドメインでアクセス可能とする
  • メーリングリストへの新しいメールはどこかでプログラムを動かし、S3 へニアリアルタイムで保存する

まず、S3 の buckets を作成し、メールデータを保存する作業から着手しました。原先生からいただいた blade のバックアップデータは 2021 年の 4 月までしかないため、Ruby コミッタである ko1 さんshugo さんに協力してもらい、2022年 12 月までのメールデータをそれぞれ提供してもらいました。これらのメールデータは eml 形式の plain text のため、閲覧者にとっては不要なヘッダがたくさん付与されています。そのようなメール1通を以下のようなフォーマットのテキストファイル1つに整形しました。

From: hsbt@...
Date: 2022-xxx
Subject: xxx

xxx (本文)

メールアドレスについては公開情報のため、隠すかどうかは迷いましたが、あえて公開する必要もアーカイブの用途としてはなく、誰か、ということだけを特定できればいいため local-part のみ残すことにしました。このようなフォーマットへ変換する Ruby のスクリプトは以下となります。

require "bundler/inline"

gemfile do
  gem "mail"
  gem "net-smtp"
end

require "pathname"
require "fileutils"

base_dir = Pathname(ARGV[0])
blade_dir = base_dir + ".blade-version"

FileUtils.mkdir(blade_dir) unless blade_dir.exist?

Dir.glob( base_dir.join("*") ).each do |file|
  next if File.directory?(file)
  mail = Mail.read( file )
  puts file
  next if File.exist?( Pathname(blade_dir).join( File.basename(file) ).to_s )
  File.open( Pathname(blade_dir).join( File.basename(file) ), "w" ) do |f|
    from = begin
      mail.header["from"].to_s.gsub(/@[a-zA-Z.\-]+/, "@...")
    rescue
      mail.header['from']
    end
    f.puts "From: #{from}"
    f.puts "Date: #{mail.date}"
    begin
      f.puts "Subject: #{mail.subject}"
    rescue Encoding::CompatibilityError
      f.puts "Subject: "
    end
    f.puts ""
    begin
      f.puts mail.body.to_s.encode("UTF-8", "ISO-2022-JP")
    rescue Encoding::CompatibilityError, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError, Mail::UnknownEncodingType, ArgumentError
      begin
        puts "retry #{file}"
        m = File.read(file)
        require "nkf"
        if NKF.guess(m) != Encoding::UTF_8
          f.puts Mail.new(m.encode("UTF-8", "EUC-JP")).body.to_s
        else
          f.puts Mail.new(m).body.to_s
        end
      rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
        f.puts mail.body.to_s.encode("UTF-8", "ISO-2022-JP", invalid: :replace, undef: :replace)
      rescue Mail::UnknownEncodingType, ArgumentError
        f.puts "(This mail is unknown encoding, so it is not displayed. Please contact webmaster@ruby-lang.org)"
      end
    end
  end
end

メーリングリストのリストごとにフォルダに配置され、多いもので 45万通程度を有します。とはいえ、数十万くらいのオーダーのテキストデータであれば、NVMe を持った MacBook Pro で処理してしまえば手元でなんとかできます。詳細については限定的な用途のコードのため割愛しますが苦労したポイントとしては

  • ISO-2022-JP なエンコードを UTF-8 へ復元する
  • ISO-2022-JP とメールエンコーディングに記載されているが実際には EUC-JP なコードを UTF-8 へ復元する
  • 途中に不正なバイトが含まれているものは取り除いて UTF-8 に復元する
  • それ以外のものは本文を直接使う
  • それでも書き込めないものは諦める

などのように主にエンコーディングまわりで試行錯誤の繰り返しがありました。メールというデータフォーマットは本当に複雑ですね。このような加工処理を行ったデータをすべて S3 の中へ転送し、次の作業である CDN である Fastly を経由して https://blade.ruby-lang.org/ruby-core/1 という URL で表示できるようにインフラ面の設定を行いました。

最後の新しいデータの保存については若干頭を悩ませました。というのも、新しいメーリングリストは前回の仕事報告

tech.andpad.co.jp

の際に紹介したように MailmanLists というサービスに引越しています。今まではオンプレミスのサーバーの上でメーリングリストを構築していたため、新たに送信されたデータを blade.ruby-lang.org に保存したい場合は自分たちのサーバーの上にプログラムを書いて動かせば可能でしたが SaaS の場合はそうもいかず、何かしらメールを受信する場所を用意し、受信したメールを取り出して処理を行う必要があります。

今回はすでにメールを受信し続けている Redmine があることを思い出したため、プラグインとして機能を開発し、メーリングリストのメールを Redmine が処理する際に blade.ruby-lang.org の S3 にアップロードするという処理を追加しました。

github.com

def record_s3(msg)
  m = Mail.new(msg)
  list_name = m.header['List-Id'].to_s.match(/\<(.*)\.ml\.ruby\-lang\.org\>/)
  list_name = list_name && list_name[1]
  post_id = m.header["Subject"].to_s.match(/\[#{list_name}:(\d+)\].*/)
  post_id = post_id && post_id[1]

  from = begin
    m.header["from"].to_s.gsub(/@[a-zA-Z.\-]+/, "@...")
  rescue
    m.header['from']
  end
  io = StringIO.new
  io.puts "From: #{from}"
  io.puts "Date: #{m.date}"
  begin
    io.puts "Subject: #{m.subject}"
  rescue Encoding::CompatibilityError
    io.puts "Subject: "
  end
  io.puts ""
  io.puts m.body.to_s.encode("UTF-8", "ISO-2022-JP", invalid: :replace, undef: :replace)

  s3 = Aws::S3::Resource.new(
    region: 'ap-northeast-1',
    access_key_id: ENV['AWS_ACCESS_KEY_ID'],
    secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
  )
  bucket = s3.bucket('blade.ruby-lang.org')
  bucket.object("#{list_name}/#{post_id}").put(body: io.string)
ensure
  io.close
end

これらの作業により、新たな blade として、今までのメールアーカイブをすべて保持しつつ、新たなメールについても追加して参照できるようになりました。また S3 + Fastly というスタックを用いたことにより従来よりもデータの永続性を確保しながらも閲覧の速度を高速にするという、単なるサーバーの移設だけではなく利便性の向上も実現しました。

今後に向けて

利便性は向上したと書いた直後ですが、新生 blade にはビューワーがないということがまだ足りていないパーツとなります。また検索もできないため、特徴的なキーワードをもとに Ruby の仕様を検索して探すということもできません。これらについてはまだ未着手の状態のため、手伝っていただける方は是非お声がけください。

最後におまけとして、データの復元の作業を行いながら見つけた興味深いメールアーカイブについて紹介します。

今回はソフトウェア開発における意思決定のアーカイブの重要性と Ruby におけるメールアーカイブにまつわる作業についてご紹介しました。次回以降も Ruby のフルタイムコミッタとしてやったことを継続して紹介していきたいと思いますのでみなさんお楽しみに。

おわりに

アンドパッドではRubyエンジニアをはじめ、さまざまなポジションのエンジニアを募集しています。 ご興味がある方は、ぜひ話を聞きにきてください。

engineer.andpad.co.jp

hrmos.co