Skip to content

Commit 22c295b

Browse files
committed
feat: improve database pooling
Thread-safe and multi-process safe auth schema initialization
1 parent 47f8c5f commit 22c295b

4 files changed

Lines changed: 359 additions & 22 deletions

File tree

config/database.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ default: &default
1919
encoding: unicode
2020
# For details on connection pooling, see Rails configuration guide
2121
# https://guides.rubyonrails.org/configuring.html#database-pooling
22-
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
22+
# DB_POOL lets you set a higher pool for Sidekiq without raising Puma threads.
23+
# e.g. in production: DB_POOL=10 for Sidekiq, RAILS_MAX_THREADS=5 for Puma.
24+
# Defaults to RAILS_MAX_THREADS (5) when DB_POOL is not set.
25+
pool: <%= ENV.fetch("DB_POOL") { ENV.fetch("RAILS_MAX_THREADS") { 5 } } %>
2326
# Connection timeout settings
2427
connect_timeout: 5
2528
checkout_timeout: 5

config/initializers/row_level_security.rb

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def ensure_schema!
2929
Rails.logger.debug "Auth schema already exists"
3030
@created = true
3131
else
32-
# Re-raise if it's a different error (permissions, etc)
3332
Rails.logger.error "Failed to create auth schema: #{e.message}"
3433
raise
3534
end
@@ -42,24 +41,32 @@ def ensure_schema!
4241

4342
# Initialize on boot
4443
Rails.application.config.after_initialize do
45-
AuthSchemaInitializer.initialize!
44+
# Tthe `auth` schema is created and managed by itself —
45+
# running CREATE SCHEMA here generates a wasteful DDL call on every Puma
46+
# worker and Sidekiq process boot (228 DDL calls seen in Query Performance).
47+
# Only run when connecting to a local/non-Supabase Postgres instance.
48+
db_url = ENV.fetch('SUPABASE_DB_URL', ENV.fetch('DATABASE_URL', ''))
49+
if db_url.exclude?('supabase')
50+
AuthSchemaInitializer.initialize!
4651

47-
# Run in thread to not block boot, but with retry logic
48-
Thread.new do
49-
retries = 0
50-
max_retries = 3
52+
Thread.new do
53+
retries = 0
54+
max_retries = 3
5155

52-
begin
53-
sleep 0.5 # Small delay to let other processes/threads start
54-
AuthSchemaInitializer.ensure_schema!
55-
rescue => e
56-
retries += 1
57-
if retries < max_retries
58-
sleep 1 * retries # Exponential backoff
59-
retry
60-
else
61-
Rails.logger.error "Failed to ensure auth schema after #{max_retries} attempts: #{e.message}"
56+
begin
57+
sleep 0.5
58+
AuthSchemaInitializer.ensure_schema!
59+
rescue => e
60+
retries += 1
61+
if retries < max_retries
62+
sleep 1 * retries
63+
retry
64+
else
65+
Rails.logger.error "Failed to ensure auth schema after #{max_retries} attempts: #{e.message}"
66+
end
6267
end
63-
end
64-
end.tap { |t| t.abort_on_exception = false }
68+
end.tap { |t| t.abort_on_exception = false }
69+
else
70+
Rails.logger.debug 'AuthSchemaInitializer skipped — Supabase manages the auth schema'
71+
end
6572
end

config/sidekiq.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
---
22
# Sidekiq Configuration File
33

4-
:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 25).to_i %>
4+
# Concurrency = number of Sidekiq threads = DB pool slots consumed.
5+
# Supabase Nano (2 vCPU / 2 GB) + PgBouncer session mode: keep this low.
6+
# Puma: 2 workers × 5 threads = 10 connections.
7+
# Sidekiq: DB_POOL should match concurrency (set both in your env together).
8+
# Recommended production env: SIDEKIQ_CONCURRENCY=10 DB_POOL=10
9+
:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 10).to_i %>
510
:timeout: <%= ENV.fetch('SIDEKIQ_TIMEOUT', 25).to_i %>
611
:verbose: <%= ENV.fetch('SIDEKIQ_VERBOSE', 'true') == 'true' %>
712
:queues:
@@ -20,9 +25,11 @@
2025
class: CleanupExpiredTokensJob
2126
description: 'Clean up expired password reset tokens and blacklisted JWT tokens'
2227

23-
# Refresh database metadata materialized views every 30 minutes
28+
# Refresh database metadata materialized views every 2 hours.
29+
# These views cache table privileges, extensions and policies which change
30+
# rarely. Running every 30m was consuming ~72% of total DB time (879ms avg).
2431
refresh_metadata_views:
25-
every: '30m'
32+
cron: '0 */2 * * *'
2633
class: RefreshMetadataViewsJob
2734
description: 'Refresh materialized views for database metadata (table privileges, extensions, policies)'
2835

0 commit comments

Comments
 (0)