RubyAdvent Calendar 2021

created at: 2021-12-14 00:00:00 +0000

うっかりしてたら過ぎ去っていました。Ruby Advent Calendar 2021 の14日目です。

今年書いたRubyのコード

普段はRailsを使ってるんですが、素朴なRubyのコードを書いていることもあるのでその中で公開できるやつをぱっと紹介してみます。

pr コマンド

git branch -a --sort=authordate | grep -e 'remotes' | grep -v -e '->' -e '*' -e 'asonas' -e 'master' | perl -pe 's/^\h+//g' | perl -pe 's#^remotes/##' | perl -nle 'print if !$c{$_}++' | peco | ruby -e 'r=STDIN.read;b=r.split("/")[1..];system("git", "switch", "-c", b.join("/").strip, r.strip)'

https://github.com/asonas/dotfiles/blob/master/.zshrc#L35-L37

ワンライナーを書く時に途中からawsとかsedで頑張る気力がなくなってrubyが登場することってありますよね。僕はだいたいそうです。 上記のコマンドは、gitのリポジトリに登録しているremote先から切り替えたいブランチを選んでcheckoutするワンライナーです。

用途としては、チームメンバーのPull Requestを読んでいて「おや?」と思ったコードを手元に持ってきて検証したいときに使います。pecoとか使ってるのでインクリメンタルサーチとかできて便利です。

これ、なんでsystemを呼び出してまでgit-checkoutしてるんでしょうね...。普通にブランチ名を出力してパイプすればいいのでは?ワンライナー書いてるときはなんか気持ちがあったんでしょうか。

リリースノート生成くん

#!/usr/bin/env ruby
require "bundler/setup"

Bundler.require "development"

Octokit.configure do |c|
  c.api_endpoint = "https://YOUR_GHE_HOSTNAME/api/v3"
end

client = Octokit::Client.new(access_token: ENV["GITHUB_ACCESS_TOKEN"])
merge_pulls = []

`git log --since="7 days ago" --merges --oneline`.split("\n").each do |merge_commit|
  next if merge_commit.include? "renovate"

  m = /[a-z0-9]{7} Merge pull request \#([0-9].*) from .*/.match merge_commit
  next if m.nil?
  pr_num = m[1]

  merge_pulls.push client.get("repos/ORG/REPOS/pulls/#{pr_num}")
end

today = Time.now
since = today - (7 * 86400)
title = "今週のリリースノート(#{since.strftime('%Y-%m-%d')}..#{today.strftime('%Y-%m-%d')})"
template = ERB.new <<EOF
# 今週リリースされたもの :rocket:

<% merge_pulls.each do |pull| %>
## <%= pull[:title] %>

<%= pull[:body].split("\n")[..2].join("\n") %>

ref: <%= pull[:html_url] %>

___
<% end %>

EOF

body = template.result

client.post("repos/ORG/REPOS/issues", title: title, body: body)

チームで今週何をリリースしたっけ?というのをGHEのIssueに起票するスクリプトです。GHEのIssueである必要はないんですが、Markdownが書けて、チームメンバー全員が見れる場所に一時的にストックできる場所にアウトプットしたかったのが目的です。

やってることは簡単で、git-log で7日前までのコミットを持ってきて、それをぐるぐる回してERBでまとめる、という感じ。マージコミットしか出力しないので、そんなに長大なリリースノートにはならないです。

最初はCHANGELOGを書く?みたいなことも提案しましたが、PRベースでの開発をしているので、それ以外でメンバーの負荷を高めないようにしました。

このスクリプトは毎週木曜日のお昼頃にJenkins氏が実行してくれます。

大量のエンドポイントにアクセスをする

あまりコードとして見せられるものは無いのですが、天気の情報を取得できるAPIがあって、およそ数万件の地区ごとに区切られてAPIが提供されています。想定された用途としては、都度地区IDのもとにアクセスしてその地区の天気を求める、というのが正攻法ぽいのですが、僕らがやりたかった事としては、その天気情報を時系列に並べてRedshiftでクエリするだったので都度地区の天気情報を取得していては日が暮れるので、大量に取得するようにしました。(もちろん、そのような使い方をする旨を先方には伝えて了承を頂いた上ですが)

基本的にはコネクションを張りっぱなしにしてどんどんリクエストを送って最後にコネクションをクローズする、という古き良きアクセスの仕方を採用しました。

module Connectable
  MAX_RETRY_COUNT = ENV["MAX_RETRY_COUNT"] || 3
  SLEEP_SECOND = ENV["SLEEP_SECOND"] || 60

  def run
    open_connection
    super
    close_connection
  end

  def request(endpoint)
    request = Net::HTTP::Get.new(endpoint)
    request["Connection"] = "Keep-Alive"
    request["x-access-key"] = API_KEY

    current_try_count = 0
    begin
      current_try_count += 1
      response = @http.request(request)
      case response
      when Net::HTTPOK
        response
      when Net::HTTPNotFound
        logger.warn("Not found #{endpoint}")
        raise response.inspect
      when Net::HTTPBadRequest, Net::HTTPForbidden
        logger.warn("Client error: #{response.body}")
        raise response.inspect
      when Net::HTTPInternalServerError, Net::HTTPServiceUnavailable
        logger.warn("Internal server error: #{response.body}")
        raise response.inspect
      else
        raise "Unknown response: #{response.inspect}"
      end
    rescue
      if current_try_count < MAX_RETRY_COUNT
        sleep SLEEP_SECOND
        retry
      else
        raise "The maximum number of retries has been reached."
      end
    end
  end

  def open_connection
    @http ||= Net::HTTP.start(FQDN, 443, use_ssl: true)
  end

  def close_connection
    @http.finish
  end
end

みせられるのは概ねこんなかんじ...。雰囲気は伝わるだろうか...。Connectableモジュールを各エンドポイントの情報を持ったクラスでincludeして呼び出す、というのをしています。

include先でrunメソッドを定義して、そこにアクセス先の情報(パスや地区のIDを知っている)やリトライ時の処理(ジョブが失敗して再実行した際に重複してレコードが入らないようにするなど)をそれぞれ書くような感じ。最終的にはJSON形式でs3に設置してあとはRedshiftに取り込む、という感じ。簡単で素朴なジョブ。

最初エンドポイントの数だけ Net::HTTP.startを呼び出していてその都度コネクションを貼っていたのでめちゃくちゃ遅かったんですが、コネクションを使い回す事でだいたい三分の一ぐらい早くなりました。

これはプロダクションで動くコードなので前の2つに比べるとわりとちゃんとしてますね。見せられる部分がちょっと少なすぎますが...。

あと当初はRactorで並列や!と思って書いてたんですが、IO heabyでもないので劇的に速度が上がるということはなかったです。単純にforkするほうがはやかったです。うーむ。

Rubyをキメると気持ち良い

prコマンド(関数)は至極個人的なコード(ワンライナー)なので読んでいてもわけわからん感じですがコレを書いていたときはキマってたんだろうな、というのが読み取れますね。

リリースノート生成くんはプロダクションコードではないですが、プロジェクトをいい感じに進めていくために書いたので概ね読めるような感じになってると思います。個人的にはいい塩梅なコード。

最後のリクエストを大量に送るコードはHTTPのステータスコードを見てエラーをハンドリングしたり、リトライもするようにしていたりとそこそこちゃんとしていると思います。リクエストするときのルールはそれをincludeするクラス側で制御できるようにしたのもちょっとお気に入りのポイントです。これはプロダクションコードで動いてますって感じがしますね。

TPOを見極めてRubyのコードも書けば、だいたい良くなりますね。今回は3つの例を出して至極個人的なものからプロダクションに乗せるコードまでお見せしてみました。Railsを書いているとRailsになる(伝われ)ので、素のRubyに近いコードをたまに書いてみると頭の体操になっていい感じ。楽しいね。