pgbouncerhero 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +80 -4
- data/app/controllers/pg_bouncer_hero/application_controller.rb +4 -3
- data/app/controllers/pg_bouncer_hero/database_controller.rb +23 -3
- data/app/views/pg_bouncer_hero/database/_menu.html.erb +9 -3
- data/app/views/pg_bouncer_hero/database/_state.html.erb +11 -0
- data/app/views/pg_bouncer_hero/database/state.html.erb +4 -0
- data/config/routes.rb +10 -2
- data/lib/generators/pgbouncerhero/templates/config.yml +5 -0
- data/lib/pgbouncerhero/connection.rb +54 -13
- data/lib/pgbouncerhero/database.rb +69 -3
- data/lib/pgbouncerhero/methods/basics.rb +26 -10
- data/lib/pgbouncerhero/version.rb +1 -1
- data/lib/pgbouncerhero.rb +123 -38
- metadata +76 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 913492602328ce8fb2d3731212ccd7cef95c2a9429fbe6d4112d9a65d5853b35
|
|
4
|
+
data.tar.gz: 5475f266d924090db51b7a242cc7a7e9b90e917dd485e562f346d7f471d3e9bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e6ceb49a3f0c1b28a2edeb0939fdc7bd9f4284c02c5361f879ecedd7cb9af17f1862f5943fb75b9c79e03f5d75d0242fefc4c8ee7b67cf43b62c1ee0414c345
|
|
7
|
+
data.tar.gz: 96bc7a41d920340e9fce91c6686b6819a71393713b8e4e559e95cf790ebcae3b5d7fba13a29b62d32342aced8dceb8376c0db92b2c29f3678252199ae4c2f4e4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## Unreleased
|
|
2
|
+
|
|
3
|
+
## 3.1.0 - 2026-08-23
|
|
4
|
+
|
|
5
|
+
**New:**
|
|
6
|
+
- Read-only mode for hiding and server-side blocking PgBouncer administrative commands
|
|
7
|
+
- PgBouncer state visibility and Resume controls
|
|
8
|
+
- Integration coverage across supported PgBouncer 1.23, 1.24, and 1.25 releases
|
|
9
|
+
- Bounded, thread-safe connection pools per PgBouncer with configurable size and checkout timeout
|
|
10
|
+
- Explicit `Database#with_connection` leases for running multiple commands on one connection
|
|
11
|
+
- Real PgBouncer integration coverage for admin queries, summaries, reloads, authentication, and reconnection
|
|
12
|
+
- A dedicated CI integration job backed by the repository's Docker Compose stack
|
|
13
|
+
- Configurable `PgBouncerHero.config_path`, resolved from the host application's `Rails.root` by default
|
|
14
|
+
- `PgBouncerHero.reset!` for reloading configuration and connections safely
|
|
15
|
+
- Request-level engine coverage and gem-package validation in CI
|
|
16
|
+
|
|
17
|
+
**Improved:**
|
|
18
|
+
- Use `SHUTDOWN WAIT_FOR_CLIENTS` for graceful shutdowns initiated from the dashboard
|
|
19
|
+
- Reconnect stale or failed pooled connections automatically and close every pool on reset
|
|
20
|
+
- Pin PostgreSQL and PgBouncer development images and wait for container health before testing
|
|
21
|
+
- Let Dependabot maintain Docker image versions alongside gems and GitHub Actions
|
|
22
|
+
- Protect PgBouncer connections from concurrent use while allowing bounded parallel queries
|
|
23
|
+
- Resolve parameterized group and database names consistently in routes and controllers
|
|
24
|
+
- Load Rails engine dependencies explicitly and declare Propshaft as a runtime dependency
|
|
25
|
+
- Cache Appraisal dependencies directly in the Ruby/Rails CI matrix
|
|
26
|
+
- Pin GitHub Actions to their current immutable release commits
|
|
27
|
+
- Add read-only workflow permissions, cancellation of superseded runs, and job timeouts
|
|
28
|
+
|
|
1
29
|
## 3.0.0
|
|
2
30
|
|
|
3
31
|
**Breaking Changes:**
|
data/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A graphical user interface for your PgBouncers.
|
|
|
14
14
|
|
|
15
15
|
- Ruby >= 3.2
|
|
16
16
|
- Rails >= 7.2
|
|
17
|
+
- PgBouncer >= 1.23
|
|
17
18
|
- Propshaft (asset pipeline)
|
|
18
19
|
- importmap-rails
|
|
19
20
|
|
|
@@ -69,6 +70,8 @@ pgbouncers:
|
|
|
69
70
|
production:
|
|
70
71
|
primary:
|
|
71
72
|
url: <%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
|
|
73
|
+
pool_size: 5
|
|
74
|
+
pool_timeout: 5
|
|
72
75
|
replica:
|
|
73
76
|
url: <%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
|
|
74
77
|
staging:
|
|
@@ -76,14 +79,69 @@ pgbouncers:
|
|
|
76
79
|
url: <%= ENV["PGBOUNCER_STAGING_PRIMARY_DATABASE_URL"] %>
|
|
77
80
|
replica:
|
|
78
81
|
url: <%= ENV["PGBOUNCER_STAGING_REPLICA_DATABASE_URL"] %>
|
|
82
|
+
|
|
83
|
+
# Optional: allow monitoring while blocking all administrative commands
|
|
84
|
+
read_only: true
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Environment-specific top-level keys are also supported. PgBouncerHero reads
|
|
88
|
+
`config/pgbouncerhero.yml` from the host application's `Rails.root` by default.
|
|
89
|
+
An initializer can point to a different file:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
PgBouncerHero.config_path = Rails.root.join("config/pgbouncerhero.production.yml")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Changing `PgBouncerHero.env` or `config_path` resets cached groups and closes
|
|
96
|
+
their open connections. Call `PgBouncerHero.reset!` after changing a config
|
|
97
|
+
file at runtime, or `PgBouncerHero.disconnect!` when only the connections need
|
|
98
|
+
to be closed. Stale connections reconnect automatically.
|
|
99
|
+
|
|
100
|
+
The PostgreSQL connection timeout defaults to five seconds and can be changed
|
|
101
|
+
with `PGBOUNCERHERO_TIMEOUT`.
|
|
102
|
+
|
|
103
|
+
PgBouncerHero keeps a bounded, lazily created connection pool for each
|
|
104
|
+
configured PgBouncer. The pool defaults to five connections with a five-second
|
|
105
|
+
checkout timeout. Set `PGBOUNCERHERO_POOL_SIZE` and
|
|
106
|
+
`PGBOUNCERHERO_POOL_TIMEOUT` to change the defaults for every PgBouncer, or set
|
|
107
|
+
`pool_size` and `pool_timeout` on an individual database entry as shown above.
|
|
108
|
+
Per-database settings take precedence over environment variables.
|
|
109
|
+
|
|
110
|
+
Code that needs to issue multiple commands on the same PostgreSQL connection
|
|
111
|
+
can lease one explicitly:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
database.with_connection do |connection|
|
|
115
|
+
connection.exec("SHOW VERSION")
|
|
116
|
+
connection.exec("SHOW CONFIG")
|
|
117
|
+
end
|
|
79
118
|
```
|
|
80
119
|
|
|
120
|
+
`database.connection` remains available as a connection-compatible proxy for
|
|
121
|
+
existing integrations. Connections are checked when borrowed, and stale or
|
|
122
|
+
failed connections are discarded and recreated automatically.
|
|
123
|
+
|
|
124
|
+
### Safe Administration
|
|
125
|
+
|
|
126
|
+
The dashboard shows PgBouncer's current state and provides Reload, Suspend,
|
|
127
|
+
Resume, and graceful shutdown controls. Dashboard shutdown uses
|
|
128
|
+
`SHUTDOWN WAIT_FOR_CLIENTS`: PgBouncer stops accepting new clients and exits
|
|
129
|
+
after existing clients disconnect. The Ruby API keeps `database.shutdown` as
|
|
130
|
+
the immediate command for compatibility and also accepts `:immediate`,
|
|
131
|
+
`:wait_for_clients`, or `:wait_for_servers`. State visibility requires
|
|
132
|
+
PgBouncer 1.19 or newer; graceful shutdown modes require PgBouncer 1.23 or
|
|
133
|
+
newer.
|
|
134
|
+
|
|
135
|
+
Set `read_only: true` at the top level of `config/pgbouncerhero.yml`, or set
|
|
136
|
+
`PGBOUNCERHERO_READ_ONLY=true`, to hide and server-side reject every
|
|
137
|
+
administrative command. The environment variable takes precedence over YAML.
|
|
138
|
+
|
|
81
139
|
## Development
|
|
82
140
|
|
|
83
141
|
Start PostgreSQL and PgBouncer with Docker:
|
|
84
142
|
|
|
85
143
|
```bash
|
|
86
|
-
docker compose up
|
|
144
|
+
docker compose up --detach --wait
|
|
87
145
|
```
|
|
88
146
|
|
|
89
147
|
Run the dummy Rails app:
|
|
@@ -98,16 +156,34 @@ Then open http://localhost:3000/pgbouncerhero.
|
|
|
98
156
|
Run the test suite:
|
|
99
157
|
|
|
100
158
|
```bash
|
|
101
|
-
bundle exec rake
|
|
102
|
-
bundle exec appraisal rake test # tests across Rails 7.2, 8.0, 8.1
|
|
159
|
+
bundle exec rake # unit/integration-free tests + rubocop + herb
|
|
160
|
+
bundle exec appraisal rake test # tests across Rails 7.2, 8.0, and 8.1
|
|
161
|
+
bundle exec rake build # build and validate the gem package
|
|
162
|
+
bundle exec rake test:integration # real PgBouncer admin-console tests
|
|
103
163
|
```
|
|
104
164
|
|
|
165
|
+
The integration task expects PostgreSQL and PgBouncer from the Compose stack.
|
|
166
|
+
Override `PGBOUNCERHERO_INTEGRATION_URL` to test another PgBouncer instance.
|
|
167
|
+
The default is `postgres://pgbouncer:pgbouncer@127.0.0.1:6432/pgbouncer`.
|
|
168
|
+
CI runs this suite against PgBouncer 1.23, 1.24, and 1.25.
|
|
169
|
+
|
|
105
170
|
Stop Docker when done:
|
|
106
171
|
|
|
107
172
|
```bash
|
|
108
|
-
docker compose down
|
|
173
|
+
docker compose down --volumes
|
|
109
174
|
```
|
|
110
175
|
|
|
176
|
+
### Releasing
|
|
177
|
+
|
|
178
|
+
Releases use a scoped RubyGems OIDC API Key Role through GitHub Actions, with no
|
|
179
|
+
long-lived RubyGems API key. The role is restricted to the `pgbouncerhero` gem,
|
|
180
|
+
the `kwent/pgbouncerhero` repository, and the `rubygems.org` audience. Store its
|
|
181
|
+
role token in the `RUBYGEMS_OIDC_ROLE` GitHub Actions secret. Then run the
|
|
182
|
+
**Release** workflow on `master` and enter the version already committed in
|
|
183
|
+
`lib/pgbouncerhero/version.rb`. The workflow verifies the version and test
|
|
184
|
+
suite, exchanges GitHub's OIDC identity for a short-lived credential, publishes
|
|
185
|
+
the gem, pushes the version tag, and creates the matching GitHub release.
|
|
186
|
+
|
|
111
187
|
## Contributing
|
|
112
188
|
|
|
113
189
|
1. Fork it
|
|
@@ -13,10 +13,11 @@ module PgBouncerHero
|
|
|
13
13
|
def set_database
|
|
14
14
|
@groups = PgBouncerHero.groups
|
|
15
15
|
if params[:group] && params[:database]
|
|
16
|
-
@
|
|
16
|
+
@group = @groups.fetch(params[:group])
|
|
17
|
+
@database = @group.databases.find { |database| database.name.parameterize == params[:database] }
|
|
17
18
|
else
|
|
18
|
-
@group = @groups.first
|
|
19
|
-
@database = @
|
|
19
|
+
_group_id, @group = @groups.first
|
|
20
|
+
@database = @group.databases.first
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module PgBouncerHero
|
|
2
2
|
class DatabaseController < ApplicationController
|
|
3
|
+
before_action :ensure_writable!, only: %i[reload suspend resume shutdown]
|
|
4
|
+
|
|
3
5
|
def summary
|
|
4
6
|
if @database.connection
|
|
5
7
|
@dbs = @database.summary
|
|
@@ -48,6 +50,14 @@ module PgBouncerHero
|
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
def state
|
|
54
|
+
if @database.connection
|
|
55
|
+
@state = @database.state
|
|
56
|
+
else
|
|
57
|
+
flash[:error] = "#{@database.name} does not look online."
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
def reload
|
|
52
62
|
execute_admin_command(:reload, "reloaded")
|
|
53
63
|
end
|
|
@@ -56,15 +66,25 @@ module PgBouncerHero
|
|
|
56
66
|
execute_admin_command(:suspend, "suspended")
|
|
57
67
|
end
|
|
58
68
|
|
|
69
|
+
def resume
|
|
70
|
+
execute_admin_command(:resume, "resumed")
|
|
71
|
+
end
|
|
72
|
+
|
|
59
73
|
def shutdown
|
|
60
|
-
execute_admin_command(:shutdown, "shut down")
|
|
74
|
+
execute_admin_command(:shutdown, "asked to shut down after its clients disconnect", :wait_for_clients)
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
private
|
|
64
78
|
|
|
65
|
-
def
|
|
79
|
+
def ensure_writable!
|
|
80
|
+
return unless PgBouncerHero.read_only?
|
|
81
|
+
|
|
82
|
+
render plain: "PgBouncerHero is configured as read-only.", status: :forbidden
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def execute_admin_command(command, past_tense, *arguments)
|
|
66
86
|
if @database.connection
|
|
67
|
-
@database.public_send(command)
|
|
87
|
+
@database.public_send(command, *arguments)
|
|
68
88
|
flash[:success] = "#{@database.name} has been #{past_tense}."
|
|
69
89
|
else
|
|
70
90
|
flash[:error] = "#{@database.name} does not look online."
|
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
<%= link_to "Pools", pools_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('pools') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
|
|
8
8
|
<%= link_to "Clients", clients_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('clients') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
|
|
9
9
|
<%= link_to "Configuration", conf_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('conf') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
|
|
10
|
+
<%= link_to "State", state_path(database: database.name.parameterize), class: "px-3 py-1.5 text-sm rounded-md #{is_active('state') ? 'bg-gray-900 text-white' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}" %>
|
|
10
11
|
</div>
|
|
11
12
|
<div class="flex items-center gap-2">
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
<% if PgBouncerHero.read_only? %>
|
|
14
|
+
<span class="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-gray-300 ring-inset">Read-only</span>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= button_to "Reload", reload_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50", form: { data: { turbo_confirm: "Reload PgBouncer configuration?" } } %>
|
|
17
|
+
<%= button_to "Suspend", suspend_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-blue-300 text-blue-700 hover:bg-blue-50", form: { data: { turbo_confirm: "Suspend all PgBouncer socket processing until Resume is selected?" } } %>
|
|
18
|
+
<%= button_to "Resume", resume_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-green-300 text-green-700 hover:bg-green-50", form: { data: { turbo_confirm: "Resume PgBouncer socket processing?" } } %>
|
|
19
|
+
<%= button_to "Graceful shutdown", shutdown_path(database: database.name.parameterize), method: :post, class: "px-3 py-1.5 text-sm font-medium rounded-md border border-red-300 text-red-700 hover:bg-red-50", form: { data: { turbo_confirm: "Stop accepting new clients and shut down after existing clients disconnect?" } } %>
|
|
20
|
+
<% end %>
|
|
15
21
|
</div>
|
|
16
22
|
</div>
|
|
17
23
|
</nav>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-4">
|
|
2
|
+
<h4 class="text-base font-semibold text-gray-900 mb-3">State</h4>
|
|
3
|
+
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
4
|
+
<% state.each do |row| %>
|
|
5
|
+
<div>
|
|
6
|
+
<dt class="text-xs font-medium text-gray-500 uppercase tracking-wider"><%= row["key"].titleize %></dt>
|
|
7
|
+
<dd class="mt-1 text-sm text-gray-900"><%= row["value"] %></dd>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
</dl>
|
|
11
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
PgBouncerHero::Engine.routes.draw do
|
|
2
|
+
group_exists = ->(request) { PgBouncerHero.groups.key?(request.params[:group]) }
|
|
3
|
+
database_exists = lambda do |request|
|
|
4
|
+
group = PgBouncerHero.groups[request.params[:group]]
|
|
5
|
+
group&.databases&.any? { |database| database.name.parameterize == request.params[:database] }
|
|
6
|
+
end
|
|
7
|
+
|
|
2
8
|
root to: "home#index"
|
|
3
|
-
scope path: ":group", constraints:
|
|
4
|
-
scope path: ":database", constraints:
|
|
9
|
+
scope path: ":group", constraints: group_exists do
|
|
10
|
+
scope path: ":database", constraints: database_exists do
|
|
5
11
|
get :summary, controller: :database
|
|
6
12
|
get :databases, controller: :database
|
|
7
13
|
get :stats, controller: :database
|
|
8
14
|
get :pools, controller: :database
|
|
9
15
|
get :clients, controller: :database
|
|
10
16
|
get :conf, controller: :database
|
|
17
|
+
get :state, controller: :database
|
|
11
18
|
post :reload, controller: :database
|
|
12
19
|
post :suspend, controller: :database
|
|
20
|
+
post :resume, controller: :database
|
|
13
21
|
post :shutdown, controller: :database
|
|
14
22
|
end
|
|
15
23
|
end
|
|
@@ -3,6 +3,8 @@ pgbouncers:
|
|
|
3
3
|
primary:
|
|
4
4
|
# eg. postgres://user:password@host:port/pgbouncer
|
|
5
5
|
# url: <%%= ENV["PGBOUNCER_PRODUCTION_PRIMARY_DATABASE_URL"] %>
|
|
6
|
+
# pool_size: 5
|
|
7
|
+
# pool_timeout: 5
|
|
6
8
|
# Add more databases
|
|
7
9
|
# replica:
|
|
8
10
|
# url: <%%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
|
|
@@ -14,3 +16,6 @@ pgbouncers:
|
|
|
14
16
|
|
|
15
17
|
# Time zone (defaults to app time zone)
|
|
16
18
|
# time_zone: "Pacific Time (US & Canada)"
|
|
19
|
+
|
|
20
|
+
# Hide and reject Reload, Suspend, Resume, and Shutdown commands
|
|
21
|
+
# read_only: true
|
|
@@ -10,19 +10,60 @@ module PgBouncerHero
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def connection
|
|
13
|
-
@connection
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
disconnect! if @connection && connection_invalid?(@connection)
|
|
14
|
+
@connection ||= connect
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
Rails.logger.error("[PGBouncerHero] Host:#{@host} | Database Name:#{@dbname} | Timeout: #{@timeout}s => #{e}")
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connected?
|
|
21
|
+
!connection.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with_connection
|
|
25
|
+
raw_connection = connection
|
|
26
|
+
return unless raw_connection
|
|
27
|
+
|
|
28
|
+
yield raw_connection
|
|
29
|
+
rescue PG::Error
|
|
30
|
+
disconnect!
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def disconnect!
|
|
35
|
+
@connection&.finish unless @connection&.finished?
|
|
36
|
+
rescue PG::Error
|
|
37
|
+
nil
|
|
38
|
+
ensure
|
|
39
|
+
@connection = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def method_missing(method_name, *, **, &)
|
|
43
|
+
with_connection { |raw_connection| raw_connection.public_send(method_name, *, **, &) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
47
|
+
PG::Connection.public_instance_methods(include_private).include?(method_name) || super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def connect
|
|
53
|
+
PG.connect(
|
|
54
|
+
host: @host,
|
|
55
|
+
port: @port,
|
|
56
|
+
user: @user,
|
|
57
|
+
password: @password,
|
|
58
|
+
dbname: @dbname,
|
|
59
|
+
connect_timeout: @timeout
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def connection_invalid?(connection)
|
|
64
|
+
connection.finished? || connection.status != PG::CONNECTION_OK
|
|
65
|
+
rescue PG::Error
|
|
66
|
+
true
|
|
26
67
|
end
|
|
27
68
|
end
|
|
28
69
|
end
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
require "connection_pool"
|
|
2
|
+
require "monitor"
|
|
3
|
+
|
|
1
4
|
module PgBouncerHero
|
|
2
5
|
class Database
|
|
3
6
|
include Methods::Basics
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
DEFAULT_POOL_SIZE = 5
|
|
9
|
+
DEFAULT_POOL_TIMEOUT = 5
|
|
10
|
+
|
|
11
|
+
attr_reader :id, :config, :group, :pool_size, :pool_timeout
|
|
6
12
|
|
|
7
13
|
def initialize(group, id, config)
|
|
8
14
|
@id = id
|
|
9
15
|
@config = config || {}
|
|
10
|
-
@url = URI.parse(config["url"].to_s)
|
|
16
|
+
@url = URI.parse(@config["url"].to_s)
|
|
11
17
|
@group = group
|
|
18
|
+
@pool_size = positive_integer_setting("pool_size", "PGBOUNCERHERO_POOL_SIZE", DEFAULT_POOL_SIZE)
|
|
19
|
+
@pool_timeout = positive_float_setting("pool_timeout", "PGBOUNCERHERO_POOL_TIMEOUT", DEFAULT_POOL_TIMEOUT)
|
|
20
|
+
@pool_monitor = Monitor.new
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
def name
|
|
@@ -16,7 +25,28 @@ module PgBouncerHero
|
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def connection
|
|
19
|
-
|
|
28
|
+
proxy = connection_proxy
|
|
29
|
+
proxy if with_connection { true }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_connection
|
|
33
|
+
pool = connection_pool
|
|
34
|
+
pool.with do |managed_connection|
|
|
35
|
+
managed_connection.with_connection { |raw_connection| yield raw_connection }
|
|
36
|
+
end
|
|
37
|
+
rescue ConnectionPool::TimeoutError => e
|
|
38
|
+
Rails.logger.error("[PGBouncerHero] #{name} connection pool timed out after #{pool_timeout}s: #{e.message}")
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def disconnect!
|
|
43
|
+
pool = @pool_monitor.synchronize do
|
|
44
|
+
current_pool = @connection_pool
|
|
45
|
+
@connection_pool = nil
|
|
46
|
+
@connection_proxy = nil
|
|
47
|
+
current_pool
|
|
48
|
+
end
|
|
49
|
+
pool&.shutdown(&:disconnect!)
|
|
20
50
|
end
|
|
21
51
|
|
|
22
52
|
def host
|
|
@@ -41,6 +71,26 @@ module PgBouncerHero
|
|
|
41
71
|
|
|
42
72
|
private
|
|
43
73
|
|
|
74
|
+
def execute(command)
|
|
75
|
+
with_connection { |raw_connection| raw_connection.exec(command) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def connection_pool
|
|
79
|
+
@pool_monitor.synchronize do
|
|
80
|
+
@connection_pool ||= ConnectionPool.new(size: pool_size, timeout: pool_timeout) { build_connection }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def connection_proxy
|
|
85
|
+
@pool_monitor.synchronize do
|
|
86
|
+
@connection_proxy ||= ConnectionPool::Wrapper.new(pool: connection_pool)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_connection
|
|
91
|
+
connection_model.new(host, port, user, password, dbname)
|
|
92
|
+
end
|
|
93
|
+
|
|
44
94
|
def connection_model
|
|
45
95
|
@connection_model ||= begin
|
|
46
96
|
Class.new(PgBouncerHero::Connection) do
|
|
@@ -50,5 +100,21 @@ module PgBouncerHero
|
|
|
50
100
|
end
|
|
51
101
|
end
|
|
52
102
|
end
|
|
103
|
+
|
|
104
|
+
def positive_integer_setting(config_key, environment_key, default)
|
|
105
|
+
value = config.fetch(config_key, ENV.fetch(environment_key, default))
|
|
106
|
+
parsed = Integer(value)
|
|
107
|
+
raise ArgumentError, "#{config_key} must be greater than zero" unless parsed.positive?
|
|
108
|
+
|
|
109
|
+
parsed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def positive_float_setting(config_key, environment_key, default)
|
|
113
|
+
value = config.fetch(config_key, ENV.fetch(environment_key, default))
|
|
114
|
+
parsed = Float(value)
|
|
115
|
+
raise ArgumentError, "#{config_key} must be a finite number greater than zero" unless parsed.positive? && parsed.finite?
|
|
116
|
+
|
|
117
|
+
parsed
|
|
118
|
+
end
|
|
53
119
|
end
|
|
54
120
|
end
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module PgBouncerHero
|
|
2
2
|
module Methods
|
|
3
3
|
module Basics
|
|
4
|
+
SHUTDOWN_MODES = {
|
|
5
|
+
nil => "",
|
|
6
|
+
immediate: "",
|
|
7
|
+
wait_for_clients: " WAIT_FOR_CLIENTS",
|
|
8
|
+
wait_for_servers: " WAIT_FOR_SERVERS"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
4
11
|
def summary
|
|
5
12
|
if connection
|
|
6
13
|
l = lists
|
|
@@ -12,31 +19,40 @@ module PgBouncerHero
|
|
|
12
19
|
end
|
|
13
20
|
end
|
|
14
21
|
def databases
|
|
15
|
-
|
|
22
|
+
execute("SHOW databases")
|
|
16
23
|
end
|
|
17
24
|
def stats
|
|
18
|
-
|
|
25
|
+
execute("SHOW stats")
|
|
19
26
|
end
|
|
20
27
|
def lists
|
|
21
|
-
|
|
28
|
+
execute("SHOW lists")
|
|
22
29
|
end
|
|
23
30
|
def pools
|
|
24
|
-
|
|
31
|
+
execute("SHOW pools")
|
|
25
32
|
end
|
|
26
33
|
def clients
|
|
27
|
-
|
|
34
|
+
execute("SHOW clients")
|
|
28
35
|
end
|
|
29
36
|
def conf
|
|
30
|
-
|
|
37
|
+
execute("SHOW config")
|
|
38
|
+
end
|
|
39
|
+
def state
|
|
40
|
+
execute("SHOW state")
|
|
31
41
|
end
|
|
32
42
|
def reload
|
|
33
|
-
|
|
43
|
+
execute("RELOAD")
|
|
34
44
|
end
|
|
35
45
|
def suspend
|
|
36
|
-
|
|
46
|
+
execute("SUSPEND")
|
|
47
|
+
end
|
|
48
|
+
def resume
|
|
49
|
+
execute("RESUME")
|
|
37
50
|
end
|
|
38
|
-
def shutdown
|
|
39
|
-
|
|
51
|
+
def shutdown(mode = nil)
|
|
52
|
+
suffix = SHUTDOWN_MODES.fetch(mode)
|
|
53
|
+
execute("SHUTDOWN#{suffix}")
|
|
54
|
+
rescue KeyError
|
|
55
|
+
raise ArgumentError, "unsupported shutdown mode: #{mode.inspect}"
|
|
40
56
|
end
|
|
41
57
|
end
|
|
42
58
|
end
|
data/lib/pgbouncerhero.rb
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
|
-
require "
|
|
2
|
-
|
|
3
|
-
require "
|
|
4
|
-
|
|
5
|
-
require "
|
|
6
|
-
require "pgbouncerhero/engine" if defined?(Rails)
|
|
7
|
-
|
|
8
|
-
require "pgbouncerhero/connection"
|
|
9
|
-
|
|
1
|
+
require "erb"
|
|
2
|
+
require "monitor"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "rails"
|
|
10
6
|
require "pg"
|
|
11
7
|
require "importmap-rails"
|
|
12
8
|
require "turbo-rails"
|
|
13
9
|
require "stimulus-rails"
|
|
14
10
|
|
|
11
|
+
require "pgbouncerhero/version"
|
|
12
|
+
require "pgbouncerhero/methods/basics"
|
|
13
|
+
require "pgbouncerhero/connection"
|
|
14
|
+
require "pgbouncerhero/group"
|
|
15
|
+
require "pgbouncerhero/engine"
|
|
16
|
+
|
|
15
17
|
module PgBouncerHero
|
|
18
|
+
class ConfigurationError < StandardError; end
|
|
19
|
+
|
|
20
|
+
BOOLEAN_SETTINGS = {
|
|
21
|
+
true => true,
|
|
22
|
+
false => false,
|
|
23
|
+
"1" => true,
|
|
24
|
+
"0" => false,
|
|
25
|
+
"true" => true,
|
|
26
|
+
"false" => false,
|
|
27
|
+
"yes" => true,
|
|
28
|
+
"no" => false,
|
|
29
|
+
"on" => true,
|
|
30
|
+
"off" => false
|
|
31
|
+
}.freeze
|
|
32
|
+
private_constant :BOOLEAN_SETTINGS
|
|
33
|
+
|
|
34
|
+
STATE_MONITOR = Monitor.new
|
|
35
|
+
private_constant :STATE_MONITOR
|
|
36
|
+
|
|
16
37
|
mattr_accessor :importmap, default: Importmap::Map.new
|
|
17
38
|
|
|
18
39
|
class << self
|
|
19
|
-
|
|
20
|
-
end
|
|
21
|
-
self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
40
|
+
attr_reader :env
|
|
22
41
|
|
|
23
|
-
class << self
|
|
24
42
|
def time_zone=(time_zone)
|
|
25
43
|
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
|
26
44
|
end
|
|
@@ -29,38 +47,105 @@ module PgBouncerHero
|
|
|
29
47
|
@time_zone || Time.zone
|
|
30
48
|
end
|
|
31
49
|
|
|
50
|
+
def env=(env)
|
|
51
|
+
STATE_MONITOR.synchronize do
|
|
52
|
+
@env = env.to_s
|
|
53
|
+
reset!
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def config_path
|
|
58
|
+
STATE_MONITOR.synchronize { @config_path || default_config_path }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def config_path=(path)
|
|
62
|
+
STATE_MONITOR.synchronize do
|
|
63
|
+
@config_path = path && Pathname(path)
|
|
64
|
+
reset!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
32
68
|
def config
|
|
33
|
-
@config ||=
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
config[env]
|
|
41
|
-
elsif config["pgbouncers"]
|
|
42
|
-
config
|
|
43
|
-
else
|
|
44
|
-
{
|
|
45
|
-
"pgbouncers" => {
|
|
46
|
-
"default" => {
|
|
47
|
-
"primary" => {
|
|
48
|
-
"url" => ENV["PGBOUNCERHERO_DATABASE_URL"]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
69
|
+
STATE_MONITOR.synchronize { @config ||= selected_config(load_config) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def groups
|
|
73
|
+
STATE_MONITOR.synchronize do
|
|
74
|
+
@groups ||= config.fetch("pgbouncers").to_h do |group_id, _databases|
|
|
75
|
+
[ group_id.parameterize, PgBouncerHero::Group.new(group_id, config.fetch("pgbouncers")) ]
|
|
53
76
|
end
|
|
54
77
|
end
|
|
55
78
|
end
|
|
56
79
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
def read_only?
|
|
81
|
+
value = ENV.fetch("PGBOUNCERHERO_READ_ONLY") { config.fetch("read_only", false) }
|
|
82
|
+
BOOLEAN_SETTINGS.fetch(value.is_a?(String) ? value.downcase : value)
|
|
83
|
+
rescue KeyError
|
|
84
|
+
raise ConfigurationError, "read_only must be a boolean"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def disconnect!
|
|
88
|
+
STATE_MONITOR.synchronize do
|
|
89
|
+
@groups&.each_value do |group|
|
|
90
|
+
group.databases.each(&:disconnect!)
|
|
61
91
|
end
|
|
62
|
-
Hash[mapped]
|
|
63
92
|
end
|
|
64
93
|
end
|
|
94
|
+
|
|
95
|
+
def reset!
|
|
96
|
+
STATE_MONITOR.synchronize do
|
|
97
|
+
disconnect!
|
|
98
|
+
@config = nil
|
|
99
|
+
@groups = nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def default_config_path
|
|
106
|
+
(Rails.root || Pathname.pwd).join("config/pgbouncerhero.yml")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_config
|
|
110
|
+
return {} unless config_path.file?
|
|
111
|
+
|
|
112
|
+
loaded = YAML.safe_load(ERB.new(config_path.read).result, aliases: true)
|
|
113
|
+
return {} if loaded.nil?
|
|
114
|
+
return loaded if loaded.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
raise ConfigurationError, "#{config_path} must contain a YAML mapping"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def selected_config(loaded)
|
|
120
|
+
selected = loaded.fetch(env, loaded)
|
|
121
|
+
unless selected.is_a?(Hash)
|
|
122
|
+
raise ConfigurationError, "configuration for #{env.inspect} must be a YAML mapping"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return default_config unless selected.key?("pgbouncers")
|
|
126
|
+
|
|
127
|
+
pgbouncers = selected["pgbouncers"]
|
|
128
|
+
unless pgbouncers.is_a?(Hash) && pgbouncers.any? && pgbouncers.values.all? { |databases| databases.is_a?(Hash) && databases.any? }
|
|
129
|
+
raise ConfigurationError, '"pgbouncers" must contain at least one group with one database'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
selected
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def default_config
|
|
136
|
+
{
|
|
137
|
+
"pgbouncers" => {
|
|
138
|
+
"default" => {
|
|
139
|
+
"primary" => {
|
|
140
|
+
"url" => ENV["PGBOUNCERHERO_DATABASE_URL"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
end
|
|
65
146
|
end
|
|
147
|
+
|
|
148
|
+
self.env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
66
149
|
end
|
|
150
|
+
|
|
151
|
+
at_exit { PgBouncerHero.disconnect! }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgbouncerhero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quentin Rousseau
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '7.2'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,29 @@ dependencies:
|
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '7.2'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: propshaft
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.0'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '2'
|
|
26
52
|
- !ruby/object:Gem::Dependency
|
|
27
53
|
name: importmap-rails
|
|
28
54
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -30,6 +56,9 @@ dependencies:
|
|
|
30
56
|
- - ">="
|
|
31
57
|
- !ruby/object:Gem::Version
|
|
32
58
|
version: '1.2'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3'
|
|
33
62
|
type: :runtime
|
|
34
63
|
prerelease: false
|
|
35
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +66,9 @@ dependencies:
|
|
|
37
66
|
- - ">="
|
|
38
67
|
- !ruby/object:Gem::Version
|
|
39
68
|
version: '1.2'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3'
|
|
40
72
|
- !ruby/object:Gem::Dependency
|
|
41
73
|
name: turbo-rails
|
|
42
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -44,6 +76,9 @@ dependencies:
|
|
|
44
76
|
- - ">="
|
|
45
77
|
- !ruby/object:Gem::Version
|
|
46
78
|
version: '1.0'
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3'
|
|
47
82
|
type: :runtime
|
|
48
83
|
prerelease: false
|
|
49
84
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +86,9 @@ dependencies:
|
|
|
51
86
|
- - ">="
|
|
52
87
|
- !ruby/object:Gem::Version
|
|
53
88
|
version: '1.0'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3'
|
|
54
92
|
- !ruby/object:Gem::Dependency
|
|
55
93
|
name: stimulus-rails
|
|
56
94
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,6 +96,9 @@ dependencies:
|
|
|
58
96
|
- - ">="
|
|
59
97
|
- !ruby/object:Gem::Version
|
|
60
98
|
version: '1.0'
|
|
99
|
+
- - "<"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2'
|
|
61
102
|
type: :runtime
|
|
62
103
|
prerelease: false
|
|
63
104
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +106,9 @@ dependencies:
|
|
|
65
106
|
- - ">="
|
|
66
107
|
- !ruby/object:Gem::Version
|
|
67
108
|
version: '1.0'
|
|
109
|
+
- - "<"
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '2'
|
|
68
112
|
- !ruby/object:Gem::Dependency
|
|
69
113
|
name: pg
|
|
70
114
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,6 +116,9 @@ dependencies:
|
|
|
72
116
|
- - ">="
|
|
73
117
|
- !ruby/object:Gem::Version
|
|
74
118
|
version: '1.2'
|
|
119
|
+
- - "<"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '2'
|
|
75
122
|
type: :runtime
|
|
76
123
|
prerelease: false
|
|
77
124
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +126,29 @@ dependencies:
|
|
|
79
126
|
- - ">="
|
|
80
127
|
- !ruby/object:Gem::Version
|
|
81
128
|
version: '1.2'
|
|
129
|
+
- - "<"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '2'
|
|
132
|
+
- !ruby/object:Gem::Dependency
|
|
133
|
+
name: connection_pool
|
|
134
|
+
requirement: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.0'
|
|
139
|
+
- - "<"
|
|
140
|
+
- !ruby/object:Gem::Version
|
|
141
|
+
version: '4'
|
|
142
|
+
type: :runtime
|
|
143
|
+
prerelease: false
|
|
144
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - ">="
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: '3.0'
|
|
149
|
+
- - "<"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '4'
|
|
82
152
|
- !ruby/object:Gem::Dependency
|
|
83
153
|
name: rake
|
|
84
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,14 +183,14 @@ dependencies:
|
|
|
113
183
|
requirements:
|
|
114
184
|
- - "~>"
|
|
115
185
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: '
|
|
186
|
+
version: '6.0'
|
|
117
187
|
type: :development
|
|
118
188
|
prerelease: false
|
|
119
189
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
190
|
requirements:
|
|
121
191
|
- - "~>"
|
|
122
192
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '
|
|
193
|
+
version: '6.0'
|
|
124
194
|
- !ruby/object:Gem::Dependency
|
|
125
195
|
name: appraisal
|
|
126
196
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -203,11 +273,13 @@ files:
|
|
|
203
273
|
- app/views/pg_bouncer_hero/database/_databases.html.erb
|
|
204
274
|
- app/views/pg_bouncer_hero/database/_menu.html.erb
|
|
205
275
|
- app/views/pg_bouncer_hero/database/_pools.html.erb
|
|
276
|
+
- app/views/pg_bouncer_hero/database/_state.html.erb
|
|
206
277
|
- app/views/pg_bouncer_hero/database/_stats.html.erb
|
|
207
278
|
- app/views/pg_bouncer_hero/database/clients.html.erb
|
|
208
279
|
- app/views/pg_bouncer_hero/database/conf.html.erb
|
|
209
280
|
- app/views/pg_bouncer_hero/database/databases.html.erb
|
|
210
281
|
- app/views/pg_bouncer_hero/database/pools.html.erb
|
|
282
|
+
- app/views/pg_bouncer_hero/database/state.html.erb
|
|
211
283
|
- app/views/pg_bouncer_hero/database/stats.html.erb
|
|
212
284
|
- app/views/pg_bouncer_hero/database/summary.html.erb
|
|
213
285
|
- app/views/pg_bouncer_hero/home/_card.html.erb
|
|
@@ -233,6 +305,7 @@ licenses:
|
|
|
233
305
|
metadata:
|
|
234
306
|
source_code_uri: https://github.com/kwent/pgbouncerhero
|
|
235
307
|
changelog_uri: https://github.com/kwent/pgbouncerhero/blob/master/CHANGELOG.md
|
|
308
|
+
documentation_uri: https://github.com/kwent/pgbouncerhero#readme
|
|
236
309
|
bug_tracker_uri: https://github.com/kwent/pgbouncerhero/issues
|
|
237
310
|
rubygems_mfa_required: 'true'
|
|
238
311
|
rdoc_options: []
|