shoryuken 7.0.0.alpha1 → 7.0.0.rc1

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +3 -3
  3. data/.github/workflows/specs.yml +27 -17
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.rspec +2 -1
  6. data/.ruby-version +1 -1
  7. data/Appraisals +6 -18
  8. data/CHANGELOG.md +200 -142
  9. data/Gemfile +1 -0
  10. data/README.md +12 -13
  11. data/bin/cli/base.rb +1 -2
  12. data/bin/cli/sqs.rb +6 -5
  13. data/bin/shoryuken +3 -2
  14. data/gemfiles/rails_7_2.gemfile +1 -0
  15. data/gemfiles/rails_8_0.gemfile +1 -0
  16. data/gemfiles/{rails_7_1.gemfile → rails_8_1.gemfile} +2 -1
  17. data/lib/shoryuken/body_parser.rb +3 -1
  18. data/lib/shoryuken/client.rb +2 -0
  19. data/lib/shoryuken/default_exception_handler.rb +2 -0
  20. data/lib/shoryuken/default_worker_registry.rb +11 -11
  21. data/lib/shoryuken/environment_loader.rb +6 -6
  22. data/lib/shoryuken/extensions/active_job_adapter.rb +21 -6
  23. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
  24. data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
  25. data/lib/shoryuken/fetcher.rb +4 -2
  26. data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
  27. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  28. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  29. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  30. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  31. data/lib/shoryuken/helpers/timer_task.rb +66 -0
  32. data/lib/shoryuken/inline_message.rb +22 -0
  33. data/lib/shoryuken/launcher.rb +16 -0
  34. data/lib/shoryuken/logging/base.rb +26 -0
  35. data/lib/shoryuken/logging/pretty.rb +25 -0
  36. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  37. data/lib/shoryuken/logging.rb +6 -12
  38. data/lib/shoryuken/manager.rb +6 -4
  39. data/lib/shoryuken/message.rb +116 -1
  40. data/lib/shoryuken/middleware/chain.rb +140 -43
  41. data/lib/shoryuken/middleware/entry.rb +30 -0
  42. data/lib/shoryuken/middleware/server/active_record.rb +2 -0
  43. data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
  44. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +11 -11
  45. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
  46. data/lib/shoryuken/middleware/server/timing.rb +2 -0
  47. data/lib/shoryuken/options.rb +9 -5
  48. data/lib/shoryuken/polling/base_strategy.rb +126 -0
  49. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  50. data/lib/shoryuken/polling/strict_priority.rb +2 -0
  51. data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
  52. data/lib/shoryuken/processor.rb +5 -2
  53. data/lib/shoryuken/queue.rb +6 -4
  54. data/lib/shoryuken/runner.rb +12 -12
  55. data/lib/shoryuken/util.rb +6 -6
  56. data/lib/shoryuken/version.rb +3 -1
  57. data/lib/shoryuken/worker/default_executor.rb +2 -0
  58. data/lib/shoryuken/worker/inline_executor.rb +3 -1
  59. data/lib/shoryuken/worker.rb +173 -0
  60. data/lib/shoryuken/worker_registry.rb +2 -0
  61. data/lib/shoryuken.rb +8 -28
  62. data/shoryuken.gemspec +6 -6
  63. data/spec/integration/active_job_continuation_spec.rb +145 -0
  64. data/spec/integration/launcher_spec.rb +2 -3
  65. data/spec/shared_examples_for_active_job.rb +13 -8
  66. data/spec/shoryuken/body_parser_spec.rb +1 -2
  67. data/spec/shoryuken/client_spec.rb +1 -1
  68. data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
  69. data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
  70. data/spec/shoryuken/environment_loader_spec.rb +9 -8
  71. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
  72. data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
  73. data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
  74. data/spec/shoryuken/extensions/active_job_continuation_spec.rb +110 -0
  75. data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
  76. data/spec/shoryuken/fetcher_spec.rb +23 -26
  77. data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  78. data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  79. data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  80. data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
  81. data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
  82. data/spec/shoryuken/helpers/timer_task_spec.rb +298 -0
  83. data/spec/shoryuken/helpers_integration_spec.rb +96 -0
  84. data/spec/shoryuken/inline_message_spec.rb +196 -0
  85. data/spec/shoryuken/launcher_spec.rb +23 -2
  86. data/spec/shoryuken/manager_spec.rb +1 -2
  87. data/spec/shoryuken/middleware/chain_spec.rb +1 -1
  88. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
  89. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
  90. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  91. data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
  92. data/spec/shoryuken/options_spec.rb +4 -4
  93. data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
  94. data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
  95. data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
  96. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
  97. data/spec/shoryuken/processor_spec.rb +1 -1
  98. data/spec/shoryuken/queue_spec.rb +2 -3
  99. data/spec/shoryuken/runner_spec.rb +1 -3
  100. data/spec/shoryuken/util_spec.rb +1 -1
  101. data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
  102. data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
  103. data/spec/shoryuken/worker_spec.rb +15 -11
  104. data/spec/shoryuken_spec.rb +1 -1
  105. data/spec/spec_helper.rb +16 -0
  106. metadata +72 -29
  107. data/.github/FUNDING.yml +0 -12
  108. data/gemfiles/rails_6_1.gemfile +0 -18
  109. data/gemfiles/rails_7_0.gemfile +0 -19
  110. data/lib/shoryuken/core_ext.rb +0 -69
  111. data/lib/shoryuken/polling/base.rb +0 -67
  112. data/shoryuken.jpg +0 -0
  113. data/spec/shoryuken/core_ext_spec.rb +0 -40
@@ -0,0 +1,196 @@
1
+ require 'spec_helper'
2
+ require 'shoryuken/helpers/atomic_boolean'
3
+
4
+ RSpec.describe Shoryuken::Helpers::AtomicBoolean do
5
+ subject { described_class.new }
6
+
7
+ describe '#initialize' do
8
+ it 'initializes with default value of false' do
9
+ boolean = described_class.new
10
+ expect(boolean.value).to eq(false)
11
+ end
12
+
13
+ it 'initializes with true value' do
14
+ boolean = described_class.new(true)
15
+ expect(boolean.value).to eq(true)
16
+ end
17
+
18
+ it 'initializes with false value' do
19
+ boolean = described_class.new(false)
20
+ expect(boolean.value).to eq(false)
21
+ end
22
+
23
+ it 'converts truthy values to true' do
24
+ boolean = described_class.new('truthy')
25
+ expect(boolean.value).to eq(true)
26
+ end
27
+
28
+ it 'converts falsy values to false' do
29
+ boolean = described_class.new(nil)
30
+ expect(boolean.value).to eq(false)
31
+ end
32
+ end
33
+
34
+ describe '#value' do
35
+ it 'returns the current value' do
36
+ expect(subject.value).to eq(false)
37
+ end
38
+
39
+ it 'returns the updated value after operations' do
40
+ subject.make_true
41
+ expect(subject.value).to eq(true)
42
+
43
+ subject.make_false
44
+ expect(subject.value).to eq(false)
45
+ end
46
+ end
47
+
48
+ describe '#make_true' do
49
+ it 'sets the value to true' do
50
+ expect { subject.make_true }.to change { subject.value }.from(false).to(true)
51
+ end
52
+
53
+ it 'returns true' do
54
+ result = subject.make_true
55
+ expect(result).to eq(true)
56
+ end
57
+
58
+ it 'keeps the value true if already true' do
59
+ subject.make_true
60
+ expect { subject.make_true }.not_to change { subject.value }
61
+ expect(subject.value).to eq(true)
62
+ end
63
+ end
64
+
65
+ describe '#make_false' do
66
+ it 'sets the value to false' do
67
+ subject.make_true # Start with true
68
+ expect { subject.make_false }.to change { subject.value }.from(true).to(false)
69
+ end
70
+
71
+ it 'returns false' do
72
+ result = subject.make_false
73
+ expect(result).to eq(false)
74
+ end
75
+
76
+ it 'keeps the value false if already false' do
77
+ expect { subject.make_false }.not_to change { subject.value }
78
+ expect(subject.value).to eq(false)
79
+ end
80
+ end
81
+
82
+ describe '#true?' do
83
+ it 'returns true when value is true' do
84
+ subject.make_true
85
+ expect(subject.true?).to eq(true)
86
+ end
87
+
88
+ it 'returns false when value is false' do
89
+ expect(subject.true?).to eq(false)
90
+ end
91
+ end
92
+
93
+ describe '#false?' do
94
+ it 'returns true when value is false' do
95
+ expect(subject.false?).to eq(true)
96
+ end
97
+
98
+ it 'returns false when value is true' do
99
+ subject.make_true
100
+ expect(subject.false?).to eq(false)
101
+ end
102
+ end
103
+
104
+ describe 'thread safety' do
105
+ it 'handles concurrent make_true operations correctly' do
106
+ boolean = described_class.new(false)
107
+ threads = []
108
+
109
+ 10.times do
110
+ threads << Thread.new do
111
+ 100.times { boolean.make_true }
112
+ end
113
+ end
114
+
115
+ threads.each(&:join)
116
+ expect(boolean.value).to eq(true)
117
+ end
118
+
119
+ it 'handles concurrent make_false operations correctly' do
120
+ boolean = described_class.new(true)
121
+ threads = []
122
+
123
+ 10.times do
124
+ threads << Thread.new do
125
+ 100.times { boolean.make_false }
126
+ end
127
+ end
128
+
129
+ threads.each(&:join)
130
+ expect(boolean.value).to eq(false)
131
+ end
132
+
133
+ it 'handles mixed concurrent operations correctly' do
134
+ boolean = described_class.new(false)
135
+ threads = []
136
+ results = []
137
+
138
+ # Multiple threads setting to true and false
139
+ 10.times do
140
+ threads << Thread.new do
141
+ 50.times do
142
+ boolean.make_true
143
+ boolean.make_false
144
+ end
145
+ end
146
+ end
147
+
148
+ # Reader threads
149
+ 5.times do
150
+ threads << Thread.new do
151
+ 100.times { results << boolean.value }
152
+ end
153
+ end
154
+
155
+ threads.each(&:join)
156
+
157
+ # All read values should be valid booleans
158
+ expect(results).to all(satisfy { |v| v == true || v == false })
159
+ end
160
+ end
161
+
162
+ describe 'drop-in replacement for Concurrent::AtomicBoolean' do
163
+ it 'provides the same basic API' do
164
+ # Test that our implementation has the same methods as Concurrent::AtomicBoolean
165
+ expect(subject).to respond_to(:value)
166
+ expect(subject).to respond_to(:make_true)
167
+ expect(subject).to respond_to(:make_false)
168
+ expect(subject).to respond_to(:true?)
169
+ expect(subject).to respond_to(:false?)
170
+ end
171
+
172
+ it 'does not expose counter-specific methods' do
173
+ expect(subject).not_to respond_to(:increment)
174
+ expect(subject).not_to respond_to(:decrement)
175
+ end
176
+
177
+ it 'behaves identically to Concurrent::AtomicBoolean for basic operations' do
178
+ # This test documents the expected behavior that matches Concurrent::AtomicBoolean
179
+ boolean = described_class.new(false)
180
+
181
+ expect(boolean.value).to eq(false)
182
+ expect(boolean.false?).to eq(true)
183
+ expect(boolean.true?).to eq(false)
184
+
185
+ boolean.make_true
186
+ expect(boolean.value).to eq(true)
187
+ expect(boolean.true?).to eq(true)
188
+ expect(boolean.false?).to eq(false)
189
+
190
+ boolean.make_false
191
+ expect(boolean.value).to eq(false)
192
+ expect(boolean.false?).to eq(true)
193
+ expect(boolean.true?).to eq(false)
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Shoryuken::Helpers::AtomicCounter do
4
+ subject { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'initializes with default value of 0' do
8
+ counter = described_class.new
9
+ expect(counter.value).to eq(0)
10
+ end
11
+
12
+ it 'initializes with custom value' do
13
+ counter = described_class.new(42)
14
+ expect(counter.value).to eq(42)
15
+ end
16
+
17
+ it 'initializes with negative value' do
18
+ counter = described_class.new(-10)
19
+ expect(counter.value).to eq(-10)
20
+ end
21
+ end
22
+
23
+ describe '#value' do
24
+ it 'returns the current value' do
25
+ expect(subject.value).to eq(0)
26
+ end
27
+
28
+ it 'returns the updated value after operations' do
29
+ subject.increment
30
+ expect(subject.value).to eq(1)
31
+
32
+ subject.decrement
33
+ expect(subject.value).to eq(0)
34
+ end
35
+ end
36
+
37
+ describe '#increment' do
38
+ it 'increments the counter by 1' do
39
+ expect { subject.increment }.to change { subject.value }.from(0).to(1)
40
+ end
41
+
42
+ it 'returns the new value' do
43
+ result = subject.increment
44
+ expect(result).to eq(1)
45
+ end
46
+
47
+ it 'can be called multiple times' do
48
+ 3.times { subject.increment }
49
+ expect(subject.value).to eq(3)
50
+ end
51
+
52
+ it 'works with negative initial values' do
53
+ counter = described_class.new(-5)
54
+ counter.increment
55
+ expect(counter.value).to eq(-4)
56
+ end
57
+ end
58
+
59
+ describe '#decrement' do
60
+ it 'decrements the counter by 1' do
61
+ subject.increment # Start at 1
62
+ expect { subject.decrement }.to change { subject.value }.from(1).to(0)
63
+ end
64
+
65
+ it 'returns the new value' do
66
+ subject.increment # Start at 1
67
+ result = subject.decrement
68
+ expect(result).to eq(0)
69
+ end
70
+
71
+ it 'can go negative' do
72
+ subject.decrement
73
+ expect(subject.value).to eq(-1)
74
+ end
75
+
76
+ it 'can be called multiple times' do
77
+ 3.times { subject.decrement }
78
+ expect(subject.value).to eq(-3)
79
+ end
80
+ end
81
+
82
+ describe 'thread safety' do
83
+ it 'handles concurrent increments correctly' do
84
+ counter = described_class.new
85
+ threads = []
86
+
87
+ 10.times do
88
+ threads << Thread.new do
89
+ 100.times { counter.increment }
90
+ end
91
+ end
92
+
93
+ threads.each(&:join)
94
+ expect(counter.value).to eq(1000)
95
+ end
96
+
97
+ it 'handles concurrent decrements correctly' do
98
+ counter = described_class.new(1000)
99
+ threads = []
100
+
101
+ 10.times do
102
+ threads << Thread.new do
103
+ 100.times { counter.decrement }
104
+ end
105
+ end
106
+
107
+ threads.each(&:join)
108
+ expect(counter.value).to eq(0)
109
+ end
110
+
111
+ it 'handles mixed concurrent operations correctly' do
112
+ counter = described_class.new
113
+ threads = []
114
+
115
+ # 5 threads incrementing
116
+ 5.times do
117
+ threads << Thread.new do
118
+ 100.times { counter.increment }
119
+ end
120
+ end
121
+
122
+ # 3 threads decrementing
123
+ 3.times do
124
+ threads << Thread.new do
125
+ 100.times { counter.decrement }
126
+ end
127
+ end
128
+
129
+ threads.each(&:join)
130
+ expect(counter.value).to eq(200) # 500 increments - 300 decrements
131
+ end
132
+
133
+ it 'provides atomic read operations' do
134
+ counter = described_class.new
135
+ values_read = []
136
+
137
+ # Writer thread
138
+ writer = Thread.new do
139
+ 1000.times { counter.increment }
140
+ end
141
+
142
+ # Reader threads
143
+ readers = 5.times.map do
144
+ Thread.new do
145
+ 100.times { values_read << counter.value }
146
+ end
147
+ end
148
+
149
+ [writer, *readers].each(&:join)
150
+
151
+ # All read values should be valid integers (not partial writes)
152
+ expect(values_read).to all(be_an(Integer))
153
+ expect(values_read).to all(be >= 0)
154
+ expect(values_read).to all(be <= 1000)
155
+ end
156
+ end
157
+
158
+ describe 'drop-in replacement for Concurrent::AtomicFixnum' do
159
+ it 'provides the same basic API' do
160
+ # Test that our implementation has the same methods as Concurrent::AtomicFixnum
161
+ expect(subject).to respond_to(:value)
162
+ expect(subject).to respond_to(:increment)
163
+ expect(subject).to respond_to(:decrement)
164
+ end
165
+
166
+ it 'behaves identically to Concurrent::AtomicFixnum for basic operations' do
167
+ # This test documents the expected behavior that matches Concurrent::AtomicFixnum
168
+ counter = described_class.new(5)
169
+
170
+ expect(counter.value).to eq(5)
171
+ expect(counter.increment).to eq(6)
172
+ expect(counter.value).to eq(6)
173
+ expect(counter.decrement).to eq(5)
174
+ expect(counter.value).to eq(5)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Shoryuken::Helpers::AtomicHash do
4
+ subject { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'creates an empty hash' do
8
+ hash = described_class.new
9
+ expect(hash.keys).to eq([])
10
+ end
11
+ end
12
+
13
+ describe '#[]' do
14
+ it 'returns nil for missing keys' do
15
+ expect(subject['missing']).to be_nil
16
+ end
17
+
18
+ it 'returns stored values' do
19
+ subject['key'] = 'value'
20
+ expect(subject['key']).to eq('value')
21
+ end
22
+
23
+ it 'works with different key types' do
24
+ subject[:symbol] = 'symbol_value'
25
+ subject[42] = 'number_value'
26
+
27
+ expect(subject[:symbol]).to eq('symbol_value')
28
+ expect(subject[42]).to eq('number_value')
29
+ end
30
+ end
31
+
32
+ describe '#[]=' do
33
+ it 'stores values' do
34
+ subject['key'] = 'value'
35
+ expect(subject['key']).to eq('value')
36
+ end
37
+
38
+ it 'overwrites existing values' do
39
+ subject['key'] = 'old_value'
40
+ subject['key'] = 'new_value'
41
+ expect(subject['key']).to eq('new_value')
42
+ end
43
+
44
+ it 'returns the assigned value' do
45
+ result = (subject['key'] = 'value')
46
+ expect(result).to eq('value')
47
+ end
48
+ end
49
+
50
+ describe '#clear' do
51
+ it 'removes all entries' do
52
+ subject['key1'] = 'value1'
53
+ subject['key2'] = 'value2'
54
+
55
+ expect(subject.keys.size).to eq(2)
56
+
57
+ subject.clear
58
+
59
+ expect(subject.keys).to eq([])
60
+ expect(subject['key1']).to be_nil
61
+ expect(subject['key2']).to be_nil
62
+ end
63
+
64
+ it 'returns the hash itself' do
65
+ subject['key'] = 'value'
66
+ result = subject.clear
67
+ expect(result).to eq({})
68
+ end
69
+ end
70
+
71
+ describe '#keys' do
72
+ it 'returns empty array for empty hash' do
73
+ expect(subject.keys).to eq([])
74
+ end
75
+
76
+ it 'returns all keys' do
77
+ subject['key1'] = 'value1'
78
+ subject[:key2] = 'value2'
79
+ subject[42] = 'value3'
80
+
81
+ keys = subject.keys
82
+ expect(keys).to contain_exactly('key1', :key2, 42)
83
+ end
84
+
85
+ it 'reflects changes after modifications' do
86
+ subject['key'] = 'value'
87
+ expect(subject.keys).to eq(['key'])
88
+
89
+ subject.clear
90
+ expect(subject.keys).to eq([])
91
+ end
92
+ end
93
+
94
+ describe '#fetch' do
95
+ it 'returns value for existing key' do
96
+ subject['key'] = 'value'
97
+ expect(subject.fetch('key')).to eq('value')
98
+ end
99
+
100
+ it 'returns default for missing key' do
101
+ expect(subject.fetch('missing', 'default')).to eq('default')
102
+ end
103
+
104
+ it 'returns nil as default when no default provided' do
105
+ expect(subject.fetch('missing')).to be_nil
106
+ end
107
+
108
+ it 'works with different default types' do
109
+ expect(subject.fetch('missing', [])).to eq([])
110
+ expect(subject.fetch('missing', {})).to eq({})
111
+ expect(subject.fetch('missing', 42)).to eq(42)
112
+ end
113
+ end
114
+
115
+ describe 'thread safety' do
116
+ it 'handles concurrent writes correctly' do
117
+ hash = described_class.new
118
+ threads = []
119
+
120
+ # 10 threads, each writing 100 different keys
121
+ 10.times do |thread_id|
122
+ threads << Thread.new do
123
+ 100.times do |i|
124
+ key = "thread_#{thread_id}_key_#{i}"
125
+ hash[key] = "value_#{i}"
126
+ end
127
+ end
128
+ end
129
+
130
+ threads.each(&:join)
131
+
132
+ # Verify all 1000 keys were written
133
+ expect(hash.keys.size).to eq(1000)
134
+
135
+ # Verify a sample of values
136
+ expect(hash['thread_0_key_0']).to eq('value_0')
137
+ expect(hash['thread_9_key_99']).to eq('value_99')
138
+ end
139
+
140
+ it 'handles concurrent reads correctly' do
141
+ hash = described_class.new
142
+
143
+ # Pre-populate hash
144
+ 100.times { |i| hash["key_#{i}"] = "value_#{i}" }
145
+
146
+ read_results = []
147
+ threads = []
148
+
149
+ # 10 threads, each reading 100 times
150
+ 10.times do
151
+ threads << Thread.new do
152
+ 100.times do |i|
153
+ key = "key_#{i % 100}"
154
+ read_results << hash[key]
155
+ end
156
+ end
157
+ end
158
+
159
+ threads.each(&:join)
160
+
161
+ # All reads should succeed
162
+ expect(read_results.size).to eq(1000)
163
+ expect(read_results.compact.size).to eq(1000)
164
+ end
165
+
166
+ it 'handles mixed concurrent read/write operations' do
167
+ hash = described_class.new
168
+ threads = []
169
+
170
+ # Writer threads
171
+ 5.times do |thread_id|
172
+ threads << Thread.new do
173
+ 50.times do |i|
174
+ hash["writer_#{thread_id}_#{i}"] = "value_#{i}"
175
+ end
176
+ end
177
+ end
178
+
179
+ # Reader threads
180
+ 5.times do
181
+ threads << Thread.new do
182
+ 100.times do |i|
183
+ # Read existing keys
184
+ hash["writer_0_#{i % 50}"]
185
+ end
186
+ end
187
+ end
188
+
189
+ # Clear operations
190
+ 2.times do
191
+ threads << Thread.new do
192
+ sleep(0.001) # Let some writes happen first
193
+ hash.clear
194
+ end
195
+ end
196
+
197
+ threads.each(&:join)
198
+
199
+ # The hash should be valid (not corrupted)
200
+ # After clear operations, it might be empty or have some keys
201
+ expect(hash.keys).to be_an(Array)
202
+ end
203
+
204
+ it 'provides thread-safe key enumeration' do
205
+ hash = described_class.new
206
+ threads = []
207
+ keys_snapshots = []
208
+
209
+ # Writer thread
210
+ writer = Thread.new do
211
+ 100.times { |i| hash["key_#{i}"] = i }
212
+ end
213
+
214
+ # Reader threads taking snapshots of keys
215
+ 5.times do
216
+ threads << Thread.new do
217
+ 20.times do
218
+ keys_snapshots << hash.keys.dup
219
+ end
220
+ end
221
+ end
222
+
223
+ [writer, *threads].each(&:join)
224
+
225
+ # All snapshots should be valid arrays
226
+ keys_snapshots.each do |snapshot|
227
+ expect(snapshot).to be_an(Array)
228
+ # Keys should be valid
229
+ snapshot.each { |key| expect(key).to be_a(String) }
230
+ end
231
+ end
232
+ end
233
+
234
+ describe 'mutex synchronization' do
235
+ it 'ensures all operations are atomic' do
236
+ hash = described_class.new
237
+
238
+ # Verify basic operations work correctly
239
+ hash['key'] = 'value'
240
+ expect(hash['key']).to eq('value')
241
+ expect(hash.keys).to eq(['key'])
242
+
243
+ hash.clear
244
+ expect(hash.keys).to eq([])
245
+ expect(hash['key']).to be_nil
246
+ end
247
+ end
248
+
249
+ describe 'drop-in replacement for Concurrent::Hash' do
250
+ it 'provides the same basic API' do
251
+ # Test that our implementation has the same methods as Concurrent::Hash
252
+ expect(subject).to respond_to(:[])
253
+ expect(subject).to respond_to(:[]=)
254
+ expect(subject).to respond_to(:clear)
255
+ expect(subject).to respond_to(:keys)
256
+ expect(subject).to respond_to(:fetch)
257
+ end
258
+
259
+ it 'behaves identically to Concurrent::Hash for basic operations' do
260
+ # This test documents the expected behavior that matches Concurrent::Hash
261
+ hash = described_class.new
262
+
263
+ # Test assignment and retrieval
264
+ hash['queue1'] = 'Worker1'
265
+ expect(hash['queue1']).to eq('Worker1')
266
+
267
+ # Test keys
268
+ expect(hash.keys).to eq(['queue1'])
269
+
270
+ # Test fetch with default
271
+ expect(hash.fetch('queue1')).to eq('Worker1')
272
+ expect(hash.fetch('missing', 'default')).to eq('default')
273
+
274
+ # Test clear
275
+ hash.clear
276
+ expect(hash.keys).to eq([])
277
+ expect(hash['queue1']).to be_nil
278
+ end
279
+
280
+ it 'matches DefaultWorkerRegistry usage patterns' do
281
+ # Test the exact patterns used in DefaultWorkerRegistry
282
+ hash = described_class.new
283
+
284
+ # Pattern from register_worker method
285
+ queue = 'test_queue'
286
+ clazz = 'TestWorker'
287
+ hash[queue] = clazz
288
+
289
+ # Pattern from batch_receive_messages? method
290
+ expect(hash[queue]).to eq(clazz)
291
+
292
+ # Pattern from fetch_worker method
293
+ expect(hash[queue]).to eq(clazz)
294
+
295
+ # Pattern from queues method
296
+ expect(hash.keys).to eq([queue])
297
+
298
+ # Pattern from workers method (with fetch and default)
299
+ expect(hash.fetch(queue, [])).to eq(clazz)
300
+ expect(hash.fetch('missing', [])).to eq([])
301
+
302
+ # Pattern from clear method
303
+ hash.clear
304
+ expect(hash.keys).to eq([])
305
+ end
306
+ end
307
+ end