pgbouncerhero 3.0.0 → 3.2.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 +45 -0
- data/README.md +119 -4
- data/app/assets/builds/pgbouncerhero/application.css +2 -2
- data/app/assets/stylesheets/pgbouncerhero/application.css +11 -1
- data/app/controllers/pg_bouncer_hero/application_controller.rb +4 -3
- data/app/controllers/pg_bouncer_hero/database_controller.rb +93 -9
- data/app/helpers/pg_bouncer_hero/application_helper.rb +54 -0
- data/app/javascript/pgbouncerhero/controllers/data_table_controller.js +183 -0
- data/app/javascript/pgbouncerhero/controllers/fleet_controller.js +52 -0
- data/app/javascript/pgbouncerhero/controllers/polling_controller.js +49 -5
- data/app/views/layouts/pg_bouncer_hero/application.html.erb +1 -1
- data/app/views/pg_bouncer_hero/database/_actions.html.erb +1 -1
- data/app/views/pg_bouncer_hero/database/_clients.html.erb +9 -8
- data/app/views/pg_bouncer_hero/database/_conf.html.erb +10 -9
- data/app/views/pg_bouncer_hero/database/_databases.html.erb +30 -9
- data/app/views/pg_bouncer_hero/database/_menu.html.erb +19 -4
- data/app/views/pg_bouncer_hero/database/_pools.html.erb +10 -9
- data/app/views/pg_bouncer_hero/database/_servers.html.erb +31 -0
- data/app/views/pg_bouncer_hero/database/_state.html.erb +11 -0
- data/app/views/pg_bouncer_hero/database/_stats.html.erb +12 -11
- data/app/views/pg_bouncer_hero/database/_users.html.erb +31 -0
- data/app/views/pg_bouncer_hero/database/clients.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/conf.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/databases.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/pools.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/servers.html.erb +6 -0
- data/app/views/pg_bouncer_hero/database/state.html.erb +6 -0
- data/app/views/pg_bouncer_hero/database/stats.html.erb +4 -2
- data/app/views/pg_bouncer_hero/database/users.html.erb +6 -0
- data/app/views/pg_bouncer_hero/home/_card.html.erb +5 -2
- data/app/views/pg_bouncer_hero/home/_card_content.html.erb +48 -31
- data/app/views/pg_bouncer_hero/home/index.html.erb +28 -8
- data/app/views/shared/_data_table_toolbar.html.erb +13 -0
- data/config/routes.rb +16 -2
- data/lib/generators/pgbouncerhero/templates/config.yml +7 -0
- data/lib/pgbouncerhero/connection.rb +54 -13
- data/lib/pgbouncerhero/database.rb +73 -3
- data/lib/pgbouncerhero/methods/basics.rb +86 -10
- data/lib/pgbouncerhero/version.rb +1 -1
- data/lib/pgbouncerhero.rb +139 -38
- metadata +83 -3
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<%= render partial: "menu", locals: {database: @database} %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<%= monitoring_panel do %>
|
|
3
|
+
<% if @clients %>
|
|
4
|
+
<%= render partial: "clients", locals: {database: @database, clients: @clients} %>
|
|
5
|
+
<% end %>
|
|
4
6
|
<% end %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<%= render partial: "menu", locals: {database: @database} %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<%= monitoring_panel do %>
|
|
3
|
+
<% if @conf %>
|
|
4
|
+
<%= render partial: "conf", locals: {database: @database, conf: @conf} %>
|
|
5
|
+
<% end %>
|
|
4
6
|
<% end %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<%= render partial: "menu", locals: {database: @database} %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<%= monitoring_panel do %>
|
|
3
|
+
<% if @dbs %>
|
|
4
|
+
<%= render partial: "databases", locals: {database: @database, databases: @dbs} %>
|
|
5
|
+
<% end %>
|
|
4
6
|
<% end %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<%= render partial: "menu", locals: {database: @database} %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<%= monitoring_panel do %>
|
|
3
|
+
<% if @pools %>
|
|
4
|
+
<%= render partial: "pools", locals: {database: @database, pools: @pools} %>
|
|
5
|
+
<% end %>
|
|
4
6
|
<% end %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<%= render partial: "menu", locals: {database: @database} %>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
<%= monitoring_panel do %>
|
|
3
|
+
<% if @stats %>
|
|
4
|
+
<%= render partial: "stats", locals: {database: @database, stats: @stats} %>
|
|
5
|
+
<% end %>
|
|
4
6
|
<% end %>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
<div class="bg-white rounded-lg border border-gray-200 shadow-
|
|
1
|
+
<div class="bg-white rounded-lg border border-gray-200 shadow-xs overflow-hidden">
|
|
2
2
|
<%= link_to databases_path(group: database.group.name.parameterize, database: database.name.parameterize), class: "block no-underline text-inherit" do %>
|
|
3
3
|
<div class="px-4 py-3 flex items-center justify-between border-b border-gray-100">
|
|
4
4
|
<div>
|
|
5
5
|
<div class="text-base font-bold text-gray-900"><%= database.name %></div>
|
|
6
6
|
<div class="text-xs text-gray-500"><%= database.host %></div>
|
|
7
7
|
</div>
|
|
8
|
+
<% if database.read_only? %>
|
|
9
|
+
<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>
|
|
10
|
+
<% end %>
|
|
8
11
|
</div>
|
|
9
12
|
<% end %>
|
|
10
|
-
<turbo-frame id="card_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" src="<%= summary_path(group: database.group.name.parameterize, database: database.name.parameterize) %>"
|
|
13
|
+
<turbo-frame id="card_<%= database.group.name.parameterize %>_<%= database.name.parameterize %>" src="<%= summary_path(group: database.group.name.parameterize, database: database.name.parameterize) %>">
|
|
11
14
|
<%= render partial: "card_loading_content" %>
|
|
12
15
|
</turbo-frame>
|
|
13
16
|
</div>
|
|
@@ -1,43 +1,60 @@
|
|
|
1
1
|
<% if database.connection %>
|
|
2
2
|
<% summary = database.summary %>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
<% health = fleet_health(summary) %>
|
|
4
|
+
<div data-fleet-health data-status="<%= health[:status] %>" data-severity="<%= health[:severity] %>" data-waiting-clients="<%= health[:waiting_clients] %>" data-high-utilization="<%= health[:max_utilization] >= 80 %>">
|
|
5
|
+
<div class="px-4 py-3 flex items-start gap-6">
|
|
6
|
+
<div class="space-y-1 text-sm text-gray-700">
|
|
7
|
+
<div>
|
|
8
|
+
<% number = summary.select { |row| row["list"] == "users" }.first["items"] %>
|
|
9
|
+
<%= pluralize(number, "user") %>
|
|
10
|
+
</div>
|
|
11
|
+
<div>
|
|
12
|
+
<% number = summary.select { |row| row["list"] == "databases" }.first["items"].to_i - 1 %>
|
|
13
|
+
<%= pluralize(number, "database") %>
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<% number = summary.select { |row| row["list"] == "pools" }.first["items"].to_i - 1 %>
|
|
17
|
+
<%= pluralize(number, "pool") %>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<% if health[:waiting_clients].positive? %>
|
|
21
|
+
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-yellow-200 ring-inset"><%= pluralize(health[:waiting_clients], "waiting client") %></span>
|
|
22
|
+
<% else %>
|
|
23
|
+
<span class="text-xs text-gray-400">No clients waiting</span>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
16
26
|
</div>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
<div class="flex-1 space-y-2">
|
|
28
|
+
<% summary.select { |row| row.key?(:databases_details) }.first[:databases_details].each do |db| %>
|
|
29
|
+
<div class="flex items-center gap-2 text-sm">
|
|
30
|
+
<span class="font-medium text-gray-900 w-24 truncate"><%= db["name"] %></span>
|
|
31
|
+
<div class="flex-1">
|
|
32
|
+
<% current = db["current_connections"].to_i %>
|
|
33
|
+
<% max = db["max_connections"].to_i %>
|
|
34
|
+
<% pct = max > 0 ? (current.to_f / max * 100).round : 0 %>
|
|
35
|
+
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
|
36
|
+
<div class="<%= pct >= 80 ? "bg-amber-500" : "bg-green-500" %> h-2.5 rounded-full" style="width: <%= [pct, 100].min %>%"></div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="text-xs text-gray-500 mt-0.5"><%= current %> / <%= max %> connections</div>
|
|
28
39
|
</div>
|
|
29
|
-
<div class="text-xs text-gray-500 mt-0.5"><%= current %> / <%= max %> connections</div>
|
|
30
40
|
</div>
|
|
31
|
-
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="px-4 py-2 border-t border-gray-100 flex items-center justify-between">
|
|
45
|
+
<% if health[:waiting_clients].positive? %>
|
|
46
|
+
<span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-yellow-200 ring-inset"><%= pluralize(health[:waiting_clients], "client") %> waiting</span>
|
|
47
|
+
<% elsif health[:max_utilization] >= 80 %>
|
|
48
|
+
<span class="inline-flex items-center rounded-md bg-amber-50 px-2 py-1 text-xs font-medium text-amber-800 ring-1 ring-amber-300 ring-inset"><%= health[:max_utilization] %>% utilized</span>
|
|
49
|
+
<% else %>
|
|
50
|
+
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">Healthy</span>
|
|
32
51
|
<% end %>
|
|
52
|
+
<span class="text-xs text-gray-400">Last refresh: <%= Time.now.strftime("%H:%M:%S %Z") %></span>
|
|
33
53
|
</div>
|
|
34
54
|
</div>
|
|
35
|
-
<div class="px-4 py-2 border-t border-gray-100 flex items-center justify-between">
|
|
36
|
-
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">Online</span>
|
|
37
|
-
<span class="text-xs text-gray-400">Last refresh: <%= Time.now.strftime("%H:%M:%S %Z") %></span>
|
|
38
|
-
</div>
|
|
39
55
|
<% else %>
|
|
40
|
-
|
|
56
|
+
<% health = fleet_health(nil) %>
|
|
57
|
+
<div class="px-4 py-6 text-center" data-fleet-health data-status="<%= health[:status] %>" data-severity="<%= health[:severity] %>" data-waiting-clients="0" data-high-utilization="false">
|
|
41
58
|
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-red-600/20 ring-inset">Offline</span>
|
|
42
59
|
</div>
|
|
43
60
|
<% end %>
|
|
@@ -1,18 +1,38 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
1
|
+
<div data-controller="fleet polling" data-polling-interval-value="60000" data-fleet-total-value="<%= @groups.values.sum { |group| group.databases.size } %>" data-action="turbo:frame-load->fleet#refresh turbo:frame-load->polling#refreshed">
|
|
2
|
+
<div class="bg-white rounded-lg border border-gray-200 shadow-xs mb-6">
|
|
3
|
+
<div class="px-4 py-3 border-b border-gray-200 flex flex-wrap items-center justify-between gap-2">
|
|
4
|
+
<div>
|
|
5
|
+
<h2 class="text-lg font-semibold text-gray-900">Fleet health</h2>
|
|
6
|
+
<p class="text-sm text-gray-500" data-fleet-target="headline" aria-live="polite">Checking <%= pluralize(@groups.values.sum { |group| group.databases.size }, "instance") %>…</p>
|
|
7
|
+
</div>
|
|
8
|
+
<span class="text-xs text-gray-400" data-polling-target="status">Loading current status…</span>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 divide-y sm:divide-y-0 sm:divide-x divide-gray-200">
|
|
11
|
+
<div class="px-4 py-3">
|
|
12
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">Offline</div>
|
|
13
|
+
<div class="text-xl font-semibold text-red-700" data-fleet-target="offline">—</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="px-4 py-3">
|
|
16
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">Waiting clients</div>
|
|
17
|
+
<div class="text-xl font-semibold text-yellow-800" data-fleet-target="waiting">—</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="px-4 py-3">
|
|
20
|
+
<div class="text-xs font-medium uppercase tracking-wider text-gray-500">High utilization</div>
|
|
21
|
+
<div class="text-xl font-semibold text-amber-800" data-fleet-target="utilization">—</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
4
24
|
</div>
|
|
5
|
-
</div>
|
|
6
25
|
|
|
7
|
-
<div data-controller="polling" data-polling-interval-value="60000">
|
|
8
26
|
<% @groups.each do |_, group| %>
|
|
9
27
|
<div class="mb-8">
|
|
10
28
|
<h3 class="text-xl font-semibold text-gray-900 mb-4 pb-2 border-b border-gray-200">
|
|
11
29
|
<%= group.name %>
|
|
12
30
|
</h3>
|
|
13
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
14
|
-
<% group.databases.
|
|
15
|
-
|
|
31
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" data-fleet-target="group">
|
|
32
|
+
<% group.databases.each_with_index do |database, index| %>
|
|
33
|
+
<div data-fleet-target="item" data-fleet-original-index="<%= index %>">
|
|
34
|
+
<%= render partial: "card", locals: { database: database } %>
|
|
35
|
+
</div>
|
|
16
36
|
<% end %>
|
|
17
37
|
</div>
|
|
18
38
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="flex items-start gap-3 flex-wrap">
|
|
2
|
+
<label class="sr-only" for="<%= table_id %>_search">Search <%= title.downcase %></label>
|
|
3
|
+
<input id="<%= table_id %>_search" type="search" placeholder="Search <%= title.downcase %>…" class="w-56 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700" data-data-table-target="query" data-action="input->data-table#filter">
|
|
4
|
+
<details class="relative text-sm text-gray-700">
|
|
5
|
+
<summary class="cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1.5 font-medium">Columns</summary>
|
|
6
|
+
<div class="absolute right-0 z-20 mt-2 w-max min-w-64 rounded-md border border-gray-200 bg-white p-3 text-xs shadow-lg">
|
|
7
|
+
<div class="grid grid-cols-2 gap-x-4 gap-y-2" data-data-table-target="columnMenu"></div>
|
|
8
|
+
<button type="button" class="mt-3 w-full border-t border-gray-200 pt-2 text-left font-medium text-gray-600 hover:text-gray-900" data-action="data-table#resetColumns">Reset columns</button>
|
|
9
|
+
</div>
|
|
10
|
+
</details>
|
|
11
|
+
<span class="py-1.5 text-xs text-gray-500" data-data-table-target="status"><%= row_count %> rows</span>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="mt-3 text-sm text-gray-500" data-data-table-target="empty" hidden>No rows match this search.</p>
|
data/config/routes.rb
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
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
|
|
16
|
+
get :servers, controller: :database
|
|
17
|
+
get :users, controller: :database
|
|
10
18
|
get :conf, controller: :database
|
|
19
|
+
get :state, controller: :database
|
|
11
20
|
post :reload, controller: :database
|
|
12
21
|
post :suspend, controller: :database
|
|
22
|
+
post :resume, controller: :database
|
|
23
|
+
post :pause_database, controller: :database
|
|
24
|
+
post :reconnect_database, controller: :database
|
|
25
|
+
post :wait_close_database, controller: :database
|
|
26
|
+
post :resume_database, controller: :database
|
|
13
27
|
post :shutdown, controller: :database
|
|
14
28
|
end
|
|
15
29
|
end
|
|
@@ -3,6 +3,9 @@ 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
|
|
8
|
+
# read_only: true
|
|
6
9
|
# Add more databases
|
|
7
10
|
# replica:
|
|
8
11
|
# url: <%%= ENV["PGBOUNCER_PRODUCTION_REPLICA_DATABASE_URL"] %>
|
|
@@ -14,3 +17,7 @@ pgbouncers:
|
|
|
14
17
|
|
|
15
18
|
# Time zone (defaults to app time zone)
|
|
16
19
|
# time_zone: "Pacific Time (US & Canada)"
|
|
20
|
+
|
|
21
|
+
# Hide and reject administrative commands for every PgBouncer unless an
|
|
22
|
+
# individual PgBouncer sets read_only explicitly
|
|
23
|
+
# 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
|
|
@@ -35,12 +65,36 @@ module PgBouncerHero
|
|
|
35
65
|
@url.password if @url
|
|
36
66
|
end
|
|
37
67
|
|
|
68
|
+
def read_only?
|
|
69
|
+
PgBouncerHero.read_only_for?(self)
|
|
70
|
+
end
|
|
71
|
+
|
|
38
72
|
def dbname
|
|
39
73
|
@url.path[1..-1] if @url
|
|
40
74
|
end
|
|
41
75
|
|
|
42
76
|
private
|
|
43
77
|
|
|
78
|
+
def execute(command)
|
|
79
|
+
with_connection { |raw_connection| raw_connection.exec(command) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def connection_pool
|
|
83
|
+
@pool_monitor.synchronize do
|
|
84
|
+
@connection_pool ||= ConnectionPool.new(size: pool_size, timeout: pool_timeout) { build_connection }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def connection_proxy
|
|
89
|
+
@pool_monitor.synchronize do
|
|
90
|
+
@connection_proxy ||= ConnectionPool::Wrapper.new(pool: connection_pool)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_connection
|
|
95
|
+
connection_model.new(host, port, user, password, dbname)
|
|
96
|
+
end
|
|
97
|
+
|
|
44
98
|
def connection_model
|
|
45
99
|
@connection_model ||= begin
|
|
46
100
|
Class.new(PgBouncerHero::Connection) do
|
|
@@ -50,5 +104,21 @@ module PgBouncerHero
|
|
|
50
104
|
end
|
|
51
105
|
end
|
|
52
106
|
end
|
|
107
|
+
|
|
108
|
+
def positive_integer_setting(config_key, environment_key, default)
|
|
109
|
+
value = config.fetch(config_key, ENV.fetch(environment_key, default))
|
|
110
|
+
parsed = Integer(value)
|
|
111
|
+
raise ArgumentError, "#{config_key} must be greater than zero" unless parsed.positive?
|
|
112
|
+
|
|
113
|
+
parsed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def positive_float_setting(config_key, environment_key, default)
|
|
117
|
+
value = config.fetch(config_key, ENV.fetch(environment_key, default))
|
|
118
|
+
parsed = Float(value)
|
|
119
|
+
raise ArgumentError, "#{config_key} must be a finite number greater than zero" unless parsed.positive? && parsed.finite?
|
|
120
|
+
|
|
121
|
+
parsed
|
|
122
|
+
end
|
|
53
123
|
end
|
|
54
124
|
end
|
|
@@ -1,42 +1,118 @@
|
|
|
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
|
|
7
14
|
d = databases
|
|
15
|
+
p = pools
|
|
8
16
|
l = l.as_json
|
|
9
17
|
d = d.as_json.reject { |a| a["name"] == "pgbouncer" }
|
|
18
|
+
p = p.as_json
|
|
10
19
|
l.push({ databases_details: d })
|
|
20
|
+
l.push({ pools_details: p })
|
|
11
21
|
l
|
|
12
22
|
end
|
|
13
23
|
end
|
|
14
24
|
def databases
|
|
15
|
-
|
|
25
|
+
execute("SHOW databases")
|
|
16
26
|
end
|
|
17
27
|
def stats
|
|
18
|
-
|
|
28
|
+
execute("SHOW stats")
|
|
19
29
|
end
|
|
20
30
|
def lists
|
|
21
|
-
|
|
31
|
+
execute("SHOW lists")
|
|
22
32
|
end
|
|
23
33
|
def pools
|
|
24
|
-
|
|
34
|
+
execute("SHOW pools")
|
|
25
35
|
end
|
|
26
36
|
def clients
|
|
27
|
-
|
|
37
|
+
execute("SHOW clients")
|
|
38
|
+
end
|
|
39
|
+
def servers
|
|
40
|
+
execute("SHOW servers")
|
|
41
|
+
end
|
|
42
|
+
def users
|
|
43
|
+
execute("SHOW users")
|
|
28
44
|
end
|
|
29
45
|
def conf
|
|
30
|
-
|
|
46
|
+
execute("SHOW config")
|
|
47
|
+
end
|
|
48
|
+
def state
|
|
49
|
+
execute("SHOW state")
|
|
31
50
|
end
|
|
32
51
|
def reload
|
|
33
|
-
|
|
52
|
+
instrument_admin_command(:reload) { execute("RELOAD") }
|
|
34
53
|
end
|
|
35
54
|
def suspend
|
|
36
|
-
|
|
55
|
+
instrument_admin_command(:suspend) { execute("SUSPEND") }
|
|
56
|
+
end
|
|
57
|
+
def pause(database_name)
|
|
58
|
+
execute_database_command(:pause, "PAUSE", database_name)
|
|
59
|
+
end
|
|
60
|
+
def reconnect(database_name)
|
|
61
|
+
execute_database_command(:reconnect, "RECONNECT", database_name)
|
|
62
|
+
end
|
|
63
|
+
def wait_close(database_name)
|
|
64
|
+
execute_database_command(:wait_close, "WAIT_CLOSE", database_name)
|
|
37
65
|
end
|
|
38
|
-
def
|
|
39
|
-
|
|
66
|
+
def resume(database_name = nil)
|
|
67
|
+
return instrument_admin_command(:resume) { execute("RESUME") } if database_name.nil?
|
|
68
|
+
|
|
69
|
+
execute_database_command(:resume, "RESUME", database_name)
|
|
70
|
+
end
|
|
71
|
+
def shutdown(mode = nil)
|
|
72
|
+
instrument_admin_command(:shutdown, mode: mode || :immediate) do
|
|
73
|
+
suffix = SHUTDOWN_MODES.fetch(mode)
|
|
74
|
+
execute("SHUTDOWN#{suffix}")
|
|
75
|
+
rescue KeyError
|
|
76
|
+
raise ArgumentError, "unsupported shutdown mode: #{mode.inspect}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def execute_database_command(action, command, database_name)
|
|
83
|
+
name = database_name.to_s
|
|
84
|
+
instrument_admin_command(action, target_database: name) do
|
|
85
|
+
raise ArgumentError, "database name must not be empty" if name.empty?
|
|
86
|
+
|
|
87
|
+
execute("#{command} #{PG::Connection.quote_ident(name)}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def instrument_admin_command(action, target_database: nil, **attributes)
|
|
92
|
+
payload = {
|
|
93
|
+
group: group.name,
|
|
94
|
+
database: name,
|
|
95
|
+
action: action,
|
|
96
|
+
target_database: target_database,
|
|
97
|
+
**attributes
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
error = nil
|
|
101
|
+
result = ActiveSupport::Notifications.instrument(PgBouncerHero::ADMIN_COMMAND_EVENT, payload) do
|
|
102
|
+
begin
|
|
103
|
+
command_result = yield
|
|
104
|
+
payload[:outcome] = command_result.nil? ? :unavailable : :success
|
|
105
|
+
command_result
|
|
106
|
+
rescue StandardError => command_error
|
|
107
|
+
error = command_error
|
|
108
|
+
payload[:outcome] = :error
|
|
109
|
+
payload[:error_class] = command_error.class.name
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
raise error if error
|
|
114
|
+
|
|
115
|
+
result
|
|
40
116
|
end
|
|
41
117
|
end
|
|
42
118
|
end
|