パパエンジニアのアウトプット帳

30歳に突入した1児のパパエンジニアのブログ

Railsアプリを型チェックできるようにSteepに入門してみた

今年も残り少なくなってきて、そういえばRailsアプリに型チェックを導入してみたいなーと思っていたのを思い出したのでSteepに入門してみた。


RailsアプリへのSteep導入は神速さんのブログがすごく丁寧だったのでそれを参考にしてやった sinsoku.hatenablog.com


差分としては

  • lib/tasks/rbs.rake を作成する
    • 現在はbin/rails g rbs_rails:installするとlib/tasks/rbs.rakeが生成される
  • 不足してるrbsを用意する
    • 自分は ActionCable::Channel::BaseActionCable::Connection::Base だけでよかった
# sig/patch.rbs
module ActionCable
  module Channel
    class Base
    end
  end

  module Connection
    class Base
    end
  end
end


あと、神速さんのブログのscaffold を試すのところにあるようにコントローラーのエラーも確かに発生していて、自分は scaffold userをしたんだけどそのエラーが下記。

# Type checking files:

..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................F.....

app/controllers/users_controller.rb:7:4: [error] Cannot find the declaration of instance variable: `@users`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @users = User.all
      ~~~~~~

app/controllers/users_controller.rb:17:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new
      ~~~~~

app/controllers/users_controller.rb:27:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new(user_params)
      ~~~~~

app/controllers/users_controller.rb:31:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully created." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:45:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:60:32: [error] Type `::UsersController` does not have method `users_url`
│ Diagnostic ID: Ruby::NoMethod
│
└       format.html { redirect_to users_url, notice: "User was successfully destroyed." }
                                  ~~~~~~~~~

app/controllers/users_controller.rb:69:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.find(params[:id])
      ~~~~~

Detected 7 problems from 1 file

path helperのやつとインスタンス変数の定義がないと怒られたので解決してみた。

path helper関連

これはrbs_rails:allをしたときにsig/rbs_rails/path_helpers.rbsが生成されているのになんでそれ見てくれないのかなと疑問だったんだけど、

# sig/rbs_rails/path_helpers.rbs

interface _RbsRailsPathHelpers
・・・
  def users_url: (*untyped) -> String
  def new_user_url: (*untyped) -> String
  def edit_user_url: (*untyped) -> String
  def user_url: (*untyped) -> String
・・・
end

このようにinterfaceで定義されているだけなので、これをコントローラーのrbsファイルでincludeしてやればよさそう。
(まだRBSが全く分かってないんだけど、RBS基礎文法最速マスター - pockestrap見つつこう使うのかなーみたいな感じでやってみたらエラーはでなくなった)

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

インスタンス変数

Cannot find the declaration of instance variable: `@user`
Cannot find the declaration of instance variable: `@users`

@userと@usersのRBS定義がないのでこれは手動で定義した。

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  @user: User
  @users: User::ActiveRecord_Relation

  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

あと注意点としては rbs_rails:allしたときにmodelの定義も生成してくれるけど、それはdb:migrateを実行しておかないと生成されない。

rbs_rails/rake_task.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

        ::ActiveRecord::Base.descendants.each do |klass|
          next unless RbsRails::ActiveRecord.generatable?(klass)

ここの RbsRails::ActiveRecord.generatable?(klass)

rbs_rails/active_record.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

    def self.generatable?(klass)
      return false if klass.abstract_class?

      klass.connection.table_exists?(klass.table_name)
    end

とあるようにDBにテーブルが存在しているかを見てるので。


これでとりあえずはsteep checkが通るようになったので、なんとか入門までは行けた感じがする。

$ bundle exec steep check
# Type checking files:

........................................................................................................................................................................................................................................

No type error detected. 🫖

ActiveJob::TestHelperでセットされるTestAdapterを無効にしたい場合

下記のように ActiveJob::TestHelperをincludeしているとqueue_adapterが強制的にTestAdapterになります。

[spec/rails_helper.rb]
RSpec.configure do |config|
  config.include ActiveJob::TestHelper
end

RSpecのJobのテストとかを書く分には便利なアサーションメソッドとかも使えて便利なのですが、本当にJobを実行したい場合もあります。 今回はActionCableのChannelでJob.perform_laterしてそのジョブで処理が完了したらbroadcastするようなものだったので、同期ではなく非同期でJobを実行したかったです。


初めは Rails.application.config.active_job.queue_adapter = :async としてqueue_adapterを切り替えようとしていたのですが、どうもTestAdapterから切り替わっていないようでした。(設定直後くらいでbinding.pryして止めても確かにAsyncAdapterにはなっている)


どうにかして部分的にAsyncAdapterに切り替えたいなと調べていると下記を見つけました。 github.com

disable_test_adapterというメソッドを呼ぶことでActiveJob::TestHelperでセットされるTestAdapterを無効にできるようです。 Rails6になった頃、config/environments/test.rbのqueue_adapter設定が無視されるのがあったんですね。

qiita.com

これでasyncアダプターに切り替えて非同期でJobを実行できるようになりました。

補足

同期実行で問題ない場合は下記で見つけた perform_enqueued_jobs を利用すると同期実行にできます。 qiita.com

RailsでPostgreSQLのVIEW定義を管理する

初めてちゃんとPostgreSQLのVIEWを使うので、Railsで使う場合の調査。


下記のgemを利用するのが定番みたい。

github.com

このgem使うと何が嬉しいのか

RailsガイドにもCREATE VIEWする方法が書かれてる。

railsguides.jp

gem利用しないとCREATE VIEWする生SQLをexecuteするだけなので

  • migrationのdownを自分で定義する必要がある
    • はじめのCREATE VIEWならdropすればいいけど以降の変更時はその前のやつに戻す必要がある
      • レビュー時に以前のmigrationを参照しないと行けないので手間が増える

マテリアライズドビューを利用するならrefreshの仕組みを用意してくれるのでさらに便利そう。


という感じかな。 あとはおまけとしてエディタでSQLシンタックスハイライトが効くので見やすいくらいですかね。

update_viewとreplace_viewはどう使い分けるのか

コメントにちゃんと書いてくれてるの助かる。

scenic/postgres.rb at 3600a485797fe1dbf30d152cc60b6d2318f04c48 · scenic-views/scenic · GitHub

CREATE OR REPLACE VIEWは、既存のビュースキーマの最後に新しいカラムを追加する場合にのみ許可されています。既存のカラムは、順番を変えたり、削除したり、型を変更することはできません [DeepLでの翻訳より]

なので、基本的にはupdate_view使う感じですね。


あとはTechRachoも目を通しておくと良さげ

techracho.bpsinc.jp

AWS Batch(AmazonLinux1)のボリュームサイズを増やす時はDockerのストレージ制限もお忘れなく

マネージド型コンピューティング環境(EC2)(AmazonLinux1)のAWS Batchのお話です。


AWS BatchでNo space left on deviceでエラーになった場合はディスク容量を増やせばとりあえずはいいのですが、EBSのサイズアップだけでなくDockerのストレージ制限もサイズアップしないとダメです。


下記2つのドキュメントをちゃんと読めば確かに今なら理解できるけど、当時はとりあえずEBSのサイズ増やせばいいんでしょ?って感じで斜め読みしてよく分かってなかった。。。

AWS Batch の「No space left on device」エラーを解決する

起動テンプレートを使用して、AWS Batch の Amazon EBS ボリュームのサイズを増やす


これでEBSのサイズ増やしてもNo space left on deviceになって??ってなっていたけど、下記の記事でようやく理解した。

https://bedford.io/projects/cli/doc/aws-batch.htmlDisk space for your jobsの項

By default, your Batch jobs will each have access to 10 GiB of scratch space. This is enough for most Nextstrain builds, but yours may require more.

Configuring more space requires a little bit of setup. It also helps to understand that Batch uses ECS to run containers on clusters of EC2 servers.

Each EC2 instance in an ECS cluster has a storage volume named /dev/xvdcz which is shared by all the containers/jobs running on that instance. The default size of this volume is 22 GiB, which comes from the AWS-managed ECS-optimized machine images (AMIs) used by Batch.

Each container/job is allowed to use up to 10 GiB of disk by default. This comes from Docker’s dm.basesize option.

In order to give your Batch jobs more disk, you have to increase both of these defaults. There are many approaches to doing this, but the simplest is to create a new EC2 launch template.

It’s quickest to create the launch template using the AWS Console, although you can also do it on the command-line.

First, add to the launch template an EBS storage volume with the device name /dev/xvdcz, volume size you want (e.g. 200 GiB), and a volume type of gp2. Make sure that the volume is marked for deletion on instance termination, or you’ll end up paying for old volumes indefinitely! This sets the size of the shared volume available to all containers on a single EC2 instance.

Next, under the “Advanced details” section, add a user data blob with the following text:


まとめると

  • ECSクラスター内の各EC2インスタンスには/dev/xvdczという名前のストレージボリュームがあり、そのインスタンス上で動作するすべてのコンテナ/ジョブで共有される
    • このボリュームのデフォルトサイズは22GiB
  • 各コンテナ/ジョブは、デフォルトでは最大10GiBのディスクを使用することができる
    • これはDockerのdm.basesizeオプションによるもの
  • もっと多くのディスクを割り当てたい場合はEBSとDockerストレージ制限の両方を増やす必要がある
    • そのシンプルな方法がEC2起動テンプレートを作ること


ちなみに/dev/xvdczAmazon Linux 1の場合でAmazon Linux 2だと/dev/xvda しかないようです。

起動テンプレートを使用して、AWS Batch の Amazon EBS ボリュームのサイズを増やす より

注: 起動テンプレートの DeviceName は、ご利用中の Amazon Elastic Container Service (Amazon ECS) に最適化された Amazon マシンイメージ (AMI) バージョンによって異なる場合があります。デフォルトでは、AWS Batch は Amazon Linux 1 に基づいており、2 つの Amazon EBS ボリューム (/dev/xvda および /dev/xvdcz) が付属しています。Amazon Linux 2 を使用している場合、AWS Batch には 1 つのボリューム (/dev/xvda) のみがあります。詳細については、AMI ストレージ設定をご参照ください。


Amazon Linux2の場合は/dev/xvdaのサイズを増やせばDockerも増えるみたいです。

dev.classmethod.jp


Amazon Linux2へのアップグレードもちゃんとやらないとな。。。

プライベートなS3バケットにあるファイルをCloudFront経由でアクセスできるようにする

なにかの度にやることがあるけど、スッと設定できずに403エラーになるのでメモで残しておく


まず、下記にあるように静的ウェブサイトホスティングとは設定が異なるので見るページ間違えないこと

CloudFront ディストリビューションを使用して Amazon S3 バケットへのアクセスを制限する

Access Denied エラーをトラブルシューティングするには、ディストリビューションのオリジンドメイン名が S3 ウェブサイトのエンドポイントか S3 REST API エンドポイントかを確認してください。

今回は静的ウェブサイトホスティングはOFFの本当にプライベートなS3バケットにあるファイルをCloudFrontから配信できるようにしたいので、見るべきドキュメントはこっち。

CloudFront ディストリビューションを使用して Amazon S3 バケットへのアクセスを制限する

あとは大体CloudFrontのコンソールでポチポチ画面から設定しているとできるけど、すでにバケットポリシーとかあるとそれが邪魔することがある

6.バケットポリシーの中で、"Effect": "Deny" が含まれる (これにより、CloudFront OAI からバケットへのアクセスを禁止しています) ステートメントを探します。CloudFront OAI がバケット内のオブジェクトにアクセスできるように、これらのステートメントを変更します。

7.バケットポリシーの中で、"Effect": "Allow" が含まれる (これにより、CloudFront OAI 以外のソースからのバケットへのアクセスを許可しています) ステートメントを探します。ユースケースの必要に応じてこれらのステートメントを変更します。

自分は下記みたいなバケットポリシーになっていて、Sid:1の方を消したらCloudFront経由でアクセスできるようになった

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
        },
        // CloudFrontのコンソールでやると足されるやつ
        {
            "Sid": "2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity xxxxxxxxxxxxxx"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
        }
    ]
}

RailsでenumerizeのカラムをSTIとして使う

これのenumerize版。

qiita.com

既存のモデルを分けたかったので、参考にさせてもらった。
すでにenumerizeの利用しているカラムをSTIのカラムにしたかったので、inheritance_columnでのSTIのデフォルトカラム(type)を変更もした。

class Payment < ActiveRecord::Base
  extend Enumerize

  self.inheritance_column = "pay_type"
  
  enumerize :pay_type, in: { credit_card: 0, bank_transfer: 10, paypal: 20 }, scope: true, predicates: true

  class << self
    def find_sti_class(type)
      type_name = pay_type.find_value(type).camelize
      "Payment::#{type_name.to_s.camelize}".constantize
    end

    def sti_name
      pay_type.find_value(name.demodulize.underscore).value
    end
  end
end

sti_nameメソッドの定義もなんでいるのかと思ったのですが、Payment::CreditCard.newとした時にinheritance_columnで定義したカラムに期待する値がちゃんと入るようにしないとなのでちゃんと定義しましょう。 (ここで使われるので)

Goでの日付変換

最近やっとのことGoを真面目にやりだしてTwitterAPIで取得したtweet.CreatedAtの日時文字列をISO8601形式の文字列にしたいなと思ったけど、time.parseのformatの指定とかが独特で少しハマった。


Rubyとかだと%Y/%m/%dとかでformat指定するけど、Goだと2006-01-02みたいな具体的な年月日などの数字を指定する必要がある。
初めなんでサンプルとかこんな中途半端な日時なんだろう?と思い実装した日に変えたろって変えたら0001/01/01 00:00:00 +0000 UTCみたいになってしまった。


調べてみると下記のように

GoのtimeのFormat表記方法でハマったこと - emahiro/b.log

結論としては goでのtimeのformat変換では使える文字が決まっている っていうことになります。

と、ありあの日時が固定らしいと分かりなんじゃそりゃ!っという感じ。


なんか特別な意味でもあるのかと思ったけど、下記を見てただふーーーん...というお気持ちになっただけなのでした。

Goのtimeパッケージのリファレンスタイム(2006年1月2日)は何の日? - Qiita

その他参考

Goでtime.Parseを使うときのタイムゾーンについて

[Go] UTCの時刻を日本時間に変換する - Qiita