patient_http-solid_queue 1.0.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.
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatientHttp
4
+ module SolidQueue
5
+ # Manages inflight request tracking in the database for crash recovery.
6
+ #
7
+ # This class maintains Active Record records for each in-flight request.
8
+ # It provides distributed locking for orphan detection and automatic
9
+ # re-enqueueing of requests interrupted by process crashes.
10
+ #
11
+ # Task ID format: "hostname:pid:hex/request-uuid"
12
+ # - hostname: sanitized hostname (colons and slashes replaced with dashes)
13
+ # - pid: process ID
14
+ # - hex: 8-character random hex for uniqueness
15
+ # - request-uuid: unique identifier for the request
16
+ class TaskMonitor
17
+ GC_LOCK_NAME = "gc"
18
+
19
+ # @return [Configuration] the configuration object
20
+ attr_reader :config
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ hostname = ::Socket.gethostname.force_encoding("UTF-8").tr(":/", "-")
25
+ pid = ::Process.pid
26
+ @lock_identifier = "#{hostname}:#{pid}:#{SecureRandom.hex(8)}".freeze
27
+ end
28
+
29
+ # Register a request as inflight in the database.
30
+ #
31
+ # @param task [PatientHttp::RequestTask] the request task to register
32
+ # @return [void]
33
+ def register(task)
34
+ job_payload = task.task_handler.active_job_data.to_json
35
+ task_id = full_task_id(task.id)
36
+ now = Time.current
37
+
38
+ InflightRequest.create!(
39
+ task_id: task_id,
40
+ process_id: @lock_identifier,
41
+ job_payload: job_payload,
42
+ heartbeat_at: now,
43
+ created_at: now
44
+ )
45
+ rescue => e
46
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to register task #{task_id}: #{e.message}")
47
+ raise if PatientHttp.testing?
48
+ end
49
+
50
+ # Unregister a request from the database (called when request completes).
51
+ #
52
+ # @param task [PatientHttp::RequestTask] the request task to unregister
53
+ # @return [void]
54
+ def unregister(task)
55
+ task_id = full_task_id(task.id)
56
+ InflightRequest.where(task_id: task_id).delete_all
57
+ rescue => e
58
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to unregister task #{task_id}: #{e.message}")
59
+ raise if PatientHttp.testing?
60
+ end
61
+
62
+ # Update heartbeat timestamps for multiple requests in a single operation.
63
+ #
64
+ # @param task_ids [Array<String>] the request IDs to update
65
+ # @return [void]
66
+ def update_heartbeats(task_ids)
67
+ return if task_ids.empty?
68
+
69
+ full_ids = task_ids.map { |id| full_task_id(id) }
70
+ InflightRequest.where(task_id: full_ids).update_all(heartbeat_at: Time.current)
71
+ rescue => e
72
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to update heartbeats: #{e.message}")
73
+ raise if PatientHttp.testing?
74
+ end
75
+
76
+ # Record or refresh this process's registration.
77
+ #
78
+ # @return [void]
79
+ def ping_process
80
+ ProcessRegistration.upsert(
81
+ {process_id: @lock_identifier, max_connections: @config.max_connections, last_seen_at: Time.current},
82
+ unique_by: :process_id
83
+ )
84
+ rescue => e
85
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to ping process: #{e.message}")
86
+ raise if PatientHttp.testing?
87
+ end
88
+
89
+ # Remove this process's registration.
90
+ #
91
+ # @return [void]
92
+ def remove_process
93
+ ProcessRegistration.where(process_id: @lock_identifier).delete_all
94
+ rescue => e
95
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to remove process: #{e.message}")
96
+ raise if PatientHttp.testing?
97
+ end
98
+
99
+ # Try to acquire the distributed garbage collection lock.
100
+ #
101
+ # Uses a single semaphore row and pessimistic locking to ensure only one
102
+ # process can claim the lock at a time.
103
+ # Returns false if another process holds a non-expired lock, or if GC was
104
+ # run recently (within heartbeat_interval).
105
+ #
106
+ # @return [Boolean] true if lock acquired, false otherwise
107
+ def acquire_gc_lock
108
+ now = Time.current
109
+ expires_at = now + gc_lock_ttl.seconds
110
+ acquired = false
111
+
112
+ ensure_gc_lock_row!
113
+
114
+ GcLock.transaction do
115
+ lock = GcLock.lock.find_by!(lock_name: GC_LOCK_NAME)
116
+
117
+ recent_gc = lock.last_gc_at && lock.last_gc_at > (now - @config.heartbeat_interval)
118
+ next if recent_gc
119
+
120
+ lock_held = lock.lock_holder.present? && lock.expires_at.present? && lock.expires_at > now
121
+ next if lock_held
122
+
123
+ lock.update!(
124
+ lock_holder: @lock_identifier,
125
+ acquired_at: now,
126
+ expires_at: expires_at
127
+ )
128
+ acquired = true
129
+ end
130
+
131
+ acquired
132
+ rescue => e
133
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to acquire GC lock: #{e.message}")
134
+ raise if PatientHttp.testing?
135
+ false
136
+ end
137
+
138
+ # Release the garbage collection lock if held by this process, and record last_gc_at.
139
+ #
140
+ # @return [void]
141
+ def release_gc_lock
142
+ GcLock.where(lock_name: GC_LOCK_NAME, lock_holder: @lock_identifier)
143
+ .update_all(last_gc_at: Time.current, lock_holder: nil, acquired_at: nil, expires_at: nil)
144
+ rescue => e
145
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to release GC lock: #{e.message}")
146
+ raise if PatientHttp.testing?
147
+ end
148
+
149
+ # Find and re-enqueue orphaned requests.
150
+ #
151
+ # @param orphan_threshold_seconds [Numeric] age threshold for considering a request orphaned
152
+ # @param logger [Logger] logger for output
153
+ # @return [Integer] number of orphaned requests re-enqueued
154
+ def cleanup_orphaned_requests(orphan_threshold_seconds, logger)
155
+ threshold = Time.current - orphan_threshold_seconds.seconds
156
+
157
+ prune_stale_process_registrations(threshold)
158
+
159
+ # Get process IDs with a recent heartbeat
160
+ active_process_ids = ProcessRegistration.where("last_seen_at >= ?", threshold).pluck(:process_id)
161
+
162
+ # Find stale requests from processes not in the active set
163
+ orphaned = InflightRequest
164
+ .where("heartbeat_at < ?", threshold)
165
+ .where.not(process_id: active_process_ids)
166
+ .to_a
167
+
168
+ return 0 if orphaned.empty?
169
+
170
+ reenqueued_count = 0
171
+
172
+ orphaned.each do |record|
173
+ reenqueued_count += 1 if reenqueue_orphaned_record(record, threshold, logger)
174
+ end
175
+
176
+ reenqueued_count
177
+ end
178
+
179
+ # Build unique task ID for a request task that includes process identifier.
180
+ #
181
+ # @param task_id [String] the request task ID
182
+ # @return [String] the unique task ID
183
+ def full_task_id(task_id)
184
+ "#{@lock_identifier}/#{task_id}"
185
+ end
186
+
187
+ # Check if a task is registered in the inflight table.
188
+ #
189
+ # @param task [PatientHttp::RequestTask] the request task
190
+ # @return [Boolean]
191
+ # @api private
192
+ def registered?(task)
193
+ InflightRequest.where(task_id: full_task_id(task.id)).exists?
194
+ end
195
+
196
+ # Clear all records. Only allowed in test environment.
197
+ #
198
+ # @raise [RuntimeError] if called outside of test environment
199
+ # @return [void]
200
+ # @api private
201
+ def self.clear_all!
202
+ unless PatientHttp.testing?
203
+ raise "clear_all! is only allowed in test environment"
204
+ end
205
+
206
+ InflightRequest.delete_all
207
+ ProcessRegistration.delete_all
208
+ GcLock.delete_all
209
+ end
210
+
211
+ private
212
+
213
+ def ensure_gc_lock_row!
214
+ GcLock.insert_all([{lock_name: GC_LOCK_NAME}], unique_by: :lock_name)
215
+ end
216
+
217
+ # Re-enqueue a single orphaned record atomically.
218
+ #
219
+ # Uses a delete-by-exact-heartbeat to handle race conditions: if the
220
+ # heartbeat was updated between our read and the delete, the delete
221
+ # returns 0 rows and we skip re-enqueueing.
222
+ #
223
+ # @param record [InflightRequest] the orphaned record
224
+ # @param threshold [Float] heartbeat threshold (only records below this are orphaned)
225
+ # @param logger [Logger] logger for output
226
+ # @return [Boolean] true if successfully re-enqueued
227
+ def reenqueue_orphaned_record(record, threshold, logger)
228
+ # Atomically remove only if still orphaned (heartbeat unchanged)
229
+ deleted = InflightRequest
230
+ .where(task_id: record.task_id, heartbeat_at: record.heartbeat_at)
231
+ .where("heartbeat_at < ?", threshold)
232
+ .delete_all
233
+
234
+ return false if deleted == 0
235
+
236
+ job_data = JSON.parse(record.job_payload)
237
+ ActiveJob::Base.deserialize(job_data).tap { |j| j.executions = 0 }.enqueue
238
+
239
+ logger&.info(
240
+ "[PatientHttp::SolidQueue] Re-enqueued orphaned request #{record.task_id} to #{job_data["job_class"]}"
241
+ )
242
+
243
+ true
244
+ rescue => e
245
+ logger&.error(
246
+ "[PatientHttp::SolidQueue] Failed to re-enqueue orphaned request #{record.task_id}: #{e.class} - #{e.message}"
247
+ )
248
+ false
249
+ end
250
+
251
+ def gc_lock_ttl
252
+ [@config.heartbeat_interval * 2, 120].max
253
+ end
254
+
255
+ def prune_stale_process_registrations(threshold)
256
+ ProcessRegistration.where("last_seen_at < ?", threshold).delete_all
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PatientHttp
4
+ module SolidQueue
5
+ # Background thread that maintains heartbeats and performs garbage collection
6
+ # for in-flight HTTP requests.
7
+ class TaskMonitorThread
8
+ include PatientHttp::TimeHelper
9
+
10
+ # Maximum seconds to sleep between monitor thread checks.
11
+ MAX_MONITOR_SLEEP = 5.0
12
+
13
+ # @return [Configuration] the configuration object
14
+ attr_reader :config
15
+
16
+ # @return [TaskMonitor] the inflight request registry
17
+ attr_reader :task_monitor
18
+
19
+ # Initialize the monitor thread.
20
+ #
21
+ # @param config [Configuration] the configuration object
22
+ # @param task_monitor [TaskMonitor] the inflight request registry
23
+ # @param inflight_ids_callback [Proc] callback to get current inflight request IDs
24
+ def initialize(config, task_monitor, inflight_ids_callback)
25
+ @config = config
26
+ @task_monitor = task_monitor
27
+ @inflight_ids_callback = inflight_ids_callback
28
+ @thread = nil
29
+ @running = Concurrent::AtomicBoolean.new(false)
30
+ @stop_signal = Concurrent::Event.new
31
+ end
32
+
33
+ # Start the monitor thread.
34
+ #
35
+ # @return [void]
36
+ def start
37
+ return if @running.true?
38
+ @running.make_true
39
+ @stop_signal.reset
40
+
41
+ @task_monitor.ping_process
42
+
43
+ @thread = Thread.new do
44
+ run
45
+ rescue => e
46
+ @config.logger&.error("[PatientHttp::SolidQueue] Monitor error: #{e.message}\n#{e.backtrace.join("\n")}")
47
+ raise if PatientHttp.testing?
48
+ end
49
+
50
+ @thread.name = "async-http-monitor"
51
+ end
52
+
53
+ # Stop the monitor thread.
54
+ #
55
+ # @return [void]
56
+ def stop
57
+ @running.make_false
58
+ @stop_signal.set
59
+ @thread&.join(1)
60
+ @thread&.kill if @thread&.alive?
61
+ @thread = nil
62
+ end
63
+
64
+ # Check if monitor thread is running.
65
+ #
66
+ # @return [Boolean]
67
+ def running?
68
+ @running.true?
69
+ end
70
+
71
+ private
72
+
73
+ def run
74
+ @config.logger&.info("[PatientHttp::SolidQueue] Monitor thread started")
75
+
76
+ last_heartbeat_update = monotonic_time - @config.heartbeat_interval
77
+ last_gc_attempt = monotonic_time - @config.heartbeat_interval
78
+
79
+ loop do
80
+ break unless @running.true?
81
+
82
+ current_time = monotonic_time
83
+
84
+ if current_time - last_heartbeat_update >= @config.heartbeat_interval
85
+ @task_monitor.ping_process
86
+ update_heartbeats
87
+ last_heartbeat_update = current_time
88
+ end
89
+
90
+ if current_time - last_gc_attempt >= @config.heartbeat_interval
91
+ attempt_garbage_collection
92
+ last_gc_attempt = current_time
93
+ end
94
+
95
+ wait_time = @config.heartbeat_interval / 2.0
96
+ wait_time = MAX_MONITOR_SLEEP if wait_time > MAX_MONITOR_SLEEP
97
+ @stop_signal.wait(wait_time)
98
+ end
99
+
100
+ @config.logger&.info("[PatientHttp::SolidQueue] Monitor thread stopped")
101
+ end
102
+
103
+ def update_heartbeats
104
+ request_ids = @inflight_ids_callback.call
105
+ return if request_ids.empty?
106
+
107
+ @task_monitor.update_heartbeats(request_ids)
108
+
109
+ @config.logger&.debug("[PatientHttp::SolidQueue] Updated heartbeats for #{request_ids.size} inflight requests")
110
+ rescue => e
111
+ @config.logger&.error("[PatientHttp::SolidQueue] Failed to update heartbeats: #{e.class} - #{e.message}")
112
+ raise if PatientHttp.testing?
113
+ end
114
+
115
+ def attempt_garbage_collection
116
+ return unless @task_monitor.acquire_gc_lock
117
+
118
+ begin
119
+ count = @task_monitor.cleanup_orphaned_requests(@config.orphan_threshold, @config.logger)
120
+
121
+ if count > 0
122
+ @config.logger&.info("[PatientHttp::SolidQueue] Garbage collection: re-enqueued #{count} orphaned requests")
123
+ end
124
+ ensure
125
+ @task_monitor.release_gc_lock
126
+ end
127
+ rescue => e
128
+ @config.logger&.error("[PatientHttp::SolidQueue] Garbage collection failed: #{e.class} - #{e.message}")
129
+ raise if PatientHttp.testing?
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "patient_http"
4
+ require "solid_queue"
5
+
6
+ # Main module for the Solid Queue Async HTTP gem.
7
+ #
8
+ # This gem provides a mechanism to offload long-running HTTP requests from Solid Queue workers
9
+ # to a dedicated async I/O processor running in the same process, freeing worker threads
10
+ # immediately while HTTP requests are in flight.
11
+ #
12
+ # == Usage
13
+ #
14
+ # request = PatientHttp::Request.new(:get, "https://api.example.com/users/123")
15
+ # PatientHttp::SolidQueue.execute(
16
+ # request,
17
+ # callback: MyCallback,
18
+ # callback_args: {user_id: 123}
19
+ # )
20
+ #
21
+ # Define a callback service class with +on_complete+ and +on_error+ methods:
22
+ #
23
+ # class MyCallback
24
+ # def on_complete(response)
25
+ # user_id = response.callback_args[:user_id]
26
+ # User.find(user_id).update!(data: response.json)
27
+ # end
28
+ #
29
+ # def on_error(error)
30
+ # Rails.logger.error("Request failed: #{error.message}")
31
+ # end
32
+ # end
33
+ module PatientHttp
34
+ module SolidQueue
35
+ VERSION = File.read(File.join(__dir__, "../../VERSION")).strip
36
+
37
+ autoload :CallbackJob, File.join(__dir__, "solid_queue/callback_job")
38
+ autoload :Configuration, File.join(__dir__, "solid_queue/configuration")
39
+ autoload :Context, File.join(__dir__, "solid_queue/context")
40
+ autoload :GcLock, File.join(__dir__, "solid_queue/gc_lock")
41
+ autoload :InflightRequest, File.join(__dir__, "solid_queue/inflight_request")
42
+ autoload :ProcessorObserver, File.join(__dir__, "solid_queue/processor_observer")
43
+ autoload :ProcessRegistration, File.join(__dir__, "solid_queue/process_registration")
44
+ autoload :Record, File.join(__dir__, "solid_queue/record")
45
+ autoload :RequestExecutor, File.join(__dir__, "solid_queue/request_executor")
46
+ autoload :RequestJob, File.join(__dir__, "solid_queue/request_job")
47
+ autoload :LifecycleHooks, File.join(__dir__, "solid_queue/lifecycle_hooks")
48
+ autoload :TaskHandler, File.join(__dir__, "solid_queue/task_handler")
49
+ autoload :TaskMonitor, File.join(__dir__, "solid_queue/task_monitor")
50
+ autoload :TaskMonitorThread, File.join(__dir__, "solid_queue/task_monitor_thread")
51
+
52
+ @processor = nil
53
+ @configuration = nil
54
+ @after_completion_callbacks = []
55
+ @after_error_callbacks = []
56
+ @external_storage = nil
57
+ @request_handler = nil
58
+
59
+ class << self
60
+ attr_writer :configuration
61
+
62
+ # Configure the gem with a block.
63
+ #
64
+ # @yield [Configuration] the configuration object
65
+ # @return [Configuration]
66
+ def configure
67
+ configuration = Configuration.new
68
+ yield(configuration) if block_given?
69
+ @configuration = configuration
70
+ end
71
+
72
+ # Return the current configuration, initializing with defaults if necessary.
73
+ #
74
+ # @return [Configuration]
75
+ def configuration
76
+ @configuration ||= Configuration.new
77
+ end
78
+
79
+ # Reset configuration to defaults (useful for testing).
80
+ #
81
+ # @return [Configuration]
82
+ def reset_configuration!
83
+ @configuration = nil
84
+ configuration
85
+ end
86
+
87
+ # Add a callback to be executed after a successful request completion.
88
+ #
89
+ # @yield [response] block to execute after an HTTP request completes
90
+ # @yieldparam response [PatientHttp::Response] the HTTP response
91
+ def after_completion(&block)
92
+ @after_completion_callbacks << block
93
+ end
94
+
95
+ # Add a callback to be executed after a request error.
96
+ #
97
+ # @yield [error] block to execute after an HTTP request errors
98
+ # @yieldparam error [PatientHttp::Error] information about the error
99
+ def after_error(&block)
100
+ @after_error_callbacks << block
101
+ end
102
+
103
+ # Check if the processor is running.
104
+ #
105
+ # @return [Boolean]
106
+ def running?
107
+ !!@processor&.running?
108
+ end
109
+
110
+ # Check if the processor is draining (not accepting new requests).
111
+ #
112
+ # @return [Boolean]
113
+ def draining?
114
+ !!@processor&.draining?
115
+ end
116
+
117
+ # Check if the processor is stopping.
118
+ #
119
+ # @return [Boolean]
120
+ def stopping?
121
+ !!@processor&.stopping?
122
+ end
123
+
124
+ # Check if the processor is stopped.
125
+ #
126
+ # @return [Boolean]
127
+ def stopped?
128
+ @processor.nil? || @processor.stopped?
129
+ end
130
+
131
+ # Get an ExternalStorage instance for storing and fetching payloads.
132
+ #
133
+ # @return [PatientHttp::ExternalStorage]
134
+ # @api private
135
+ def external_storage
136
+ @external_storage ||= PatientHttp::ExternalStorage.new(configuration)
137
+ end
138
+
139
+ # Execute an async HTTP request.
140
+ #
141
+ # @param request [PatientHttp::Request] the HTTP request to execute
142
+ # @param callback [Class, String] Callback service class with +on_complete+ and +on_error+
143
+ # instance methods, or its fully qualified class name.
144
+ # @param callback_args [#to_h, nil] Arguments to pass to callback
145
+ # @param raise_error_responses [Boolean] If true, treats non-2xx responses as errors
146
+ # @return [String] the request ID
147
+ def execute(request, callback:, callback_args: nil, raise_error_responses: false)
148
+ PatientHttp::CallbackValidator.validate!(callback)
149
+ callback_name = callback.is_a?(Class) ? callback.name : callback.to_s
150
+ callback_args = PatientHttp::CallbackValidator.validate_callback_args(callback_args)
151
+ request_id = SecureRandom.uuid
152
+
153
+ encrypted = encrypt(request.as_json)
154
+
155
+ data = if external_storage.enabled?
156
+ external_storage.store(encrypted, max_size: configuration.payload_store_threshold)
157
+ else
158
+ encrypted
159
+ end
160
+
161
+ RequestJob.perform_later(data, callback_name, raise_error_responses, callback_args, request_id)
162
+
163
+ request_id
164
+ end
165
+
166
+ # Start the processor.
167
+ #
168
+ # @return [void]
169
+ def start
170
+ return if running?
171
+
172
+ @processor = PatientHttp::Processor.new(configuration)
173
+ @processor.observe(ProcessorObserver.new(@processor))
174
+ @processor.start
175
+
176
+ @request_handler ||= lambda do |request:, callback:, raise_error_responses:, callback_args:|
177
+ execute(
178
+ request,
179
+ callback: callback,
180
+ raise_error_responses: raise_error_responses,
181
+ callback_args: callback_args
182
+ )
183
+ end
184
+
185
+ PatientHttp.register_handler(@request_handler)
186
+ end
187
+
188
+ # Signal the processor to drain (stop accepting new requests).
189
+ #
190
+ # @return [void]
191
+ def quiet
192
+ return unless running?
193
+
194
+ @processor.drain
195
+ end
196
+
197
+ # Stop the processor gracefully.
198
+ #
199
+ # @param timeout [Float, nil] maximum time to wait for in-flight requests to complete
200
+ # @return [void]
201
+ def stop(timeout: nil)
202
+ return unless @processor
203
+
204
+ if @request_handler
205
+ PatientHttp.unregister_handler(@request_handler)
206
+ end
207
+
208
+ @processor.stop(timeout: timeout)
209
+ @processor = nil
210
+ end
211
+
212
+ # Reset all state (useful for testing).
213
+ #
214
+ # @return [void]
215
+ # @api private
216
+ def reset!
217
+ if @request_handler
218
+ PatientHttp.unregister_handler(@request_handler)
219
+ @request_handler = nil
220
+ end
221
+ @processor&.stop(timeout: 0)
222
+ @processor = nil
223
+ @configuration = nil
224
+ @external_storage = nil
225
+ @after_completion_callbacks = []
226
+ @after_error_callbacks = []
227
+ end
228
+
229
+ # Invoke the registered completion callbacks.
230
+ #
231
+ # @param response [PatientHttp::Response] the HTTP response
232
+ # @return [void]
233
+ # @api private
234
+ def invoke_completion_callbacks(response)
235
+ @after_completion_callbacks.each do |callback|
236
+ callback.call(response)
237
+ rescue => e
238
+ configuration.logger&.error("[PatientHttp::SolidQueue] after_completion callback error: #{e.class} - #{e.message}")
239
+ end
240
+ end
241
+
242
+ # Invoke the registered error callbacks.
243
+ #
244
+ # @param error [PatientHttp::Error] information about the error
245
+ # @return [void]
246
+ # @api private
247
+ def invoke_error_callbacks(error)
248
+ @after_error_callbacks.each do |callback|
249
+ callback.call(error)
250
+ rescue => e
251
+ configuration.logger&.error("[PatientHttp::SolidQueue] after_error callback error: #{e.class} - #{e.message}")
252
+ end
253
+ end
254
+
255
+ # Encrypt a value using the configured encryptor.
256
+ #
257
+ # @param value [Object] the value to encrypt
258
+ # @return [String] the encrypted value
259
+ def encrypt(value)
260
+ configuration.encryptor.encrypt(value)
261
+ end
262
+
263
+ # Decrypt a value using the configured encryptor.
264
+ #
265
+ # @param value [String] the encrypted value to decrypt
266
+ # @return [Object] the decrypted value
267
+ def decrypt(value)
268
+ configuration.encryptor.decrypt(value)
269
+ end
270
+
271
+ # Returns the processor instance.
272
+ #
273
+ # @return [PatientHttp::Processor, nil]
274
+ # @api private
275
+ attr_accessor :processor
276
+ end
277
+ end
278
+ end
279
+
280
+ if defined?(::Rails::Engine)
281
+ require_relative "solid_queue/engine"
282
+ end
283
+
284
+ PatientHttp::SolidQueue::LifecycleHooks.register
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "patient_http/solid_queue"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Rake::Task.task_defined?("patient_http_solid_queue:install:migrations")
4
+ Rake::Task["patient_http_solid_queue:install:migrations"].clear
5
+ end
6
+
7
+ namespace :patient_http_solid_queue do
8
+ namespace :install do
9
+ desc "Copy migrations from patient_http_solid_queue to the queue database migration path"
10
+ task migrations: :"db:load_config" do
11
+ ENV["FROM"] = "patient_http_solid_queue"
12
+ ENV["DATABASE"] = "queue" if ENV["DATABASE"].nil? || ENV["DATABASE"].empty?
13
+
14
+ if Rake::Task.task_defined?("railties:install:migrations")
15
+ Rake::Task["railties:install:migrations"].invoke
16
+ else
17
+ Rake::Task["app:railties:install:migrations"].invoke
18
+ end
19
+ end
20
+ end
21
+ end