Vim で snake_case を CamelCase に置換する(Visualモード)

概要

APIのレスポンスのキーはsnake_caseだけど、クライアント側でTypeScriptの型のキーに変換したいという場面に遭遇した。
visual mode => 変換!みたいなことがしたかったので、VsCodeVimモードにて検証した。

手順

1: visualモードで変換したい範囲を選択する

shift + vとかでvisualモードで選択する。

2: commandモードにする。

shift + !とかでcommandモードでかつ!が付与される。

:'<,'>!と表示されることがわかる。
これは選択したラインを意味するので、これはこのまま使う。
!でこれ以降のコマンドを実行するという意味になる。

3: 置換を実行する

:'<,'>!perl -pe 's#(_|^)(.)#\u$2#g'

リファレンス

Convert snake_case to CamelCase in Vim

Workerの処理でActiveRecord::ConnectionNotEstablishedが発生した件

概要

SQSからキューを取って、DBに書き込むWorkerがActiveRecord::ConnectionNotEstablishedで落ちた件について、調査した。

SQSからのキューが一時的に止まる事象が発生して、長時間DBアクセスが起きない時に発生した。DBはMySQLを利用していた。

エラーの再現

事象からDB側の設定のwait_timeoutが問題だろうと検討をつけて、config/database.ymlwait_timeout: 5の設定を追加して、 以下のようなworkerを動かしてみた。

module Workers
  class TestWorker
    def random_update_user!
      while true
        begin
          # キューを取る時間が`wait_timeout`の設定である5秒をランダムで上回る設定
          sleep 3 + rand(5)

          puts "ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          user = User.all.sample
          user.update!(name: "taro#{Time.current.nsec}")
        rescue => e
          puts "rescued ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          pp e
        end
      end
    end
  end
end

たしかにActiveRecord::ConnectionNotEstablishedが発生することを確かめることができた。

rescued ActiveRecord::Base.connection_pool.stat: {:size=>5, :connections=>1, :busy=>1, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5.0}
#<ActiveRecord::ConnectionNotEstablished: MySQL client is not connected>

ActiveRecord::ConnectionNotEstablishedが一度発生すると、ずっと発生し続けた。 別のコネクションに切り替えたりするのかな?と思ったけれど、そうではないみたい。

ActiveRecord::Base.connection_pool.statでコネクションプール全体の利用状況が把握できるが、1つのコネクションを利用し続けていることを確認できる。

解決策: reconnectの設定をする

config/database.ymlreconnect: trueを追加するだけ。

rescueされて1回だけActiveRecord::ConnectionNotEstablishedが発生するが、それ以降はエラーは発生せず、コネクションプール全体の利用状況も1つだけ使っている状況は変わらなかった。

ただし、この方法は常に利用できるわけではなく、

ActiveRecordのDBコネクションの接続切れと再接続について。reconnectオプションは危険だなーとかも

変数を使ったりした場合等の接続セッションに依存するような場合のコードを書くと、途中で接続がきれて変数等がリセットされてるのにきづかずに次の変数等に依存したsqlを実行してしまう可能性があるからです。 トランザクション周りも危険っぽいですね。ロールバックしてるのに気づかず自動コミットに戻って実行されちゃうとかかなー 怖い怖い。

なので、気をつけて使う。できれば使わない。

解決策: 処理の初めに明示的にリコネクトする

ActiveRecord::Base.connection.reconnect!を利用して、明示的にリコネクトすればActiveRecord::ConnectionNotEstablished`から回復できることを確認した。 今回のような、SQSからキューを取って、DBに書き込むWorkerの処理では、これで良さそう。

module Workers
  class TestWorker
    def random_update_user!
      while true
        begin
          sleep 3 + rand(5)

          puts "ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          user = User.all.sample
          user.update!(name: "taro#{Time.current.nsec}")
        rescue => e
          puts "rescued ActiveRecord::Base.connection_pool.stat: #{ActiveRecord::Base.connection_pool.stat}"
          ActiveRecord::Base.connection.reconnect!
          pp e
        end
      end
    end
  end
end

異なるファイルに対して共通のフローの処理をしたい場合のディレクトリ構造

概要

異なる種類のファイル(hoge_file, foo_file)に対して、以下の共通の処理をしたいケースを考えます。

1: データ抽出
2: フォーマット
3: データ保存
4: データアップロード

ファイルごとにディレクトリを分ける

ファイルごとに共通な処理がないような場合、以下のようなディレクトリ構成が思い浮かぶかもしれません。

ただ、この形だとファイルをまたいだ共通処理を書きたい場合に、適切な場所がなくなり、将来的に困る可能性が高いです。
(concernsディレクトリで共通の処理をmoduleで作る方法もあるかもしれないけど、concernsは注意深く使う必要があります)

app/models/hoge_file/extractor.rb
                    /formatter.rb
                    /creator.rb
                    /uploader.rb
app/models/foo_file/extractor.rb
                   /formatter.rb
                   /creator.rb
                   /uploader.rb

機能ごとにディレクトリを分ける

機能ごとに分ける方が他の箇所で共通で利用することもできるし、処理ごとに共通の処理を同一のディレクトリの中におけるので、 重複したコードもなくしやすいです。

app/models/extractors/hoge_extractor.rb
                     /foo_extracor.rb
app/models/formatters/hoge_formatter.rb
                    /foo_formatter.rb
app/models/creators/hoge_creator.rb
                   /foo_creator.rb

例えば、データのアップロードにどちらもAWSのS3バケットを利用するのであれば、バケットごとに 以下のような具体的なディレクトリ名をつけることで使いやすくなるかと思います。

app/models/s3_buckets/hoge_bucket.rb
                     /foo_bucket.rb

リファレンス

我々はConcernsとどう向き合うか

SentryのメッセージにドキュメントのURLを表示したい

アラートには対応するドキュメントがつきもの

インシデントが発生した時にSentry通知したいことはよくあリます。
そして対応手順をGitHubのWikiだったりQiitaTeamなどにまとめているチームも多いかと思います。

Sentry通知が来てから、対応手順が書かれたドキュメントにたどりつくのを簡単にしたい。そのために、Slack通知のメッセージにドキュメントのURLを載せてしまおうというのがこの記事の紹介するところです。

実装

例えば、チームでQiitaTeamを利用していて、そこからGitHubのWikiに移動しましょうといのはありそうです。
そうした状況に対応しやすいように、アプリケーションのコードの中に書き換えるべきURLが散財するのは避けたいので、 YAMLでURLを管理して、ドキュメントへのアクセスするクラスから値を取得するようにしてます。

また、置き場所はドメインに関するものではないので/libに置くことにしました。

ドキュメントへのアクセスするクラスを作成する

lib/data/document.yml

インシデントAの対応方法: https://hoge.qiita.com/shared/items/aaaaaaaaaaaaaaaaa
インシデントBの対応方法: https://foo.qiita.com/shared/items/bbbbbbbbbbbbbbbbb

lib/document.rb

class Document
  DOCUMENT_YAML_PATH = Rails.root.join('lib/data/document.yml')

  class << self
    def document_urls
      YAML.safe_load(File.read(DOCUMENT_YAML_PATH))
    end
  end
end

呼び出す側の簡単な例

document_url = Document.document_urls['Aの対応方法']
message = "インシデントAが発生しました!!!(ドキュメントURL: #{document_url})"
Sentry.capture_message(message, level: "error")

リファレンス

Railsアプリのモジュールはどこに置くべきか問題 (公開版)

Rails6はいつまでにRails7にあげないといけないか?

最低ライン

Severe Security Issuesに関して、Rails6.0.Zのサポートが2023年の6月までサポートと宣言されているので、Rails6.1系にあげている場合でも、 このラインを意識しておくと良いかと思いました。

さらばWebpacker

Rails7ではWebpackerが利用されなくなるとのことだったので、気になって調べたのでした。

https://twitter.com/rails/status/1483772667756957699

RETIREMENT: Webpacker has served the Rails community for over five years as a bridge to compiled and bundled JavaScript. This bridge is no longer needed for most people in most situations following the release of Rails 7.

Webpackerの代用として

ElMassimo/vite_rubyが利用できそうです。 開発環境ではESModulesを利用することで、ビルド時間が短縮され開発体験が良くなることも期待できそうです(プロダクションのビルドは、Rollupを用いて行われます)。
Viteは「Native ESM時代」を感じさせるビルドツールですね。

リファレンス

3 Security Issues
Viteで爆速なフロントエンド開発環境を作る
Native ESM時代とはなにか

HTMLの差分検知の時間を短縮する方法は?

短い文字列比較の方が高速なのでは?というアイデア

クローラーを作成する時に、対象のURLの内容の変更を検知したいことはよくあるかと思います。

そこで単純にHMTL同士を比較するよりも、ハッシュ関数を利用して作成したダイジェストを比較することで、 時間を短縮することができるのではないかと思い、検証してみました。

ただし、ハッシュ関数を利用する場合、前回取得したダイジェストと今回取得したHTMLをハッシュ関数を利用してダイジェストに変換する処理が必要になるので、 以下のような2ステップ必要すると仮定します。

1: 取得したHTMLをハッシュ関数を利用してダイジェストを求める
2: 保存しておいたダイジェストと今取得したダイジェストを比較する

どのハッシュ関数が良いか?

今回の利用目的では脆弱性は問題とならないので、ダイジェストを作成する時の時間に注目して、ハッシュ関数を選びたいと思います。

require 'benchmark/ips'
require 'digest'

target = 'hello' * 1000

Benchmark.ips do |x|
  x.report('Digest::MD5') { Digest::MD5.new.update(target).digest }
  x.report('Digest::SHA1') { Digest::SHA1.new.update(target).digest }
  x.report('Digest::SHA512') { Digest::SHA512.new.update(target).digest }

  x.compare!
end

結果です。

Warming up --------------------------------------
         Digest::MD5    10.426k i/100ms
        Digest::SHA1    10.226k i/100ms
      Digest::SHA512     5.500k i/100ms
Calculating -------------------------------------
         Digest::MD5    118.802k (± 1.3%) i/s -    594.282k in   5.003149s
        Digest::SHA1     99.109k (± 1.3%) i/s -    501.074k in   5.056664s
      Digest::SHA512     53.405k (± 5.2%) i/s -    269.500k in   5.064988s

Comparison:
         Digest::MD5:   118801.8 i/s
        Digest::SHA1:    99109.1 i/s - 1.20x  (± 0.00) slower
      Digest::SHA512:    53405.4 i/s - 2.22x  (± 0.00) slower

※ 「繰り返し回数/時間(i/ms)」で処理を評価します。つまり、数値が大きいほど高速ということを意味しています。

生成するdigestの長さは、MD5: 16文字, SHA1: 20文字, SHA512: 64文字なので妥当な結果だと思われます。
今回の目的では「MD5」を利用するのが良さそうです。

ハッシュ関数 vs 文字列

冒頭で説明したハッシュ関数と単純な文字列を利用した方法の比較です。

require 'benchmark/ips'
require 'digest'

target = 'hello' * 10000
md5_digest = Digest::MD5.new.update(target).digest

def digest_compare(str, digest)
  digest == Digest::MD5.new.update(str).digest
end

def str_compare(str1, str2)
  str1 == str2
end

Benchmark.ips do |x|
  x.report('digest_compare') { digest_compare(target, md5_digest) }
  x.report('str_compare') { str_compare(target, target) }

  x.compare!
end

結果です。

Warming up --------------------------------------
      digest_compare     1.231k i/100ms
         str_compare     1.485M i/100ms
Calculating -------------------------------------
      digest_compare     13.293k (± 0.9%) i/s -     66.474k in   5.000995s
         str_compare     13.957M (± 7.1%) i/s -     69.813M in   5.047691s

Comparison:
         str_compare: 13957048.8 i/s
      digest_compare:    13293.2 i/s - 1049.94x  (± 0.00) slower

単純な文字列比較の方がはやいことがわかります。。。。
これは文字の長さを長くすると、ハッシュ関数の処理時間が増えることが原因か、差はさらに広がります。

結論

単純な文字列比較を行いましょう。

Amazon RDS DB インスタンスを変更する時にダウンタイムは発生するのか?

結論

ダウンタイムは発生します。 すなはち、アプリケーションとDBの接続は切断されます。

ダウンタイムを短くするために

ほとんどのケースでは、Read Replicaは変更予定のインスタンスを用意して、Write ReplicaとRead Replicaを切り替える方法が最適解かもしれません(Failover)。
フェイルオーバーは開始から終了まで通常 30 秒以内に完了するようです。

もっとはやく

Amazon RDS Proxyを利用すればダウンタイムは数秒で行われるらしいです。
必要なことと費用を天秤にかけて決めるのが良さそうです。

リファレンス

Amazon RDS DB インスタンスを変更する
Amazon AuroraのDB インスタンスクラス変更方法まとめ