-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 934f25b
Showing
18 changed files
with
674 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
name: Ruby CI | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
pull_request: | ||
branches: [ main ] | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
ruby-version: ['3.2', '3.1', '3.0', '2.7', '2.6'] | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Run PostgreSQL 11 | ||
run: | | ||
docker run --env POSTGRES_USER=postgres \ | ||
--env POSTGRES_DB=pg-locks-monitor-test \ | ||
--env POSTGRES_PASSWORD=secret \ | ||
-d -p 5432:5432 postgres:11.18-alpine \ | ||
postgres -c shared_preload_libraries=pg_stat_statements | ||
- name: Run PostgreSQL 12 | ||
run: | | ||
docker run --env POSTGRES_USER=postgres \ | ||
--env POSTGRES_DB=pg-locks-monitor-test \ | ||
--env POSTGRES_PASSWORD=secret \ | ||
-d -p 5433:5432 postgres:12.13-alpine \ | ||
postgres -c shared_preload_libraries=pg_stat_statements | ||
- name: Run PostgreSQL 13 | ||
run: | | ||
docker run --env POSTGRES_USER=postgres \ | ||
--env POSTGRES_DB=pg-locks-monitor-test \ | ||
--env POSTGRES_PASSWORD=secret \ | ||
-d -p 5434:5432 postgres:13.9-alpine \ | ||
postgres -c shared_preload_libraries=pg_stat_statements | ||
- name: Run PostgreSQL 14 | ||
run: | | ||
docker run --env POSTGRES_USER=postgres \ | ||
--env POSTGRES_DB=pg-locks-monitor-test \ | ||
--env POSTGRES_PASSWORD=secret \ | ||
-d -p 5435:5432 postgres:14.6-alpine \ | ||
postgres -c shared_preload_libraries=pg_stat_statements | ||
- name: Run PostgreSQL 15 | ||
run: | | ||
docker run --env POSTGRES_USER=postgres \ | ||
--env POSTGRES_DB=pg-locks-monitor-test \ | ||
--env POSTGRES_PASSWORD=secret \ | ||
-d -p 5436:5432 postgres:15.1-alpine \ | ||
postgres -c shared_preload_libraries=pg_stat_statements | ||
sleep 15 | ||
- name: Set up Ruby ${{ matrix.ruby-version }} | ||
uses: ruby/setup-ruby@v1 | ||
with: | ||
ruby-version: ${{ matrix.ruby-version }} | ||
- name: Setup dependencies | ||
run: | | ||
gem install bundler -v 2.4.22 | ||
sudo apt-get update --allow-releaseinfo-change | ||
sudo apt install postgresql-client | ||
sudo apt install libpq-dev | ||
bundle config set --local path 'vendor/bundle' | ||
bundle install | ||
sleep 10 | ||
- name: Run tests for PG 11 | ||
env: | ||
PG_VERSION: 11 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5432/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
- name: Run tests for PG 11 | ||
env: | ||
PG_VERSION: 11 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5432/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
- name: Run tests for PG 12 | ||
env: | ||
PG_VERSION: 12 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5433/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
- name: Run tests for PG 13 | ||
env: | ||
PG_VERSION: 13 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5434/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
- name: Run tests for PG 14 | ||
env: | ||
PG_VERSION: 14 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5435/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
- name: Run tests for PG 15 | ||
env: | ||
PG_VERSION: 15 | ||
POSTGRES_HOST: localhost | ||
POSTGRES_USER: postgres | ||
POSTGRES_DB: pg-locks-monitor-test | ||
POSTGRES_PASSWORD: secret | ||
DATABASE_URL: postgresql://postgres:secret@localhost:5436/pg-locks-monitor-test | ||
run: | | ||
bundle exec rspec spec/ | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
Gemfile.lock | ||
.ruby-version | ||
pkg/ | ||
*.gem | ||
docker-compose.yml | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
source "https://rubygems.org" | ||
|
||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright © Paweł Urbanek 2024 | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | ||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | ||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | ||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
# PG Locks Monitor [![Gem Version](https://badge.fury.io/rb/pg-locks-monitor.svg)](https://badge.fury.io/rb/pg-locks-monitor) [![GH Actions](https://github.com/pawurb/pg-locks-monitor/actions/workflows/ci.yml/badge.svg)](https://github.com/pawurb/pg-locks-monitor/actions) | ||
|
||
This gem allows to observe database locks generated by a Rails application. By default locks data is not persisted anywhere in the PostgreSQL logs, so the only way to monitor it is via analyzing the transient state of the `pg_locks` metadata table. `pg-locks-monitor` is a simple tool that makes this process quick to implement and adjust to each app's individual requirements. | ||
|
||
## Usage | ||
|
||
`PgLocksMonitor` class provides a `snapshot!` method which notifies selected channels about database locks that match a configured criteria. | ||
|
||
Start by adding the gem to your Gemfile: | ||
|
||
```ruby | ||
gem "pg-locks-monitor" | ||
``` | ||
|
||
Then run `bundle install` and `rake pg_locks_monitor:init`. It creates a default configuration file with the following settings: | ||
|
||
`config/initializers/pg_locks_monitor.rb` | ||
```ruby | ||
PgLocksMonitor.configure do |config| | ||
config.locks_limit = 5 | ||
|
||
config.monitor_locks = true | ||
config.locks_min_duration_ms = 200 | ||
|
||
config.monitor_blocking = true | ||
config.blocking_min_duration_ms = 100 | ||
|
||
config.notify_logs = true | ||
|
||
config.notify_slack = false | ||
config.slack_webhook_url = "" | ||
config.slack_channel = "" | ||
|
||
config.notifier_class = PgLocksMonitor::DefaultNotifier | ||
end | ||
``` | ||
|
||
- `locks_limit` - specify the max number of locks to report in a single notification | ||
- `notify_locks` - observe database locks even if they don't conflict with a different SQL query | ||
- `locks_min_duration_ms` - notify about locks which execeed this duration threshold in milliseconds | ||
- `notify_blocking` - observe database locks which cause other SQL query to wait from them to release | ||
- `blocking_min_duration_ms` - notify about blocking locks which execeed this duration threshold in milliseconds | ||
- `notify_logs` - send notification about detected locks to using `Rails.logger.info` method | ||
- `notify_slack` - send notification about detected locks to the configured Slack channel | ||
- `slack_webhook_url` - webhook necessary for Slack notification to work | ||
- `slack_channel` - name of the targer Slack channel | ||
- `notifier_class` - customizable notifier class | ||
|
||
|
||
## Testing the notification channels | ||
|
||
Before configuring a recurring invocation of the `snapshot!` method it's recommended to first manually trigger the notification to test the configured channels. | ||
|
||
You can generate an _"artificial"_ blocking lock and observe it by running the following code in the Rails console: | ||
|
||
```ruby | ||
user = User.last | ||
|
||
Thread.new do | ||
User.transaction do | ||
user.update(email: "email-#{SecureRandom.hex(2)}@example.com") | ||
sleep 5 | ||
raise ActiveRecord::Rollback | ||
end | ||
end | ||
|
||
Thread.new do | ||
User.transaction do | ||
user.update(email: "email-#{SecureRandom.hex(2)}@example.com") | ||
sleep 5 | ||
raise ActiveRecord::Rollback | ||
end | ||
end | ||
|
||
sleep 0.5 | ||
PgLocksMonitor.snapshot! | ||
``` | ||
|
||
Please remember to adjust the update operation to match your app's schema. | ||
|
||
As a result of running the above snippet your should receive a notification about the acquired blocking database lock. | ||
|
||
### Sample notification | ||
|
||
Received notifications contain data that's useful in debugging the cause of long lasting locks. | ||
|
||
And here's a sample blocking lock notification: | ||
|
||
```ruby | ||
[ | ||
{ | ||
# PID process which was blocking another query | ||
"blocking_pid": 29, | ||
# SQL query blocking other SQL query | ||
"blocking_statement": "UPDATE \"users\" SET \"updated_at\" = $1 WHERE \"users\".\"id\" = $2 from/sidekiq_job:UserUpdater/", | ||
# the duration of blocking SQL query | ||
"blocking_duration": "PT0.972116S", | ||
# app that triggered the blocking SQL query | ||
"blocking_sql_app": "bin/sidekiq", | ||
|
||
# PID of process which was blocked by other query | ||
"blocked_pid": 30, | ||
# SQL query blocked by other SQL query | ||
"blocked_statement": "UPDATE \"users\" SET \"last_active_at\" = $1 WHERE \"users\".\"id\" = $2 from/controller_with_namespace:UsersController,action:update/", | ||
# the duration of blocked SQL query | ||
"blocked_duration": "PT0.483309S", | ||
# app that triggered the blocked SQL query | ||
"blocked_sql_app": "bin/puma" | ||
} | ||
] | ||
``` | ||
|
||
This sample blocking notification shows than query originating from `UserUpdater` Sidekiq job is blocking an update operation on the same user for `UsersController#update` action. Remember to configure [marginalia gem](https://github.com/basecamp/marginalia) to enable these helpful query source annotations. | ||
|
||
Here's a sample lock notication: | ||
|
||
```ruby | ||
[ | ||
{ | ||
# PID of the process which acquired the lock | ||
"pid": 50, | ||
# name of affected table/index | ||
"relname": "users", | ||
# ID of the source transaction | ||
"transactionid": null, | ||
# bool indicating if the lock is already granted | ||
"granted": true, | ||
# type of the acquired lock | ||
"mode": "RowExclusiveLock", | ||
# SQL query which acquired the lock | ||
"query_snippet": "UPDATE \"users\" SET \"updated_at\" = $1 WHERE \"users\".\"id\" = $2 from/sidekiq_job:UserUpdater/", | ||
# age of the lock | ||
"age": "PT0.94945S", | ||
# app that acquired the lock | ||
"application": "bin/sidekiq" | ||
}, | ||
``` | ||
|
||
You can read [this blogpost](https://pawelurbanek.com/rails-postgresql-locks) for more detailed info on locks in the Rails apps. | ||
|
||
## Background job config | ||
|
||
This gem is intended to be used via a recurring background job but it is agnostic of the background job provider. Here's a sample Sidekiq implementation: | ||
|
||
`app/jobs/pg_locks_monitor_job.rb` | ||
```ruby | ||
require 'pg-locks-monitor' | ||
|
||
class PgLocksMonitoringJob | ||
include Sidekiq::Worker | ||
sidekiq_options retry: false | ||
|
||
def perform | ||
PgLocksMonitor.snapshot! | ||
ensure | ||
if ENV["PG_LOCKS_MONITOR_ENABLED"] | ||
PgLocksMonitoringJob.perform_in(1.minute) | ||
end | ||
end | ||
end | ||
``` | ||
|
||
Remember to schedule this job when your app starts: | ||
`config/pg_locks_monitor.rb` | ||
|
||
```ruby | ||
#... | ||
|
||
if ENV["PG_LOCKS_MONITOR_ENABLED"] | ||
PgLocksMonitoringJob.perform | ||
end | ||
``` | ||
|
||
A background job that schedules itself is not the cleanest pattern. So alternatively you can use [sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron), [whenever](https://github.com/javan/whenever) or [clockwork](https://github.com/adamwiggins/clockwork) gems to trigger the `PgLocksMonitor.snapshot!` invocation periodically. | ||
|
||
A recommended frequency of invocation depends on your app's traffic. From my experience even 1 minute apart snapshots can provide a lot of useful data, but it all depends on how often the locks are occuring in your Rails application. | ||
|
||
## Custom notifier class | ||
|
||
`PgLocksMonitor::DefaultNotifier` supports sending lock notifications to `Rails.logger` or Slack channel. If you want to use different notification channels you can define your custom notifier like that: | ||
|
||
`config/initializers/pg_locks_monitor.rb` | ||
```ruby | ||
class PgLocksEmailNotifier | ||
def self.call(locks_data) | ||
LocksMailer.with(locks_data: locks_data).notification.deliver_now | ||
end | ||
end | ||
|
||
PgLocksMonitor.configure do |config| | ||
# ... | ||
|
||
config.notifier_class = PgLocksEmailNotifier | ||
end | ||
|
||
``` | ||
|
||
## Contributions | ||
|
||
This is in a very early stage of development so feedback and PRs are welcome. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
require "bundler/gem_tasks" | ||
require "rspec/core/rake_task" | ||
|
||
RSpec::Core::RakeTask.new(:spec) | ||
|
||
desc "Test all PG versions" | ||
task :test_all do | ||
system("PG_VERSION=11 bundle exec rspec spec/ && PG_VERSION=12 bundle exec rspec spec/ && PG_VERSION=13 bundle exec rspec spec/ && PG_VERSION=14 bundle exec rspec spec/") | ||
end |
Oops, something went wrong.