redstream 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08b45ec057fc3fa2404ff0c48c9f1befdd7dee5177b526690cb5edb907990ac7'
4
- data.tar.gz: '081b1cbd5ed872b3d320b1ffe6e7f4342c5bfcb101b293a5cf9348241f8dfe44'
3
+ metadata.gz: 20db64a5cecd9accc59e117b19ecd608236d0dfae1c177c47aff0079335ead7d
4
+ data.tar.gz: 34f383997e4e15a1ce03c83650445b20683b00a6adefcae01248525087fa90a9
5
5
  SHA512:
6
- metadata.gz: df13ccbbfe9b62e79d5dab0ac461f9c433aff8b6a2990079a4f5c007cb72e93cecf95fd65eb5aa9780da3542a95be01aa7ab83ac109b19b3af93faed69678e58
7
- data.tar.gz: ab2e5e8cc1c2ec8b8031cd27621a53e302b1ca23b1d50accb17a348742a0cebdefbf9d92adc6f216534dfcd145262f098f128e3539fd64d57fae994e54745f5e
6
+ metadata.gz: 911800fad8f34ce373e3edd884ce45c1804bd82ea2c0d0ae14b69fbc2f8e998aa19210db6ddb65c14ffb1af9daf03164269f8da4a913364eaf8c3f471e88f013
7
+ data.tar.gz: 3e3b9b0651c4a6d8d4f570df68d8e2ea982a3109450cf62b7f7fc02ebe7b05d3652a136016ea800e2a66ccb0059a59d2a6b703b6804803093c8030c1ac2cbc31
@@ -5,10 +5,11 @@ jobs:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  matrix:
8
- ruby: ['2.5', '2.6', '2.7', '3.0']
8
+ ruby: ['2.7', '3.0', '3.2']
9
9
  redis:
10
10
  - redis:5.0
11
11
  - redis:6.0
12
+ - redis:7.0
12
13
  services:
13
14
  redis:
14
15
  image: ${{ matrix.redis }}
@@ -25,7 +26,6 @@ jobs:
25
26
  path: vendor/bundler
26
27
  key: ${{ hashFiles('Gemfile.lock') }}-${{ matrix.ruby }}
27
28
  - run: |
28
- gem install bundler
29
29
  bundle install --path=vendor/bundler
30
30
  bundle exec rspec
31
31
  bundle exec rubocop
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.6.0
4
+ * Allow sharding records into multiple streams
5
+
3
6
  ## v0.5.0
4
7
  * No longer delete delay messages
5
8
 
data/Gemfile CHANGED
@@ -1,3 +1,15 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ gem "activerecord"
6
+ gem "bundler"
7
+ gem "concurrent-ruby"
8
+ gem "database_cleaner"
9
+ gem "factory_bot"
10
+ gem "rake"
11
+ gem "rspec"
12
+ gem "rspec-instafail"
13
+ gem "rubocop"
14
+ gem "sqlite3"
15
+ gem "timecop"
data/README.md CHANGED
@@ -231,6 +231,33 @@ array of records to `Redstream::Producer#bulk`, like shown above. If you pass
231
231
  an `ActiveRecord::Relation`, the `#bulk` method will convert it to an array,
232
232
  i.e. load the whole result set into memory.
233
233
 
234
+ ## Sharding
235
+
236
+ When you want to attach multiple consumers to a single stream, you maybe want
237
+ to add sharding. This can be accomplished by specifying a dynamic stream name
238
+ where you compute the shard key by hashing the primary key.
239
+
240
+ ```ruby
241
+ class Product < ActiveRecord::Base
242
+ include Redstream::Model
243
+
244
+ NUM_SHARDS = 4
245
+
246
+ def self.redstream_name(shard)
247
+ "products-#{shard}"
248
+ end
249
+
250
+ def redstream_name
251
+ self.class.redstream_name(Digest::SHA1.hexdigest(id.to_s)[0, 4].to_i(16) % NUM_SHARDS)
252
+ end
253
+ end
254
+ ```
255
+
256
+ The sharding via hashing the primary key is neccessary, because we want each
257
+ change of a specific object to end up in the same stream. Otherwise the order
258
+ of changes for a specific object gets mixed up. Subsequently, you can add
259
+ consumers, etc for each individual stream name.
260
+
234
261
  ## Namespacing
235
262
 
236
263
  In case you are using a shared redis, where multiple appications read/write
@@ -90,7 +90,7 @@ module Redstream
90
90
  commit offset
91
91
  end
92
92
 
93
- sleep(5) unless got_lock
93
+ @lock.wait(5) unless got_lock
94
94
  rescue StandardError => e
95
95
  @logger.error e
96
96
 
@@ -21,6 +21,21 @@ module Redstream
21
21
  # end
22
22
 
23
23
  class Lock
24
+ class Signal
25
+ def initialize
26
+ @mutex = Mutex.new
27
+ @condition_variable = ConditionVariable.new
28
+ end
29
+
30
+ def wait(timeout)
31
+ @mutex.synchronize { @condition_variable.wait(@mutex, timeout) }
32
+ end
33
+
34
+ def signal
35
+ @condition_variable.signal
36
+ end
37
+ end
38
+
24
39
  def initialize(name:)
25
40
  @name = name
26
41
  @id = SecureRandom.hex
@@ -28,33 +43,45 @@ module Redstream
28
43
 
29
44
  def acquire(&block)
30
45
  got_lock = get_lock
31
- keep_lock(&block) if got_lock
46
+
47
+ if got_lock
48
+ keep_lock(&block)
49
+ release_lock
50
+ end
51
+
32
52
  got_lock
33
53
  end
34
54
 
55
+ def wait(timeout)
56
+ @wait_redis ||= Redstream.connection_pool.with(&:dup)
57
+ @wait_redis.brpop("#{Redstream.lock_key_name(@name)}.notify", timeout: timeout)
58
+ end
59
+
35
60
  private
36
61
 
37
62
  def keep_lock(&block)
38
- stop = false
39
- mutex = Mutex.new
63
+ stopped = false
64
+ signal = Signal.new
40
65
 
41
- Thread.new do
42
- until mutex.synchronize { stop }
43
- Redstream.connection_pool.with { |redis| redis.expire(Redstream.lock_key_name(@name), 5) }
66
+ thread = Thread.new do
67
+ until stopped
68
+ Redstream.connection_pool.with do |redis|
69
+ redis.expire(Redstream.lock_key_name(@name), 5)
70
+ end
44
71
 
45
- sleep 3
72
+ signal.wait(3)
46
73
  end
47
74
  end
48
75
 
49
76
  block.call
50
77
  ensure
51
- mutex.synchronize do
52
- stop = true
53
- end
78
+ stopped = true
79
+ signal&.signal
80
+ thread&.join
54
81
  end
55
82
 
56
83
  def get_lock
57
- @get_lock_script = <<~GET_LOCK_SCRIPT
84
+ @get_lock_script = <<~SCRIPT
58
85
  local lock_key_name, id = ARGV[1], ARGV[2]
59
86
 
60
87
  local cur = redis.call('get', lock_key_name)
@@ -70,9 +97,30 @@ module Redstream
70
97
  end
71
98
 
72
99
  return false
73
- GET_LOCK_SCRIPT
100
+ SCRIPT
101
+
102
+ Redstream.connection_pool.with do |redis|
103
+ redis.eval(@get_lock_script, argv: [Redstream.lock_key_name(@name), @id])
104
+ end
105
+ end
106
+
107
+ def release_lock
108
+ @release_lock_script = <<~SCRIPT
109
+ local lock_key_name, id = ARGV[1], ARGV[2]
74
110
 
75
- Redstream.connection_pool.with { |redis| redis.eval(@get_lock_script, argv: [Redstream.lock_key_name(@name), @id]) }
111
+ local cur = redis.call('get', lock_key_name)
112
+
113
+ if cur and cur == id then
114
+ redis.call('del', lock_key_name)
115
+ end
116
+
117
+ redis.call('del', lock_key_name .. '.notify')
118
+ redis.call('rpush', lock_key_name .. '.notify', '1')
119
+ SCRIPT
120
+
121
+ Redstream.connection_pool.with do |redis|
122
+ redis.eval(@release_lock_script, argv: [Redstream.lock_key_name(@name), @id])
123
+ end
76
124
  end
77
125
  end
78
126
  end
@@ -58,5 +58,28 @@ module Redstream
58
58
  def redstream_payload
59
59
  { id: id }
60
60
  end
61
+
62
+ # Override to customize the stream name. By default, the stream name
63
+ # is determined by the class name. If you override the instance method,
64
+ # please also override the class method.
65
+ #
66
+ # @example Sharding
67
+ # class Product
68
+ # include Redstream::Model
69
+ #
70
+ # NUM_SHARDS = 4
71
+ #
72
+ # def redstream_name
73
+ # self.class.redstream_name(Digest::SHA1.hexdigest(id.to_s)[0, 4].to_i(16) % NUM_SHARDS)
74
+ # end
75
+ #
76
+ # def self.redstream_name(shard)
77
+ # "products-#{shard}
78
+ # end
79
+ # end
80
+
81
+ def redstream_name
82
+ self.class.redstream_name
83
+ end
61
84
  end
62
85
  end
@@ -31,7 +31,6 @@ module Redstream
31
31
 
32
32
  def initialize(wait: false)
33
33
  @wait = wait
34
- @stream_name_cache = {}
35
34
 
36
35
  super()
37
36
  end
@@ -72,7 +71,7 @@ module Redstream
72
71
  Redstream.connection_pool.with do |redis|
73
72
  redis.pipelined do |pipeline|
74
73
  slice.each do |object|
75
- pipeline.xadd(Redstream.stream_key_name("#{stream_name(object)}.delay"), { payload: JSON.dump(object.redstream_payload) })
74
+ pipeline.xadd(Redstream.stream_key_name("#{object.redstream_name}.delay"), { payload: JSON.dump(object.redstream_payload) })
76
75
  end
77
76
  end
78
77
  end
@@ -96,7 +95,7 @@ module Redstream
96
95
  Redstream.connection_pool.with do |redis|
97
96
  redis.pipelined do |pipeline|
98
97
  slice.each do |object|
99
- pipeline.xadd(Redstream.stream_key_name(stream_name(object)), { payload: JSON.dump(object.redstream_payload) })
98
+ pipeline.xadd(Redstream.stream_key_name(object.redstream_name), { payload: JSON.dump(object.redstream_payload) })
100
99
  end
101
100
  end
102
101
  end
@@ -115,7 +114,7 @@ module Redstream
115
114
 
116
115
  def delay(object)
117
116
  Redstream.connection_pool.with do |redis|
118
- res = redis.xadd(Redstream.stream_key_name("#{stream_name(object)}.delay"), { payload: JSON.dump(object.redstream_payload) })
117
+ res = redis.xadd(Redstream.stream_key_name("#{object.redstream_name}.delay"), { payload: JSON.dump(object.redstream_payload) })
119
118
  redis.wait(@wait, 0) if @wait
120
119
  res
121
120
  end
@@ -129,18 +128,10 @@ module Redstream
129
128
 
130
129
  def queue(object)
131
130
  Redstream.connection_pool.with do |redis|
132
- redis.xadd(Redstream.stream_key_name(stream_name(object)), { payload: JSON.dump(object.redstream_payload) })
131
+ redis.xadd(Redstream.stream_key_name(object.redstream_name), { payload: JSON.dump(object.redstream_payload) })
133
132
  end
134
133
 
135
134
  true
136
135
  end
137
-
138
- private
139
-
140
- def stream_name(object)
141
- synchronize do
142
- @stream_name_cache[object.class] ||= object.class.redstream_name
143
- end
144
- end
145
136
  end
146
137
  end
@@ -1,3 +1,3 @@
1
1
  module Redstream
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
data/redstream.gemspec CHANGED
@@ -16,19 +16,6 @@ Gem::Specification.new do |spec|
16
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_development_dependency "activerecord"
20
- spec.add_development_dependency "bundler"
21
- spec.add_development_dependency "concurrent-ruby"
22
- spec.add_development_dependency "database_cleaner"
23
- spec.add_development_dependency "factory_bot"
24
- spec.add_development_dependency "mocha"
25
- spec.add_development_dependency "rake"
26
- spec.add_development_dependency "rspec"
27
- spec.add_development_dependency "rspec-instafail"
28
- spec.add_development_dependency "rubocop"
29
- spec.add_development_dependency "sqlite3"
30
- spec.add_development_dependency "timecop"
31
-
32
19
  spec.add_dependency "activesupport"
33
20
  spec.add_dependency "connection_pool"
34
21
  spec.add_dependency "json"
@@ -86,5 +86,44 @@ RSpec.describe Redstream::Consumer do
86
86
 
87
87
  expect(redis.get(Redstream.offset_key_name(stream_name: "products", consumer_name: "consumer"))).to eq(all_messages.last[0])
88
88
  end
89
+
90
+ it "does not starve" do
91
+ product = create(:product)
92
+ stopped = false
93
+ results = Concurrent::Array.new
94
+
95
+ thread1 = Thread.new do
96
+ until stopped
97
+ Redstream::Consumer.new(name: "consumer", stream_name: "products").run_once do
98
+ results.push("thread1")
99
+
100
+ product.touch
101
+
102
+ sleep 0.1
103
+ end
104
+ end
105
+ end
106
+
107
+ thread2 = Thread.new do
108
+ until stopped
109
+ Redstream::Consumer.new(name: "consumer", stream_name: "products").run_once do
110
+ results.push("thread2")
111
+
112
+ product.touch
113
+
114
+ sleep 0.1
115
+ end
116
+ end
117
+ end
118
+
119
+ sleep 6
120
+
121
+ stopped = true
122
+ [thread1, thread2].each(&:join)
123
+
124
+ expect(results.size).to be > 10
125
+ expect(results.count("thread1").to_f / results.size).to be > 0.2
126
+ expect(results.count("thread2").to_f / results.size).to be > 0.2
127
+ end
89
128
  end
90
129
  end
@@ -34,7 +34,7 @@ RSpec.describe Redstream::Lock do
34
34
  end
35
35
  end
36
36
 
37
- sleep 6
37
+ sleep 5
38
38
 
39
39
  threads << Thread.new do
40
40
  Redstream::Lock.new(name: "lock").acquire do
@@ -62,5 +62,87 @@ RSpec.describe Redstream::Lock do
62
62
  expect(calls).to eq(2)
63
63
  expect(lock_results).to eq([1, 1])
64
64
  end
65
+
66
+ it "releases the lock and notifies" do
67
+ lock = Redstream::Lock.new(name: "lock")
68
+
69
+ expect(redis.llen("#{Redstream.lock_key_name("lock")}.notify")).to eq(0)
70
+
71
+ lock.acquire do
72
+ # nothing
73
+ end
74
+
75
+ expect(redis.exists?(Redstream.lock_key_name("lock"))).to eq(false)
76
+ expect(redis.llen("#{Redstream.lock_key_name("lock")}.notify")).to eq(1)
77
+ end
78
+
79
+ it "does not release the lock when the lock is already taken again" do
80
+ lock = Redstream::Lock.new(name: "lock")
81
+
82
+ lock.acquire do
83
+ redis.set(Redstream.lock_key_name("lock"), "other")
84
+ end
85
+
86
+ expect(redis.get(Redstream.lock_key_name("lock"))).to eq("other")
87
+ end
88
+
89
+ it "acquires the lock as soon as it gets released" do
90
+ time = nil
91
+
92
+ thread = Thread.new do
93
+ Redstream::Lock.new(name: "lock").acquire do
94
+ time = Time.now.to_f
95
+
96
+ sleep 2
97
+ end
98
+ end
99
+
100
+ sleep 1
101
+
102
+ lock = Redstream::Lock.new(name: "lock")
103
+ lock.wait(10) until lock.acquire { "nothing" }
104
+
105
+ thread.join
106
+
107
+ expect(Time.now.to_f - time).to be < 3
108
+ end
109
+ end
110
+
111
+ describe "#wait" do
112
+ it "blocks for the specified time max" do
113
+ stopped = false
114
+
115
+ thread = Thread.new do
116
+ Redstream::Lock.new(name: "lock").acquire do
117
+ sleep 0.1 until stopped
118
+ end
119
+ end
120
+
121
+ time = Time.now.to_f
122
+
123
+ Redstream::Lock.new(name: "lock").wait(2)
124
+
125
+ expect(Time.now.to_f - time).to be < 3
126
+
127
+ stopped = true
128
+
129
+ thread.join
130
+ end
131
+
132
+ it "wakes up when the lock gets released" do
133
+ thread = Thread.new do
134
+ Redstream::Lock.new(name: "lock").acquire do
135
+ sleep 2
136
+ end
137
+ end
138
+
139
+ time = Time.now.to_f
140
+
141
+ Redstream::Lock.new(name: "lock").wait(10)
142
+
143
+ expect(Time.now.to_f - time).to be < 3
144
+
145
+ thread.join
146
+ end
65
147
  end
66
148
  end
@@ -10,6 +10,17 @@ RSpec.describe Redstream::Producer do
10
10
  expect { Redstream::Producer.new.queue(product) }.to change { redis.xlen(stream_key_name) }.by(1)
11
11
  expect(redis.xrange(stream_key_name, "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
12
12
  end
13
+
14
+ it "uses a custom stream name when specified" do
15
+ product = create(:product)
16
+
17
+ allow(product).to receive(:redstream_name).and_return("stream-name")
18
+
19
+ stream_key_name = Redstream.stream_key_name("stream-name")
20
+
21
+ expect { Redstream::Producer.new.queue(product) }.to change { redis.xlen(stream_key_name) }.by(1)
22
+ expect(redis.xrange(stream_key_name, "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
23
+ end
13
24
  end
14
25
 
15
26
  describe "#delay" do
@@ -22,7 +33,18 @@ RSpec.describe Redstream::Producer do
22
33
  expect(redis.xrange(stream_key_name, "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
23
34
  end
24
35
 
25
- it "resepects wait" do
36
+ it "uses a custom stream name when specified" do
37
+ product = create(:product)
38
+
39
+ allow(product).to receive(:redstream_name).and_return("stream-name")
40
+
41
+ stream_key_name = Redstream.stream_key_name("stream-name.delay")
42
+
43
+ expect { Redstream::Producer.new.delay(product) }.to change { redis.xlen(stream_key_name) }.by(1)
44
+ expect(redis.xrange(stream_key_name, "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
45
+ end
46
+
47
+ it "respects wait" do
26
48
  product = create(:product)
27
49
 
28
50
  stream_key_name = Redstream.stream_key_name("products.delay")
@@ -51,6 +73,24 @@ RSpec.describe Redstream::Producer do
51
73
  end
52
74
  end
53
75
 
76
+ it "uses a custom stream name when specified" do
77
+ allow_any_instance_of(Product).to receive(:redstream_name).and_return("stream-name")
78
+
79
+ create_list(:product, 2)
80
+
81
+ stream_key_name = Redstream.stream_key_name("stream-name")
82
+
83
+ expect(redis.xlen(stream_key_name)).to eq(2)
84
+ expect(redis.xlen("#{stream_key_name}.delay")).to eq(2)
85
+
86
+ Redstream::Producer.new.bulk(Product.all) do
87
+ # nothing
88
+ end
89
+
90
+ expect(redis.xlen(stream_key_name)).to eq(4)
91
+ expect(redis.xlen("#{stream_key_name}.delay")).to eq(4)
92
+ end
93
+
54
94
  it "adds bulk queue messages for scopes" do
55
95
  products = create_list(:product, 2)
56
96
 
@@ -86,6 +126,16 @@ RSpec.describe Redstream::Producer do
86
126
  { "payload" => JSON.dump(products[1].redstream_payload) }
87
127
  ])
88
128
  end
129
+
130
+ it "uses a custom stream nameadds bulk queue messages for scopes" do
131
+ allow_any_instance_of(Product).to receive(:redstream_name).and_return("stream-name")
132
+
133
+ create_list(:product, 2)
134
+
135
+ stream_key_name = Redstream.stream_key_name("stream-name")
136
+
137
+ expect { Redstream::Producer.new.bulk_queue(Product.all) }.to change { redis.xlen(stream_key_name) }.by(2)
138
+ end
89
139
  end
90
140
 
91
141
  describe "#bulk_delay" do
@@ -104,6 +154,16 @@ RSpec.describe Redstream::Producer do
104
154
  ])
105
155
  end
106
156
 
157
+ it "uses a custom stream name when specified" do
158
+ allow_any_instance_of(Product).to receive(:redstream_name).and_return("stream-name")
159
+
160
+ create_list(:product, 2)
161
+
162
+ stream_key_name = Redstream.stream_key_name("stream-name.delay")
163
+
164
+ expect { Redstream::Producer.new.bulk_delay(Product.all) }.to change { redis.xlen(stream_key_name) }.by(2)
165
+ end
166
+
107
167
  it "should respect wait for delay" do
108
168
  create(:product)
109
169
 
@@ -23,7 +23,7 @@ RSpec.describe Redstream::Trimmer do
23
23
 
24
24
  it "sleeps for the specified time if there's nothing to trim" do
25
25
  trimmer = Redstream::Trimmer.new(interval: 1, stream_name: "default", consumer_names: ["unknown_consumer"])
26
- trimmer.expects(:sleep).with(1).returns(true)
26
+ allow(trimmer).to receive(:sleep).with(1).and_return(true)
27
27
  trimmer.run_once
28
28
  end
29
29
  end
data/spec/spec_helper.rb CHANGED
@@ -4,13 +4,8 @@ require "factory_bot"
4
4
  require "database_cleaner"
5
5
  require "timecop"
6
6
  require "concurrent"
7
- require "mocha"
8
7
  require "rspec/instafail"
9
8
 
10
- RSpec.configure do |config|
11
- config.mock_with :mocha
12
- end
13
-
14
9
  ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "/tmp/redstream.sqlite3")
15
10
 
16
11
  ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS products"
metadata CHANGED
@@ -1,183 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Vetter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-30 00:00:00.000000000 Z
11
+ date: 2024-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activerecord
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: concurrent-ruby
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: database_cleaner
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: factory_bot
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: mocha
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: rake
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: rspec
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rspec-instafail
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: rubocop
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: sqlite3
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: timecop
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - ">="
172
- - !ruby/object:Gem::Version
173
- version: '0'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - ">="
179
- - !ruby/object:Gem::Version
180
- version: '0'
181
13
  - !ruby/object:Gem::Dependency
182
14
  name: activesupport
183
15
  requirement: !ruby/object:Gem::Requirement