Vim で snake_case を CamelCase に置換する(Visualモード)
概要
APIのレスポンスのキーはsnake_caseだけど、クライアント側でTypeScriptの型のキーに変換したいという場面に遭遇した。
visual mode => 変換!みたいなことがしたかったので、VsCodeのVimモードにて検証した。
手順
1: visualモードで変換したい範囲を選択する
shift + v
とかでvisualモードで選択する。
2: commandモードにする。
shift + !
とかでcommandモードでかつ!が付与される。
:'<,'>!
と表示されることがわかる。
これは選択したラインを意味するので、これはこのまま使う。
!
でこれ以降のコマンドを実行するという意味になる。
3: 置換を実行する
:'<,'>!perl -pe 's#(_|^)(.)#\u$2#g'
リファレンス
Workerの処理でActiveRecord::ConnectionNotEstablishedが発生した件
概要
SQSからキューを取って、DBに書き込むWorkerがActiveRecord::ConnectionNotEstablishedで落ちた件について、調査した。
SQSからのキューが一時的に止まる事象が発生して、長時間DBアクセスが起きない時に発生した。DBはMySQLを利用していた。
エラーの再現
事象からDB側の設定のwait_timeout
が問題だろうと検討をつけて、config/database.yml
にwait_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.yml
にreconnect: 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
リファレンス
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")
リファレンス
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時代」を感じさせるビルドツールですね。
リファレンス
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を利用すればダウンタイムは数秒で行われるらしいです。
必要なことと費用を天秤にかけて決めるのが良さそうです。