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に近いコードをたまに書いてみると頭の体操になっていい感じ。楽しいね。