patorashのブログ

方向性はまだない

Mojaveでrailsを起動するとpgでセグメンテーション違反になる場合がある

追記

Macの不具合が原因らしいことを突き止めたので色々やったら直ったぽいのでリンクを貼っておきます。

patorash.hatenablog.com

以下、元々の記事

開発マシンを新しいMBPに変えたと同時にMojaveになっていたのですが、そこで開発していると、時々Railsが全く起動しなくなりました。rails consoleとかは問題ないのですが、rails serverすると落ちる。しかもエラーメッセージがあまりにも長いので、標準エラー出力をファイルに出力して確認してみました。

$ bin/rails s 2> log/error.log

すると、以下のような結果に。

/Users/******/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/pg-1.1.4/lib/pg.rb:56: [BUG] Segmentation fault at 0x0000000104994a3a ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

-- Crash Report log information -------------------------------------------- See Crash Report log file under the one of following: * ~/Library/Logs/DiagnosticReports * /Library/Logs/DiagnosticReports for more details. Don't forget to include the above Crash Report log file in bug reports.

-- Control frame information ----------------------------------------------- c:0058 p:---- s:0433 e:000432 CFUNC :initialize c:0057 p:---- s:0430 e:000429 CFUNC :new c:0056 p:0016 s:0425 e:000424 METHOD /Users/toko/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/pg-1.1.4/lib/pg.rb:56 ...略

pgでSegmentation fault

pgでSegmentation faultが発生してます。解決法がわからず、ググってみると、pgを入れ直してみるのがよいとあったので、このエラーが発生するたびに入れ直してました。

https://bitbucket.org/ged/ruby-pg/issues/291/crashbitbucket.org

とりあえず、入れ直すと動くようになりました。

$ bundle pristine pg

しかし、本当にpgのバグなんかなー…🤔自分の設定がどこかおかしいんじゃないか?と疑いつつも騙し騙しの運用をしてました。

pumaを疑う

そして、遂にpgを入れ直しても発生するようになってしまい、どうしたもんか?これは自分の環境だけのことなのか?と思い、周囲に聞いてみるも「その現象はまだ起きてない」と言われるので、再びググることに。rails consoleでは起きずにrails serverだけで起きるため、pumaとかが怪しいのでは?と薄々思っていたところで、そういう情報がヒット。

stackoverflow.com

worker数を複数にするのをやめたら起きないという情報が…。 puma.rbでworker数を指定するところを、developmentの場合は除外するようにしてみます。

workers ENV.fetch("WEB_CONCURRENCY") { 2 } unless Rails.env.development?

半信半疑でやってみたところ、確かに起きなくなりました。

解決?

ひとまず、これでMojaveで開発を継続できるようにはなったのですが、解決したわけではないし、むしろなぜworkerを複数起動するだけでこうなるのか?という疑問が残ります。プロセスをforkするタイミングでPostgreSQLへの接続がおかしくなるのだろうか?とりあえずこの件については時々調査してみようと思います。

Herokuでpuma_worker_killerを適切に設定する

昨年、こんな記事を書いていました。

patorash.hatenablog.com

これで設定できていたと思ったのですが、アクセスが集中したときにワーカーの再起動が起きず、スワップが発生してR14(メモリ関連のエラー)が頻発していました。このときに行った設定はどうも一定時間経過したらpumaのワーカーを再起動するという設定だけで、メモリ使用量が増えたらワーカーを再起動させる設定をしていなかったようでした(完全にミス…)。

puma_worker_killerの設定を行う

puma_worker_killerの設定はREADMEに書いてあったので、今回はそれをちゃんと行いました。

github.com

まず、 config/puma.rb に以下を追加しました。

before_fork do
  PumaWorkerKiller.config do |config|
    config.ram           = 1024 # 単位はMB。デフォルトは512MB
    config.frequency     = 10    # 単位は秒
    config.percent_usage = 0.90 # ramを90%以上を使用したらワーカー再起動
    config.rolling_restart_frequency = 6 * 3600 # 6時間
  end
  PumaWorkerKiller.start
end

その後、bin/rails sをしてみます。

動作確認

puma_worker_killerが起動していたら、以下のようなログが出ます。

$ bin/rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
[37580] Puma starting in cluster mode...
[37580] * Version 4.0.1 (ruby 2.6.3-p62), codename: 4 Fast 4 Furious
[37580] * Min threads: 5, max threads: 5
[37580] * Environment: development
[37580] * Process workers: 2
[37580] * Preloading application
[37580] * Listening on tcp://localhost:3000
[37580] Use Ctrl-C to stop
[37580] - Worker 0 (pid: 37906) booted, phase: 0
[37580] - Worker 1 (pid: 37907) booted, phase: 0
[37580] PumaWorkerKiller: Consuming 764.96484375 mb with master and 2 workers.
[37580] PumaWorkerKiller: Consuming 764.96484375 mb with master and 2 workers.

PumaWorkerKillerが、pumaが使っているメモリの全体量をログに出します。これでいえば、約765MB使ってるということです。

ワーカーが再起動するかチェック

今度は敢えてたくさんのワーカーを起動させて、多くのメモリを使わせてみます。ワーカー数の設定は環境変数WEB_CONCURRENCYにしているので、今回はそれを3に指定してみます。

$ env WEB_CONCURRENCY=3 bin/rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
[38214] Puma starting in cluster mode...
[38214] * Version 4.0.1 (ruby 2.6.3-p62), codename: 4 Fast 4 Furious
[38214] * Min threads: 5, max threads: 5
[38214] * Environment: development
[38214] * Process workers: 3
[38214] * Preloading application
[38214] * Listening on tcp://localhost:3000
[38214] Use Ctrl-C to stop
[38214] - Worker 0 (pid: 38523) booted, phase: 0
[38214] - Worker 1 (pid: 38524) booted, phase: 0
[38214] - Worker 2 (pid: 38525) booted, phase: 0
[38214] PumaWorkerKiller: Out of memory. 3 workers consuming total: 1010.109375 mb out of max: 921.6 mb. Sending TERM to pid 38525 consuming 252.52734375 mb.
[38214] - Worker 2 (pid: 38534) booted, phase: 0
[38214] PumaWorkerKiller: Out of memory. 3 workers consuming total: 1010.12109375 mb out of max: 921.6 mb. Sending TERM to pid 38534 consuming 252.53125 mb.
[38214] - Worker 2 (pid: 38543) booted, phase: 0

921MB以上を使用し(1010MB)、Out of memoryになったため、再起動させているのが確認できました。

Heroku環境ではどう設定すればいいか?

ここで私が気になったのは、config.ramの指定です。上記では1024と固定値にしており、プロダクション環境でもStandard-2X Dynoを使っているので(2X Dynoのメモリは1024MB)これでも問題ないのですが、もしかしたらお試しの環境を準備する際にStandard-1Xを使うこともあり得るかもしれません。もしくは、performance-mとかを使う可能性だってあり得ます。その時に、わざわざこの値を編集したくありません。

Dynoのメモリ量を取得する方法がないかと思い、bashで接続してfreeコマンドを打ってみたら、メモリが60GBと表示されたので(おそらくホストのメモリ量)、軽く絶望していたのですが、なんとなく環境変数を確認したところ、Dynoのメモリ量が取得できました!MEMORY_AVAILABLEで取得可能です。一応、試してみました。

Standard-1X Dyno

$ heroku run --size=Standard-1X bash --app hoge 
Running bash on ⬢ hoge... up, run.9315 (Standard-1X)
~ $ printenv
略…
MEMORY_AVAILABLE=512
略…

Standard-2X Dyno

$ heroku run --size=Standard-2X bash --app hoge 
Running bash on ⬢ hoge... up, run.6999 (Standard-2X)
~ $ printenv
略…
MEMORY_AVAILABLE=1024
略…

Performance-M Dyno

$ heroku run --size=Performance-M bash --app hoge 
Running bash on ⬢ hoge... up, run.2701 (Performance-M)
~ $ printenv
略…
MEMORY_AVAILABLE=2560
略…

puma_worker_killerに設定する

これらを元に設定します。puma_worker_killerのデフォルト値が512MBなので、そうしておきます。

before_fork do
  PumaWorkerKiller.config do |config|
    config.ram           = Integer(ENV.fetch('MEMORY_AVAILABLE', 512))
    # その他は省略
  end
  PumaWorkerKiller.start
end

これで、どのWeb Dynoにも対応したpuma_worker_killerの設定が完了しました🎉

ローカルでワーカーの再起動を起こさないようにする

このままだと、ローカルでrailsを起動すると、環境変数MEMORY_AVAILABLEがないため、512MBになってしまい、ワーカーが複数起動する設定だと、ワーカーの再起動を繰り返すだけになってしまいます。dotenvを入れていたら、この環境変数を追加しておきましょう。

MEMORY_AVAILABLE=1024

オープンセミナー岡山2019に参加してきた

6月29日(土)に開催されたオープンセミナー岡山2019に参加してきました。

oso.connpass.com

今年のテーマは「Technology X Society」で、講演タイトルからは技術で社会を良くしていく話、ということかなと考えてました。

当日のツイートはtogetterにまとめられています。毎度毎度ありがとうございます。

togetter.com

講演内容の特徴

今回は講師の方々は経営層寄りの方が多く、本当に興味深いというか心に刺さる話が聞けました。

テクノロジーで社会を変える系

テクノロジーで社会を変えていくという点では、Kyashの中澤 望さんの講演で、Kyashのビジョンや戦略を知ることができました。その中で、「情報、コミュニケーションは進化したが、お金はまだまだ。お金を進化させたい」という話がありました。こういうところでFinTechが熱くなってるんだなというのが伝わってきました。確かにお金のやり取りにはリードタイムといいますか、売上が上がってから実際に支払われるまでにかなりのタイムラグがありますし、手数料も結構取られます。これをKyash払いにすることで、利用者側の都合で売上をすぐKyashとして利用できるのは、コスト的にも体験的にも大きな変化だと思います。クレオフーガがAudioStockの売上をKyash払いで受け取れるようにしたという話がありましたが、これはクリエイターにとっては売上をすぐに受け取れるし、すごく良い試みだと思いました。個別だと大きな売上が立ちにくいマーケットだと、売上を受け取る前に力尽きてしまいそうなので、すぐに売上を受け取られるという成功体験を得やすいというのは、クリエイターのモチベーションアップにもなりそうだなと思いました。

ビズ・クリエイションの初谷 昌彦さんの講演も、住宅業界を変えていきたいという熱意と、戦略が面白かったです。モデルハウスではなく、住宅見学会のスケジュールや場所を見える化して、リアルな家を見学してもらうというのは、家を既に建てている私からすると、「当時あってほしかったなぁ~!」と思いました。モデルハウスの見学や、住宅見学会に行くのって、めちゃくちゃ楽しいんですが、基本的に有名どころのビルダーしか情報が入ってこないので、工務店の情報とかほぼありませんでした。うちも色んな住宅展示場を回ったりしましたが、基本的には大手しかありませんでした。調べるのも労力がかかるので、住宅雑誌を買ったり、サイトから資料取寄せしたり等はしました。資料を読むのも面白いのですが、検索性はありませんし、同じようなことが書いてあるようにも感じ始めるので、実際に見て、説明が丁寧か、とかがやはり大事だなと思います。 うちが建てたところは結構IT化されていたので、やりとりはメール or LINEで、建設中はWebカメラで様子を見たり、途中経過の画像が随時追加されていったりしてよかったのですが、初谷さんの話だとやはり工務店はまだまだIT化されていないということでした。が、それがまた余地があって面白いのだろうなと思います。建設Techも楽しそう! 大手が儲かって下請けが搾取される構造になっているのが課題で、そこに取り組んでいるというのがIT業界にも似たところがあるので共感した人は多かったんじゃないかなと思います。

経営者目線や組織の話系

アイネットの山本 由佳里さんの講演は、起業ではなく父親からの事業継承で社長になったというならではの話だったなと感じました。エンジニア上がりだからこそ感じる違和感や責務を持って、社長業をされているからこその重い言葉や熱い筋の通った信念があり、こういった方が社長でアイネットの社員は幸せだろうなと思いました。 エンジニアの幸せの秘訣についての中で、励ましの言葉になったのは、「アイデアなんてなくてもいいと思おう」でした。アイデア勝負な昨今でろくなアイデアが浮かばない自分はダメなんじゃないだろうか?と割とすぐに考えてしまいがちだけれど、アイデアは地続きでしか出てこないから、出てくるまで気長に待ちながら技術を磨くなりしていればいいかなと思えました。 IT業界に限らない話かもしれないけれど、結構、「呪いの言葉」みたいなものがあって、その呪縛に知らず知らずに囚われてしまって、幸せを感じづらくなっているんじゃないか?と講演を聞いて思いました。経験がないから、年だから、マイナーな言語だから、男だから、女だから…のような言葉。 あと小さい案件から大きな経験を得たというの、めちゃくちゃ同意で、これは個人開発でも言えることなんだけれど、小さい案件で一通りの経験ができるというのは本当に尊いことだと後でわかりました。

ヌーラボの橋本 正徳さんの講演も面白かったです。twitterで質問を集めて途中途中で回答しながら進めていくのはリアルタイム感があってよかったです。ヌーラボの製品はbacklogとcacooは知っていましたが、typetalkは知りませんでした。cacooは時々使っていますが、他は全然使ったことがなく…。 一番大事なことに掲げていたのが「哲学」でした。ビジョンより大事って言われてたけれど、確かにそうだなと思いました。「ツールを導入するということは、そのツールを作っている会社の哲学を導入するのと一緒」というのは、重い言葉だなと思います。安易に値段を基準にしてツールを導入してしまうと、その哲学に引っ張られてしまうこともあるだろうな、と思います。個人で色々ツールを探していて、そのときに感じていたものを言葉にしてもらえたなと思いました。 組織設計が主な話題だったと思いますが、自分的にはアンガーマネジメント研修いいなぁ…と思いました。 個人的には怒ってるまでいってないんだけれど声がでかくなるみたいで怒ってると思われてしまうことがあり、気を付けているんだけれどずっとそのイメージが残っているのか最近でも延々とそのことを指摘されることがあってもう正直ウンザリしているんですが、延々と言われるくらいなら研修受けてみたいなぁと強く思いました。指摘されるだけだと、個人で気を付けてるつもりでも、別にそれからフィードバックをくれるわけでもないし…。 そういえばリーナスもそういう研修受けたという話だし、アンガーマネジメント、効果絶大なのでは…。

翔泳社の岩切 晃子さんの講演は、手書きスライドでOHPを思い出しました。講演内容は「全ては未来会議から始まった」というタイトルで、組織パターンという書籍のワークショップに参加して、それを仕事にも使ったという話だったのですが、講演中に気持ちが昂ってしまうほど、壮絶であり、しかしとても大勢の良い仲間に協力していただいて成し得たのだなと思います。生の講演ならではのことでオフレコだけれど貴重な体験を沢山話していただけて、とても感銘を受けました。心理的安全の確保や、小さくてもいいから成功体験を早く作ることなど、ノウハウもありつつの素晴らしいお話でした。

スーパーマン

高野さんの講演「ひとりでできるもん!」は、かけなびさんが「あなたしかできないでしょ!」と言ってたけれど、超すごかったです。バスの運行システムを1人で作成して全国から声がかかるようになってて、またそのシステムがめちゃくちゃ良くできていてコストも安い…。ゲーム作りから様々なノウハウを得て、絵も音楽も動画もシステム開発も全部1人でやってしまうというのはほんまにスーパーマンだなと思います。え、音楽も!?って思いましたもん。それがまさかの、バンドをやっていたから音楽も作れたというエピソード、そして、実は調理師免許も持っているとか…。 1人でできるメリット、デメリットのところで、「人を使うのが苦手だから。そして絶対にブラック企業になる」というふうに語っておられたので、これが正解なのだろうなと思いました。特異だけれど、こういうケースもあるのだろうと。 なんていうか、技術トレンドの選択が~とかよりも、やってみてからでいいじゃん、という気持ちになりました。もっと頑張らんと!という勇気をいただけました。

全体を通して

普段なかなか聞けない話を聞く機会を得られて本当によかったです。OSO最高!阿部さんすごいなー!と思いました。 また、旅するagile本箱という展示があり、休憩時間にパラパラと読んだのですが、気になる本があったので、また注文して読んでみようと思います。

実行委員の皆様、お疲れさまでした。ありがとうございました。

再帰クエリを書いてみた

まんまなのですが、再帰クエリを書いてみたら一発で処理がキマったのでよかったという話です。

Railsで親子関係を表現するデータで再帰処理をやっていたのですが、当然ですが再帰するたびにクエリが発行されていてなんだか微妙でした。 そうこうするあたりで、「再帰クエリ」という文字を見かけたので、「お、これはやってみるチャンスやな」と思ってやってみた次第。

モデルの定義

テーブル作成

Userモデルの親子関係を表現するテーブルとして、familiesテーブルを作ったとします。 外部キー制約とユニーク制約を入れておきました(子供が2つの親を持てないようにするため)

class CreateFamilies < ActiveRecord::Migration[5.2]
  def change
    create_table :families do |t|
      t.integer :parent_id, null: false
      t.integer :child_id, null: false

      t.timestamps
    end
    add_foreign_key :families, :users, column: :parent_id
    add_foreign_key :families, :users, column: :child_id
    add_index :families, :child_id, unique: true
  end
end

Familyモデルの定義

検証を定義しておきます。子供が自分の親を子供として登録できないような検証check_circular_referenceを定義しておきました。ここで使っているmy_familiesメソッドで、再帰クエリを使っています。

class Family < ApplicationRecord
  belongs_to :parent, class_name: 'User'
  belongs_to :child, class_name: 'User'

  validates :parent,
            presence: true,
            uniqueness: { scope: :child_id, message: '既に子供として登録済みです' }
  validates :child,
            presence: true,
            uniqueness: { message: '既にどこかの子供として登録済みです' }
  validate :check_circular_reference

  private

    def check_circular_reference
      if parent.my_families.exists?(id: child)
        errors.add(:child, '親や子供の子供を登録することはできません')
      end
    end
end

Userモデルの修正

透過的に親子関係が扱えるように関連を記述し、あとは親子関係とかをチェックするためのメソッドを追加等。再帰クエリはmy_familiesメソッドで使っています。

class User < ApplicationRecord

  # 親子関係を参照するための関連を記述
  has_many :families,
           foreign_key: :parent_id,
           dependent: :destroy

  has_many :children,
           class_name: 'User',
           through: :families,
           foreign_key: :child_id

  has_one  :family,
           foreign_key: :child_id,
           dependent: :destroy

  has_one :parent,
          class_name: 'User',
          through: :family,
          foreign_key: :parent_id


  # 親かどうか確認する
  # @return [Boolean] 子を持っているかルート親であればtrueを、それ以外はfalseを返す
  def parent?
    self.children.exists? || self.root_parent?
  end

  # 子かどうか確認する
  # @return [Boolean] 子ならばtrueを、親ならばfalseを返す
  def child?
    self.parent.present?
  end

  # ルート親かどうか確認する
  # @return [Boolean] ルート親ならばtrueを、そうでなければfalseを返す
  def root_parent?
    !self.child?
  end

  # ルート親を取得する
  # @return [User] ルート親を返す
  def root_parent
    return self if root_parent?
    self.parent.root_parent
  end

  # 親子を全て返す
  # 再帰問い合わせを使って子を取得する
  # @return [User::ActiveRecord_Relation] メソッドを実行したUserの家族を全て返す
  def my_families
    User.from(<<~SQL)
      (
        WITH RECURSIVE my_families AS (
          SELECT
            families.parent_id
            , families.child_id
          FROM families
          WHERE #{User.sanitize_sql_for_conditions(['parent_id = ?', self.root_parent.id])}
          UNION ALL
          SELECT
            families.parent_id
            , families.child_id
          FROM families, my_families
          WHERE families.parent_id = my_families.child_id
        )
        SELECT *
        FROM #{User.quoted_table_name}
        WHERE id IN (
          SELECT parent_id AS user_id
          FROM my_families
          UNION
          SELECT child_id AS user_id
          FROM my_families
        )
      ) users
    SQL
  end
end

再帰クエリ

再帰クエリに関しては、Let's Postgresにちょうどいい説明があって助かりました🙏

lets.postgresql.jp

木構造のデータを取っていくのにちょうどいいです。ただし、複雑な木構造だったりするとパフォーマンスが悪かったり、循環してしまっているような構造だと無限ループになる可能性があるため、取り扱いには注意が必要です。今回は循環しないように検証を追加しているため、単純な木構造のデータですし、そこまで親子関係も深くならない想定での実装です。

WITH句

PostgreSQLにはWITH句があります。これは、サブクエリに名前を付けることができる機能です。複雑なSQLの場合、JOINしたりUNIONしたりする際に同じサブクエリを書くケースがありますが、そういうのはWITH句を使って事前に一時テーブルを作っておけば、シンプルに書くことができます。

Before

なんかいい例が思いつかなかったのですが、例えばcurrent_loginsという今期のログイン実績のテーブルと、previous_loginsという前期のログイン実績のテーブルがあったとして、それの権限がadminのもののみを合わせて抽出したいとします。すると、以下のような感じで、INNER JOINの条件が同じになります。

SELECT current_logins.*
FROM current_logins
INNER JOIN (
  SELECT id, name
  FROM users
  WHERE role_id = 1
) admin_users ON current_logins.user_id = admin_users.id
UNION ALL
SELECT previous_logins.*
FROM previous_logins
INNER JOIN (
  SELECT id, name
  FROM users
  WHERE role_id = 1
) admin_users ON previous_logins.user_id = admin_users.id
After

これをWITH句を使うと、以下のように書くことができます。同じ条件のサブクエリの再利用が捗りますね。

WITH admin_users AS (
  SELECT id, name
  FROM users
  WHERE role_id = 1
)
SELECT current_logins.*
FROM current_logins
INNER JOIN admin_users ON current_logins.user_id = admin_users.id
UNION ALL
SELECT previous_logins.*
FROM previous_logins
INNER JOIN admin_users ON previous_logins.user_id = admin_users.id

WITH RECURSIVE句

WITH RECURSIVE句は初回の取得データを元に再帰的にUNION( ALL)を繰り返す等をしてデータを取得できます。あとはWITH句と同じく、その後に続くクエリで利用することが可能です。 わかりやすくするために、さっきのUserモデルで書いていたものを修正したものが以下になります。初回のparent_idを1に指定しています。

WITH RECURSIVE my_families AS (
  SELECT
    families.parent_id
    , families.child_id
  FROM families
  WHERE parent_id = 1
  UNION ALL
  SELECT
    families.parent_id
    , families.child_id
  FROM families, my_families
  WHERE families.parent_id = my_families.child_id
)
SELECT *
FROM users
WHERE id IN (
  SELECT parent_id AS user_id
  FROM my_families
  UNION
  SELECT child_id AS user_id
  FROM my_families
)

UNION ALL以下では、FROMにWITH RECURSIVEで指定したmy_familiesテーブルを追加し、WHERE条件で利用しています。ここでは、User id 1の子供が親になっているデータを抽出、その子がまた親になっているデータを抽出…というように繰り返し処理が行われます。

そして、再帰クエリで取得できたmy_familiesを利用して家族として該当するidのみをusersテーブルから抽出しています。一発で階層構造のデータを取得できるのはいいですね!👍

やってみた感想

最初は同じことをする処理をRubyで実装していたのですが、だんだん頭がこんがらがってきてしまっていました。シンプルな再帰処理ならサクッと書けるのですが、複雑なやつは苦手です😭何度もクエリが発行されてパフォーマンスも悪くなりがちですし。しかしSQLならば1度の発行で済むため、パフォーマンスもよさそうです。 とはいえ、プログラムが複雑になるか、SQLが複雑になるか、というトレードオフにはなるんですが、これくらいだったら再帰クエリを使ってもいいかなと思えました。

また、こういう親子関係を管理するgemにancestryというやつがあるのですが、再帰クエリをマスターしたら、これを使わずに済むのではないか?と思えました。ancestryは1つのカラムに/で子idを区切って登録したりしているので、なんとなく辞めたいなという気持ちがあります。再帰クエリを使ったancestry的なgemを作ってみたいと考えています(いつになるやら)

RailsにPR送ったらマージされた

Railsに含まれているgem web-consoleに自分のPRが取り込まれた🎉

github.com

感動を残しておくためにブログに書いとく。

事の始まり

先日、Rails 6.0.0 rc1でrails newして、ごにょごにょと進めていたら、ログに以下のメッセージが出ていた。

Cannot render console from Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255

今はWindowsからHyper-V上のUbuntuに接続して開発しているため、web-consoleがホスト側のIPを許可していないってことだった。早速ググって、設定を追加しようとした。

qiita.com

おー、これだこれだ…と思い、追加。

config.web_console.whitelisted_ips = '0.0.0.0/0'

そして、再度アクセスすると、まだログが出るじゃないですか。どういうことですか、これは…。

ソースコードを読みに行く

すると、最新のweb-consoleのコードでは、config.web_console.whitelisted_ipsではなく、config.web_console.permissionsを利用するようになっていた。

Railsアプリ側でwhitelisted_ipspermissionsに変更すれば、エラーログも消えて問題なくなった。

config.web_console.permissions = '0.0.0.0/0'

しかし、互換性を保つため、config.web_console.whitelisted_ipsも利用できるように見えていたんですよ、一見は。

initializer "web_console.permissions" do
  if permissions = config.web_console.permissions || config.web_console.whitelisted_ips
    Request.permissions = Permissions.new(permissions)
  end
end

しかし、上のほうでconfig.web_console.permissions = %w( 127.0.0.1 ::1 )って初期化されているため、config.web_console.permissionsnilになることは絶対になく、config.web_console.whitelisted_ipsが使われることはないことになっていた。これ今までのコードで設定していた人たちがRails6にアップデートしたら急にweb-consoleが使えなくなる地雷じゃない?というかバグじゃない?と思い、PRを作ってみることに。

Pull Request作成

ひとまず、上記のところの順番を逆にするだけのPRを作って送った。最初にconfig.web_console.whitelisted_ipsを評価してしまえばそれでいいからだ。

initializer "web_console.permissions" do
  if permissions = config.web_console.whitelisted_ips || config.web_console.permissions
    Request.permissions = Permissions.new(permissions)
  end
end

そしたら、「テストを追加してくれ」と言われたので、やろうとしたのだけれど、うまくいかなかった。railtieのテストでRailsの初期化をもう一度やる仕組みが定義されていたのだが、それに則るとconfig.web_console.permissionsが設定されていなくてnilになっており、もしwhitelisted_ipsに値を入れても評価されるかどうかのテストがあったとしてもパスしていただろう。つまりテストもバグってた。

どーしたもんかなー…と思い悩んでいたのだけれど、コードとテスト見ると、ループバックアドレス127.0.0.1のこと)はデフォルトで常に許可されるように定義されていたため、「あれ?これってconfig.web_console.permissions = %w( 127.0.0.1 ::1 )の初期値の定義いらないよな?🤔」と思い、初期値の行の削除とpermissionsの定義を行うメソッドを必ず実行するように修正した。

# config.web_console.permissions = %w( 127.0.0.1 ::1 ) は削除済み
initializer "web_console.permissions" do
  permissions = config.web_console.permissions || config.web_console.whitelisted_ips
  Request.permissions = Permissions.new(permissions) # ifが不要だったので外した
end

whitelisted_ipsを使ってもpermissionsが設定されることを確認するテストを追加して、コードを引用しながら拙い英語で頑張ってみたところ、マージされた!!😂Rails6には間に合う模様。嬉しい!!

感想

やってみるもんです。

トレイボーを貰った

誕生日だったので、妻に最近気になっていたトレイボーをプレゼントに買ってもらいました😊 トレイボーはヨギボーのトレイ付きクッションです。膝にPCを置いて作業をするとPCが熱くなったり、肩が凝ったりしていたので、楽な姿勢で安定して作業したかったのです。 届いたので早速レビューを!

箱はしっかりとしています!

f:id:patorash:20190516072127j:plain

プチプチで丁寧な梱包が行われていました。

f:id:patorash:20190516072159j:plain

緑のクッションと引っ掛かりのための両面テープ付きバーがありました。

f:id:patorash:20190516072254j:plain

恐らくバーを貼るとこんな感じになるかと思います(が、貼っていません) 溝がありますが、そこにはタブレットをひっかけることができます。動画を見るためにタブレットを持たなくていいのでいいですね!一度Kindle Fireで動画を見てみたのですが、とてもよかったです。

f:id:patorash:20190516072355j:plain

実際に使うとこんな感じです。ビーズクッションなので、平行にするだけでなく、傾けたりしても太ももにフィットして安定性がありますし、PCの熱さもないし、めちゃくちゃ快適です!普通のソファーがPCの作業机として使えます。猫背にならずにキーボードを打てるので肩凝りにもならなさそうです。

f:id:patorash:20190526182130j:plain

感想

椅子さえあればどこでも作業しやすくなりそうです。例えば、長時間移動の時とかに持っておくとよさそう!自宅で作業するのもいいですが、車で作業するとかにも使えそう。非常にいいものをプレゼントしてもらったなと思います👍

RubyでUTF-8のCSVをExcelに変換するコマンドを作った

お客様や社内の他の部署のメンバーとファイルのやりとりをすることってあると思うのですが、いつも大変なのが、CSVファイル。HerokuのDataclipsがバージョンアップされてExcel形式でダウンロードできなくなったので、仕方なくCSVでダウンロードして、Excel起動してインポートでそのCSVを取り込んで…ってやっていたのですが、ファイル数が多くなると、とても苦痛です。

それができるようなコマンドやサービスなりを探そうとしたのですが、いいのがなかったので、もう面倒だから自分でgemを作りました。

rubygems.org

実行コマンド付きのgemなので、CLIで利用可能です。

CLIで利用

$ csv2excel -f foo.csv # => foo.xlsxが作成される
$ csv2excel -f bar/baz.csv # => bar/baz.xlsxが作成される
$ csv2excel -d ./bar # => barディレクトリ内の全てのcsvファイルからxlsxファイルを作成する

Rubyのコードで利用

CSVクラスをオープンクラスしてメソッドを追加したので、利用も簡単。

require 'csv2excel'
CSV.to_xlsx(file: 'foo.csv') # => foo.xlsxが作成される
CSV.to_xlsx(dir: './bar') # => barディレクトリ内の全てのcsvファイルからxlsxファイルを作成する

まとめ

ほとんどgem axlsxのおかげです。でも我ながらめっちゃ便利で重宝しています。