stampeded 0.3.1-x86_64-darwin

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad19088f24b0b655f7688915b34ca674f1299a905c5c9e55bb445c71363556b7
4
+ data.tar.gz: ea4f514ad4803f51af5c5a7b56d5b9b44d5d10ac70e109084d054a9dc0a278bd
5
+ SHA512:
6
+ metadata.gz: 9ca24b62e3df42aa0da4b64775d056a2472fafa4f9218cfca9e0a641037b216f0cfca2e62be98cf24824eb68c7188a71a629c341b115df98ce06a92ac6056113
7
+ data.tar.gz: d59ab4e715d9d56eff4ea08ad9070c7c9ff5b5bc360987903c5d405ead0057e16d11b72cb53de685d353addef6a9e07fe5f205ec574166b919d433e8391d69a7
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Abdelkader Boudih
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # stampeded (Ruby)
2
+
3
+ Rails integration for [Stampeded](https://github.com/seuros/stampeded): an
4
+ ActiveJob adapter, a worker, and a launcher for the daemon.
5
+
6
+ ```ruby
7
+ gem "stampeded"
8
+ ```
9
+
10
+ ```ruby
11
+ # config/application.rb
12
+ config.active_job.queue_adapter = :stampeded
13
+ ```
14
+
15
+ ```bash
16
+ bin/rails stampeded:install # apply the schema (Postgres 18+)
17
+ bin/rails stampeded:work # run your jobs
18
+ bin/rails stampeded:daemon # scheduler, reaper, pruner, dashboard
19
+ ```
20
+
21
+ ## Two processes
22
+
23
+ `stampeded:work` runs your jobs. `stampeded:daemon` runs the Rust daemon:
24
+ scheduler, reaper, pruner, expirer, health checks, HTTP API and dashboard. It
25
+ never runs a job itself. Without the daemon, jobs still run, but there is no
26
+ cron, no recovery of jobs from dead workers, no retention and no dashboard.
27
+
28
+ ## Transactional enqueue
29
+
30
+ ```ruby
31
+ Order.transaction do
32
+ order.save!
33
+ ShipOrderJob.perform_later(order) # same transaction
34
+ end # rolled back? the job is gone too
35
+ ```
36
+
37
+ The enqueue is an `INSERT` on your own connection. This only holds when the
38
+ jobs live in the same database as your data; with a separate database
39
+ (`config.connection_class`), the job commits on its own.
40
+
41
+ ## Jobs other runtimes can run
42
+
43
+ ```ruby
44
+ class ParseInvoiceJob < ApplicationJob
45
+ queue_as :parsing
46
+ stampeded task: "invoice.parse"
47
+
48
+ def perform(document_id:) = ...
49
+ end
50
+
51
+ ParseInvoiceJob.perform_later(document_id: document.id)
52
+ # task "invoice.parse", args {"document_id": "..."}
53
+ ```
54
+
55
+ Any worker that handles `invoice.parse` can run it: Rust, Go, or this gem's
56
+ worker. Its arguments must be keywords with plain JSON values (no records, times
57
+ or symbols); anything else raises `Stampeded::PortableArgumentsError` at
58
+ enqueue. A job another runtime enqueues as `invoice.parse` runs
59
+ `ParseInvoiceJob` here, with `args` as its keywords.
60
+
61
+ Every other job works as before. Keyword calls with plain values are stored the
62
+ same standard way; anything else (records, positional arguments) keeps
63
+ ActiveJob's own format, which only a Ruby worker can run.
64
+
65
+ ## Retries
66
+
67
+ ActiveJob's `retry_on` raises `Stampeded::RetryConflict` when the job class
68
+ loads: it re-enqueues retries as new jobs, while stampeded counts attempts on
69
+ the job itself. Declare stampeded's options instead:
70
+
71
+ ```ruby
72
+ class ChargeCard < ApplicationJob
73
+ stampeded max_attempts: 5,
74
+ backoff_base_ms: 1_000, backoff_max_ms: 300_000,
75
+ dead_letter_queue: "payments_dlq",
76
+ expire_seconds: 120
77
+ end
78
+ ```
79
+
80
+ - `raise Stampeded::RetryIn.new(seconds)` picks the next delay, e.g. a
81
+ server's `Retry-After`.
82
+ - `stampeded_attempt` is the attempt number. ActiveJob's `executions` is always
83
+ 1 here.
84
+ - `discard_on` works as usual.
85
+ - Jobs inside installed gems (Rails' `ActiveStorage::AnalyzeJob`, for one) may
86
+ keep `retry_on`; they get a warning at load instead.
87
+
88
+ Other options: `unique:` / `unique_period:` (one waiting job per key) and
89
+ `destination:`.
90
+
91
+ ## Continuations
92
+
93
+ Rails 8.1 continuations work as written:
94
+
95
+ ```ruby
96
+ class ImportJob < ApplicationJob
97
+ include ActiveJob::Continuable
98
+
99
+ def perform(import_id)
100
+ step :rows, start: 0 do |rows|
101
+ Row.where(import_id:).find_each(start: rows.cursor) do |row|
102
+ row.process!
103
+ rows.advance!(from: row.id)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ ```
109
+
110
+ - Progress is kept on the job's row, so a crashed worker's job resumes too.
111
+ - A deploy interrupts the job at its next checkpoint; it goes back to the queue
112
+ without spending an attempt and resumes on the same row.
113
+ - An error after progress is an attempt: `max_attempts` and backoff apply, and
114
+ the next attempt resumes from the last checkpoint.
115
+
116
+ ## Recurring jobs
117
+
118
+ ```ruby
119
+ Stampeded::Schedule.recurring(
120
+ key: "nightly-report",
121
+ job: ReportJob,
122
+ cron: "0 3 * * *",
123
+ args: [Date.current],
124
+ timezone: "America/New_York"
125
+ )
126
+ ```
127
+
128
+ The daemon fires it; a new schedule first runs at its next occurrence. An
129
+ expression that names its hours runs once on a DST fall-back day; one using `*`
130
+ or a step runs on real elapsed time. `Stampeded::Schedule.pause`, `.remove` and
131
+ `.all` manage them.
132
+
133
+ ## Workers
134
+
135
+ The pool is threads, which suits IO-bound jobs; for CPU-bound jobs run more
136
+ processes.
137
+
138
+ ```bash
139
+ QUEUE=mail CONCURRENCY=10 bin/rails stampeded:work
140
+ QUEUES=default,mail,reports bin/rails stampeded:work # one pool, several queues
141
+ QUEUES='*' QUEUES_EXCEPT=payments_dlq bin/rails stampeded:work
142
+ ```
143
+
144
+ Or configure it once:
145
+
146
+ ```ruby
147
+ config.queues = %w[default mail reports] # or :all
148
+ config.except_queues = %w[payments_dlq] # with :all
149
+ ```
150
+
151
+ - With named queues, name every queue your jobs use: a job in a queue nobody
152
+ polls waits forever.
153
+ - With `:all`, the worker serves every queue that has work. Leave out
154
+ dead-letter queues and queues other languages' workers own: this worker runs
155
+ everything as ActiveJob and discards a job whose class it cannot find.
156
+ - `SIGTERM` lets in-flight jobs finish before exiting. Jobs still running after
157
+ `shutdown_timeout` are reclaimed by the daemon.
158
+ - The worker runs on `libstampeded_ffi`, the same core every runtime's worker
159
+ uses; see [The binaries](#the-binaries). `STAMPEDED_LOG=1` logs each job;
160
+ `STAMPEDED_LOG=debug` (or `info`, `warn`, ...) also sets the core's level.
161
+
162
+ ## Queue policies
163
+
164
+ ```ruby
165
+ Stampeded::QueuePolicy.set("extraction", max_running: 2) # at most two at a time
166
+ Stampeded::QueuePolicy.set("mail", max_wait: 60) # first in line once its next job waits a minute
167
+ ```
168
+
169
+ The daemon turns these into a verdict per queue every second; the worker claims
170
+ rushed queues first and skips held ones. `max_running` also limits what one
171
+ worker process holds from that queue, with or without the daemon.
172
+
173
+ ## Without Rails
174
+
175
+ ```ruby
176
+ Stampeded.config.database_url = ENV["DATABASE_URL"]
177
+
178
+ Stampeded.enqueue(queue: "mail", task: "welcome", args: { user_id: 42 })
179
+
180
+ worker = Stampeded::Worker.new(queue: "mail")
181
+ worker.register("welcome") { |job| Mailer.welcome(job.args["user_id"]).deliver_now }
182
+ worker.run
183
+ ```
184
+
185
+ ## The binaries
186
+
187
+ Platform gems include the daemon and the worker's library, `libstampeded_ffi`.
188
+ Otherwise (a platform without them, or the gem from git), build both and point
189
+ at them:
190
+
191
+ ```bash
192
+ cargo build --release -p stampeded -p stampeded-ffi --bin stampeded --lib
193
+ export STAMPEDED_INSTALL_DIR=/path/to/stampeded/target/release
194
+ ```
195
+
196
+ The worker, schedules, steps and job state checks load the library; a process
197
+ that only enqueues needs neither.
198
+
199
+ ## Tests
200
+
201
+ ```bash
202
+ createdb stampeded_test
203
+ STAMPEDED_TEST_DATABASE_URL=postgres://localhost/stampeded_test bundle exec rake test
204
+ ```
205
+
206
+ `rake test` builds the library first, unless `STAMPEDED_INSTALL_DIR` names one.
data/exe/stampeded ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "stampeded/version"
5
+ require "stampeded/errors"
6
+ require "stampeded/daemon"
7
+
8
+ begin
9
+ Stampeded::Daemon.exec!(*ARGV)
10
+ rescue Stampeded::DaemonNotFound => e
11
+ warn e.message
12
+ exit 1
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+ require "stampeded"
5
+
6
+ module ActiveJob
7
+ module QueueAdapters
8
+ class StampededAdapter < AbstractAdapter
9
+ def initialize(**options)
10
+ super()
11
+ @options = options
12
+ end
13
+
14
+ def enqueue(job)
15
+ job.provider_job_id = insert(job, nil)
16
+ end
17
+
18
+ def enqueue_at(job, timestamp)
19
+ job.provider_job_id = insert(job, Time.at(timestamp).utc)
20
+ end
21
+
22
+ def enqueue_all(jobs)
23
+ jobs.count do |job|
24
+ id = insert(job, job.scheduled_at && Time.at(job.scheduled_at).utc)
25
+ job.provider_job_id = id
26
+ !id.nil?
27
+ end
28
+ end
29
+
30
+ def stopping? = Stampeded.stopping?
31
+
32
+ private
33
+
34
+ def insert(job, scheduled_at)
35
+ options = job.class.try(:stampeded_options) || {}
36
+
37
+ task, args, meta = Stampeded::Envelope.from_active_job(job)
38
+
39
+ Stampeded::Client.enqueue(
40
+ queue: job.queue_name.to_s,
41
+ task: task,
42
+ args: args,
43
+ meta: meta,
44
+ scheduled_at: scheduled_at,
45
+ priority: job.priority,
46
+ max_attempts: options[:max_attempts],
47
+ destination: options[:destination],
48
+ dead_letter_queue: options[:dead_letter_queue],
49
+ expire_seconds: options[:expire_seconds],
50
+ unique: options[:unique]&.then { |u| u.respond_to?(:call) ? u.call(job) : u },
51
+ unique_period: options[:unique_period]&.to_s,
52
+ backoff_base_ms: options[:backoff_base_ms],
53
+ backoff_max_ms: options[:backoff_max_ms]
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Stampeded
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ class_option :queue, type: :string, default: "default",
11
+ desc: "Queue to provision a partition for"
12
+
13
+ def create_initializer
14
+ template "stampeded.rb", "config/initializers/stampeded.rb"
15
+ end
16
+
17
+ def install_schema
18
+ if Stampeded::Schema.installed?
19
+ say_status :schema, "already installed, skipping", :blue
20
+ else
21
+ say_status :schema, "applying stampeded schema (Postgres 18+)"
22
+ Stampeded::Schema.install!
23
+ end
24
+
25
+ Stampeded.create_queue(options[:queue])
26
+ rescue StandardError => e
27
+ say_status :error, e.message, :red
28
+ say <<~MSG
29
+ The schema was not applied. Check that the database exists and runs
30
+ Postgres 18 or newer, then: bin/rails stampeded:install
31
+ MSG
32
+ end
33
+
34
+ def show_next_steps
35
+ say <<~MSG
36
+
37
+ Stampeded installed.
38
+
39
+ config.active_job.queue_adapter = :stampeded # in config/application.rb
40
+
41
+ bin/rails stampeded:work # runs your jobs
42
+ bin/rails stampeded:daemon # scheduler, reaper, pruner, dashboard
43
+ MSG
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Stampeded.configure do |config|
4
+ config.concurrency = 5
5
+
6
+ config.batch_size = 5
7
+
8
+ config.poll_interval = 1.0
9
+
10
+ config.heartbeat_interval = 30.0
11
+
12
+ config.shutdown_timeout = 25.0
13
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "continuable_ext"
4
+
5
+ module Stampeded
6
+ module ActiveJobExt
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ def self.installed_gem?(file)
12
+ Gem.path.any? { |dir| file.start_with?(File.join(dir, "")) }
13
+ end
14
+
15
+ def stampeded_checkpoint
16
+ Stampeded::Current.context&.checkpoint
17
+ end
18
+
19
+ def stampeded_checkpoint!(state = nil, conn: nil, **rest)
20
+ ctx = Stampeded::Current.context
21
+ raise Stampeded::Error, "no Stampeded job context: is this running under a Stampeded worker?" if ctx.nil?
22
+
23
+ ctx.checkpoint!(state || rest, conn: conn)
24
+ end
25
+
26
+ def stampeded_step(name, cursor: nil, &)
27
+ ctx = Stampeded::Current.context
28
+ raise Stampeded::Error, "no Stampeded job context: is this running under a Stampeded worker?" if ctx.nil?
29
+
30
+ ctx.step(name, cursor: cursor, &)
31
+ end
32
+
33
+ def stampeded_resumed?
34
+ !Stampeded::Current.context&.checkpoint.nil?
35
+ end
36
+
37
+ def stampeded_attempt
38
+ Stampeded::Current.context&.attempt
39
+ end
40
+
41
+ module ClassMethods
42
+ def stampeded_task = @stampeded_task
43
+
44
+ def stampeded_options
45
+ @stampeded_options ||= superclass.respond_to?(:stampeded_options) ? superclass.stampeded_options.dup : {}
46
+ end
47
+
48
+ def stampeded(**options)
49
+ unknown = options.keys - %i[task max_attempts destination dead_letter_queue
50
+ expire_seconds unique unique_period
51
+ backoff_base_ms backoff_max_ms]
52
+ raise ArgumentError, "unknown stampeded options: #{unknown.join(", ")}" if unknown.any?
53
+
54
+ if (task = options.delete(:task))
55
+ Stampeded::Envelope.register(task, self)
56
+ @stampeded_task = task.to_s
57
+ end
58
+
59
+ base = options[:backoff_base_ms]
60
+ if base && base.to_i > 32_767
61
+ raise ArgumentError,
62
+ "backoff_base_ms is at most 32767 (smallint on the row), got #{base}. " \
63
+ "It is the FIRST retry delay, which doubles from there; the ceiling " \
64
+ "you want is probably backoff_max_ms, which is an integer."
65
+ end
66
+
67
+ stampeded_options.merge!(options)
68
+ end
69
+
70
+ def retry_on(*, **)
71
+ if stampeded_adapter?
72
+ site = caller_locations(1, 1).first&.path.to_s
73
+ raise Stampeded::RetryConflict, name || "This job" unless Stampeded::ActiveJobExt.installed_gem?(site)
74
+
75
+ Kernel.warn "[stampeded] #{name} (#{site}) declares retry_on; its retries are not counted as attempts"
76
+ end
77
+
78
+ super
79
+ end
80
+
81
+ def stampeded_adapter?
82
+ adapter = respond_to?(:queue_adapter) ? queue_adapter : nil
83
+ adapter.class.name.to_s.include?("Stampeded")
84
+ rescue StandardError
85
+ false
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stampeded"
4
+
5
+ module Stampeded
6
+ class ActiveJobWorker < Worker
7
+ def initialize(**kwargs)
8
+ super
9
+ @handlers.default_proc = proc { |_h, _task| method(:execute_active_job) }
10
+ end
11
+
12
+ private
13
+
14
+ def execute_active_job(job)
15
+ payload = Envelope.to_active_job_payload(id: job.id, queue: job.queue, task: job.task,
16
+ args: job.args, meta: job.meta, checkpoint: job.checkpoint)
17
+
18
+ klass = payload["job_class"]
19
+ begin
20
+ Object.const_get(klass)
21
+ rescue NameError
22
+ raise UnknownTask, "#{klass} is not defined in this worker"
23
+ end
24
+
25
+ Current.with(job) { ActiveJob::Base.execute(payload) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Stampeded
6
+ module Client
7
+ module_function
8
+
9
+ def enqueue(queue:, task:, args: {}, meta: {}, scheduled_at: nil, priority: nil,
10
+ max_attempts: nil, tenant_id: nil, unique: nil, unique_period: nil,
11
+ destination: nil, dead_letter_queue: nil, expire_seconds: nil,
12
+ backoff_base_ms: nil, backoff_max_ms: nil)
13
+ sql = <<~SQL
14
+ INSERT INTO stampeded_jobs
15
+ (queue, task, args, tenant_id, priority, max_attempts, scheduled_at,
16
+ dedup_scope, dedup_period, destination, dead_letter_queue, expire_seconds,
17
+ backoff_base_ms, backoff_max_ms, meta)
18
+ VALUES ($1, $2, $3, $4, coalesce($5, 0), coalesce($6, 25), coalesce($7, now()),
19
+ $8, $9, $10, $11, coalesce($12, 3600),
20
+ coalesce($13, 1000), coalesce($14, 3600000), $15)
21
+ ON CONFLICT DO NOTHING
22
+ RETURNING id
23
+ SQL
24
+
25
+ params = [
26
+ queue, task, JSON.generate(args.nil? ? {} : args), tenant_id, priority, max_attempts,
27
+ scheduled_at&.then { |t| t.utc.iso8601(6) },
28
+ unique, unique_period, destination, dead_letter_queue, expire_seconds,
29
+ backoff_base_ms, backoff_max_ms, JSON.generate(meta.nil? ? {} : meta)
30
+ ]
31
+
32
+ Connection.translating_errors do
33
+ Connection.with { |c| c.exec_params(sql, params) }
34
+ .then { |r| r.ntuples.zero? ? nil : r[0]["id"] }
35
+ end
36
+ end
37
+
38
+ def fetch(queue, id)
39
+ Connection.translating_errors do
40
+ Connection.with do |c|
41
+ r = c.exec_params("SELECT * FROM stampeded_jobs WHERE queue = $1 AND id = $2", [queue, id])
42
+ r.ntuples.zero? ? nil : Job.new(r[0])
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ class Configuration
5
+ attr_accessor :database_url
6
+
7
+ attr_accessor :default_queue
8
+
9
+ attr_accessor :queues
10
+
11
+ attr_accessor :except_queues
12
+
13
+ attr_accessor :concurrency
14
+
15
+ attr_accessor :batch_size
16
+
17
+ attr_accessor :poll_interval
18
+
19
+ attr_accessor :heartbeat_interval
20
+
21
+ attr_accessor :shutdown_timeout
22
+
23
+ attr_accessor :connection_class
24
+
25
+ attr_accessor :job_timeout
26
+
27
+ def initialize
28
+ @database_url = ENV.fetch("STAMPEDED_DATABASE_URL", nil) || ENV.fetch("DATABASE_URL", nil)
29
+ @default_queue = "default"
30
+ @queues = nil
31
+ @except_queues = nil
32
+ @concurrency = 5
33
+ @batch_size = 5
34
+ @poll_interval = 1.0
35
+ @heartbeat_interval = 30.0
36
+ @shutdown_timeout = 25.0
37
+ @connection_class = nil
38
+ @job_timeout = 900.0
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require "uri"
5
+
6
+ module Stampeded
7
+ module Connection
8
+ module_function
9
+
10
+ def with(&block)
11
+ if active_record?
12
+ ar_class.connection.raw_connection.then(&block)
13
+ else
14
+ own(&block)
15
+ end
16
+ end
17
+
18
+ def own
19
+ conn = PG.connect(connection_params)
20
+ begin
21
+ yield conn
22
+ ensure
23
+ conn.close
24
+ end
25
+ end
26
+
27
+ def connection_params
28
+ explicit = Stampeded.config.database_url
29
+
30
+ if active_record?
31
+ cfg = ar_class.connection_db_config.configuration_hash
32
+ return {
33
+ host: cfg[:host] || "localhost",
34
+ port: cfg[:port],
35
+ dbname: cfg[:database],
36
+ user: cfg[:username],
37
+ password: cfg[:password],
38
+ **cfg.slice(*TLS_KEYS)
39
+ }.compact
40
+ end
41
+
42
+ explicit or raise Error, <<~MSG
43
+ No database configured.
44
+
45
+ Set Stampeded.config.database_url, or STAMPEDED_DATABASE_URL, or run
46
+ inside a Rails app where ActiveRecord is configured.
47
+ MSG
48
+ end
49
+
50
+ TLS_KEYS = %i[sslmode sslrootcert sslcert sslkey].freeze
51
+
52
+ def url(params = connection_params)
53
+ return params if params.is_a?(String)
54
+
55
+ query = params.filter_map { |k, v| "#{k}=#{URI.encode_www_form_component(v.to_s)}" unless v.nil? }
56
+ "postgres://?#{query.join("&")}"
57
+ end
58
+
59
+ def translating_errors
60
+ yield
61
+ rescue PG::UndefinedTable, PG::UndefinedFunction => e
62
+ raise SchemaMissing, e if e.message.include?("stampeded_")
63
+
64
+ raise
65
+ end
66
+
67
+ def ar_class
68
+ name = Stampeded.config.connection_class
69
+ return ActiveRecord::Base if name.nil?
70
+
71
+ Object.const_get(name.to_s)
72
+ rescue NameError
73
+ raise Error, "Stampeded.config.connection_class is #{name.inspect}, which does not exist"
74
+ end
75
+
76
+ def active_record?
77
+ defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:connection_db_config)
78
+ rescue StandardError
79
+ false
80
+ end
81
+ end
82
+ end