Rack::Timeoutによるリクエストタイムアウトの仕組み

はじめに

Rackミドルウェアの一つであるRack::Timeoutを使うと、Rackアプリがリクエストを受け取ってから一定時間が経過すると、アプリのどの場所でコードが実行されていても、その場所からRack::Timeoutの例外RequestTimeoutExceptionが発生する。

サンプルコードと、タイムアウト時のスタックトレースは次のようになる。

# config.ru
require 'rack-timeout'
require_relative './app'

use Rack::Timeout, service_timeout: 5
run App
# app.rb
require 'sinatra/base'

class App < Sinatra::Base
  get '/timeout' do
    sleep 6
    'Time out'
  end
end
[2021-04-12 20:20:17] INFO  WEBrick::HTTPServer#start: pid=1362 port=9292
source=rack-timeout id=287a1d6a-d9b2-47b1-8d03-27094d707e9d timeout=5000ms state=ready at=info
source=rack-timeout id=287a1d6a-d9b2-47b1-8d03-27094d707e9d timeout=5000ms service=5006ms state=timed_out at=error
2021-04-12 20:20:44 - Rack::Timeout::RequestTimeoutException - Request ran for longer than 5000ms :
    /Users/kymmt90/path/to/app/app.rb:5:in `sleep'
    /Users/kymmt90/path/to/app/app.rb:5:in `block in <class:App>'
    /Users/kymmt90/path/to/app/vendor/bundle/ruby/3.0.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1675:in `call'
(snip)
source=rack-timeout id=287a1d6a-d9b2-47b1-8d03-27094d707e9d timeout=5000ms service=5013ms state=completed at=info
::1 - - [12/Apr/2021:20:20:44 +0900] "GET /timeout HTTP/1.1" 500 30 5.0176

sleepしているapp.rbの13行目からRack::Attackの例外Rack::Timeout::RequestTimeoutExceptionが発生している。この仕組みが気になったので調べた。

先に結論

Rack::Timeoutでタイムアウトするとき、次のように動作する。

  • 渡したブロックを一定時間後に実行できるスケジューラを作る
  • Rack::Timeoutの#callメソッドを呼ぶとき
    • リクエストをハンドリングするスレッドでThread#raiseしてRack::Timeout::RequestTimeoutExceptionを発生させるブロックをスケジューラに渡す
    • 先のRackアプリに対して#callを実行し、タイムアウトの時間が経過すると渡したブロックを実行する

こうして、一定時間以内にRackアプリがレスポンスを返せばそのレスポンスを返し、そうでなければスケジューラに渡したブロックがアプリのスレッドの外から例外を発生させている。

Rack::Timeoutの概要

目的

Rack::TimeoutのREADMEには、その目的がこう書いてある:

Rack::Timeout is not a solution to the problem of long-running requests, it's a debug and remediation tool.

あくまでもタイムアウトをきっかけとしてアプリケーションを改善するために使っていこうというもの。

設定方法

Rack::RuntimeがRackミドルウェアとして入っているRackアプリなら、rack-timeoutをGemfileに追加するだけでRack::Timeoutの機能が追加される。そうでないときやRackミドルウェアスタックのどこにミドルウェアを入れたいかを制御したいなら自前で設定を書く必要がある。

設定項目の意味

Rack::Timeoutはさまざまな設定項目を持つ。それぞれの効果をあらためて確認した。

Service Timeout

Rackアプリの実行時間のタイムアウト。

Wait Timeout

リクエストがWebサーバのキューに入ってからRackアプリが処理し始めるまでの時間のタイムアウト。

たとえばHeroku Routerは30秒でタイムアウトするので、リクエストのキューに入って30秒たってからRackアプリが処理し始めるのは意味をなさない。そういうリクエストをRackアプリの手前で落としてRack::Timeout::RequestExpiryErrorをraiseする。

30秒のタイムアウトのとき、Rackアプリの前段で時間がかかって、Rackアプリの処理が始まるまでに20秒使ったら、service timeoutが実質10秒になってしまう。これを無効にするのが service_past_wait

X-Request-Startを使ってリクエスト開始時間をUNIXエポックで取得しているので、このヘッダがなかったらwait timeoutは発生しない。

Wait Overtime

wait timeoutでX-Request-Startを使っていることもあって、ボディが大きなPOSTリクエスト(ファイルアップロードとか)で受信しはじめから受信終わりまでに時間がかかると、受信完了前にwait timeout扱いになることがある。これを防ぐために、wait timeoutに下駄を履かせる*1

Term on timeout

たとえば1を設定すると、あるリクエストでタイムアウトしたらWebサーバにSIGTERMを送る。また、たとえば5を設定すると、5件のリクエストがタイムアウトするまでSIGTERMしない。

この機能はPumaなどWebサーバがマルチワーカーなときに使わないと、masterプロセスが終了してしまう。

コードの概要

この記事ではRack::Timeout v0.6.0を読む。コメントが豊富に書かれており、読みやすい。

lib/rack/timeout/core.rbにRackミドルウェアとしてのRack::Timeout#callが存在する。

module Rack
  class Timeout

  # ...

    RT = self # shorthand reference
    def call(env)
      # ...
    end

スケジューラRack::Timeout::Scheduler::Timeout(以下RT::Scheduler::Timeout)はタイムアウト処理を実現するにあたっての肝となるクラスである。Rack::Timeout#callでは、次のように次のミドルウェアまたはRackアプリに対しての#callRT::Scheduler::Timeout#timeoutに渡すブロックで包むことでタイムアウト処理を実現している。これにより、一定時間以上でタイムアウトするRackアプリが完成する。

      timeout = RT::Scheduler::Timeout.new do |app_thread|
        # タイムアウトしたときの処理を設定する
      end

      # 一定時間経過したとき上で渡したブロックを実行するために、#timeoutに@app.callを実行するブロックを渡す
      response = timeout.timeout(info.timeout) do
        begin
          @app.call(env)
        rescue RequestTimeoutException => e
          raise RequestTimeoutError.new(env), e.message, e.backtrace
        ensure
          register_state_change.call :completed
        end
      end

lib/rack/timeout/support/scheduler.rbにあるRT::Scheduler::Timeout#timeoutは次のとおり。本質は@scheduler.run_inの部分で#run_inは渡した秒数が経つとブロックを実行する。つまり、タイムアウト用のブロックを実行する。ここで得ているThread.currentはRackアプリへのリクエストハンドリングを行っているスレッドとなるので、スタックトレース上はRackアプリ内でRequestTimeoutExceptionが発生する形となる。

  def timeout(secs, &block)
    return block.call if secs.nil? || secs.zero?
    thr = Thread.current
    job = @scheduler.run_in(secs) { @on_timeout.call thr }
    return block.call
  ensure
    job.cancel! if job
  end

#run_inは次のとおり。fsecsはmonotonic clock*2から得られる時刻。スケジューラに渡せるイベントの一種としてRunEventがあり、これは渡された秒数経過後に1回処理を実行するというもの。

  def run_in(secs, &block)
    schedule RunEvent.new(fsecs + secs, block)
  end

#scheduleにイベントを渡すことで、Rack::Timeoutは新たなスレッドで無限ループしながら、実行が必要なイベントを随時実行する。

  def schedule(event)
    @mx_events.synchronize { @events << event }
    runner.run # 無限ループしながら実行が必要なイベントを実行するまで待つ#run_loop!を別スレッドで実行する
    return event
  end

#run_loop!は詳細は割愛するが、コメントが丁寧なのでやっていること自体は把握しやすい。現在実行すべきイベントを取り出して実行する、の繰り返しになっている。

*1:ベストな解決策はS3などオブジェクトストレージへのダイレクトアップロード

*2:現実の時刻は閏年などで補正されたり巻き戻ったりすることがあるが、monotonic clockは単調増加する

開発体験(DX)改善について知るために2021年1Qに読んだリソース

ここで開発体験(DX: developer experience)はおおむね次の記事で説明されている概念とする。digital transformationではない。

DX: Developer Experience (開発体験)は重要だ - Islands in the byte stream

2021年になり、担当するWebサービスのDXの改善を任務とするチームに所属しはじめた。こういうチームができるぐらいなので、開発体験を悪化させるという感覚を開発者に与える課題はいくつかわかっている。それらを一つ一つ解決していけば、改善業務をある程度は進められそうだと考えてはいた。一方で、あくまで各開発者の感覚に依存して課題を把握していることが多いので*1どれぐらいよく/悪くなっているかはわかりにくく、改善がどの程度Webサービスのビジネスそのものに影響するのかというのもぼんやりとしていた。

こういう不安感はDXの改善についての体系的な知識に疎かったのにも原因があるだろうと思い、そのようなテーマに関連しそうな書籍や記事を読んでみた。

わかったこと

ソフトウェア開発に科学を持ち込むという考えが大事なことがわかった。といっても難しい話ではなく、バリューストリームのうち、開発者が大きく関わる部分(つまりDXが効果を発揮する部分)に測定可能なメトリクス

  • リードタイム
  • デプロイ頻度
  • MTTR (Mean Time To Repair)
  • 変更失敗率

があり、上記のメトリクスを参考にしてDXの改善をめざすことで、結果的にバリューストリームの先にいる顧客がサービスで価値を得られるまでの時間を短くできるということだった。これは市場におけるサービスの競争力が上がることにつながる。さらに、開発者が効率的に開発できる方法も突き詰めていくことになるので、結果として障害発生率の低下や燃え尽き症候群の防止にも効果がある。

読んだリソース

Maximizing Developer Effectiveness

2021年に入ってDXの改善を担当するチームに入ってからTwitterのタイムラインを眺めていたときに、タイミングよくMartin Flower氏のツイートで知った記事。Fowler氏のWebサイトに載っているThoughtWorksのメンバーTim Cochran氏による記事。

Maximizing Developer Effectiveness

開発効率の最大化におけるマイクロフィードバックループの重要性について説いている。例えば、マイクロフィードバックループの一例として、実装中のコードのテストを手元で実行して結果を見たいとする。このテストの実行速度が数十秒遅いだけでも、手元でのテストは1日に何度も実行することがあるので、塵も積もれば山となり開発者の効率は長期的にかなり下がってしまう。しかも、そのようなマイクロフィードバックループの劣化は気づきにくいので、そのような問題を解決していくことが、より上位のメトリクスの改善のために重要だという。

他にも、Spotifyが技術情報ポータルのシステムを自作し、そこにサービスカタログなどを置くことで、情報を探す時間を削減している事例が紹介されている*2

この記事で次に紹介する"Accelerate"について言及されていたので、次はそれを読むことにした。

Accelerate

書籍。邦題は『LeanとDevOpsの科学』

この本はDevOps界隈では有名な本のよう。その目的として、統計的に有意な方法でパフォーマンスの改善を促すケイパビリティ*3を特定・理解する方法を確立すると述べられている。ソフトウェアのデリバリー速度を改善するには何が必要かを先に述べてから、その科学的な検証プロセスを具体的に説明した本。なので、後半は統計学や計量心理学の話がかなり出てくる。

要点は、上述したケイパビリティを表すメトリクスとしてリードタイムデプロイ頻度MTTR変更失敗率を改善すれば高速なデリバリーが可能であることを特定したので、これを強化していってください、という内容。それぞれのメトリクスの詳細は次のとおり:

  • リードタイム
    • コードのコミットから本番稼働までの所要時間
  • デプロイ頻度
    • バッチサイズ(一度にやる作業の量)を減らすとサイクルタイム短縮/変動の低減、高速なフィードバックの取得などメリットがあるが、バッチサイズの測定が難しいのでデプロイ頻度で考える
  • MTTR
    • パフォーマンスの高い組織が安定性を犠牲にしているかどうか調べたい
    • サービスをやっていたら絶対に失敗するので、サービス停止や機能障害の復旧速度をモニタリングする
  • 変更失敗率
    • デプロイ後になんらかの修正が必要だった場合の比率

また、「デリバリ速度とシステム安定性/品質はトレードオフである」という言説はハイパフォーマーには当てはまらないことがわかった、ということも述べられており、重要な点だと思う。他には、ソフトウェアがビジネスそのものになりつつあるなか、ソフトウェアデリバリーの速度を改善すると組織の業績も向上するという結果が出ており、ソフトウェア開発能力を会社の中核として据えるべき根拠と言えるという。

著者の一人Forsgren博士は現在GitHubのVP, Research & Strategyに就いている。この本で紹介されている上述のメトリクスを測定する機能がGitHubのロードマップにも入っている。実装されるのが楽しみ。

Insights: Engineering Leader and Organizational DORA Reports · Issue #127 · github/roadmap

Forsgren博士は最近だとコロナ禍でのリモートワーク環境における開発者の生産性の分析記事を書いていた。

Octoverseレポート特別編:COVID-19影響によるリモートワーク環境でのソフトウェア開発者生産性に関する分析 - GitHubブログ

The DevOps Handbook

Accelerateの参考文献からたどって読んだ書籍。邦題は『The DevOps ハンドブック 理論・原則・実践のすべて』

The DevOpsシリーズはいくつか本があって、これは2作目らしい。1作目はDevOpsをテーマにした小説っぽい感じ"The Phoenix Project"*4だがこちらは未読。

DevOpsは2009年から始まったムーブメントで、私はその当時一介の学生だったので、業界のなかでそのムーブメントを体験したわけではないが、そのときに出てきた数々のプラクティスのうちいくつかは実際にいまも取り組んでいるものであった。しかし、この本で説明されているように

  1. バリューストリームのフローを速くする
    • カンバン、アンドン、WIP制限、独力でバリューストリームを完遂できる職種横断(devとops)で市場指向な組織編成、デプロイパイプライン、自動テスト、トランクベース開発
  2. バリューストリームの末端から先頭へフィードバックが回るようにする
    • 本番環境やデプロイパイプラインからのメトリクス収集、問題発生時にチームですぐに復旧に取り組みMTTRの短縮、デベロッパーのオンコール参加、コードレビュー依頼時のバッチサイズの小ささの意識
  3. バリューストリームの途中でも細かくフィードバックが得られるようにする
    • 一部門の発見を共有リポジトリなどを通じて全社に広める、セキュリティや品質に関するメトリクスをバリューストリームの前段階へ、技術的に社内統制の課題を解決することでDevOpsと統一

という三つの観点でそれぞれのプラクティスが存在している、という背景まで踏み込んで考えたことはなかった。背景を含めて体系的に説明されていて、それなりに長い本だが考えが整理されて役に立った。あと、このあたりの話はトヨタ生産方式に端を発するリーン運動の話も多く出てくるので、そちらに関するテーマの本も読んでみたいところ。

Bliki

Bliki*5はMartin Fowler氏のWebサイトで公開されているソフトウェア開発プラクティスのシリーズ記事…という認識で合っているのだろうか*6

最近公開されていた記事2件がAccelerateやDevOps Handbookでの内容とも関連していると思ったので読んだ。

Pull Request

PullRequest

GitHubやGitLabが備えるpull request/merge request(以降"PR"とする)という機能はソフトウェア開発に欠かせないものとなっているが、PRをデフォルトと思い込んでいないか?ということを問うている記事のように感じた。主に次のようなことが書かれている。

  • 本来の意味での継続的インテグレーション、つまりできるだけトランクベースで開発するためにPRを使うなら、十分に小さいPRにしよう
  • 毎日、個人がメインラインに1回は統合できるようにチームとしてPRに反応していこう
  • PRだけがコードレビューの場ではないことを思い出そう

PRだけがコードレビューの場ではないという点については、例えばペアプログラミングでナビゲータ側が継続的にコードをレビューすることや、refinement code reviewという手法について言及している。refinement code reviewは次で説明する。

Refinement Code Review

RefinementCodeReview

おおまかな内容としては次のとおり。

  • ソフトウェアは建築で例えられてきたが、どちらかというと都市計画ではないか
    • by Erik Dörnenburg
    • ソフトウェアの価値に対する理解が深まるにつれて変化し続ける
  • 例えば開発中に一見してよくわからないコードを理解したら、その理解をコードに反映させる責任がある
    • by Ward Cunningham
    • あとの人が困らない
  • コードを読むたびにコードレビューが発動するようなものである
  • PRでのコードレビューだけでは、PRで入れる変更が既存コードベースで今後どのような状態になっていくかを見逃してしまう

これができればメリットは多そうと思うが、既存の開発フローにどう組み込めそうかについては、すぐにいい解は思いつかない…。このプラクティスにはテストコード、継続的インテグレーション、リファクタリング、(弱い)コードの所有権という4点が必要とのこと。

所感

これまでコードをどう書くかということばかり考えていて、メタな視点でソフトウェア開発を捉えることはあまりしてこなかったが、いろいろなリソースを読んで少し慣れてきた。"Accelerate"ですら、まだまだ消化し切れていないところがあるが、ほかにも『継続的デリバリー』なども読んでみようと思う。

あと、いろいろ読んだ結果、どうやるのがよしとされているかはなんとなくわかったが、それが自分の頭の中にしかないので、理想像を描いてチームメンバーに見てもらったり、一緒に実践していくなどの行動を起こす必要があると常々感じている。

*1:個人の感想

*2:Backstage Service Catalog and Developer Platform · An open platform for building developer portals

*3:組織の保持する能力

*4:邦題が『The DevOps 逆転だ!究極の継続的デリバリー』…

*5:本来はWiki機能をあわせ持ったブログのことを指すようだが、少なくともFowler氏のサイトではそのような機能はなさそう

*6:日本語版もある

不安定なテストが存在することをSlackに通知するGitHub Actionsワークフロー

あるGitHubリポジトリのmaster(や最近はmain)ブランチで確率的に落ちるテストは間違いなく不安定なテスト*1であるという考えのもと、不安定なテストを見つけたときに自動でSlackに通知するGitHub Actionsワークフローの書きかたについて説明する*2

なお、この記事ではテスト自体の書きかたの良し悪しについては言及しない。

方法

  • 不安定なテストを通知するジョブは、テストを実行するジョブの実行結果に依存する
  • 現在のブランチがmaster、かつテストの終了ステータスが正常でないとき通知する
    • 今回はテストが1件でも失敗したなら、テストのコマンドは終了ステータスとして1を返すとする

これをYAMLに落とし込むとこうなる*3

test:
  # リポジトリのテストを実行

flaky-test-notification:
  needs:
    - test
  runs-on: ubuntu-latest
  if: always()
  steps:
    - name: Notify flaky tests
      if: github.ref == 'refs/heads/master' && needs.test.result == 'failure'
      uses: tokorom/action-slack-incoming-webhook@main
      env:
        INCOMING_WEBHOOK_URL: {{ secrets.WEBHOOK_URL }}
      with:
        text: 'Flaky tests found: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'

まず、testジョブの結果に依存するのでneedstestを指定する。しかし、needsに指定したtestジョブはテストに失敗したとき終了ステータスが1なので失敗とみなされる。このときneedsを持つジョブはデフォルトでスキップされてしまう*4。この場合もflaky-test-notificationは実行したいので、ステップ自体はつねに実行されるようにif: always()を指定する。

通知用のステップで現在mainブランチかつテストが失敗しているか(すなわち不安定なテストが見つかったか)をgithub.refneeds.test.resultで判定する。判定がtrueならSlackへ通知する。GitHubフローで開発しているなら、masterブランチへはpull requestのマージでコミットが追加されるはずであり、それはpushと見なされるので、flaky-test-notificationはpull requestマージ時に1回だけ実行される。

Slackへ通知する既存のActionはいくつか存在するので何かを使えばいいが、ここではtokorom/action-slack-incoming-webhookを使わせていただく。といっても、リポジトリのActions用シークレット*5にSlackのIncoming Webhook URLを登録しておき、送信するテキストを設定すれば終わり。ここではgithub.repositorygithub.run_idからGitHub Actionsの実行画面のURLを作成して、Slackからすぐに結果を確認しに行けるようにしている。

不安定なテストが見つかると次のようにSlackに通知が来る。

f:id:kymmt90:20210216230146p:plain

このあとは粛々とテストを直しましょう。

*1:flaky testsともいう https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html

*2:業務で必要に駆られて作成しチームメンバーにレビューしてもらった

*3:本当はGitHub Enterprise Serverで作ったものをgithub.com風に書き換えた

*4:"If a job fails, all jobs that need it are skipped unless the jobs use a conditional expression that causes the job to continue"

*5:リポジトリ配下の/settings/secrets/actionsから設定できる

2020年まとめ

2020年は特異な年だったというのもあり、個人的なことも含めて2020年にあったことをまとめた。

仕事

リモートワーク

2020年1月15日に日本での新型コロナウイルス感染者が確認されたあと、国内ではかなり早い段階で所属企業を含むグループ全社でリモートワークが始まった。その開始をなぜかTwitterを通じて知り、「報道で知る」という感覚を初体験した。

結局ペパボはリモートワークを原則とする勤務体制に変更された。会社関連の用事で渋谷に行ったのは3回だけだった*1

ずっと家にいるので、仕事用の机のまわりの設備を改善した。とはいえ、基本的にMacBook Pro1台だけで作業することにしているので、充電器と椅子とデスクライトを買った程度。

定額給付金はBaronに充てさせてもらった*2。会社の椅子がBaronだったのでそれに合わせた。シートタイプはクッションにした。

仕事の能率を上げる方法として古典的だがポモドーロテクニックを試していた。

ポモドーロ・テクニックを2か月間やってみての感想 - kymmt

いったん原典のとおりに厳密にやっていて、新規に開発しているときなどはハマっている感じがしてよかった。調査業などとはどうしても相性が悪い感じがしている。最近はタスクリストの書き出しとポモドーロの計測だけという緩い運用になっていることが多い。

マンション自体の作りや立地の問題で、自分にとっていまの家はリモートワークに向いていないと感じている。そのわりに家賃がやたらと高く、来年で契約更新になるという事情もあるので、2021年の上期には引越したいと考えている。

業務

サービス基盤チーム所属という肩書きで、Eコマースプラットフォーム*3のWebアプリケーションレイヤを全般的に改善するという役回りだった。所属しているチームの概要についてはhrysdがスライドにまとめてくれている

主にサービスにおける事業者側の玄関口にあたる認証/認可部分のセキュリティ向上や新しいアーキテクチャへの移行に取り組みつつ、Webアプリケーションの細かい改善をやっていた。また、コロナ禍でEコマースの需要が高まったことを背景に、一時的ではあるが久しぶりに新機能をチーム開発したりもした。

15年の歴史を持つサービスの認証/認可の設計改善についてはなかなか進めづらいものだが、今年はセキュリティ向上の作業に抱き合わせることで(?)よりよいアーキテクチャに移行していけそうに見えたので、理想とするアーキテクチャの案を上長や各ロールを担当するエンジニアと共有し、みんなで協力して今年やるべき分については開発を進められたのでよかった。しかし、この分野はとにかく気を使うので大変と感じる。OAuth 2.0の認可サーバの実装だけでも、ライブラリを入れるだけではなくSecurity Best Current PracticeをはじめとするRFCを読んだり情報収集したうえで、適切に実装するのが必須であるということを身を持って感じた。令和の時代に新規にWebアプリケーションを作るなら認証/認可は原則IDaaSに任せるのがいいのだろうなという気持ちになっている。

また、下期はWebアプリケーションを運用し続けると溜まる問題(省みられないエラー通知、古いライブラリのバージョンアップなど)の解決に発生しがちな属人性を減らして、チームや事業部レベルで解決できるように進めかたを考えて、まずはチームで実践したりしていた。ここは道半ばなので引き続き。

アウトプット

記事

個人ブログの記事は14件。だいたい月末になにか書くことがないか考えて書くという感じで、テーマがバラバラだし内容も未来の自分向けが多い。そういえば年初はHaskellを勉強していたけど、すっかり触らなくなってしまった。

2020-01-01から1年間の記事一覧 - kymmt

会社のブログにWebアプリケーションのセッション管理に関する記事を書いた。会社の名前を借りてWebに情報発信させてもらえるということもあり、個人ブログよりはよっぽど読まれたようだった。

SPA+SSR+APIで構成したWebアプリケーションのセッション管理 - ペパボテックブログ

その他はQiitaにtipsレベルの記事を数本書いたぐらい。

OSS

自分のOSSにもらったPRを対応したり、業務の関係上doorkeeper-gemのgemをよく読んだり使ったりしていたので、そのあたりで少しだけ貢献した。

仕事の関係で小さいgemを作成した。

総じてOSSをバリバリやる感じではなかった。

その他

読書

79冊読んでいた*4

Yamamoto Kōheiさんの読書記録

技術書はOAuth/OIDC関連の本が多め。Auth屋さんの本には助けられた。なんとなく量子コンピュータが気になって関連する本を数冊読んだが、手を動かさずに終わっている。

6月ぐらいにKindleでちくま新書のセールをやっていたときに畑違いの分野の本をここぞとばかりに買って読んでいた。また、Kindle Oasisを買って一般書の読書体験はかなり向上した。電子ペーパーのほうが圧倒的に紙に近く読みやすいし、片手でそれなりの画面サイズのデバイスを持って読み進められるのが個人的に体験がよく感じる。

Emacs

Emacsを使うのをやめて、Visual Studio Codeだけで開発するようになった。また、これまで雑にメモを書いたり一時的に文字列を置きたいときはEmacsの*scratch*バッファを使っていたが、いまはSpotlightでTextEdit.appを立ち上げて用を済ませている。

Emacsとの出会いは大学に入った2008年4月*5で、本格的に使い始めたのは研究室に入ってまともにプログラミングをし始めた2011年〜2012年の間だと思う*6。当時『Emacs実践入門』の初版が出たころで、開発環境整備の重要性はこの本から教わったように思う。その後、時代は移り変わり、VS Code主流の世の中でも惰性でEmacsを使っていたが、リモートワーク下での開発作業でVS CodeのLive Shareを使いたい場面があり、そのために自分がボトルネックになるのはよくないので、いい機会と思って移行してしまった。この記事もVS Codeで書いている。

Awesome Emacs Keymapを使っているので、まだ脱Emacsし切っているとはいえないが、手元のマシンからはEmacsをアンインストールした。

Try to quit using Emacs for a time · kymmt90/dotfiles@85ca748

健康

通勤がなくなったので、朝の散歩と軽い自重トレーニングは毎日やっている。一念発起して、花粉症の治療のために6月ごろからシダキュアを服用しはじめた。月1回通院があるが散歩だと思って通っている。シダキュアはHabitifyで見る限り202日連続服用できていて挫折はしていない。

悪いできごととしては口腔外科系の病気になってしまったので、来年の医療費がどうなるか読めず不安感がある。また、生活環境が変わった影響か息抜きがうまくできておらず、そのわりに精神的に焦るだけで何もできていないという自覚があるので、来年は改善したい。

所感

総じて大変な年だった。まだまだやれていないことや知らないことだらけなので来年もがんばりたい。


*1:荷物の回収、感染拡大が落ち着いていた時期に久々にチームで顔合わせ、インフル予防接種

*2:14万円弱したので足は出ている

*3:https://shop-pro.jp / https://colorme-repeat.jp

*4:漫画を含む。再読した本が計算に入ってなさそうで、実際は80冊は超えていると思う

*5:大学の演習室にあったのはLeopardの入ったMac Proで、演習の授業の初回でEmacsが導入された

*6:研究室の支給PCはWindows 7が入っていたので、最初は雰囲気でxyzzyを使っていた。そのあとNTEmacsに移行した

Railsエンジンのappディレクトリ配下のクラスを親アプリでオーバーライドする

やりたいこと

Railsエンジンのappディレクトリ配下に存在するクラス(モデルやコントローラ)のメソッドをオーバーライドしたい。

結論

RailsガイドのRailsエンジンについての記事に全部書いてある。Railsエンジンのapp配下のオーバーライドは、to_prepareを使って、親アプリの初期化が終わったあとに実行する。オーバーライドするクラスはclass_evalでリオープンする。

# config/application.rb
module TestApp
  class Application < Rails::Application
    # ...

    # アプリの初期化が終わったときに呼ばれるフック
    config.to_prepare do
      # もしZeitwerkなら`require_dependency`が非推奨なので`load`を使う
      require_dependency Rails.root.join('lib/monkey_patch/foo_bar_engine.rb')
    end
  end
end
# lib/monkey_patch/foo_bar_engine.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # 上書きする
    end
  end
end

FooBarEngine::FooBarsController.class_eval do
  prepend ::MonkeyPatch::FooBarEngine
end

詳細

Railsガイドを読めば問題は解決するのだが、思いつく他の方法で試してみて、なぜだめだったかを見てみる。

オーバーライドに失敗する例: その1

親アプリ側のディレクトリに次のようなコードを書くと、このアクションに対応するエンドポイントにリクエストを送るとき、AbstractController::ActionNotFoundのエラーになる。

# app/controllers/foo_bar_engine/foo_bars_controller.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # 上書きする
    end
  end
end

module FooBarEngine
  class FooBarsController
    prepend ::MonkeyPatch::FooBarEngine
  end
end

開発環境では、定数参照時にconst_missingになるとActive Supportがautoload_pathsからパスの規約などに基づいて定数を探す。autoload_pathsは例えば次のように確認できる:

[1] pry(main)> puts ActiveSupport::Dependencies.autoload_paths
/usr/src/app/app/assets
/usr/src/app/app/controllers
/usr/src/app/app/controllers/concerns
/usr/src/app/app/helpers
/usr/src/app/app/jobs
/usr/src/app/app/mailers
/usr/src/app/app/models
/usr/src/app/app/models/concerns
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/assets
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/controllers
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/models
/usr/local/bundle/gems/devise-4.7.3/app/controllers
/usr/local/bundle/gems/devise-4.7.3/app/helpers
/usr/local/bundle/gems/devise-4.7.3/app/mailers
/usr/src/app/spec/mailers/previews
=> nil

ここではletter_opener_webやdeviseなどのRailsエンジンのapp配下もautoload_pathsの後ろのほうに入っている。

FooBarsControllerを読み込んでいないとき、アクション実行時にFooBarEngine::FooBarsControllerという定数を解決することになる。autoload_pathsに従うとRailsエンジンより先に親アプリの定義を見てしまい、中身がほぼ空のコントローラのアクションを呼び出してしまってAbstractController::ActionNotFoundになる。

オーバーライドに失敗する例: その2

config/application.rbの末尾でclass_evalでオーバーライド対象のクラスをリオープンしてオーバーライド用のモジュールをprependすると、uninitialized constant FooBarEngine::FooBarsController (NameError)のエラーになる。

# config/application.rb
module TestApp
  class Application < Rails::Application
    # ...

    # これがないとlibにパスが通らない
    config.eager_load_paths << "#{Rails.root}/lib"
  end
end

require_dependency 'lib/monkey_path/foo_bar_engine.rb'
# lib/monkey_path/foo_bar_engine.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # オーバーライドする
    end
  end
end

FooBarEngine::FooBarsController.class_eval # この定数が見つからない
  prepend ::MonkeyPatch::FooBarEngine
end

これは、config/environment.rbでconfig/application.rbを読み込んだ時点ではアプリの初期化が終わっておらず、オートロードの準備もできていないので、Railsエンジン配下の定数を探索できないのが理由。

オーバーライドに成功する例

結論に書いたとおり、config.prepare_toフックでオーバーライドする。prepare_toはアプリの初期化が終わった時点で呼び出されるので、オートロードも可能であり、Railsエンジン配下の定数を探索することもできる。