patient_http-solid_queue 1.0.1 → 1.0.2
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 +13 -0
- data/README.md +9 -1
- data/VERSION +1 -1
- data/lib/patient_http/solid_queue/callback_job.rb +15 -14
- data/lib/patient_http/solid_queue/request_job.rb +29 -14
- data/lib/patient_http/solid_queue/task_handler.rb +20 -0
- data/lib/patient_http/solid_queue/task_monitor.rb +26 -10
- data/lib/patient_http/solid_queue/task_monitor_thread.rb +24 -13
- data/lib/patient_http/solid_queue.rb +25 -13
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b17f1e1d5c477d8ca2dac603c0521dd7a842469d24db38dadb5f24858443dca
|
|
4
|
+
data.tar.gz: 25d38a530f6ec873e5b224462d0335fa99912e6c1c36cad46f2c072ff9a5162a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1d0021667606b1b527eb4b1013fb418c5e9593d89d8daba3102415305f3a337d9258fd9c4387474c0f913b421619ceeeead1d59495fdf5d814635e1df2760b0
|
|
7
|
+
data.tar.gz: b27c9cfa86a5265061b3510bd168f7780b61bab702efc621d2214dac72c4bc79efb58c3c814c01a6ad71b7a7d986b85697d73e608ccd3bdfa8834a692f9f4555
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.0.2
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Externally stored request payloads are no longer deleted as soon as the request is submitted to the processor. They are now retained until the request completes so that Active Job retries, shutdown re-enqueues, and crash recovery can still fetch them. Discarded `RequestJob` jobs clean up their stored payloads via an `after_discard` hook.
|
|
12
|
+
- `RequestJob` now declares `retry_on` for `PatientHttp::MaxCapacityError` and `PatientHttp::NotRunningError` with a polynomial backoff. Previously these errors (raised as normal backpressure when the processor is at max capacity) sent jobs straight to the failed jobs list because Active Job does not retry by default.
|
|
13
|
+
- `CallbackJob` no longer deletes externally stored payloads when the callback raises, so retries can fetch the payload and re-run the callback.
|
|
14
|
+
- Crash recovery no longer silently loses a request if the process crashes between removing the inflight record and re-enqueueing the job. The record is now claimed first and only deleted after the job has been enqueued, so a failed recovery attempt is retried by a later garbage collection pass.
|
|
15
|
+
- Fixed process registration and garbage collection on MySQL, where passing an explicit conflict target to `upsert`/`insert_all` raises an error. Crash recovery was silently disabled on MySQL.
|
|
16
|
+
- `PatientHttp::SolidQueue.configure` and `reset_configuration!` now reset the memoized external storage so it picks up the new configuration.
|
|
17
|
+
- Fixed race conditions that could create duplicate processors or task monitor threads from concurrent lifecycle calls; starting while the processor is draining no longer replaces it.
|
|
18
|
+
- The task monitor thread no longer holds a database connection from the pool while sleeping between heartbeats.
|
|
19
|
+
|
|
7
20
|
## 1.0.1
|
|
8
21
|
|
|
9
22
|
### Fixed
|
data/README.md
CHANGED
|
@@ -94,6 +94,14 @@ The `response.callback_args` and `error.callback_args` provide access to the arg
|
|
|
94
94
|
|
|
95
95
|
> [!IMPORTANT]
|
|
96
96
|
> Do not re-raise errors in the `on_error` callback as a means to retry the request. That will just retry the error callback job. If you want to retry the original request, you can enqueue a new request from within `on_error`. Be careful with this approach, though, as it can lead to infinite retry loops if the error condition is not resolved.
|
|
97
|
+
|
|
98
|
+
Callback jobs are not retried by default. If you want failed callbacks to be retried before being discarded, configure `retry_on` in an initializer:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
PatientHttp::SolidQueue::CallbackJob.retry_on StandardError, wait: :polynomially_longer, attempts: 5
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Note that Active Job runs `after_discard` hooks for any unhandled exception, not just when configured retries are exhausted. Without `retry_on`, the first callback failure will trigger the `on_retries_exhausted` handler and delete any externally stored payload, even though the failed job can still be retried manually from Mission Control (such retries will fail if the payload was stored externally).
|
|
97
105
|
>
|
|
98
106
|
> Also note that the error callback is only called when an exception occurs during the HTTP request (timeout, connection failure, etc). HTTP error status codes (4xx, 5xx) do not trigger the error callback by default. Instead, they are treated as completed requests and passed to the `on_complete` callback. See the "Handling HTTP Error Responses" section below for how to treat HTTP errors as exceptions.
|
|
99
107
|
|
|
@@ -436,7 +444,7 @@ See the [Configuration](lib/patient_http/solid_queue/configuration.rb) class for
|
|
|
436
444
|
|
|
437
445
|
> [!IMPORTANT]
|
|
438
446
|
>
|
|
439
|
-
> One difference between using this gem and making synchronous HTTP requests from a Solid Queue job is that if `max_connections` is reached due to slow asynchronous requests, new requests will trigger an error on the Active Job. The
|
|
447
|
+
> One difference between using this gem and making synchronous HTTP requests from a Solid Queue job is that if `max_connections` is reached due to slow asynchronous requests, new requests will trigger an error on the Active Job. The job declares `retry_on` for this error with a polynomial backoff, so it will automatically be retried until the processor has capacity again.
|
|
440
448
|
>
|
|
441
449
|
> In contrast, slow synchronous HTTP requests will fill up the worker pool and block new jobs from being dequeued until a worker thread becomes free.
|
|
442
450
|
>
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.2
|
|
@@ -52,21 +52,22 @@ module PatientHttp
|
|
|
52
52
|
actual_data = ref_data ? PatientHttp::SolidQueue.external_storage.fetch(data) : data
|
|
53
53
|
actual_data = PatientHttp::SolidQueue.decrypt(actual_data)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
raise ArgumentError, "Unknown result_type: #{result_type}"
|
|
66
|
-
end
|
|
67
|
-
ensure
|
|
68
|
-
PatientHttp::SolidQueue.external_storage.delete(ref_data) if ref_data
|
|
55
|
+
if result_type == "response"
|
|
56
|
+
response = PatientHttp::Response.load(actual_data)
|
|
57
|
+
PatientHttp::SolidQueue.invoke_completion_callbacks(response)
|
|
58
|
+
callback_service.on_complete(response)
|
|
59
|
+
elsif result_type == "error"
|
|
60
|
+
error = PatientHttp::Error.load(actual_data)
|
|
61
|
+
PatientHttp::SolidQueue.invoke_error_callbacks(error)
|
|
62
|
+
callback_service.on_error(error)
|
|
63
|
+
else
|
|
64
|
+
raise ArgumentError, "Unknown result_type: #{result_type}"
|
|
69
65
|
end
|
|
66
|
+
|
|
67
|
+
# Only delete the stored payload after the callback succeeds so that
|
|
68
|
+
# retries can still fetch it. Discarded jobs are cleaned up by the
|
|
69
|
+
# after_discard hook.
|
|
70
|
+
PatientHttp::SolidQueue.external_storage.delete(ref_data) if ref_data
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -10,36 +10,51 @@ module PatientHttp
|
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
12
|
class RequestJob < ActiveJob::Base
|
|
13
|
+
# Rejection due to backpressure is part of normal operation: retry until the
|
|
14
|
+
# processor has capacity again. NotRunningError covers jobs that run during
|
|
15
|
+
# the narrow window when the processor is draining or stopping.
|
|
16
|
+
retry_on PatientHttp::MaxCapacityError, PatientHttp::NotRunningError, wait: :polynomially_longer, attempts: :unlimited
|
|
17
|
+
|
|
13
18
|
# Capture the Active Job serialized hash into Context so RequestExecutor can use it.
|
|
14
19
|
around_perform do |job, block|
|
|
15
20
|
PatientHttp::SolidQueue::Context.with_job(job.serialize) { block.call }
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
# Clean up the externally stored request payload when the job is discarded.
|
|
24
|
+
# The payload is normally deleted by TaskHandler when the request completes,
|
|
25
|
+
# so this only fires for requests that never made it that far.
|
|
26
|
+
after_discard do |job, _exception|
|
|
27
|
+
PatientHttp::SolidQueue.external_storage.delete(job.arguments[0])
|
|
28
|
+
rescue => e
|
|
29
|
+
PatientHttp::SolidQueue.configuration.logger&.warn(
|
|
30
|
+
"[PatientHttp::SolidQueue] Failed to delete stored payload for dead job: #{e.class.name} #{e.message}".strip
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
18
34
|
# @param data [Hash] Request data (possibly a storage reference)
|
|
19
35
|
# @param callback_service_name [String] Fully qualified callback service class name
|
|
20
36
|
# @param raise_error_responses [Boolean, nil] Whether to treat non-2xx responses as errors
|
|
21
37
|
# @param callback_args [Hash, nil] Arguments to pass to the callback
|
|
22
38
|
# @param request_id [String, nil] Unique request ID for tracking
|
|
23
39
|
def perform(data, callback_service_name, raise_error_responses, callback_args, request_id)
|
|
24
|
-
|
|
25
|
-
actual_data = ref_data ? PatientHttp::SolidQueue.external_storage.fetch(data) : data
|
|
40
|
+
actual_data = PatientHttp::ExternalStorage.storage_ref?(data) ? PatientHttp::SolidQueue.external_storage.fetch(data) : data
|
|
26
41
|
actual_data = PatientHttp::SolidQueue.decrypt(actual_data)
|
|
27
42
|
|
|
28
43
|
request = PatientHttp::Request.load(actual_data)
|
|
29
44
|
active_job_data = PatientHttp::SolidQueue::Context.current_job
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
# The stored payload must not be deleted here: this job data is re-enqueued
|
|
47
|
+
# for Active Job retries (e.g. MaxCapacityError), processor shutdown retries,
|
|
48
|
+
# and crash recovery, all of which need to fetch the payload again.
|
|
49
|
+
# TaskHandler deletes it when the request completes.
|
|
50
|
+
RequestExecutor.execute(
|
|
51
|
+
request,
|
|
52
|
+
callback: callback_service_name,
|
|
53
|
+
raise_error_responses: raise_error_responses,
|
|
54
|
+
callback_args: callback_args,
|
|
55
|
+
active_job_data: active_job_data,
|
|
56
|
+
request_id: request_id
|
|
57
|
+
)
|
|
43
58
|
end
|
|
44
59
|
end
|
|
45
60
|
end
|
|
@@ -18,11 +18,13 @@ module PatientHttp
|
|
|
18
18
|
def on_complete(response, callback)
|
|
19
19
|
data = store_if_needed(response.as_json)
|
|
20
20
|
CallbackJob.perform_later(data, "response", callback)
|
|
21
|
+
delete_stored_request_payload
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def on_error(error, callback)
|
|
24
25
|
data = store_if_needed(error.as_json)
|
|
25
26
|
CallbackJob.perform_later(data, "error", callback)
|
|
27
|
+
delete_stored_request_payload
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def retry
|
|
@@ -39,6 +41,24 @@ module PatientHttp
|
|
|
39
41
|
|
|
40
42
|
private
|
|
41
43
|
|
|
44
|
+
# Delete the externally stored request payload once the request has
|
|
45
|
+
# completed. Until then the payload must remain fetchable because the
|
|
46
|
+
# Active Job data referencing it can be re-enqueued by Active Job retries,
|
|
47
|
+
# processor shutdown retries, and crash recovery. Only applies to
|
|
48
|
+
# RequestJob jobs; other job types own their own arguments.
|
|
49
|
+
def delete_stored_request_payload
|
|
50
|
+
return unless @active_job_data["job_class"] == RequestJob.name
|
|
51
|
+
|
|
52
|
+
data = @active_job_data["arguments"]&.first
|
|
53
|
+
return unless PatientHttp::ExternalStorage.storage_ref?(data)
|
|
54
|
+
|
|
55
|
+
PatientHttp::SolidQueue.external_storage.delete(data)
|
|
56
|
+
rescue => e
|
|
57
|
+
PatientHttp::SolidQueue.configuration.logger&.warn(
|
|
58
|
+
"[PatientHttp::SolidQueue] Failed to delete stored request payload: #{e.class.name} #{e.message}".strip
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
42
62
|
def store_if_needed(data)
|
|
43
63
|
encrypted = PatientHttp::SolidQueue.encrypt(data)
|
|
44
64
|
external_storage = PatientHttp::SolidQueue.external_storage
|
|
@@ -79,7 +79,7 @@ module PatientHttp
|
|
|
79
79
|
def ping_process
|
|
80
80
|
ProcessRegistration.upsert(
|
|
81
81
|
{process_id: @lock_identifier, max_connections: @config.max_connections, last_seen_at: Time.current},
|
|
82
|
-
unique_by: :process_id
|
|
82
|
+
unique_by: upsert_unique_by(:process_id)
|
|
83
83
|
)
|
|
84
84
|
rescue => e
|
|
85
85
|
@config.logger&.error("[PatientHttp::SolidQueue] Failed to ping process: #{e.message}")
|
|
@@ -211,31 +211,47 @@ module PatientHttp
|
|
|
211
211
|
private
|
|
212
212
|
|
|
213
213
|
def ensure_gc_lock_row!
|
|
214
|
-
GcLock.insert_all([{lock_name: GC_LOCK_NAME}]
|
|
214
|
+
GcLock.insert_all([{lock_name: GC_LOCK_NAME}])
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
-
#
|
|
217
|
+
# MySQL does not support an explicit conflict target for upserts; it always
|
|
218
|
+
# resolves conflicts via the table's unique indexes (ON DUPLICATE KEY UPDATE).
|
|
219
|
+
# Adapters with conflict targets (PostgreSQL, SQLite) require one to be given.
|
|
218
220
|
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
|
|
221
|
+
# @param column [Symbol] the unique column to use as the conflict target
|
|
222
|
+
# @return [Symbol, nil] the column, or nil when the adapter forbids a target
|
|
223
|
+
def upsert_unique_by(column)
|
|
224
|
+
Record.connection.supports_insert_conflict_target? ? column : nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Re-enqueue a single orphaned record.
|
|
228
|
+
#
|
|
229
|
+
# Uses a claim-by-exact-heartbeat update to handle race conditions: if the
|
|
230
|
+
# heartbeat was updated between our read and the claim, the update returns
|
|
231
|
+
# 0 rows and we skip re-enqueueing. Claiming (refreshing the heartbeat)
|
|
232
|
+
# before enqueueing means a crash mid-recovery leaves the record behind to
|
|
233
|
+
# go stale and be retried by a later GC pass, instead of losing the request.
|
|
234
|
+
# The record is only deleted after the job has been enqueued.
|
|
222
235
|
#
|
|
223
236
|
# @param record [InflightRequest] the orphaned record
|
|
224
237
|
# @param threshold [Float] heartbeat threshold (only records below this are orphaned)
|
|
225
238
|
# @param logger [Logger] logger for output
|
|
226
239
|
# @return [Boolean] true if successfully re-enqueued
|
|
227
240
|
def reenqueue_orphaned_record(record, threshold, logger)
|
|
228
|
-
# Atomically
|
|
229
|
-
|
|
241
|
+
# Atomically claim only if still orphaned (heartbeat unchanged). The dead
|
|
242
|
+
# process_id is left in place so a failed recovery becomes orphaned again.
|
|
243
|
+
claimed = InflightRequest
|
|
230
244
|
.where(task_id: record.task_id, heartbeat_at: record.heartbeat_at)
|
|
231
245
|
.where("heartbeat_at < ?", threshold)
|
|
232
|
-
.
|
|
246
|
+
.update_all(heartbeat_at: Time.current)
|
|
233
247
|
|
|
234
|
-
return false if
|
|
248
|
+
return false if claimed == 0
|
|
235
249
|
|
|
236
250
|
job_data = JSON.parse(record.job_payload)
|
|
237
251
|
ActiveJob::Base.deserialize(job_data).tap { |j| j.executions = 0 }.enqueue
|
|
238
252
|
|
|
253
|
+
InflightRequest.where(task_id: record.task_id).delete_all
|
|
254
|
+
|
|
239
255
|
logger&.info(
|
|
240
256
|
"[PatientHttp::SolidQueue] Re-enqueued orphaned request #{record.task_id} to #{job_data["job_class"]}"
|
|
241
257
|
)
|
|
@@ -34,11 +34,10 @@ module PatientHttp
|
|
|
34
34
|
#
|
|
35
35
|
# @return [void]
|
|
36
36
|
def start
|
|
37
|
-
return
|
|
38
|
-
@running.make_true
|
|
37
|
+
return unless @running.make_true
|
|
39
38
|
@stop_signal.reset
|
|
40
39
|
|
|
41
|
-
@task_monitor.ping_process
|
|
40
|
+
with_connection { @task_monitor.ping_process }
|
|
42
41
|
|
|
43
42
|
@thread = Thread.new do
|
|
44
43
|
run
|
|
@@ -80,16 +79,24 @@ module PatientHttp
|
|
|
80
79
|
break unless @running.true?
|
|
81
80
|
|
|
82
81
|
current_time = monotonic_time
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
update_heartbeats_due = current_time - last_heartbeat_update >= @config.heartbeat_interval
|
|
83
|
+
gc_attempt_due = current_time - last_gc_attempt >= @config.heartbeat_interval
|
|
84
|
+
|
|
85
|
+
if update_heartbeats_due || gc_attempt_due
|
|
86
|
+
# Check out a database connection only for the duration of the work
|
|
87
|
+
# so the thread does not hold a slot from the pool while it sleeps.
|
|
88
|
+
with_connection do
|
|
89
|
+
if update_heartbeats_due
|
|
90
|
+
@task_monitor.ping_process
|
|
91
|
+
update_heartbeats
|
|
92
|
+
last_heartbeat_update = current_time
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if gc_attempt_due
|
|
96
|
+
attempt_garbage_collection
|
|
97
|
+
last_gc_attempt = current_time
|
|
98
|
+
end
|
|
99
|
+
end
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
wait_time = @config.heartbeat_interval / 2.0
|
|
@@ -100,6 +107,10 @@ module PatientHttp
|
|
|
100
107
|
@config.logger&.info("[PatientHttp::SolidQueue] Monitor thread stopped")
|
|
101
108
|
end
|
|
102
109
|
|
|
110
|
+
def with_connection(&block)
|
|
111
|
+
Record.connection_pool.with_connection(&block)
|
|
112
|
+
end
|
|
113
|
+
|
|
103
114
|
def update_heartbeats
|
|
104
115
|
request_ids = @inflight_ids_callback.call
|
|
105
116
|
return if request_ids.empty?
|
|
@@ -55,6 +55,7 @@ module PatientHttp
|
|
|
55
55
|
@after_error_callbacks = []
|
|
56
56
|
@external_storage = nil
|
|
57
57
|
@request_handler = nil
|
|
58
|
+
@lifecycle_mutex = Mutex.new
|
|
58
59
|
|
|
59
60
|
class << self
|
|
60
61
|
attr_writer :configuration
|
|
@@ -66,8 +67,10 @@ module PatientHttp
|
|
|
66
67
|
def configure
|
|
67
68
|
configuration = Configuration.new
|
|
68
69
|
yield(configuration) if block_given?
|
|
69
|
-
register_handler
|
|
70
70
|
@configuration = configuration
|
|
71
|
+
@external_storage = nil
|
|
72
|
+
register_handler
|
|
73
|
+
configuration
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# Return the current configuration, initializing with defaults if necessary.
|
|
@@ -82,6 +85,7 @@ module PatientHttp
|
|
|
82
85
|
# @return [Configuration]
|
|
83
86
|
def reset_configuration!
|
|
84
87
|
@configuration = nil
|
|
88
|
+
@external_storage = nil
|
|
85
89
|
configuration
|
|
86
90
|
end
|
|
87
91
|
|
|
@@ -168,11 +172,13 @@ module PatientHttp
|
|
|
168
172
|
#
|
|
169
173
|
# @return [void]
|
|
170
174
|
def start
|
|
171
|
-
|
|
175
|
+
@lifecycle_mutex.synchronize do
|
|
176
|
+
return if @processor && !@processor.stopped?
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
@processor = PatientHttp::Processor.new(configuration)
|
|
179
|
+
@processor.observe(ProcessorObserver.new(@processor))
|
|
180
|
+
@processor.start
|
|
181
|
+
end
|
|
176
182
|
|
|
177
183
|
register_handler
|
|
178
184
|
end
|
|
@@ -181,9 +187,11 @@ module PatientHttp
|
|
|
181
187
|
#
|
|
182
188
|
# @return [void]
|
|
183
189
|
def quiet
|
|
184
|
-
|
|
190
|
+
@lifecycle_mutex.synchronize do
|
|
191
|
+
return unless running?
|
|
185
192
|
|
|
186
|
-
|
|
193
|
+
@processor.drain
|
|
194
|
+
end
|
|
187
195
|
end
|
|
188
196
|
|
|
189
197
|
# Stop the processor gracefully.
|
|
@@ -191,14 +199,16 @@ module PatientHttp
|
|
|
191
199
|
# @param timeout [Float, nil] maximum time to wait for in-flight requests to complete
|
|
192
200
|
# @return [void]
|
|
193
201
|
def stop(timeout: nil)
|
|
194
|
-
return unless @processor
|
|
195
|
-
|
|
196
202
|
if @request_handler
|
|
197
203
|
PatientHttp.unregister_handler(@request_handler)
|
|
198
204
|
end
|
|
199
205
|
|
|
200
|
-
@
|
|
201
|
-
|
|
206
|
+
@lifecycle_mutex.synchronize do
|
|
207
|
+
return unless @processor
|
|
208
|
+
|
|
209
|
+
@processor.stop(timeout: timeout)
|
|
210
|
+
@processor = nil
|
|
211
|
+
end
|
|
202
212
|
end
|
|
203
213
|
|
|
204
214
|
# Reset all state (useful for testing).
|
|
@@ -209,8 +219,10 @@ module PatientHttp
|
|
|
209
219
|
if @request_handler
|
|
210
220
|
PatientHttp.unregister_handler(@request_handler)
|
|
211
221
|
end
|
|
212
|
-
@
|
|
213
|
-
|
|
222
|
+
@lifecycle_mutex.synchronize do
|
|
223
|
+
@processor&.stop(timeout: 0)
|
|
224
|
+
@processor = nil
|
|
225
|
+
end
|
|
214
226
|
@configuration = nil
|
|
215
227
|
@external_storage = nil
|
|
216
228
|
@after_completion_callbacks = []
|