Madogiwa Blog

主に技術系の学習メモに使っていきます。

rspecの`--pattern`オプションを使って特定のパスのテストだけ実行するMEMO📝

Railsを使っていてRSpecでststem specとrequest specのテストだけ実行したい時とかに--pattern関連オプションを利用すると便利だったので使い方をメモ📝

RSpec--patternの使い方

公式Doc

relishapp.com

Help

$ bundle exec rspec -h
# ...
    -P, --pattern PATTERN              Load files matching pattern (default: "spec/**/*_spec.rb").
        --exclude-pattern PATTERN      Load files except those matching pattern. Opposite effect of --pattern.
# ...

基本的には上記の公式ドキュメント・helpの通りです。例えばRailsにおいてmodelのテストだけ実行したい場合は以下のようにします。

$ bundle exec rspec --pattern "spec/models/**/*_spec.rb"

ただこれだと単純にbundle exec rspec spec/modelsとするのと大して変わらないのですが、 例えばE2E関連のテスト(sysytem spec, feature spec)を実行したいといったときに、以下のようにスッキリ書けるので便利です。

$ bundle exec rspec --pattern "spec/{system,features}/**/*_spec.rb"

--exclude-patternを使って特定のパターンを除外する

--exclude-patternを使うと--patternとは逆にマッチしないものを対象をすることが出来ます。 例えば実行時間がかかるのでE2Eテストの実行は除外して全体のテストを流したいといった場合には以下のようにすると実現できます。

$ bundle exec rspec --exclude-pattern "spec/{system,features}/**/*_spec.rb"

また--exclude-pattern--patternと併用できるので、以下のようにE2Eテストから内部利用の画面のテストを外したりといったこともできます。

$ bundle exec rspec --pattern "spec/{system,features}/**/*_spec.rb" --exclude-pattern "spec/**/{admin,private}/**/*_spec.rb"

おわりに

RSpecのオブション、調べてみると結構便利なものがありますね!

参考

motty72.hatenablog.com

Ruby on Rails: Redisを使わないActiveJobのバックエンド「GoodJob」を使ってみるMEMO📝

何かしらの非同期なバッチ処理を実装したい場合にはSidekiqをActiveJobのバックエンドとして利用することが多いと思いますが、SidekiqはRedisに依存しており、個人のWebサービスとかでなるべくコストを抑えたいときにはRedisを立てずに実装したいものです。

今回はそういう場合に便利そうなPosgreSQLベースのGoodJobを使ってみたのでMEMO📝

github.com

GoodJobとは?

GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
good_job/README.md at main · bensheldon/good_job · GitHub

上記の記載の通り、マルチスレッドで動作するPostgresSQLベースのActiveJob backendです。 PostgresSQLベースのため、Redisを使わずにApplicationで使用しているデータベースでキューを管理することができます。

DBベースのActiveJob backendはDelayedJob等がありますが、GoodJobは以下の通り他と比較してMultithreadedで動作したり、structure.sqlが不要だったりする点が差別化要素となっているようです。

good_job/README.md at main · bensheldon/good_job · GitHub

1日に100万回エンキューぐらいの性能は担保されているようです👀

For most workloads. Targets full-stack teams, economy-minded solo developers, and applications that enqueue 1-million jobs/day and more. good_job/README.md at main · bensheldon/good_job · GitHub

GoodJobの導入方法

GoodJobの導入は簡単で以下のREADMEに記載の通りですが、私がやった方法も記載しておきます。

https://github.com/bensheldon/good_job/blob/main/README.md#set-up

GoodJobのinstall

以下のコマンドを実行してGoodJobのinstall及び必要なtableのmigrationを実行します。

$ bundle add good_job
$ bin/rails g good_job:install
$ bin/rails db:migrate

GoodJobの初期設定

ActiveJobのバックエンドをGoodJobに変更します。

# config/application.rb or config/environments/{RAILS_ENV}.rb
config.active_job.queue_adapter = :good_job

またconfig/initializers/good_job.rbのようなファイルを作成して、GoodJobの設定を行います。(私は以下のように設定してますが、環境に合わせて調整してください)

Rails.application.configure do
  config.good_job.queues = '*'  # 全てのキューを実行対象にする
  config.good_job.execution_mode = :external # 外部プロセスとしてGoodJobを起動する
  config.good_job.cleanup_preserved_jobs_before_seconds_ago = 1.hour.to_i # 1時間経過した履歴は削除する
  config.good_job.max_threads = 3 # 3スレッドで並列実行する
end

以下のコマンドを実行するとGoodJobを起動できます🙆

$ bundle exec good_job start
[GoodJob] [215] GoodJob started scheduler with queues=* max_threads=3.

Tips

Retry

GoodJobは以下の通りActiveJobのRetry機構を利用しています。

Retries By default, GoodJob relies on ActiveJob's retry functionality. ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. https://github.com/bensheldon/good_job/blob/main/README.md#retries

そのため以下のようにActiveJobのリトライ機構を利用してあげればRetryが行えます。

class FooJob < ApplicationJob
  retry_on StandardError, attempts: 5

  def perform(feed_id)
  end
end

Sidekiqを使っていると以下のようなハマりポイントがあったりするので、逆に分かりやすいかもですね。

Active Jobのリトライは、単に「sidekiqに新しいジョブを実行させる」というもので、Active Job内で例外を出したジョブはsidekiq側では正常終了した扱いになります。Active Jobのジョブが5回連続で失敗して初めてsidekiq側で例外をキャッチします。 そしてsidekiqのリトライが始まります。sidekiqのリトライ回数はデフォルト25回です。
Railsクイズ、何問解けるかな? - SmartHR Tech Blog

Cron

GoodJobは以下の通りデフォルトでcronで定義できるスケジュール実行の機能を備えています。

GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron. https://github.com/bensheldon/good_job/blob/main/README.md#cron-style-repeatingrecurring-jobs

以下のような感じで設定に追記してあげると、毎時0分でFooJobを実行することができます。

Rails.application.configure do
  # ...
  config.good_job.enable_cron = true
  config.good_job.cron = {
   foo_job: { cron: '0 * * * *', class: 'FooJob' }
  }
end

Dashboard

GoodJobはRails enginでDashboardを導入することができます。

GoodJob includes a Dashboard as a mountable Rails::Engine. https://github.com/bensheldon/good_job/blob/main/README.md#dashboard

そのため以下のようにmountすればDashboardを導入することができます。

Rails.application.routes.draw do
  namespace :admin do
    authenticated :admin_user do # Deviseで認証かける
      require 'good_job/engine'
      mount GoodJob::Engine => 'good_job'
    end
  end
end

https://github.com/bensheldon/good_job/blob/main/README.md#dashboard

おわりに

思ったよりも高機能で使いやすかったので開発初期でコストを抑えたいとか、インフラをシンプルにしたいとかそういう場合に良さそう✨

JestのcacheをCIでも利用して高速化するメモ📝

Jestはデフォルトでcacheを利用するのですが、CIでも利用できるようにしとくと高速化できそうだったのでやり方をメモ📝

Whether to use the cache. Defaults to true. Disable the cache using --no-cache.

Jest CLI Options · Jest

jestでcacheを保存するパスを指定する

CIでcacheしやすいようにcacheの配置先をcacheDirectoryで明示的に指定します。

module.exports = {
  cacheDirectory: "node_modules/.cache/jest"
  // ...
}

The directory where Jest should store its cached dependency information.

Configuring Jest · Jest

これでCI上でもcacheを利用しやすくなりました!

CIでjestのcacheを利用する

以下jestのcacheを利用するような設定を入れたGitHub Actionのサンプルです。

name: frontend ci

on: [push]

jobs:
  ci:
    runs-on: ubuntu-latest
    env:
      TZ: Asia/Tokyo
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v3
        with:
          node-version: "16"
          cache: 'yarn'
      - name: install node deps
        run: yarn install --frozen-lockfile
      - name: cache jest
        uses: actions/cache@v3
        with:
          path: node_modules/.cache/jest
          key: jest-v1-${hashFiles('yarn.lock')}-${{ github.ref_name }}-${{ github.sha }}
          restore-keys: |
            jest-v1-
      - name: test js
        run: yarn test:coverage

簡単ですね!

参考

engineering.meetsmore.com

Ruby on Rails: DBからの取得結果をいい感じにCSVにして出力を実装したいMEMO

普段、CSV出力系の機能を作ることが稀に良くあるが、割と毎回同じようなことを実装してる気もするのでいい感じに共通化できないか実装を検討したのでメモ

ベンチマークは特に取ってない

CSV出力の実装検討メモ

考えたこと

  • CSV出力時にエラーが発生した場合に、ハンドリングしやいようにカスタムエラーをraiseする
  • 各行の出力ロジックは算出値を利用することもあるので外部から注入できるようにする
  • ActiveRecord::Relationからよしななオブジェクトを返す
    • in_batchesで取得してDB負荷を抑制
  • アプリケーション側のメモリ負荷抑制のためにCSVを分割して出力する(スコープ外)
    • 直接send dataでレスポンスとして返すようなケースもありそうなので今回はスコープ外
  • よしなにBOMをつける
    • エクセルは対応しなくてもいいケースもありそうなので今回はスコープ外

実装してみる

module Csv
  class Genarator
    def initialize(relation:, headers:, row_storategy:, batch_size: 1000)
      @relation = relation
      @headers = headers
      @row_storategy = row_storategy
      @batch_size = batch_size
    end

    def call!
      raw_rows = genarate_raw_rows(@relation, @row_storategy, @batch_size)
      rows = genarate_rows(@headers, raw_rows)
      genarate_csv(rows)
    rescue StandardError => error
      raise GenarateFailed, error
    end

    private

    def genarate_raw_rows(relation, strategy, batch_size)
      relation.in_batches(of: batch_size)
              .reduce([]) { |result, records| result + strategy.call(records) }
    end

    def genarate_rows(headers, raw_rows)
      generate_row = ->(raw_row) { CSV::Row.new(headers, raw_row) }
      raw_rows.map(&generate_row)
    end

    def genarate_csv(rows)
      CSV::Table.new(rows)
    end
  end
end

補足

  • row_storategyでProcオブジェクトを渡すことで行生成のロジックを外部から注入できるようにした
    • 本当だったらpluckとかその辺りの最適化のロジックも良しなに組み込みたかったけど、そこは責務から除外した。この辺を矯正したいならclumnsみたいなのを引数で撮ってrelationにpluckで適用するような処理を内部に持つようにしても良いかもしれない。
  • in_batches + reduceで一定件数ずつ抽出 + 処理して結果を返すようにした
  • CSV::Rowを生成してCSV::Tableを返すようにした
    • この辺はpureなHashとかArrayを生成してCSV.generateした方が負荷的には良いのかもしれない

利用例

headerと行の抽出ロジックを渡せば良しなに生成してくれる。

headers = %i[id title endpoint last_published_at created_at updated_at]
row_storategy = ->(records) { records.pluck(*headers) }
result = Csv::Genarator.new(relation: feeds, headers: headers, row_storategy: row_storategy).call!
result.to_csv # CSV文字列を返す

参考

docs.ruby-lang.org

docs.ruby-lang.org

Ruby: オレオレWebフレームワークMakanaiをRack3に対応しました

2022/9/6にRack3がリリースされました🎉

github.com

Security fixが入ってたり、破壊的変更が入ったりもしてるようですね👀

RailsSinatraでもアップデートの準備が進められているようです。

github.com

github.com

自分も個人で細々とメンテしているオレオレWebフレームワークをRack3対応してアップデートしたので、 PRの通りで大したことしてないけど必要だったこととかメモ📝

github.com

オレオレWebフレームワークMakanaiが気になる人は、以下を参照してください。

madogiwa0124.hatenablog.com

Rack3のアップデートで必要だったこと

gem rackupをinstallする

以下のPRにて

github.com

rackup系の機能が別のgem rackupに切り出されたので別途installするようにしました。

github.com

内部でRack::Handler等を利用してはいなかったので私は影響がなかったのですが、このあたりのnamespace名がRackup::Handler等に変わっているようなので利用している人は注意です。

The following classes were moved:

  • Rack::Server -> Rackup::Server
  • Rack::Handler -> Rackup::Handler. Some minimal function was retained for compatibility.
  • Rack::Lobster -> Rackup::Lobster.

https://github.com/rack/rack/pull/1937

おわりに

規模の小さいオレオレWebフレームワークだったからか、ほとんどコードを変更せずにRack3に対応できました✨ 特に大したことをせずに挙げられましたが、そこそこ破壊的変更が入っているので、当たり前のことですがCHANGELOGを読みつつ影響がないか確認してあげるのが良さそうです🙇

`@swc/jest`でjestによるTypeScriptのテストを高速化するMEMO📝

JestでTypeScriptのテストを行う際にはts-jestを利用することが多いと思うのですが、@swc/jest を利用するとテストを高速化できるとのことで個人のWebサービスで試してみたのでメモ📝

前提事項

  • jest: 29.0.3
  • @swc/jest: 0.2.22

@swc/jestとは

SWC (stands for Speedy Web Compiler) is a super-fast TypeScript / JavaScript compiler written in Rust.
https://github.com/swc-project/swc

@swc/jest
To make your Jest tests run faster, you can swap out the default JavaScript-based runner (ts-jest) for a drop-in Rust replacement using SWC.
https://swc.rs/docs/usage/jest

SWCとはRustで再実装されたTypeScriptのコンパイラで、それを使ってts-jestを置き換えたのが@swc/jestということですね!

@swc/jestのinstall

インストールは簡単で以下のコマンドでinstallし、

# if you use npm
npm i -D jest @swc/core @swc/jest

# if you use yarn
yarn add -D jest @swc/core @swc/jest

以下のようにts-jest@swc/jestに差し替えてあげれば大丈夫です!

--     "^.+\\.ts$": "ts-jest",
++    "^.+\\.ts$": "@swc/jest",

以下のように必要に応じて任意の設定を作って渡すこともできます。

const swcConfig = {
  module: {
    type: "commonjs",
  },
};
--     "^.+\\.ts$": "ts-jest",
++    "^.+\\.ts$": ["@swc/jest", swcConfig],

どれくらい早くなるか

個人のプロジェクトなのでそんなにテストコードの数が無くアレですが、半分ぐらいの実行速度になったので結構早くなってそうです🚅✨

// swc/jest
✨  Done in 2.36s.
✨  Done in 1.56s.
✨  Done in 1.78s.
✨  Done in 1.80s.
✨  Done in 1.81s.

// ts-jest
✨  Done in 2.66s.
✨  Done in 3.38s.
✨  Done in 3.24s.
✨  Done in 3.41s.
✨  Done in 3.19s.

参考

t-yng.jp

GitHub Actionでスケジュール実行するworkflowを作成するメモ📝

以下の通りHeroku有償化に個人で運用しているbotの移行先を検討していたところ、GitHub Actionが結構よさそうだったのでメモ📝

blog.heroku.com

GitHub Actionの概要とかは過去記事で記載してます。

madogiwa0124.hatenablog.com

Github Actionの価格

以下の通り 2022/09/04 時点では無料アカウントでも500MBのストレージと2000分の実行時間が使えるようです🙏

Product Storage Minutes (per month)
GitHub Free 500 MB 2,000

最新の価格は以下を参照してください。

docs.github.com

Github Actionでスケジュール実行する

以下の通りon: scheduleを利用するとcronで時間を指定して実行することが出来ます。

The schedule event allows you to trigger a workflow at a scheduled time. You can schedule a workflow to run at specific UTC times using POSIX cron syntax.
https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule

以下の通り遅延することもあるので厳密に実行する必要がある場合などは注意です。

Note: The schedule event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. To decrease the chance of delay, schedule your workflow to run at a different time of the hour.
https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule

以下が毎日19:00(日本時間)に実行するworkflowのサンプルです。 ※workflow_dispatchも指定して手動でも実行できるようにしてます。

name: cron

on:
  workflow_dispatch:
  schedule:
    - cron: "0 10 * * *"

jobs:
  run:
    runs-on: ubuntu-latest
    env:
      TZ: Asiz/Tokyo
      TOKEN: ${{ secrets.TOKEN }}
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - name: install ruby deps
        run: bundle install --jobs 4 --retry 3
      - name: post
        run: bundle exec ruby script.rb

時間はUTC指定になるので注意です。issueは上がっているので今後はTZ指定ができるようになるかもしれません!

github.com

おわりに

簡単なBotであればGitHub Actionで良さそうですね🤖

参考

obel.hatenablog.jp