shoryuken 6.2.1 → 7.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.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +36 -0
  3. data/.github/workflows/specs.yml +49 -44
  4. data/.github/workflows/verify-action-pins.yml +16 -0
  5. data/.gitignore +4 -1
  6. data/.rspec +3 -1
  7. data/.rubocop.yml +6 -1
  8. data/.ruby-version +1 -0
  9. data/.yard-lint.yml +279 -0
  10. data/CHANGELOG.md +308 -139
  11. data/Gemfile +1 -8
  12. data/Gemfile.lint +9 -0
  13. data/Gemfile.lint.lock +69 -0
  14. data/README.md +16 -33
  15. data/Rakefile +6 -10
  16. data/bin/clean_sqs +52 -0
  17. data/bin/cli/base.rb +22 -2
  18. data/bin/cli/sqs.rb +74 -7
  19. data/bin/integrations +275 -0
  20. data/bin/scenario +154 -0
  21. data/bin/shoryuken +3 -2
  22. data/docker-compose.yml +6 -0
  23. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +20 -6
  24. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  25. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  26. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  27. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  28. data/lib/shoryuken/body_parser.rb +11 -1
  29. data/lib/shoryuken/client.rb +16 -0
  30. data/lib/shoryuken/default_exception_handler.rb +11 -0
  31. data/lib/shoryuken/default_worker_registry.rb +39 -11
  32. data/lib/shoryuken/environment_loader.rb +85 -15
  33. data/lib/shoryuken/errors.rb +36 -0
  34. data/lib/shoryuken/fetcher.rb +41 -3
  35. data/lib/shoryuken/helpers/atomic_boolean.rb +58 -0
  36. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  37. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  38. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  39. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  40. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  41. data/lib/shoryuken/inline_message.rb +22 -0
  42. data/lib/shoryuken/launcher.rb +55 -0
  43. data/lib/shoryuken/logging/base.rb +26 -0
  44. data/lib/shoryuken/logging/pretty.rb +25 -0
  45. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  46. data/lib/shoryuken/logging.rb +43 -15
  47. data/lib/shoryuken/manager.rb +84 -5
  48. data/lib/shoryuken/message.rb +116 -1
  49. data/lib/shoryuken/middleware/chain.rb +141 -43
  50. data/lib/shoryuken/middleware/entry.rb +30 -0
  51. data/lib/shoryuken/middleware/server/active_record.rb +10 -0
  52. data/lib/shoryuken/middleware/server/auto_delete.rb +12 -0
  53. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +37 -11
  54. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +34 -3
  55. data/lib/shoryuken/middleware/server/non_retryable_exception.rb +95 -0
  56. data/lib/shoryuken/middleware/server/timing.rb +13 -0
  57. data/lib/shoryuken/options.rb +154 -13
  58. data/lib/shoryuken/polling/base_strategy.rb +127 -0
  59. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  60. data/lib/shoryuken/polling/strict_priority.rb +41 -0
  61. data/lib/shoryuken/polling/weighted_round_robin.rb +44 -0
  62. data/lib/shoryuken/processor.rb +37 -3
  63. data/lib/shoryuken/queue.rb +99 -8
  64. data/lib/shoryuken/runner.rb +54 -16
  65. data/lib/shoryuken/util.rb +32 -7
  66. data/lib/shoryuken/version.rb +4 -1
  67. data/lib/shoryuken/worker/default_executor.rb +23 -1
  68. data/lib/shoryuken/worker/inline_executor.rb +33 -2
  69. data/lib/shoryuken/worker.rb +224 -0
  70. data/lib/shoryuken/worker_registry.rb +35 -0
  71. data/lib/shoryuken.rb +27 -38
  72. data/renovate.json +62 -0
  73. data/shoryuken.gemspec +8 -4
  74. data/spec/integration/.rspec +1 -0
  75. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  76. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  77. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  78. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  79. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  80. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  81. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  82. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  83. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  84. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  85. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  86. data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +63 -0
  87. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  88. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  89. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  90. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  91. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  92. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  93. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  94. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  95. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  96. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  97. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  98. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  99. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  100. data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
  101. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  102. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  103. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  104. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  105. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  106. data/spec/integration/launcher/launcher_spec.rb +40 -0
  107. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  108. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  109. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  110. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  111. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  112. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  113. data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +149 -0
  114. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  115. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  116. data/spec/integration/rails/rails_72/Gemfile +6 -0
  117. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  118. data/spec/integration/rails/rails_80/Gemfile +6 -0
  119. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  120. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  121. data/spec/integration/rails/rails_81/Gemfile +6 -0
  122. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  123. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  124. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  125. data/spec/integration/spec_helper.rb +7 -0
  126. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  127. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  128. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  129. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  130. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  131. data/spec/integrations_helper.rb +243 -0
  132. data/spec/lib/active_job/extensions_spec.rb +225 -0
  133. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  134. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +5 -4
  135. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +6 -5
  136. data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +2 -4
  137. data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +1 -1
  138. data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +9 -10
  139. data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +1 -2
  140. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +10 -9
  141. data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +23 -26
  142. data/spec/lib/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  143. data/spec/lib/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  144. data/spec/lib/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  145. data/spec/lib/shoryuken/helpers/hash_utils_spec.rb +145 -0
  146. data/spec/lib/shoryuken/helpers/string_utils_spec.rb +124 -0
  147. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  148. data/spec/lib/shoryuken/helpers_integration_spec.rb +96 -0
  149. data/spec/lib/shoryuken/inline_message_spec.rb +196 -0
  150. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +23 -2
  151. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  152. data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +1 -2
  153. data/spec/lib/shoryuken/message_spec.rb +109 -0
  154. data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +1 -1
  155. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  156. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  157. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +1 -1
  158. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +51 -1
  159. data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  160. data/spec/lib/shoryuken/middleware/server/non_retryable_exception_spec.rb +214 -0
  161. data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +1 -1
  162. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +49 -6
  163. data/spec/lib/shoryuken/polling/base_strategy_spec.rb +280 -0
  164. data/spec/lib/shoryuken/polling/queue_configuration_spec.rb +195 -0
  165. data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +1 -1
  166. data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +1 -1
  167. data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +1 -1
  168. data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +2 -3
  169. data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +1 -3
  170. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +2 -2
  171. data/spec/lib/shoryuken/version_spec.rb +17 -0
  172. data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +1 -1
  173. data/spec/lib/shoryuken/worker/inline_executor_spec.rb +105 -0
  174. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  175. data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +15 -11
  176. data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +1 -1
  177. data/spec/shared_examples_for_active_job.rb +40 -15
  178. data/spec/spec_helper.rb +48 -2
  179. metadata +295 -101
  180. data/.codeclimate.yml +0 -20
  181. data/.devcontainer/Dockerfile +0 -17
  182. data/.devcontainer/base.Dockerfile +0 -43
  183. data/.devcontainer/devcontainer.json +0 -35
  184. data/.github/FUNDING.yml +0 -12
  185. data/.github/dependabot.yml +0 -6
  186. data/.github/workflows/stale.yml +0 -20
  187. data/.reek.yml +0 -5
  188. data/Appraisals +0 -42
  189. data/gemfiles/.gitignore +0 -1
  190. data/gemfiles/aws_sdk_core_2.gemfile +0 -21
  191. data/gemfiles/rails_4_2.gemfile +0 -20
  192. data/gemfiles/rails_5_2.gemfile +0 -21
  193. data/gemfiles/rails_6_0.gemfile +0 -21
  194. data/gemfiles/rails_6_1.gemfile +0 -21
  195. data/gemfiles/rails_7_0.gemfile +0 -22
  196. data/lib/shoryuken/core_ext.rb +0 -69
  197. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -103
  198. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  199. data/lib/shoryuken/polling/base.rb +0 -67
  200. data/shoryuken.jpg +0 -0
  201. data/spec/integration/launcher_spec.rb +0 -128
  202. data/spec/shoryuken/core_ext_spec.rb +0 -40
  203. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -7
  204. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -84
  205. data/spec/shoryuken/worker/inline_executor_spec.rb +0 -49
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # A thread-safe hash implementation using Ruby's Mutex for all operations.
6
+ #
7
+ # This class provides a hash-like interface with thread-safe operations, serving as a
8
+ # drop-in replacement for Concurrent::Hash without requiring external dependencies.
9
+ # The implementation uses a single mutex to protect both read and write operations,
10
+ # ensuring complete thread safety across all Ruby implementations including JRuby.
11
+ #
12
+ # Since hash operations (lookup, assignment) are very fast, the mutex overhead is
13
+ # minimal while providing guaranteed safety and simplicity. This approach avoids
14
+ # the complexity of copy-on-write while maintaining excellent performance for
15
+ # typical usage patterns.
16
+ #
17
+ # @note This implementation uses mutex synchronization for all operations,
18
+ # ensuring complete thread safety with minimal performance impact.
19
+ #
20
+ # @note All operations are atomic and will never see partial effects from
21
+ # concurrent operations.
22
+ #
23
+ # @example Basic hash operations
24
+ # hash = Shoryuken::Helpers::AtomicHash.new
25
+ # hash['key'] = 'value'
26
+ # hash['key'] # => 'value'
27
+ # hash.keys # => ['key']
28
+ # hash.clear
29
+ # hash['key'] # => nil
30
+ #
31
+ # @example Worker registry usage
32
+ # @workers = Shoryuken::Helpers::AtomicHash.new
33
+ #
34
+ # # Registration (infrequent writes)
35
+ # @workers['queue_name'] = WorkerClass
36
+ #
37
+ # # Lookups (frequent reads)
38
+ # worker_class = @workers['queue_name']
39
+ # available_queues = @workers.keys
40
+ # worker_class = @workers.fetch('queue_name', DefaultWorker)
41
+ #
42
+ # @example Thread-safe concurrent access
43
+ # hash = Shoryuken::Helpers::AtomicHash.new
44
+ #
45
+ # # Multiple threads can safely write
46
+ # Thread.new { hash['key1'] = 'value1' }
47
+ # Thread.new { hash['key2'] = 'value2' }
48
+ #
49
+ # # Multiple threads can safely read concurrently
50
+ # Thread.new { puts hash['key1'] }
51
+ # Thread.new { puts hash.keys.size }
52
+ class AtomicHash
53
+ # Creates a new empty atomic hash.
54
+ #
55
+ # The hash starts empty and ready to accept key-value pairs through
56
+ # thread-safe operations.
57
+ #
58
+ # @return [AtomicHash] A new empty atomic hash instance
59
+ #
60
+ # @example Creating an empty hash
61
+ # hash = Shoryuken::Helpers::AtomicHash.new
62
+ # hash.keys # => []
63
+ def initialize
64
+ @mutex = Mutex.new
65
+ @hash = {}
66
+ end
67
+
68
+ # Returns the value associated with the given key.
69
+ #
70
+ # This operation is thread-safe and will return a consistent value
71
+ # even when called concurrently with write operations.
72
+ #
73
+ # @param key [Object] The key to look up
74
+ # @return [Object, nil] The value associated with the key, or nil if not found
75
+ #
76
+ # @example Reading values
77
+ # hash = Shoryuken::Helpers::AtomicHash.new
78
+ # hash['existing'] = 'value'
79
+ # hash['existing'] # => 'value'
80
+ # hash['missing'] # => nil
81
+ #
82
+ # @example Works with any key type
83
+ # hash = Shoryuken::Helpers::AtomicHash.new
84
+ # hash[:symbol] = 'symbol_value'
85
+ # hash[42] = 'number_value'
86
+ # hash[:symbol] # => 'symbol_value'
87
+ # hash[42] # => 'number_value'
88
+ def [](key)
89
+ @mutex.synchronize { @hash[key] }
90
+ end
91
+
92
+ # Sets the value for the given key.
93
+ #
94
+ # This is a thread-safe write operation that ensures data integrity
95
+ # when called concurrently with other read or write operations.
96
+ #
97
+ # @param key [Object] The key to set
98
+ # @param value [Object] The value to associate with the key
99
+ # @return [Object] The assigned value
100
+ #
101
+ # @example Setting values
102
+ # hash = Shoryuken::Helpers::AtomicHash.new
103
+ # hash['queue1'] = 'Worker1'
104
+ # hash['queue2'] = 'Worker2'
105
+ # hash['queue1'] # => 'Worker1'
106
+ #
107
+ # @example Overwriting values
108
+ # hash = Shoryuken::Helpers::AtomicHash.new
109
+ # hash['key'] = 'old_value'
110
+ # hash['key'] = 'new_value'
111
+ # hash['key'] # => 'new_value'
112
+ def []=(key, value)
113
+ @mutex.synchronize { @hash[key] = value }
114
+ end
115
+
116
+ # Removes all key-value pairs from the hash.
117
+ #
118
+ # This is a thread-safe write operation that ensures atomicity
119
+ # when called concurrently with other operations.
120
+ #
121
+ # @return [Hash] An empty hash (for compatibility with standard Hash#clear)
122
+ #
123
+ # @example Clearing all entries
124
+ # hash = Shoryuken::Helpers::AtomicHash.new
125
+ # hash['key1'] = 'value1'
126
+ # hash['key2'] = 'value2'
127
+ # hash.keys.size # => 2
128
+ # hash.clear
129
+ # hash.keys.size # => 0
130
+ # hash['key1'] # => nil
131
+ def clear
132
+ @mutex.synchronize { @hash.clear }
133
+ end
134
+
135
+ # Returns an array of all keys in the hash.
136
+ #
137
+ # This operation is thread-safe and will return a consistent snapshot
138
+ # of keys even when called concurrently with write operations.
139
+ #
140
+ # @return [Array] An array containing all keys in the hash
141
+ #
142
+ # @example Getting all keys
143
+ # hash = Shoryuken::Helpers::AtomicHash.new
144
+ # hash['queue1'] = 'Worker1'
145
+ # hash['queue2'] = 'Worker2'
146
+ # hash.keys # => ['queue1', 'queue2'] (order not guaranteed)
147
+ #
148
+ # @example Empty hash returns empty array
149
+ # hash = Shoryuken::Helpers::AtomicHash.new
150
+ # hash.keys # => []
151
+ def keys
152
+ @mutex.synchronize { @hash.keys }
153
+ end
154
+
155
+ # Returns the value for the given key, or a default value if the key is not found.
156
+ #
157
+ # This operation is thread-safe and will return a consistent value
158
+ # even when called concurrently with write operations.
159
+ #
160
+ # @param key [Object] The key to look up
161
+ # @param default [Object] The value to return if the key is not found
162
+ # @return [Object] The value associated with the key, or the default value
163
+ #
164
+ # @example Fetching with defaults
165
+ # hash = Shoryuken::Helpers::AtomicHash.new
166
+ # hash['existing'] = 'found'
167
+ # hash.fetch('existing', 'default') # => 'found'
168
+ # hash.fetch('missing', 'default') # => 'default'
169
+ #
170
+ # @example Default parameter is optional
171
+ # hash = Shoryuken::Helpers::AtomicHash.new
172
+ # hash.fetch('missing') # => nil
173
+ #
174
+ # @example Useful for providing fallback collections
175
+ # hash = Shoryuken::Helpers::AtomicHash.new
176
+ # workers = hash.fetch('queue_name', []) # => [] if not found
177
+ def fetch(key, default = nil)
178
+ @mutex.synchronize { @hash.fetch(key, default) }
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # Utility methods for hash manipulation.
6
+ #
7
+ # This module provides helper methods for common hash operations that were
8
+ # previously implemented as core class extensions. By using a dedicated
9
+ # helper module, we avoid polluting the global namespace while maintaining
10
+ # the same functionality.
11
+ #
12
+ # @example Basic usage
13
+ # hash = { 'key1' => 'value1', 'key2' => { 'nested' => 'value2' } }
14
+ # symbolized = Shoryuken::Helpers::HashUtils.deep_symbolize_keys(hash)
15
+ # # => { key1: 'value1', key2: { nested: 'value2' } }
16
+ module HashUtils
17
+ class << self
18
+ # Recursively converts hash keys to symbols.
19
+ #
20
+ # This method traverses a hash structure and converts all string keys
21
+ # to symbols, including nested hashes. Non-hash values are left unchanged.
22
+ # This is useful for normalizing configuration data loaded from YAML files.
23
+ #
24
+ # @param hash [Hash, Object] The hash to convert, or any other object
25
+ # @return [Hash, Object] Hash with symbolized keys, or the original object if not a hash
26
+ #
27
+ # @example Converting a simple hash
28
+ # hash = { 'key1' => 'value1', 'key2' => 'value2' }
29
+ # HashUtils.deep_symbolize_keys(hash)
30
+ # # => { key1: 'value1', key2: 'value2' }
31
+ #
32
+ # @example Converting a nested hash
33
+ # hash = { 'config' => { 'timeout' => 30, 'retries' => 3 } }
34
+ # HashUtils.deep_symbolize_keys(hash)
35
+ # # => { config: { timeout: 30, retries: 3 } }
36
+ #
37
+ # @example Handling non-hash input gracefully
38
+ # HashUtils.deep_symbolize_keys('not a hash')
39
+ # # => 'not a hash'
40
+ #
41
+ # @example Mixed value types
42
+ # hash = { 'string' => 'value', 'number' => 42, 'nested' => { 'bool' => true } }
43
+ # HashUtils.deep_symbolize_keys(hash)
44
+ # # => { string: 'value', number: 42, nested: { bool: true } }
45
+ def deep_symbolize_keys(hash)
46
+ return hash unless hash.is_a?(Hash)
47
+
48
+ hash.each_with_object({}) do |(key, value), result|
49
+ symbol_key = key.is_a?(String) ? key.to_sym : key
50
+ result[symbol_key] = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # Utility methods for string manipulation.
6
+ #
7
+ # This module provides helper methods for common string operations that were
8
+ # previously implemented as core class extensions. By using a dedicated
9
+ # helper module, we avoid polluting the global namespace while maintaining
10
+ # the same functionality.
11
+ #
12
+ # @example Basic usage
13
+ # klass = Shoryuken::Helpers::StringUtils.constantize('MyWorker')
14
+ # # => MyWorker
15
+ module StringUtils
16
+ class << self
17
+ # Converts a string to a constant.
18
+ #
19
+ # This method takes a string representation of a constant name and returns
20
+ # the actual constant. It handles nested constants (e.g., 'Foo::Bar') and
21
+ # leading double colons (e.g., '::Object'). This is commonly used for
22
+ # dynamically loading worker classes from configuration.
23
+ #
24
+ # @param string [String] The string to convert to a constant
25
+ # @return [Class, Module] The constant represented by the string
26
+ # @raise [NameError] if the constant is not found or not defined
27
+ #
28
+ # @example Converting a simple class name
29
+ # StringUtils.constantize('String')
30
+ # # => String
31
+ #
32
+ # @example Converting a nested constant
33
+ # StringUtils.constantize('Shoryuken::Worker')
34
+ # # => Shoryuken::Worker
35
+ #
36
+ # @example Handling leading double colon
37
+ # StringUtils.constantize('::Object')
38
+ # # => Object
39
+ #
40
+ # @example Worker class loading
41
+ # worker_class = StringUtils.constantize('MyApp::EmailWorker')
42
+ # worker_instance = worker_class.new
43
+ #
44
+ # @example Error handling
45
+ # begin
46
+ # StringUtils.constantize('NonExistentClass')
47
+ # rescue NameError => e
48
+ # puts "Class not found: #{e.message}"
49
+ # end
50
+ def constantize(string)
51
+ names = string.split('::')
52
+ names.shift if names.empty? || names.first.empty?
53
+
54
+ constant = Object
55
+
56
+ names.each do |name|
57
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
58
+ end
59
+
60
+ constant
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # A thread-safe timer task implementation.
6
+ # Drop-in replacement for Concurrent::TimerTask without external dependencies.
7
+ class TimerTask
8
+ # Initializes a new TimerTask
9
+ #
10
+ # @param execution_interval [Float] interval in seconds between task executions
11
+ # @param task [Proc] the task to execute on each interval (provided as a block)
12
+ # @return [TimerTask] a new TimerTask instance
13
+ # @raise [ArgumentError] if no block is provided or interval is not positive
14
+ # @yield the task to execute on each interval
15
+ def initialize(execution_interval:, &task)
16
+ raise ArgumentError, 'A block must be provided' unless block_given?
17
+
18
+ @execution_interval = Float(execution_interval)
19
+ raise ArgumentError, 'execution_interval must be positive' if @execution_interval <= 0
20
+
21
+ @task = task
22
+ @mutex = Mutex.new
23
+ @thread = nil
24
+ @running = false
25
+ @killed = false
26
+ end
27
+
28
+ # Starts the timer task execution
29
+ #
30
+ # @return [TimerTask] self for method chaining
31
+ def execute
32
+ @mutex.synchronize do
33
+ return self if @running || @killed
34
+
35
+ @running = true
36
+ @thread = Thread.new { run_timer_loop }
37
+ end
38
+ self
39
+ end
40
+
41
+ # Stops and kills the timer task
42
+ #
43
+ # @return [Boolean] true if killed, false if already killed
44
+ def kill
45
+ @mutex.synchronize do
46
+ return false if @killed
47
+
48
+ @killed = true
49
+ @running = false
50
+
51
+ @thread.kill if @thread&.alive?
52
+ end
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ # Runs the timer loop in a separate thread
59
+ #
60
+ # @return [void]
61
+ def run_timer_loop
62
+ until @killed
63
+ sleep(@execution_interval)
64
+ break if @killed
65
+
66
+ begin
67
+ @task.call
68
+ rescue => e
69
+ # Log the error but continue running
70
+ # This matches the behavior of Concurrent::TimerTask
71
+ warn "TimerTask execution error: #{e.message}"
72
+ warn e.backtrace.join("\n") if e.backtrace
73
+ end
74
+ end
75
+ ensure
76
+ @mutex.synchronize { @running = false }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ # A high-performance alternative to OpenStruct for representing SQS messages.
5
+ #
6
+ # InlineMessage is a Struct-based implementation that provides the same interface
7
+ # as the previous OpenStruct-based message representation but with significantly
8
+ # better performance characteristics. It contains all the essential attributes
9
+ # needed to represent an Amazon SQS message within the Shoryuken framework.
10
+ InlineMessage = Struct.new(
11
+ :body,
12
+ :attributes,
13
+ :md5_of_body,
14
+ :md5_of_message_attributes,
15
+ :message_attributes,
16
+ :message_id,
17
+ :receipt_handle,
18
+ :delete,
19
+ :queue_name,
20
+ keyword_init: true
21
+ )
22
+ end
@@ -1,11 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
4
+ # Launches and coordinates Shoryuken's message processing managers.
5
+ # Handles the lifecycle of the processing system including startup, shutdown, and health checks.
2
6
  class Launcher
3
7
  include Util
4
8
 
9
+ # Initializes a new Launcher with managers for each processing group
5
10
  def initialize
6
11
  @managers = create_managers
12
+ @stopping = false
13
+ end
14
+
15
+ # Indicates whether the launcher is in the process of stopping.
16
+ #
17
+ # This flag is set to true when either {#stop} or {#stop!} is called,
18
+ # and is used by ActiveJob adapters to signal jobs that they should
19
+ # checkpoint and prepare for graceful shutdown.
20
+ #
21
+ # @return [Boolean] true if stopping, false otherwise
22
+ def stopping?
23
+ @stopping
7
24
  end
8
25
 
26
+ # Starts the message processing system
27
+ #
28
+ # @return [void]
9
29
  def start
10
30
  logger.info { 'Starting' }
11
31
 
@@ -13,7 +33,11 @@ module Shoryuken
13
33
  start_managers
14
34
  end
15
35
 
36
+ # Forces an immediate stop of all processing
37
+ #
38
+ # @return [void]
16
39
  def stop!
40
+ @stopping = true
17
41
  initiate_stop
18
42
 
19
43
  # Don't await here so the timeout below is not delayed
@@ -25,7 +49,11 @@ module Shoryuken
25
49
  fire_event(:stopped)
26
50
  end
27
51
 
52
+ # Gracefully stops all processing, waiting for in-flight messages
53
+ #
54
+ # @return [void]
28
55
  def stop
56
+ @stopping = true
29
57
  fire_event(:quiet, true)
30
58
 
31
59
  initiate_stop
@@ -39,6 +67,9 @@ module Shoryuken
39
67
  fire_event(:stopped)
40
68
  end
41
69
 
70
+ # Checks if all processing groups are healthy
71
+ #
72
+ # @return [Boolean] true if all groups are running normally
42
73
  def healthy?
43
74
  Shoryuken.groups.keys.all? do |group|
44
75
  manager = @managers.find { |m| m.group == group }
@@ -48,30 +79,48 @@ module Shoryuken
48
79
 
49
80
  private
50
81
 
82
+ # Stops all managers from dispatching new messages
83
+ #
84
+ # @return [void]
51
85
  def stop_new_dispatching
52
86
  @managers.each(&:stop_new_dispatching)
53
87
  end
54
88
 
89
+ # Waits for any in-progress dispatching to complete
90
+ #
91
+ # @return [void]
55
92
  def await_dispatching_in_progress
56
93
  @managers.each(&:await_dispatching_in_progress)
57
94
  end
58
95
 
96
+ # Returns the executor for running async operations
97
+ #
98
+ # @return [Concurrent::ExecutorService] the executor service
59
99
  def executor
60
100
  @_executor ||= Shoryuken.launcher_executor || Concurrent.global_io_executor
61
101
  end
62
102
 
103
+ # Starts all managers in parallel futures
104
+ #
105
+ # @return [void]
63
106
  def start_managers
64
107
  @managers.each do |manager|
65
108
  Concurrent::Future.execute { manager.start }
66
109
  end
67
110
  end
68
111
 
112
+ # Initiates the stop sequence
113
+ #
114
+ # @return [void]
69
115
  def initiate_stop
70
116
  logger.info { 'Shutting down' }
71
117
 
72
118
  stop_callback
73
119
  end
74
120
 
121
+ # Executes the start callback and fires startup event
122
+ #
123
+ # @return [void]
75
124
  def start_callback
76
125
  if (callback = Shoryuken.start_callback)
77
126
  logger.debug { 'Calling start_callback' }
@@ -81,6 +130,9 @@ module Shoryuken
81
130
  fire_event(:startup)
82
131
  end
83
132
 
133
+ # Executes the stop callback and fires shutdown event
134
+ #
135
+ # @return [void]
84
136
  def stop_callback
85
137
  if (callback = Shoryuken.stop_callback)
86
138
  logger.debug { 'Calling stop_callback' }
@@ -90,6 +142,9 @@ module Shoryuken
90
142
  fire_event(:shutdown, true)
91
143
  end
92
144
 
145
+ # Creates managers for each configured processing group
146
+ #
147
+ # @return [Array<Shoryuken::Manager>] the created managers
93
148
  def create_managers
94
149
  Shoryuken.groups.map do |group, options|
95
150
  Shoryuken::Manager.new(
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # Base formatter class that provides common functionality for Shoryuken log formatters.
6
+ # Provides thread ID generation and context management.
7
+ class Base < ::Logger::Formatter
8
+ # Generates a thread ID for the current thread.
9
+ # Uses a combination of thread object_id and process ID to create a unique identifier.
10
+ #
11
+ # @return [String] A base36-encoded thread identifier
12
+ def tid
13
+ Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
14
+ end
15
+
16
+ # Returns the current logging context as a formatted string.
17
+ # Context is set using {Shoryuken::Logging.with_context}.
18
+ #
19
+ # @return [String] Formatted context string or empty string if no context
20
+ def context
21
+ c = Shoryuken::Logging.current_context
22
+ c ? " #{c}" : ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A pretty log formatter that includes timestamps, process ID, thread ID,
6
+ # context information, and severity in a human-readable format.
7
+ #
8
+ # Output format: "TIMESTAMP PID TID-THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Sample output
11
+ # # 2023-08-15T10:30:45Z 12345 TID-abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class Pretty < Base
13
+ # Formats a log message with timestamp and full context information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param time [Time] Timestamp when the log entry was created
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, time, _program_name, message)
21
+ "#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A log formatter that excludes timestamps from output.
6
+ # Useful for environments where timestamps are added by external logging systems.
7
+ #
8
+ # Output format: "pid=PID tid=THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Sample output
11
+ # # pid=12345 tid=abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class WithoutTimestamp < Base
13
+ # Formats a log message without timestamp information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param _time [Time] Timestamp (unused)
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, _time, _program_name, message)
21
+ "pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end