From 774995d7bf61b97f312b77b7a2e6c309f5f148b3 Mon Sep 17 00:00:00 2001 From: Kevin Berry Date: Sun, 6 Sep 2026 12:55:52 -0500 Subject: [PATCH 1/2] feat(table): add configurable autovacuum settings for outbox tables - Add ALTER TABLE SET (autovacuum_...) by default after table creation - Default values: vacuum_scale_factor=0, vacuum_threshold=50, analyze_scale_factor=0, analyze_threshold=50, vacuum_cost_delay=0 - All thresholds are configurable via options - Opt-out with autovacuum: false - Add --no-autovacuum CLI flag to outboxify - Add tests verifying default, disabled, and custom settings --- exe/outboxify | 7 +++- lib/sequel/pgt_outbox/table.rb | 39 +++++++++++++++++++++ test/sequel/test_pgt_outbox.rb | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/exe/outboxify b/exe/outboxify index 8484dd9..14fc1d2 100755 --- a/exe/outboxify +++ b/exe/outboxify @@ -4,9 +4,13 @@ require 'sequel/pgt_outbox' require 'optparse' +autovacuum = true + opts = OptionParser.new do |o| o.banner = 'Usage: outboxify [options] ' + o.on('--no-autovacuum') { autovacuum = false } end +opts.parse! db_uri = ARGV.shift if db_uri.nil? @@ -25,5 +29,6 @@ if table.nil? exit 2 end -function = DB.pgt_outbox_setup(table) +options = { autovacuum: } +function = DB.pgt_outbox_setup(table, **options) DB.pgt_outbox_events(table, function) diff --git a/lib/sequel/pgt_outbox/table.rb b/lib/sequel/pgt_outbox/table.rb index 830ed2d..ea5c967 100644 --- a/lib/sequel/pgt_outbox/table.rb +++ b/lib/sequel/pgt_outbox/table.rb @@ -28,6 +28,7 @@ def create! string_columns! jsonb_columns! indexes! + autovacuum_settings! self end @@ -99,6 +100,30 @@ def uuid_function @uuid_function ||= opts.fetch(:uuid_function, :uuid_generate_v4) end + def autovacuum? + @autovacuum ||= opts.fetch(:autovacuum, true) + end + + def autovacuum_vacuum_scale_factor + opts.fetch(:autovacuum_vacuum_scale_factor, 0) + end + + def autovacuum_vacuum_threshold + opts.fetch(:autovacuum_vacuum_threshold, 50) + end + + def autovacuum_analyze_scale_factor + opts.fetch(:autovacuum_analyze_scale_factor, 0) + end + + def autovacuum_analyze_threshold + opts.fetch(:autovacuum_analyze_threshold, 50) + end + + def autovacuum_vacuum_cost_delay + opts.fetch(:autovacuum_vacuum_cost_delay, 0) + end + def function @function ||= Function.create!(self, opts:) end @@ -156,6 +181,20 @@ def indexes! db.add_index name, Sequel.desc(attempted_column) self end + + def autovacuum_settings! + return unless autovacuum? + + settings = { + autovacuum_vacuum_scale_factor: autovacuum_vacuum_scale_factor, + autovacuum_vacuum_threshold: autovacuum_vacuum_threshold, + autovacuum_analyze_scale_factor: autovacuum_analyze_scale_factor, + autovacuum_analyze_threshold: autovacuum_analyze_threshold, + autovacuum_vacuum_cost_delay: autovacuum_vacuum_cost_delay + } + db.run "ALTER TABLE #{quoted_name} SET (#{settings.map { |k, v| "#{k} = #{v}" }.join(", ")})" + self + end end end end diff --git a/test/sequel/test_pgt_outbox.rb b/test/sequel/test_pgt_outbox.rb index fa42bc4..00bf9fa 100755 --- a/test/sequel/test_pgt_outbox.rb +++ b/test/sequel/test_pgt_outbox.rb @@ -230,4 +230,67 @@ def depth_sql(depth) end end +if DB.server_version >= 90_400 + describe 'Autovacuum Settings' do # rubocop:disable Metrics/BlockLength + def get_reloptions(table_name) + DB['SELECT reloptions FROM pg_class WHERE relname = ?', table_name].first[:reloptions] + end + + after do + DB.drop_table(:accounts, :accounts_outbox) + begin + DB.drop_function(:spgt_outbox_events) + rescue Sequel::DatabaseError + # function may not exist + end + end + + it 'should apply default autovacuum settings by default' do + DB.create_table!(:accounts) do + integer :id + String :s + end + DB.pgt_outbox_setup(:accounts, function_name: :spgt_outbox_events) + + opts = get_reloptions('accounts_outbox') + + _(opts).must_include 'autovacuum_vacuum_scale_factor=0' + _(opts).must_include 'autovacuum_vacuum_threshold=50' + _(opts).must_include 'autovacuum_analyze_scale_factor=0' + _(opts).must_include 'autovacuum_analyze_threshold=50' + _(opts).must_include 'autovacuum_vacuum_cost_delay=0' + end + + it 'should skip autovacuum settings when autovacuum: false' do + DB.create_table!(:accounts) do + integer :id + String :s + end + DB.pgt_outbox_setup(:accounts, autovacuum: false, function_name: :spgt_outbox_events) + + opts = get_reloptions('accounts_outbox') + + _(opts).must_be_nil + end + + it 'should apply custom autovacuum thresholds' do + DB.create_table!(:accounts) do + integer :id + String :s + end + DB.pgt_outbox_setup(:accounts, + function_name: :spgt_outbox_events, + autovacuum_vacuum_threshold: 100, + autovacuum_analyze_threshold: 200, + autovacuum_vacuum_cost_delay: 20) + + opts = get_reloptions('accounts_outbox') + + _(opts).must_include 'autovacuum_vacuum_threshold=100' + _(opts).must_include 'autovacuum_analyze_threshold=200' + _(opts).must_include 'autovacuum_vacuum_cost_delay=20' + end + end +end + # vim: ft=ruby sts=2 sw=2 ts=2 et From c336bb3d95bebbc6d74ce7c80ef8472aff8a49f8 Mon Sep 17 00:00:00 2001 From: Kevin Berry Date: Sun, 6 Sep 2026 14:03:06 -0500 Subject: [PATCH 2/2] style: fix rubocop offenses and update rubocop plugin syntax --- .rubocop.yml | 2 +- lib/sequel/pgt_outbox/function.rb | 1 + test/sequel/test_pgt_outbox.rb | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3ad8418..5623b76 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ --- -require: +plugins: - rubocop-rake - rubocop-minitest diff --git a/lib/sequel/pgt_outbox/function.rb b/lib/sequel/pgt_outbox/function.rb index f93caeb..addea9f 100644 --- a/lib/sequel/pgt_outbox/function.rb +++ b/lib/sequel/pgt_outbox/function.rb @@ -8,6 +8,7 @@ module PgtOutbox # The Outbox Function class Function include PgtOutbox + DEFAULT_OPTS = { language: :plpgsql, returns: :trigger, replace: true }.freeze attr_reader(*%i[outbox db opts]) diff --git a/test/sequel/test_pgt_outbox.rb b/test/sequel/test_pgt_outbox.rb index 00bf9fa..279daeb 100755 --- a/test/sequel/test_pgt_outbox.rb +++ b/test/sequel/test_pgt_outbox.rb @@ -60,8 +60,10 @@ def depth_sql(depth) ds = DB[:accounts] ds.insert(id: 1, s: 'string') + _(ds.all).must_equal [{ id: 1, s: 'string' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h).must_equal(id: 1, @@ -75,8 +77,10 @@ def depth_sql(depth) metadata: nil) ds.where(id: 1).update(s: 'string2') + _(ds.all).must_equal [{ id: 1, s: 'string2' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h).must_equal(id: 2, @@ -90,8 +94,10 @@ def depth_sql(depth) metadata: nil) ds.delete + _(ds.all).must_equal [] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h).must_equal(id: 3, @@ -130,21 +136,27 @@ def depth_sql(depth) ds = DB[:accounts] ds.insert(id: 1, s: 'string') + _(ds.all).must_equal [{ id: 1, s: 'string' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, completed: nil, event_type: 'accounts_created', last_error: nil, data_before: nil, data_after: { 's' => 'string', 'id' => 1 }, metadata: nil) ds.where(id: 1).update(s: 'string2') + _(ds.all).must_equal [{ id: 1, s: 'string2' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, @@ -156,11 +168,14 @@ def depth_sql(depth) metadata: nil) ds.delete + _(ds.all).must_equal [] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, completed: nil, event_type: 'accounts_deleted', last_error: nil, data_before: { 's' => 'string2', 'id' => 1 }, data_after: nil, metadata: nil) @@ -192,21 +207,27 @@ def depth_sql(depth) ds = DB[:accounts] ds.insert(id: 1, s: 'string') + _(ds.all).must_equal [{ id: 1, s: 'string' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, completed: false, event_type: 'accounts_created', last_error: nil, data_before: nil, data_after: { 's' => 'string', 'id' => 1 }, metadata: nil) ds.where(id: 1).update(s: 'string2') + _(ds.all).must_equal [{ id: 1, s: 'string2' }] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, @@ -218,11 +239,14 @@ def depth_sql(depth) metadata: nil) ds.delete + _(ds.all).must_equal [] h = @logs.first + _(h.delete(:created).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) _(h.delete(:updated).to_i).must_be_close_to(10, DB.get(Sequel::CURRENT_TIMESTAMP).to_i) id = h.delete(:id) + _(id).must_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/) _(h).must_equal(attempts: 0, attempted: nil, completed: false, event_type: 'accounts_deleted', last_error: nil, data_before: { 's' => 'string2', 'id' => 1 }, data_after: nil, metadata: nil)