redstream 0.5.0 → 0.6.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 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