patient_http-sidekiq 1.0.0 → 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 +19 -0
- data/README.md +16 -3
- data/VERSION +1 -1
- data/lib/patient_http/sidekiq/callback_worker.rb +16 -15
- data/lib/patient_http/sidekiq/request_worker.rb +25 -15
- data/lib/patient_http/sidekiq/stats.rb +1 -1
- data/lib/patient_http/sidekiq/task_handler.rb +20 -0
- data/lib/patient_http/sidekiq/task_monitor.rb +49 -6
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +4 -5
- data/lib/patient_http/sidekiq.rb +39 -18
- 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: f244dd52326602169e67b8471527b939c3e451fd11dcbcfe424305d5ad0929dd
|
|
4
|
+
data.tar.gz: 28b502c3aca097c48e364195e338e475d67f4f584209797054e2cc8ed75dda90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12bb283a157d40689ffadfb5fdb58b74260374017409f99185a650f989d54da2ff858bbccac294fd6fe7095c0150cc024a0e15649280d0d1aaf5da04d2211b12
|
|
7
|
+
data.tar.gz: a27fbd9f52d110b05bb4177dd675642dde873c3a519078ea8c95f295c96f8be499e8fff119e79a8c0f446d1c6d608e537c374dceda993d98bcdf2081ef9e0a00
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ 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
|
+
- Crash recovery now recovers requests from crashed processes. Previously a crashed process's ID remained in the process registry indefinitely, permanently excluding its in-flight requests from orphan detection unless the Sidekiq Web UI happened to prune it.
|
|
12
|
+
- Fixed calls to nonexistent `Sidekiq.testing?` in error handlers, which would raise `NoMethodError` and permanently kill the task monitor thread on the first transient Redis error.
|
|
13
|
+
- 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 Sidekiq retries, shutdown re-enqueues, and crash recovery can still fetch them. Dead `RequestWorker` jobs clean up their stored payloads when retries are exhausted.
|
|
14
|
+
- `CallbackWorker` no longer deletes externally stored payloads when the callback raises, so Sidekiq retries can fetch the payload and re-run the callback.
|
|
15
|
+
- Fixed dead job payload cleanup in `CallbackWorker`, which silently failed by calling a nonexistent method.
|
|
16
|
+
- `PatientHttp::Sidekiq.configure` and `reset_configuration!` now reset the memoized external storage so it picks up the new configuration.
|
|
17
|
+
- Heartbeat updates now refresh the TTL on the inflight tracking keys so they cannot expire while long-running requests are still in flight.
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
## 1.0.1
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Ensure `PatientHttp::Sidekiq` is automatically registered as the HTTP processor on a process not running the Sidekiq server. This gem will now auto register itself on a client process either by calling `PatientHttp::Sidekiq.configure` or `PatientHttp::Sidekiq.register_handler` directly.
|
|
25
|
+
|
|
7
26
|
## 1.0.0
|
|
8
27
|
|
|
9
28
|
### Added
|
data/README.md
CHANGED
|
@@ -46,7 +46,17 @@ The async processor runs in a dedicated thread within your Sidekiq process, usin
|
|
|
46
46
|
|
|
47
47
|
## Quick Start
|
|
48
48
|
|
|
49
|
-
### 1.
|
|
49
|
+
### 1. Configure The Gem
|
|
50
|
+
|
|
51
|
+
Configure the gem in an initializer (see the Configuration section below for all available options):
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
PatientHttp::Sidekiq.configure do |config|
|
|
55
|
+
config.max_connections = 256
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Create a Callback Service
|
|
50
60
|
|
|
51
61
|
Define a callback service class with `on_complete` and `on_error` methods:
|
|
52
62
|
|
|
@@ -69,7 +79,7 @@ class FetchDataCallback
|
|
|
69
79
|
end
|
|
70
80
|
```
|
|
71
81
|
|
|
72
|
-
###
|
|
82
|
+
### 3. Make HTTP Requests
|
|
73
83
|
|
|
74
84
|
Make HTTP requests from anywhere in your code using `PatientHttp`:
|
|
75
85
|
|
|
@@ -82,7 +92,7 @@ PatientHttp.get(
|
|
|
82
92
|
)
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
###
|
|
95
|
+
### 4. That's It!
|
|
86
96
|
|
|
87
97
|
The request will be enqueued as a Sidekiq job and passed to a [PatientHttp](https://github.com/bdurand/patient_http) processor to execute asynchronously. When the HTTP request completes, your callback's `on_complete` method is executed in another Sidekiq job.
|
|
88
98
|
|
|
@@ -451,6 +461,9 @@ end
|
|
|
451
461
|
|
|
452
462
|
See the [Configuration](lib/patient_http/sidekiq/configuration.rb) class for all available options.
|
|
453
463
|
|
|
464
|
+
> [!NOTE]
|
|
465
|
+
> You **must** call `PatientHttp::Sidekiq.configure` or `PatientHttp::Sidekiq.register_handler` in order to register the request handler and start processing requests. If you do not call either method, you will get errors making HTTP requests with `PatientHttp.execute`.
|
|
466
|
+
|
|
454
467
|
### Tuning Tips
|
|
455
468
|
|
|
456
469
|
- `max_connections`: Adjust this based on your system's resources. Each connection uses memory and file descriptors. A tuned system with sufficient resources can handle thousands of concurrent connections.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.2
|
|
@@ -53,7 +53,7 @@ module PatientHttp
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
begin
|
|
56
|
-
|
|
56
|
+
Sidekiq.external_storage.delete(data)
|
|
57
57
|
rescue => e
|
|
58
58
|
PatientHttp::Sidekiq.configuration.logger&.warn(
|
|
59
59
|
"[PatientHttp::Sidekiq] Failed to delete stored payload for dead job: #{e.class.name} #{e.message}".strip
|
|
@@ -75,21 +75,22 @@ module PatientHttp
|
|
|
75
75
|
actual_data = ref_data ? Sidekiq.external_storage.fetch(data) : data
|
|
76
76
|
actual_data = Sidekiq.decrypt(actual_data)
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
raise ArgumentError, "Unknown result_type: #{result_type}"
|
|
89
|
-
end
|
|
90
|
-
ensure
|
|
91
|
-
Sidekiq.external_storage.delete(ref_data) if ref_data
|
|
78
|
+
if result_type == "response"
|
|
79
|
+
response = PatientHttp::Response.load(actual_data)
|
|
80
|
+
PatientHttp::Sidekiq.invoke_completion_callbacks(response)
|
|
81
|
+
callback_service.on_complete(response)
|
|
82
|
+
elsif result_type == "error"
|
|
83
|
+
error = PatientHttp::Error.load(actual_data)
|
|
84
|
+
PatientHttp::Sidekiq.invoke_error_callbacks(error)
|
|
85
|
+
callback_service.on_error(error)
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "Unknown result_type: #{result_type}"
|
|
92
88
|
end
|
|
89
|
+
|
|
90
|
+
# Only delete the stored payload after the callback succeeds so that
|
|
91
|
+
# Sidekiq retries can still fetch it. Failed jobs are cleaned up by
|
|
92
|
+
# the sidekiq_retries_exhausted hook.
|
|
93
|
+
Sidekiq.external_storage.delete(ref_data) if ref_data
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
end
|
|
@@ -15,6 +15,17 @@ module PatientHttp
|
|
|
15
15
|
class RequestWorker
|
|
16
16
|
include ::Sidekiq::Job
|
|
17
17
|
|
|
18
|
+
# Clean up the externally stored request payload when the job exhausts all
|
|
19
|
+
# retries. The payload is normally deleted by TaskHandler when the request
|
|
20
|
+
# completes, so this only fires for requests that never made it that far.
|
|
21
|
+
sidekiq_retries_exhausted do |job, _exception|
|
|
22
|
+
Sidekiq.external_storage.delete(job["args"][0])
|
|
23
|
+
rescue => e
|
|
24
|
+
PatientHttp::Sidekiq.configuration.logger&.warn(
|
|
25
|
+
"[PatientHttp::Sidekiq] Failed to delete stored payload for dead job: #{e.class.name} #{e.message}".strip
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
18
29
|
# Perform the HTTP request.
|
|
19
30
|
#
|
|
20
31
|
# @param data [Hash] Request data (possibly a storage reference) with keys:
|
|
@@ -26,31 +37,30 @@ module PatientHttp
|
|
|
26
37
|
# - "max_redirects" [Integer, nil] Maximum redirects to follow
|
|
27
38
|
# @param callback_service_name [String] Fully qualified callback service class name
|
|
28
39
|
# @param raise_error_responses [Boolean, nil] Whether to treat non-2xx responses as errors;
|
|
29
|
-
#
|
|
40
|
+
# nil is treated as false
|
|
30
41
|
# @param callback_args [Hash, nil] Arguments to pass to the callback
|
|
31
42
|
# @param request_id [String, nil] Unique request ID for tracking
|
|
32
43
|
# @return [void]
|
|
33
44
|
def perform(data, callback_service_name, raise_error_responses, callback_args, request_id)
|
|
34
45
|
# Fetch from external storage if needed
|
|
35
|
-
|
|
36
|
-
actual_data = ref_data ? Sidekiq.external_storage.fetch(data) : data
|
|
46
|
+
actual_data = PatientHttp::ExternalStorage.storage_ref?(data) ? Sidekiq.external_storage.fetch(data) : data
|
|
37
47
|
actual_data = Sidekiq.decrypt(actual_data)
|
|
38
48
|
|
|
39
49
|
request = PatientHttp::Request.load(actual_data)
|
|
40
50
|
sidekiq_job = Sidekiq::Context.current_job
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
# The stored payload must not be deleted here: this job hash is re-pushed
|
|
53
|
+
# for Sidekiq retries (e.g. MaxCapacityError), processor shutdown retries,
|
|
54
|
+
# and crash recovery, all of which need to fetch the payload again.
|
|
55
|
+
# TaskHandler deletes it when the request completes.
|
|
56
|
+
RequestExecutor.execute(
|
|
57
|
+
request,
|
|
58
|
+
callback: callback_service_name,
|
|
59
|
+
raise_error_responses: raise_error_responses,
|
|
60
|
+
callback_args: callback_args,
|
|
61
|
+
sidekiq_job: sidekiq_job,
|
|
62
|
+
request_id: request_id
|
|
63
|
+
)
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
end
|
|
@@ -29,6 +29,7 @@ module PatientHttp
|
|
|
29
29
|
def on_complete(response, callback)
|
|
30
30
|
data = store_if_needed(response.as_json)
|
|
31
31
|
CallbackWorker.perform_async(data, "response", callback)
|
|
32
|
+
delete_stored_request_payload
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# Trigger the error callback with the error.
|
|
@@ -42,6 +43,7 @@ module PatientHttp
|
|
|
42
43
|
def on_error(error, callback)
|
|
43
44
|
data = store_if_needed(error.as_json)
|
|
44
45
|
CallbackWorker.perform_async(data, "error", callback)
|
|
46
|
+
delete_stored_request_payload
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
# Re-enqueue the original Sidekiq job for retry.
|
|
@@ -67,6 +69,24 @@ module PatientHttp
|
|
|
67
69
|
|
|
68
70
|
private
|
|
69
71
|
|
|
72
|
+
# Delete the externally stored request payload once the request has
|
|
73
|
+
# completed. Until then the payload must remain fetchable because the
|
|
74
|
+
# Sidekiq job hash referencing it can be re-pushed by Sidekiq retries,
|
|
75
|
+
# processor shutdown retries, and crash recovery. Only applies to
|
|
76
|
+
# RequestWorker jobs; other job types own their own arguments.
|
|
77
|
+
def delete_stored_request_payload
|
|
78
|
+
return unless @sidekiq_job["class"] == RequestWorker.name
|
|
79
|
+
|
|
80
|
+
data = @sidekiq_job["args"]&.first
|
|
81
|
+
return unless PatientHttp::ExternalStorage.storage_ref?(data)
|
|
82
|
+
|
|
83
|
+
PatientHttp::Sidekiq.external_storage.delete(data)
|
|
84
|
+
rescue => e
|
|
85
|
+
PatientHttp::Sidekiq.configuration.logger&.warn(
|
|
86
|
+
"[PatientHttp::Sidekiq] Failed to delete stored request payload: #{e.class.name} #{e.message}".strip
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
70
90
|
def store_if_needed(data)
|
|
71
91
|
encrypted = Sidekiq.encrypt(data)
|
|
72
92
|
external_storage = PatientHttp::Sidekiq.external_storage
|
|
@@ -226,6 +226,11 @@ module PatientHttp
|
|
|
226
226
|
task_ids.each do |task_id|
|
|
227
227
|
pipeline.call("ZADD", INFLIGHT_INDEX_KEY, "XX", timestamp_ms, full_task_id(task_id))
|
|
228
228
|
end
|
|
229
|
+
# Keep the inflight keys alive while requests are still in flight;
|
|
230
|
+
# otherwise they only get their TTL refreshed when new requests
|
|
231
|
+
# are registered.
|
|
232
|
+
pipeline.call("EXPIRE", INFLIGHT_INDEX_KEY, inflight_ttl)
|
|
233
|
+
pipeline.call("EXPIRE", INFLIGHT_JOBS_KEY, inflight_ttl)
|
|
229
234
|
end
|
|
230
235
|
end
|
|
231
236
|
end
|
|
@@ -401,10 +406,8 @@ module PatientHttp
|
|
|
401
406
|
orphaned_request_ids_by_process = all_orphaned_request_ids.group_by do |request_id|
|
|
402
407
|
request_id.split("/", 2).first
|
|
403
408
|
end
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
end
|
|
407
|
-
orphaned_request_ids = orphaned_request_ids_by_process.except(*all_process_ids).values.flatten
|
|
409
|
+
live_process_ids = prune_stale_processes(orphaned_request_ids_by_process.keys)
|
|
410
|
+
orphaned_request_ids = orphaned_request_ids_by_process.except(*live_process_ids).values.flatten
|
|
408
411
|
|
|
409
412
|
return [] if orphaned_request_ids.empty?
|
|
410
413
|
|
|
@@ -416,6 +419,42 @@ module PatientHttp
|
|
|
416
419
|
orphaned_request_ids.zip(job_payloads).reject { |_id, payload| payload.nil? }
|
|
417
420
|
end
|
|
418
421
|
|
|
422
|
+
# Determine which of the given process IDs belong to live processes,
|
|
423
|
+
# removing dead ones from the process set.
|
|
424
|
+
#
|
|
425
|
+
# Membership in the process set alone doesn't prove liveness: a crashed
|
|
426
|
+
# process never removes itself from the set. A process is only considered
|
|
427
|
+
# live if its max_connections key (refreshed on every heartbeat with a
|
|
428
|
+
# short TTL) still exists. Stale members are removed from the set so
|
|
429
|
+
# their inflight requests can be recovered.
|
|
430
|
+
#
|
|
431
|
+
# @param process_ids [Array<String>] candidate process IDs
|
|
432
|
+
#
|
|
433
|
+
# @return [Array<String>] the subset of process IDs that are live
|
|
434
|
+
def prune_stale_processes(process_ids)
|
|
435
|
+
registered_ids = ::Sidekiq.redis do |redis|
|
|
436
|
+
redis.smembers(PROCESS_SET_KEY)
|
|
437
|
+
end
|
|
438
|
+
candidates = process_ids & registered_ids
|
|
439
|
+
return [] if candidates.empty?
|
|
440
|
+
|
|
441
|
+
max_connection_values = ::Sidekiq.redis do |redis|
|
|
442
|
+
redis.mget(*candidates.map { |process_id| max_connections_key_for(process_id) })
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
stale_process_ids, live_process_ids = candidates.zip(max_connection_values)
|
|
446
|
+
.partition { |_process_id, max_conn| max_conn.nil? }
|
|
447
|
+
.map { |pairs| pairs.map(&:first) }
|
|
448
|
+
|
|
449
|
+
unless stale_process_ids.empty?
|
|
450
|
+
::Sidekiq.redis do |redis|
|
|
451
|
+
redis.srem(PROCESS_SET_KEY, stale_process_ids)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
live_process_ids
|
|
456
|
+
end
|
|
457
|
+
|
|
419
458
|
# Re-enqueue all orphaned jobs.
|
|
420
459
|
#
|
|
421
460
|
# @param orphaned_requests [Array<Array(String, String)>] array of [request_id, job_payload] pairs
|
|
@@ -503,7 +542,7 @@ module PatientHttp
|
|
|
503
542
|
# @return [Integer] TTL in seconds
|
|
504
543
|
def inflight_ttl
|
|
505
544
|
# Set to 3x the orphan threshold, with a minimum of 1 hour
|
|
506
|
-
[config.orphan_threshold * 3, 3600].max
|
|
545
|
+
[config.orphan_threshold * 3, 3600].max.round
|
|
507
546
|
end
|
|
508
547
|
|
|
509
548
|
# Calculate the TTL for the garbage collection lock.
|
|
@@ -535,7 +574,11 @@ module PatientHttp
|
|
|
535
574
|
end
|
|
536
575
|
|
|
537
576
|
def max_connections_key
|
|
538
|
-
|
|
577
|
+
max_connections_key_for(@lock_identifier)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def max_connections_key_for(process_id)
|
|
581
|
+
"#{PROCESS_SET_KEY}:#{process_id}:max_connections"
|
|
539
582
|
end
|
|
540
583
|
end
|
|
541
584
|
end
|
|
@@ -35,8 +35,7 @@ module PatientHttp
|
|
|
35
35
|
#
|
|
36
36
|
# @return [void]
|
|
37
37
|
def start
|
|
38
|
-
return
|
|
39
|
-
@running.make_true
|
|
38
|
+
return unless @running.make_true
|
|
40
39
|
@stop_signal.reset
|
|
41
40
|
|
|
42
41
|
@task_monitor.ping_process
|
|
@@ -46,7 +45,7 @@ module PatientHttp
|
|
|
46
45
|
rescue => e
|
|
47
46
|
# Log error but don't crash
|
|
48
47
|
@config.logger&.error("[PatientHttp::Sidekiq] Monitor error: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
49
|
-
raise if
|
|
48
|
+
raise if PatientHttp.testing?
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
@thread.name = "patient-http-monitor"
|
|
@@ -120,7 +119,7 @@ module PatientHttp
|
|
|
120
119
|
@config.logger&.debug("[PatientHttp::Sidekiq] Updated heartbeats for #{request_ids.size} inflight requests")
|
|
121
120
|
rescue => e
|
|
122
121
|
@config.logger&.error("[PatientHttp::Sidekiq] Failed to update heartbeats: #{e.class} - #{e.message}")
|
|
123
|
-
raise if
|
|
122
|
+
raise if PatientHttp.testing?
|
|
124
123
|
end
|
|
125
124
|
|
|
126
125
|
# Attempt to acquire GC lock and clean up orphaned requests.
|
|
@@ -147,7 +146,7 @@ module PatientHttp
|
|
|
147
146
|
end
|
|
148
147
|
rescue => e
|
|
149
148
|
@config.logger&.error("[PatientHttp::Sidekiq] Garbage collection failed: #{e.class} - #{e.message}")
|
|
150
|
-
raise if
|
|
149
|
+
raise if PatientHttp.testing?
|
|
151
150
|
end
|
|
152
151
|
end
|
|
153
152
|
end
|
data/lib/patient_http/sidekiq.rb
CHANGED
|
@@ -87,6 +87,7 @@ module PatientHttp
|
|
|
87
87
|
@after_error_callbacks = []
|
|
88
88
|
@external_storage = nil
|
|
89
89
|
@request_handler = nil
|
|
90
|
+
@lifecycle_mutex = Mutex.new
|
|
90
91
|
|
|
91
92
|
class << self
|
|
92
93
|
attr_writer :configuration
|
|
@@ -98,6 +99,9 @@ module PatientHttp
|
|
|
98
99
|
configuration = Configuration.new
|
|
99
100
|
yield(configuration) if block_given?
|
|
100
101
|
@configuration = configuration
|
|
102
|
+
@external_storage = nil
|
|
103
|
+
register_handler
|
|
104
|
+
configuration
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
# Ensure configuration is initialized
|
|
@@ -110,6 +114,7 @@ module PatientHttp
|
|
|
110
114
|
# @return [Configuration]
|
|
111
115
|
def reset_configuration!
|
|
112
116
|
@configuration = nil
|
|
117
|
+
@external_storage = nil
|
|
113
118
|
configuration
|
|
114
119
|
end
|
|
115
120
|
|
|
@@ -231,19 +236,11 @@ module PatientHttp
|
|
|
231
236
|
request_id
|
|
232
237
|
end
|
|
233
238
|
|
|
234
|
-
#
|
|
239
|
+
# Register Sidekiq as the request handler for processing HTTP requests. This is called
|
|
240
|
+
# automatically when the processor starts or you call PatientHttp::Sidekiq.configure.
|
|
235
241
|
#
|
|
236
242
|
# @return [void]
|
|
237
|
-
def
|
|
238
|
-
return if running?
|
|
239
|
-
|
|
240
|
-
@processor = PatientHttp::Processor.new(configuration)
|
|
241
|
-
@processor.observe(ProcessorObserver.new(@processor))
|
|
242
|
-
configuration.observers.each do |observer|
|
|
243
|
-
@processor.observe(observer)
|
|
244
|
-
end
|
|
245
|
-
@processor.start
|
|
246
|
-
|
|
243
|
+
def register_handler
|
|
247
244
|
@request_handler ||= lambda do |request:, callback:, raise_error_responses:, callback_args:|
|
|
248
245
|
execute(
|
|
249
246
|
request,
|
|
@@ -256,13 +253,33 @@ module PatientHttp
|
|
|
256
253
|
PatientHttp.register_handler(@request_handler)
|
|
257
254
|
end
|
|
258
255
|
|
|
256
|
+
# Start the processor
|
|
257
|
+
#
|
|
258
|
+
# @return [void]
|
|
259
|
+
def start
|
|
260
|
+
@lifecycle_mutex.synchronize do
|
|
261
|
+
return if @processor && !@processor.stopped?
|
|
262
|
+
|
|
263
|
+
@processor = PatientHttp::Processor.new(configuration)
|
|
264
|
+
@processor.observe(ProcessorObserver.new(@processor))
|
|
265
|
+
configuration.observers.each do |observer|
|
|
266
|
+
@processor.observe(observer)
|
|
267
|
+
end
|
|
268
|
+
@processor.start
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
register_handler
|
|
272
|
+
end
|
|
273
|
+
|
|
259
274
|
# Signal the processor to drain (stop accepting new requests)
|
|
260
275
|
#
|
|
261
276
|
# @return [void]
|
|
262
277
|
def quiet
|
|
263
|
-
|
|
278
|
+
@lifecycle_mutex.synchronize do
|
|
279
|
+
return unless running?
|
|
264
280
|
|
|
265
|
-
|
|
281
|
+
@processor.drain
|
|
282
|
+
end
|
|
266
283
|
end
|
|
267
284
|
|
|
268
285
|
# Stop the processor gracefully
|
|
@@ -274,10 +291,12 @@ module PatientHttp
|
|
|
274
291
|
PatientHttp.unregister_handler(@request_handler)
|
|
275
292
|
end
|
|
276
293
|
|
|
277
|
-
|
|
294
|
+
@lifecycle_mutex.synchronize do
|
|
295
|
+
return unless @processor
|
|
278
296
|
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
@processor.stop(timeout: timeout)
|
|
298
|
+
@processor = nil
|
|
299
|
+
end
|
|
281
300
|
end
|
|
282
301
|
|
|
283
302
|
# Reset all state (useful for testing)
|
|
@@ -285,8 +304,10 @@ module PatientHttp
|
|
|
285
304
|
# @return [void]
|
|
286
305
|
# @api private
|
|
287
306
|
def reset!
|
|
288
|
-
@
|
|
289
|
-
|
|
307
|
+
@lifecycle_mutex.synchronize do
|
|
308
|
+
@processor&.stop(timeout: 0)
|
|
309
|
+
@processor = nil
|
|
310
|
+
end
|
|
290
311
|
@configuration = nil
|
|
291
312
|
@external_storage = nil
|
|
292
313
|
@after_completion_callbacks = []
|