patient_http-sidekiq 1.2.0 → 1.4.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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +31 -10
- data/CHANGELOG.md +32 -0
- data/README.md +126 -2
- data/VERSION +1 -1
- data/lib/patient_http/sidekiq/configuration.rb +258 -1
- data/lib/patient_http/sidekiq/direct_task_handler.rb +46 -0
- data/lib/patient_http/sidekiq/processor_observer.rb +162 -21
- data/lib/patient_http/sidekiq/redis_pool.rb +88 -0
- data/lib/patient_http/sidekiq/request_executor.rb +40 -10
- data/lib/patient_http/sidekiq/request_worker.rb +5 -2
- data/lib/patient_http/sidekiq/stats.rb +230 -33
- data/lib/patient_http/sidekiq/task_handler.rb +9 -3
- data/lib/patient_http/sidekiq/task_monitor.rb +410 -121
- data/lib/patient_http/sidekiq/task_monitor_thread.rb +41 -7
- data/lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css +29 -71
- data/lib/patient_http/sidekiq/web_ui/locales/ar.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/cs.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/da.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/de.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/el.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/en.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/es.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/fa.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/fr.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/gd.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/he.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/hi.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/it.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ja.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ko.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/lt.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/nb.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/nl.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pl.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/pt.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ru.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/sv.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ta.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/tr.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/uk.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/ur.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/vi.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml +10 -5
- data/lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb +99 -44
- data/lib/patient_http/sidekiq/web_ui.rb +53 -1
- data/lib/patient_http/sidekiq.rb +303 -31
- data/patient_http-sidekiq.gemspec +1 -1
- metadata +6 -4
data/lib/patient_http/sidekiq.rb
CHANGED
|
@@ -77,7 +77,9 @@ module PatientHttp
|
|
|
77
77
|
autoload :CallbackWorker, File.join(__dir__, "sidekiq/callback_worker")
|
|
78
78
|
autoload :Configuration, File.join(__dir__, "sidekiq/configuration")
|
|
79
79
|
autoload :Context, File.join(__dir__, "sidekiq/context")
|
|
80
|
+
autoload :DirectTaskHandler, File.join(__dir__, "sidekiq/direct_task_handler")
|
|
80
81
|
autoload :ProcessorObserver, File.join(__dir__, "sidekiq/processor_observer")
|
|
82
|
+
autoload :RedisPool, File.join(__dir__, "sidekiq/redis_pool")
|
|
81
83
|
autoload :RequestExecutor, File.join(__dir__, "sidekiq/request_executor")
|
|
82
84
|
autoload :RequestWorker, File.join(__dir__, "sidekiq/request_worker")
|
|
83
85
|
autoload :LifecycleHooks, File.join(__dir__, "sidekiq/lifecycle_hooks")
|
|
@@ -87,13 +89,17 @@ module PatientHttp
|
|
|
87
89
|
autoload :TaskMonitorThread, File.join(__dir__, "sidekiq/task_monitor_thread")
|
|
88
90
|
autoload :WebUI, File.join(__dir__, "sidekiq/web_ui")
|
|
89
91
|
|
|
90
|
-
@
|
|
92
|
+
@processors = {}
|
|
91
93
|
@configuration = nil
|
|
92
94
|
@after_completion_callbacks = []
|
|
93
95
|
@after_error_callbacks = []
|
|
94
96
|
@external_storage = nil
|
|
95
97
|
@request_handler = nil
|
|
96
98
|
@lifecycle_mutex = Mutex.new
|
|
99
|
+
@redis_pool = nil
|
|
100
|
+
@stats = nil
|
|
101
|
+
@task_monitor = nil
|
|
102
|
+
@monitor_thread = nil
|
|
97
103
|
|
|
98
104
|
class << self
|
|
99
105
|
attr_writer :configuration
|
|
@@ -110,6 +116,9 @@ module PatientHttp
|
|
|
110
116
|
yield(configuration) if block_given?
|
|
111
117
|
@configuration = configuration
|
|
112
118
|
@external_storage = nil
|
|
119
|
+
# Rebuild the stats aggregator from the new configuration unless a
|
|
120
|
+
# running processor already owns it.
|
|
121
|
+
@stats = nil unless running?
|
|
113
122
|
register_handler
|
|
114
123
|
PatientHttp.default_configuration = configuration
|
|
115
124
|
configuration
|
|
@@ -160,33 +169,33 @@ module PatientHttp
|
|
|
160
169
|
end
|
|
161
170
|
end
|
|
162
171
|
|
|
163
|
-
# Check if
|
|
172
|
+
# Check if any processor is running.
|
|
164
173
|
#
|
|
165
174
|
# @return [Boolean]
|
|
166
175
|
def running?
|
|
167
|
-
|
|
176
|
+
@processors.values.any?(&:running?)
|
|
168
177
|
end
|
|
169
178
|
|
|
170
|
-
# Check if
|
|
179
|
+
# Check if any processor is draining (not accepting new requests
|
|
171
180
|
# but still processing in-flight ones).
|
|
172
181
|
#
|
|
173
182
|
# @return [Boolean]
|
|
174
183
|
def draining?
|
|
175
|
-
|
|
184
|
+
@processors.values.any?(&:draining?)
|
|
176
185
|
end
|
|
177
186
|
|
|
178
|
-
# Check if
|
|
187
|
+
# Check if any processor is in the process of stopping.
|
|
179
188
|
#
|
|
180
189
|
# @return [Boolean]
|
|
181
190
|
def stopping?
|
|
182
|
-
|
|
191
|
+
@processors.values.any?(&:stopping?)
|
|
183
192
|
end
|
|
184
193
|
|
|
185
|
-
# Check if
|
|
194
|
+
# Check if all processors are stopped or none have been started.
|
|
186
195
|
#
|
|
187
196
|
# @return [Boolean]
|
|
188
197
|
def stopped?
|
|
189
|
-
@
|
|
198
|
+
@processors.values.all?(&:stopped?)
|
|
190
199
|
end
|
|
191
200
|
|
|
192
201
|
# Get an ExternalStorage instance for storing and fetching payloads.
|
|
@@ -220,7 +229,9 @@ module PatientHttp
|
|
|
220
229
|
# Nested calls merge options with the innermost values taking precedence.
|
|
221
230
|
# If the options include a queue, the callback job for the request is
|
|
222
231
|
# enqueued on that queue as well. Options only apply to requests enqueued
|
|
223
|
-
# in the same fiber as the block.
|
|
232
|
+
# in the same fiber as the block. Requests made in the block always go
|
|
233
|
+
# through the Sidekiq queue, even when direct execution is enabled, so
|
|
234
|
+
# that Sidekiq applies the options. This method has no effect
|
|
224
235
|
# when jobs run inline with Sidekiq::Testing.inline!.
|
|
225
236
|
#
|
|
226
237
|
# @param options [Hash] Sidekiq job options (symbol or string keys)
|
|
@@ -253,14 +264,18 @@ module PatientHttp
|
|
|
253
264
|
# +error.callback_args+ using symbol or string keys.
|
|
254
265
|
# @param raise_error_responses [Boolean] If true, treats non-2xx responses as errors
|
|
255
266
|
# and calls +on_error+ instead of +on_complete+. Defaults to false.
|
|
267
|
+
# @param processor [Symbol, String, nil] Name of the processor profile that should
|
|
268
|
+
# execute the request. Defaults to the request's own processor name, a
|
|
269
|
+
# "processor" value from with_sidekiq_options, or :default.
|
|
256
270
|
# @return [String] the request ID
|
|
257
|
-
def execute(request, callback:, callback_args: nil, raise_error_responses: false)
|
|
271
|
+
def execute(request, callback:, callback_args: nil, raise_error_responses: false, processor: nil)
|
|
258
272
|
PatientHttp::CallbackValidator.validate!(callback)
|
|
259
273
|
callback_name = callback.is_a?(Class) ? callback.name : callback.to_s
|
|
260
274
|
callback_args = PatientHttp::CallbackValidator.validate_callback_args(callback_args)
|
|
261
275
|
request_id = SecureRandom.uuid
|
|
262
276
|
|
|
263
|
-
|
|
277
|
+
request_json = request.as_json
|
|
278
|
+
encrypted = encrypt(request_json)
|
|
264
279
|
|
|
265
280
|
data = if external_storage.enabled?
|
|
266
281
|
external_storage.store(encrypted, max_size: configuration.payload_store_threshold)
|
|
@@ -269,12 +284,28 @@ module PatientHttp
|
|
|
269
284
|
end
|
|
270
285
|
|
|
271
286
|
options = current_sidekiq_options
|
|
287
|
+
processor_name = resolve_processor_name(processor, request, options)
|
|
272
288
|
if options&.any?
|
|
289
|
+
options = options.except("processor")
|
|
273
290
|
queue = options["queue"]
|
|
274
291
|
options = options.merge("patient_http_callback_queue" => queue.to_s) if queue
|
|
275
|
-
|
|
292
|
+
end
|
|
293
|
+
args = [data, callback_name, raise_error_responses, callback_args, request_id, processor_name]
|
|
294
|
+
|
|
295
|
+
if direct_execution?(options)
|
|
296
|
+
execute_on_local_processor(
|
|
297
|
+
request_json,
|
|
298
|
+
args,
|
|
299
|
+
callback_name: callback_name,
|
|
300
|
+
raise_error_responses: raise_error_responses,
|
|
301
|
+
callback_args: callback_args,
|
|
302
|
+
request_id: request_id,
|
|
303
|
+
processor_name: processor_name
|
|
304
|
+
)
|
|
305
|
+
elsif options&.any?
|
|
306
|
+
RequestWorker.set(options).perform_async(*args)
|
|
276
307
|
else
|
|
277
|
-
RequestWorker.perform_async(
|
|
308
|
+
RequestWorker.perform_async(*args)
|
|
278
309
|
end
|
|
279
310
|
|
|
280
311
|
request_id
|
|
@@ -297,36 +328,62 @@ module PatientHttp
|
|
|
297
328
|
PatientHttp.register_handler(@request_handler)
|
|
298
329
|
end
|
|
299
330
|
|
|
300
|
-
# Start
|
|
331
|
+
# Start a processor for each configured processor profile, along with the
|
|
332
|
+
# shared crash-recovery monitor and stats.
|
|
301
333
|
#
|
|
302
334
|
# @return [void]
|
|
303
335
|
def start
|
|
304
336
|
@lifecycle_mutex.synchronize do
|
|
305
|
-
return if @
|
|
337
|
+
return if @processors.any? && !@processors.values.all?(&:stopped?)
|
|
338
|
+
|
|
339
|
+
warn_about_blocking_redis_driver
|
|
306
340
|
|
|
307
|
-
@
|
|
308
|
-
@
|
|
309
|
-
|
|
310
|
-
|
|
341
|
+
@redis_pool ||= RedisPool.new(configuration)
|
|
342
|
+
@stats ||= Stats.new(configuration)
|
|
343
|
+
@task_monitor ||= TaskMonitor.new(
|
|
344
|
+
configuration,
|
|
345
|
+
processors: -> { processor_capacity_snapshot }
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@processors = {}
|
|
349
|
+
configuration.processor_profiles.each_key do |name|
|
|
350
|
+
processor = PatientHttp::Processor.new(configuration.processor_config(name), name: name)
|
|
351
|
+
processor.observe(ProcessorObserver.new(processor, stats: @stats, task_monitor: @task_monitor))
|
|
352
|
+
configuration.observers.each do |observer|
|
|
353
|
+
processor.observe(observer)
|
|
354
|
+
end
|
|
355
|
+
@processors[name] = processor
|
|
311
356
|
end
|
|
312
|
-
@
|
|
357
|
+
@processors.each_value(&:start)
|
|
358
|
+
|
|
359
|
+
# A restart after the processors stopped on their own (e.g. a reactor
|
|
360
|
+
# error) leaves the previous monitor thread running; stop it before
|
|
361
|
+
# replacing the reference so only one thread ever heartbeats.
|
|
362
|
+
@monitor_thread&.stop
|
|
363
|
+
@monitor_thread = TaskMonitorThread.new(
|
|
364
|
+
configuration,
|
|
365
|
+
@task_monitor,
|
|
366
|
+
-> { @processors.values.flat_map(&:tracked_request_ids) },
|
|
367
|
+
stats: @stats
|
|
368
|
+
)
|
|
369
|
+
@monitor_thread.start
|
|
313
370
|
end
|
|
314
371
|
|
|
315
372
|
register_handler
|
|
316
373
|
end
|
|
317
374
|
|
|
318
|
-
# Signal
|
|
375
|
+
# Signal all processors to drain (stop accepting new requests)
|
|
319
376
|
#
|
|
320
377
|
# @return [void]
|
|
321
378
|
def quiet
|
|
322
379
|
@lifecycle_mutex.synchronize do
|
|
323
380
|
return unless running?
|
|
324
381
|
|
|
325
|
-
@
|
|
382
|
+
@processors.each_value(&:drain)
|
|
326
383
|
end
|
|
327
384
|
end
|
|
328
385
|
|
|
329
|
-
# Stop
|
|
386
|
+
# Stop all processors gracefully
|
|
330
387
|
#
|
|
331
388
|
# @param timeout [Float, nil] maximum time to wait for in-flight requests to complete
|
|
332
389
|
# @return [void]
|
|
@@ -336,10 +393,12 @@ module PatientHttp
|
|
|
336
393
|
end
|
|
337
394
|
|
|
338
395
|
@lifecycle_mutex.synchronize do
|
|
339
|
-
|
|
396
|
+
# Shared services can outlive the processors when a start failed part
|
|
397
|
+
# way through, so tear them down whenever any of them exist.
|
|
398
|
+
return if @processors.empty? && @redis_pool.nil? && @task_monitor.nil? && @monitor_thread.nil?
|
|
340
399
|
|
|
341
|
-
|
|
342
|
-
|
|
400
|
+
stop_processors(timeout)
|
|
401
|
+
shutdown_shared_services
|
|
343
402
|
end
|
|
344
403
|
end
|
|
345
404
|
|
|
@@ -349,8 +408,8 @@ module PatientHttp
|
|
|
349
408
|
# @api private
|
|
350
409
|
def reset!
|
|
351
410
|
@lifecycle_mutex.synchronize do
|
|
352
|
-
|
|
353
|
-
|
|
411
|
+
stop_processors(0)
|
|
412
|
+
shutdown_shared_services
|
|
354
413
|
end
|
|
355
414
|
@configuration = nil
|
|
356
415
|
@external_storage = nil
|
|
@@ -381,20 +440,233 @@ module PatientHttp
|
|
|
381
440
|
end
|
|
382
441
|
end
|
|
383
442
|
|
|
384
|
-
# Returns
|
|
443
|
+
# Returns a processor instance by name (internal accessor).
|
|
385
444
|
#
|
|
445
|
+
# @param name [Symbol, String] the processor name
|
|
386
446
|
# @return [PatientHttp::Processor, nil]
|
|
387
447
|
# @api private
|
|
388
|
-
|
|
448
|
+
def processor(name = :default)
|
|
449
|
+
@processors[name.to_sym]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Set the default processor (internal, for testing).
|
|
453
|
+
#
|
|
454
|
+
# @param value [PatientHttp::Processor, nil]
|
|
455
|
+
# @api private
|
|
456
|
+
def processor=(value)
|
|
457
|
+
if value.nil?
|
|
458
|
+
@processors.delete(:default)
|
|
459
|
+
else
|
|
460
|
+
@processors[:default] = value
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# The gem's dedicated Redis pool, or nil when no processor has started
|
|
465
|
+
# in this process (e.g. web or client processes).
|
|
466
|
+
#
|
|
467
|
+
# @return [RedisPool, nil]
|
|
468
|
+
# @api private
|
|
469
|
+
attr_reader :redis_pool
|
|
470
|
+
|
|
471
|
+
# The shared stats aggregator for this process. Available before start so
|
|
472
|
+
# rejection stats can be recorded from any path.
|
|
473
|
+
#
|
|
474
|
+
# @return [Stats]
|
|
475
|
+
# @api private
|
|
476
|
+
def stats
|
|
477
|
+
@stats ||= Stats.new(configuration)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Yield a Redis connection: the gem's dedicated pool when it exists,
|
|
481
|
+
# falling back to Sidekiq's pool selection otherwise.
|
|
482
|
+
#
|
|
483
|
+
# @param retry_on_connection_error [Boolean] whether a connection-level
|
|
484
|
+
# failure may replay the block. Pass false when the block is not
|
|
485
|
+
# idempotent, such as a batch of counter increments the server may
|
|
486
|
+
# already have applied.
|
|
487
|
+
# @yield [conn] the Redis connection
|
|
488
|
+
# @return [Object] the block's return value
|
|
489
|
+
# @api private
|
|
490
|
+
def redis(retry_on_connection_error: true, &block)
|
|
491
|
+
pool = @redis_pool
|
|
492
|
+
if pool
|
|
493
|
+
pool.with(retry_on_connection_error: retry_on_connection_error, &block)
|
|
494
|
+
else
|
|
495
|
+
::Sidekiq.redis(&block)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Run a block with Sidekiq client pushes routed through the gem's
|
|
500
|
+
# dedicated Redis pool. Used for pushes made from gem-owned threads
|
|
501
|
+
# (completion workers, the monitor thread) so they do not contend with
|
|
502
|
+
# Sidekiq's internal pool.
|
|
503
|
+
#
|
|
504
|
+
# @return [Object] the block's return value
|
|
505
|
+
# @api private
|
|
506
|
+
def with_redis_pool(&block)
|
|
507
|
+
pool = @redis_pool&.pool
|
|
508
|
+
if pool
|
|
509
|
+
::Sidekiq::Client.via(pool, &block)
|
|
510
|
+
else
|
|
511
|
+
yield
|
|
512
|
+
end
|
|
513
|
+
end
|
|
389
514
|
|
|
390
515
|
private
|
|
391
516
|
|
|
517
|
+
# Snapshot of each processor's capacity in this process, published with
|
|
518
|
+
# the heartbeat so the Web UI can report capacity per processor. The
|
|
519
|
+
# inflight count is the number of requests counted against the
|
|
520
|
+
# processor's capacity: queued, pending, and in flight.
|
|
521
|
+
#
|
|
522
|
+
# @return [Hash] processor name => { inflight:, max_capacity: }
|
|
523
|
+
def processor_capacity_snapshot
|
|
524
|
+
@processors.each_with_object({}) do |(name, processor), snapshot|
|
|
525
|
+
snapshot[name] = {
|
|
526
|
+
inflight: processor.total_count,
|
|
527
|
+
max_capacity: processor.config.max_connections
|
|
528
|
+
}
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Stop every processor and clear the registry. Each processor waits up to
|
|
533
|
+
# the full timeout for its in-flight requests, so they are stopped in
|
|
534
|
+
# parallel; stopping them in sequence would multiply the shutdown
|
|
535
|
+
# deadline by the number of processor profiles and overrun the time
|
|
536
|
+
# Sidekiq allows before it kills the process.
|
|
537
|
+
#
|
|
538
|
+
# @param timeout [Float, nil] maximum time to wait for in-flight requests
|
|
539
|
+
# @return [void]
|
|
540
|
+
def stop_processors(timeout)
|
|
541
|
+
processors = @processors.values
|
|
542
|
+
@processors = {}
|
|
543
|
+
return if processors.empty?
|
|
544
|
+
|
|
545
|
+
if processors.size == 1
|
|
546
|
+
processors.first.stop(timeout: timeout)
|
|
547
|
+
return
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
processors.map { |processor|
|
|
551
|
+
Thread.new do
|
|
552
|
+
processor.stop(timeout: timeout)
|
|
553
|
+
rescue => e
|
|
554
|
+
configuration.logger&.error(
|
|
555
|
+
"[PatientHttp::Sidekiq] Failed to stop processor #{processor.name}: #{e.inspect}"
|
|
556
|
+
)
|
|
557
|
+
end
|
|
558
|
+
}.each(&:join)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Stop the shared monitor thread, flush pending stats, remove this
|
|
562
|
+
# process from the registry, and shut down the Redis pool. Called with
|
|
563
|
+
# the lifecycle mutex held after all processors have stopped.
|
|
564
|
+
def shutdown_shared_services
|
|
565
|
+
@monitor_thread&.stop
|
|
566
|
+
@monitor_thread = nil
|
|
567
|
+
begin
|
|
568
|
+
@stats&.flush
|
|
569
|
+
rescue => e
|
|
570
|
+
configuration.logger&.error("[PatientHttp::Sidekiq] Failed to flush stats during shutdown: #{e.inspect}")
|
|
571
|
+
end
|
|
572
|
+
begin
|
|
573
|
+
@task_monitor&.remove_process
|
|
574
|
+
rescue => e
|
|
575
|
+
configuration.logger&.error("[PatientHttp::Sidekiq] Failed to remove process registration: #{e.inspect}")
|
|
576
|
+
end
|
|
577
|
+
@task_monitor = nil
|
|
578
|
+
@stats = nil
|
|
579
|
+
@redis_pool&.shutdown
|
|
580
|
+
@redis_pool = nil
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Log a warning when a blocking C-level Redis driver is installed.
|
|
584
|
+
# Blocking Redis I/O on the reactor thread stalls every in-flight HTTP
|
|
585
|
+
# request; the gem's own calls run on worker threads, but application
|
|
586
|
+
# observers may still call Redis from processor callbacks.
|
|
587
|
+
def warn_about_blocking_redis_driver
|
|
588
|
+
return unless defined?(RedisClient) && RedisClient.default_driver.name.to_s.include?("Hiredis")
|
|
589
|
+
|
|
590
|
+
configuration.logger&.warn(
|
|
591
|
+
"[PatientHttp::Sidekiq] hiredis-client detected. The hiredis driver performs blocking I/O " \
|
|
592
|
+
"that does not yield to the fiber scheduler. Avoid Redis calls from processor observers " \
|
|
593
|
+
"or callbacks that run on the reactor thread."
|
|
594
|
+
)
|
|
595
|
+
rescue => e
|
|
596
|
+
configuration.logger&.debug("[PatientHttp::Sidekiq] Redis driver check failed: #{e.inspect}")
|
|
597
|
+
end
|
|
598
|
+
|
|
392
599
|
# Current scoped Sidekiq options, if any.
|
|
393
600
|
#
|
|
394
601
|
# @return [Hash, nil]
|
|
395
602
|
def current_sidekiq_options
|
|
396
603
|
Thread.current[:patient_http_sidekiq_options]
|
|
397
604
|
end
|
|
605
|
+
|
|
606
|
+
# Resolve the processor profile name for a request. Precedence: the
|
|
607
|
+
# explicit +processor:+ argument, the request's own processor name, a
|
|
608
|
+
# "processor" value from with_sidekiq_options, then :default.
|
|
609
|
+
#
|
|
610
|
+
# @param explicit [Symbol, String, nil] the processor: argument
|
|
611
|
+
# @param request [PatientHttp::Request] the request
|
|
612
|
+
# @param options [Hash, nil] scoped Sidekiq options
|
|
613
|
+
# @return [String] the processor name
|
|
614
|
+
def resolve_processor_name(explicit, request, options)
|
|
615
|
+
name = explicit || request.processor || options&.[]("processor") || :default
|
|
616
|
+
name.to_s
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Check if the request can go directly to a processor running in the current
|
|
620
|
+
# process. Requests made in a with_sidekiq_options block always go through
|
|
621
|
+
# the queue so that Sidekiq applies the options (queue routing, scheduling,
|
|
622
|
+
# retry), and Sidekiq testing modes must keep their normal enqueue semantics.
|
|
623
|
+
#
|
|
624
|
+
# @param options [Hash, nil] scoped Sidekiq options for the request
|
|
625
|
+
# @return [Boolean]
|
|
626
|
+
def direct_execution?(options)
|
|
627
|
+
return false unless options.nil?
|
|
628
|
+
return false unless configuration.direct_execution?
|
|
629
|
+
return false unless running?
|
|
630
|
+
return false if defined?(::Sidekiq::Testing) && ::Sidekiq::Testing.enabled?
|
|
631
|
+
|
|
632
|
+
true
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Hand the request to the processor running in the current process. If the
|
|
636
|
+
# processor cannot accept the request (at capacity or shutting down), the
|
|
637
|
+
# request is enqueued as a normal RequestWorker job instead through the
|
|
638
|
+
# same retry path the processor uses when it drains.
|
|
639
|
+
#
|
|
640
|
+
# @param request_json [Hash] the serialized request
|
|
641
|
+
# @param args [Array] the RequestWorker job arguments
|
|
642
|
+
# @param callback_name [String] the callback service class name
|
|
643
|
+
# @param raise_error_responses [Boolean] whether non-2xx responses are errors
|
|
644
|
+
# @param callback_args [Hash, nil] arguments to pass to the callback
|
|
645
|
+
# @param request_id [String] unique request ID
|
|
646
|
+
# @param processor_name [String] name of the processor profile to run on
|
|
647
|
+
# @return [void]
|
|
648
|
+
def execute_on_local_processor(request_json, args, callback_name:, raise_error_responses:, callback_args:, request_id:, processor_name: "default")
|
|
649
|
+
task_handler = DirectTaskHandler.new(args)
|
|
650
|
+
|
|
651
|
+
begin
|
|
652
|
+
# Reload the request from its serialized form so the direct path
|
|
653
|
+
# processes the same reconstructed request a RequestWorker job would.
|
|
654
|
+
RequestExecutor.execute(
|
|
655
|
+
PatientHttp::Request.load(request_json),
|
|
656
|
+
callback: callback_name,
|
|
657
|
+
raise_error_responses: raise_error_responses,
|
|
658
|
+
callback_args: callback_args,
|
|
659
|
+
task_handler: task_handler,
|
|
660
|
+
request_id: request_id,
|
|
661
|
+
processor_name: processor_name
|
|
662
|
+
)
|
|
663
|
+
rescue PatientHttp::NotRunningError, PatientHttp::MaxCapacityError => e
|
|
664
|
+
configuration.logger&.info(
|
|
665
|
+
"[PatientHttp::Sidekiq] Falling back to enqueuing request: #{e.message}"
|
|
666
|
+
)
|
|
667
|
+
task_handler.retry
|
|
668
|
+
end
|
|
669
|
+
end
|
|
398
670
|
end
|
|
399
671
|
end
|
|
400
672
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: patient_http-sidekiq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.
|
|
32
|
+
version: 1.5.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.
|
|
39
|
+
version: 1.5.0
|
|
40
40
|
description: This gem provides a mechanism to offload long-running HTTP requests from
|
|
41
41
|
Sidekiq workers to a dedicated async I/O processor running in the same process,
|
|
42
42
|
freeing the worker thread immediately while the HTTP request is in flight.
|
|
@@ -56,8 +56,10 @@ files:
|
|
|
56
56
|
- lib/patient_http/sidekiq/callback_worker.rb
|
|
57
57
|
- lib/patient_http/sidekiq/configuration.rb
|
|
58
58
|
- lib/patient_http/sidekiq/context.rb
|
|
59
|
+
- lib/patient_http/sidekiq/direct_task_handler.rb
|
|
59
60
|
- lib/patient_http/sidekiq/lifecycle_hooks.rb
|
|
60
61
|
- lib/patient_http/sidekiq/processor_observer.rb
|
|
62
|
+
- lib/patient_http/sidekiq/redis_pool.rb
|
|
61
63
|
- lib/patient_http/sidekiq/request_executor.rb
|
|
62
64
|
- lib/patient_http/sidekiq/request_worker.rb
|
|
63
65
|
- lib/patient_http/sidekiq/stats.rb
|
|
@@ -120,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
122
|
- !ruby/object:Gem::Version
|
|
121
123
|
version: '0'
|
|
122
124
|
requirements: []
|
|
123
|
-
rubygems_version:
|
|
125
|
+
rubygems_version: 4.0.3
|
|
124
126
|
specification_version: 4
|
|
125
127
|
summary: Offload long-running HTTP requests from Sidekiq workers to a dedicated async
|
|
126
128
|
I/O processor
|