前置き
GitLabでPostgreSQLを使う時にPgBouncer経由でDBに接続してる(database.ymlにpgbouncerの接続先を書いている)のですが、 その状態で rake db:migrate
すると下記のようなエラーが出ます。
ActiveRecord::ConcurrentMigrationError: Failed to release advisory lock from active_record/migration.rb:1385:in `with_advisory_lock' from active_record/migration.rb:1229:in `migrate' from active_record/migration.rb:1061:in `up' from active_record/migration.rb:1036:in `migrate' from active_record/tasks/database_tasks.rb:238:in `migrate' from active_record/railties/databases.rake:86:in `block (3 levels) in <top (required)>' from active_record/railties/databases.rake:84:in `each' from active_record/railties/databases.rake:84:in `block (2 levels) in <top (required)>' from rake/task.rb:273:in `block in execute' from rake/task.rb:273:in `each' from rake/task.rb:273:in `execute' from rake/task.rb:214:in `block in invoke_with_call_chain' from monitor.rb:235:in `mon_synchronize' from rake/task.rb:194:in `invoke_with_call_chain' from rake/task.rb:183:in `invoke' from rake/application.rb:160:in `invoke_task' from rake/application.rb:116:in `block (2 levels) in top_level' from rake/application.rb:116:in `each' from rake/application.rb:116:in `block in top_level' from rake/application.rb:125:in `run_with_threads' from rake/application.rb:110:in `top_level' from rake/application.rb:83:in `block in run' from rake/application.rb:186:in `standard_exception_handling' from rake/application.rb:80:in `run' from bundle/ruby/2.6.0/gems/rake-12.3.3/exe/rake:27:in `<top (required)>' from bundle/ruby/2.6.0/bin/rake:23:in `load' from bundle/ruby/2.6.0/bin/rake:23:in `<main>'
https://docs.gitlab.com/omnibus/update/README.html に
If you’re using PgBouncer:
You’ll need to bypass PgBouncer and connect directly to the database master before running migrations.
とあるように db:migrate
する時だけPgBouncerを使わず直接PostgreSQLにつなぐのが大正解です。
しかし
- GitLabのバージョンアップの時だけdatabase.ymlを書き換えるのは面倒
- 忘れがちなので自動化すべき
- 運用でカバーするのは時代遅れなので仕組み化すべき
- いつもCIからのデプロイでGitLabのバージョンアップを行っているのだが、database.ymlの接続先をPostgreSQLにしてデプロイ&自動でdb:migrate(1回目のデプロイ)と接続先をPgBouncerに戻す作業(2回目のデプロイ)で2回デプロイするのが面倒
- 忘れがちなので(ry
- 仮に2回デプロイ行うとしても一時的にPostgreSQLのコネクションが少なくなるのでオンラインでバージョンアップを行うとコネクションが足りずにエラーが多発する可能性がある
- workerの台数によるけど常時100コネクション近く張り付いてる状態なのでメンテいれずにpgbouncer外すのは厳しいと判断
- SwarmでBlue/GreenデプロイでGitLabをノーメンテでバージョンアップするのに慣れすぎて*1 、バージョンアップ程度でメンテ入れるのが嫌
という理由で、今のデプロイフローをなるべく変えずにどうにかdb:migrateの時だけ自動で接続先を切り替える方法がないか考えました。
モンキーパッチの縛りプレイの内容
モンキーパッチを作るにあたって下記のような縛りプレイの条件がありました
- RailsやGitLabのバージョンアップでなるべく壊れないようにする
- Dockerイメージで配布されているGitLabを使っているので *2 、既存のソースは全て変更不能
- 既存のソースが変更できないのでGitLabに入っていないgemを使うのは難しい
- bundle installされた状態でDockerイメージとして配布されてるので、そこに新しいgemを追加するのは茨の道
- 利用しているDockerイメージは https://github.com/sameersbn/docker-gitlab でだが、起動時の処理はDockerイメージ内のスクリプト *4 にあるため、そこに特定の処理を差し込むのはほぼ不可能
モンキーパッチ
このようなモンキーパッチを
version: '3.7' services: gitlab: volumes: - "./monkey_patch/db_migrate_monkey_patch.rake:/home/git/gitlab/lib/tasks/db_migrate_monkey_patch.rake:ro"
のような感じでDockerコンテナ内にmountして使っています。
注意点
- かなり黒魔術なので自己責任でお願いします
- database.ymlの中身は適宜書き換えてください
- オンラインで実行しても問題ないはずですが、
rake db:migrate
実行中にpumaやunicornをrestartすると意図しない設定が読み込まれるので注意してください
モンキーパッチのポイント
rake taskの実体は Rake::Task
のインスタンスで、rake task実行時には Rake::Task#invoke
が呼ばれます。
そのため Rake::Task["db:migrate"].invoke
という特異メソッドに対してモンキーパッチ用のmoduleを prepend
することで rake db:migrate
実行時のみに特定の処理を差し込んでいます。
ボツ案1
Rake::Task["db:migrate"].enhance(["db:migrate:before"]) do Rake::Task["db:migrate:after"].invoke end
みたいな感じで db:migrate:before
でdatabase.ymlを書き換えて db:migrate:after
で元に戻すという方法もあったのですが、これだと db:migrate
でエラーになった時に db:migrate:after
が実行されずにdatabase.ymlが元に戻せなくなる不具合があるのでボツになりました。
そのため、上の方で書いているように Rake::Task["db:migrate"].invoke
に対してモンキーパッチをあてるしかないと思ってます。
ボツ案2
begin Rake::Task["db:migrate"].enhance(["db:migrate:before"]) ensure # ロールバック処理 end
これだとrake taskの定義時に ensure
が評価されてrake taskの実行時には ensure
は評価されないので意図通りに動かないです。