redstream 0.0.1 → 0.4.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: 0e1ddc2700836c469d1ca61069e3416c21e657e05725b92e75969aa8110768e3
4
- data.tar.gz: c8565f3754b3fd4f66823d6d7035e814f73e27abdb15936ecd906f5f07dd8643
3
+ metadata.gz: 160386bd218415c11c556b1cdfc8824ed347e7279fac67f390c07b37c7128eff
4
+ data.tar.gz: '087e4ecaee15bd2f9db4367f53297a72f01c8de1cc093ae25ff3d4eb8ae29b90'
5
5
  SHA512:
6
- metadata.gz: edd496df8d06b98b9318b9796f400e2c0870edfc84c3aa7f9c7946dbe6cf91c5a8c0ab32425d627bc20c585389eab92ed1b290e57e0df856e8995547d8a9b7c6
7
- data.tar.gz: 4893d2197f427479e4df0821ca29a23ee98a604fa73680f955da8d2c71cbdb192d006c476dc3bd6c03c719da327c3d9b6f207842082a64133f0fe2383771aef5
6
+ metadata.gz: d4d0945a2d2cb4b38dd398e5368963b5c8ba63aa93bc065bcb0c78a0e6bd7d2c0a7f825dd76b7b1d0ff79b6874bf1c3d0cb8f29843977d7676d341cfa584ed5d
7
+ data.tar.gz: b53bc33ce21244d64307b22f98185367e7c5806aeed9e50ed2b9dd123278b19236a049042bde26870dacd99702df49966674d1c629bf73ad955ff176f3f25a7b
@@ -0,0 +1,28 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ ruby: ['2.5', '2.6', '2.7']
9
+ services:
10
+ redis:
11
+ image: redis
12
+ ports:
13
+ - 6379:6379
14
+ steps:
15
+ - uses: actions/checkout@v1
16
+ - uses: actions/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ - uses: actions/cache@v1
20
+ id: cache
21
+ with:
22
+ path: vendor/bundler
23
+ key: ${{ hashFiles('Gemfile.lock') }}-${{ matrix.ruby }}
24
+ - run: |
25
+ gem install bundler
26
+ bundle install --path=vendor/bundler
27
+ bundle exec rspec
28
+ bundle exec rubocop
data/.rubocop.yml ADDED
@@ -0,0 +1,122 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Lint/AssignmentInCondition:
5
+ Enabled: false
6
+
7
+ Gemspec/RequiredRubyVersion:
8
+ Enabled: false
9
+
10
+ Lint/AmbiguousBlockAssociation:
11
+ Enabled: false
12
+
13
+ Style/FrozenStringLiteralComment:
14
+ Enabled: false
15
+
16
+ Lint/RedundantRequireStatement:
17
+ Enabled: false
18
+
19
+ Layout/ArgumentAlignment:
20
+ EnforcedStyle: with_fixed_indentation
21
+
22
+ Layout/FirstArrayElementIndentation:
23
+ EnforcedStyle: consistent
24
+
25
+ Style/PercentLiteralDelimiters:
26
+ Enabled: false
27
+
28
+ Style/SpecialGlobalVars:
29
+ EnforcedStyle: use_english_names
30
+
31
+ Security/Eval:
32
+ Enabled: false
33
+
34
+ Style/WordArray:
35
+ EnforcedStyle: brackets
36
+
37
+ Style/ClassAndModuleChildren:
38
+ Enabled: false
39
+
40
+ Style/TrivialAccessors:
41
+ Enabled: false
42
+
43
+ Style/Alias:
44
+ Enabled: false
45
+
46
+ Style/StringLiteralsInInterpolation:
47
+ EnforcedStyle: double_quotes
48
+
49
+ Metrics/ClassLength:
50
+ Enabled: false
51
+
52
+ Naming/MethodParameterName:
53
+ Enabled: false
54
+
55
+ Style/SymbolArray:
56
+ EnforcedStyle: brackets
57
+
58
+ Layout/RescueEnsureAlignment:
59
+ Enabled: false
60
+
61
+ Layout/LineLength:
62
+ Enabled: false
63
+
64
+ Metrics/MethodLength:
65
+ Enabled: false
66
+
67
+ Metrics/ModuleLength:
68
+ Enabled: false
69
+
70
+ Style/ZeroLengthPredicate:
71
+ Enabled: false
72
+
73
+ Metrics/PerceivedComplexity:
74
+ Enabled: false
75
+
76
+ Metrics/AbcSize:
77
+ Enabled: false
78
+
79
+ Metrics/CyclomaticComplexity:
80
+ Enabled: false
81
+
82
+ Metrics/BlockLength:
83
+ Enabled: false
84
+
85
+ Metrics/BlockNesting:
86
+ Enabled: false
87
+
88
+ Style/NumericPredicate:
89
+ Enabled: false
90
+
91
+ Naming/AccessorMethodName:
92
+ Enabled: false
93
+
94
+ Naming/MemoizedInstanceVariableName:
95
+ Enabled: false
96
+
97
+ Style/StringLiterals:
98
+ EnforcedStyle: double_quotes
99
+
100
+ Style/Documentation:
101
+ Enabled: false
102
+
103
+ Naming/ConstantName:
104
+ Enabled: false
105
+
106
+ Style/MutableConstant:
107
+ Enabled: false
108
+
109
+ Layout/MultilineMethodCallIndentation:
110
+ EnforcedStyle: indented
111
+
112
+ Layout/ParameterAlignment:
113
+ EnforcedStyle: with_fixed_indentation
114
+
115
+ Lint/UnusedMethodArgument:
116
+ Enabled: false
117
+
118
+ Style/IfUnlessModifier:
119
+ Enabled: false
120
+
121
+ Style/RedundantBegin:
122
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,10 +1,13 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ cache: bundler
3
4
  rvm:
4
- - ruby-head
5
+ - ruby-2.6.2
5
6
  before_install:
6
7
  - docker-compose up -d
7
8
  - sleep 10
8
9
  install:
9
10
  - travis_retry bundle install
10
- script: rspec
11
+ script:
12
+ - rspec
13
+ - rubocop
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # CHANGELOG
2
+
3
+ ## v0.4.0
4
+ * Make delay message id params in queue methods optional
5
+
6
+ ## v0.3.0
7
+ * Pipeline deletion of delay messages
8
+
9
+ ## v0.2.0
10
+ * Delete delay messages after queue messages are sent
11
+
12
+ ## v0.1.1
13
+ * Fix missing queue message in `after_commit on: :destroy`
14
+
15
+ ## v0.1.0
16
+ * No longer queue/delay in `after_save`/`after_commit` if no changes occurred
17
+ * Added `Redstream.stream_size`
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
-
2
1
  source "https://rubygems.org"
3
2
 
4
3
  gemspec
5
-
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  **Using redis streams to keep your primary database in sync with secondary
5
5
  datastores (e.g. elasticsearch).**
6
6
 
7
- [![Build Status](https://secure.travis-ci.org/mrkamel/redstream.png?branch=master)](http://travis-ci.org/mrkamel/redstream)
7
+ [![Build Status](https://github.com/mrkamel/redstream/workflows/test/badge.svg?branch=master)](https://github.com/mrkamel/redstream/actions?query=workflow%3Atest)
8
8
 
9
9
  ## Installation
10
10
 
@@ -86,9 +86,9 @@ end
86
86
  More concretely, `after_save`, `after_touch` and `after_destroy` only write
87
87
  "delay" messages to an additional redis stream. Delay message are like any
88
88
  other messages, but they get processed by a `Redstream::Delayer` and the
89
- `Delayer`will wait for some (configurable) delay/time before processing them.
89
+ `Delayer` will wait for some (configurable) delay/time before processing them.
90
90
  As the `Delayer` is neccessary to fix inconsistencies, the delay must be at
91
- least as long as your maxiumum database transaction time. Contrary,
91
+ least as long as your maximum database transaction time. Contrary,
92
92
  `after_commit` writes messages to a redis stream from which the messages can
93
93
  be fetched immediately to keep the secondary datastores updated in
94
94
  near-realtime. The reasoning of all this is simple: usually, i.e. by using only
@@ -97,7 +97,9 @@ any errors occurring in between `after_save` and `after_commit` result in
97
97
  inconsistencies between your primary and secondary datastore. By using these
98
98
  kinds of "delay" messages triggered by `after_save` and fetched after e.g. 5
99
99
  minutes, errors occurring in between `after_save` and `after_commit` can be
100
- fixed when the delay message get processed.
100
+ fixed when the delay message get processed. Please note that redstream deletes
101
+ delay messages after the messages for immediate retrieval have been
102
+ successfully sent, such that messages will not be processed twice, usually.
101
103
 
102
104
  Any messages are fetched in batches, such that e.g. elasticsearch can be
103
105
  updated using its bulk API. For instance, depending on which elasticsearch ruby
data/Rakefile CHANGED
@@ -6,4 +6,3 @@ Rake::TestTask.new(:test) do |t|
6
6
  t.pattern = "test/**/*_test.rb"
7
7
  t.verbose = true
8
8
  end
9
-
data/lib/redstream.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  require "active_support/inflector"
3
2
  require "connection_pool"
4
3
  require "redis"
@@ -59,6 +58,17 @@ module Redstream
59
58
  @namespace
60
59
  end
61
60
 
61
+ # Returns the length of the specified stream.
62
+ #
63
+ # @param stream_name [String] The stream name
64
+ # @return [Integer] The length of the stream
65
+
66
+ def self.stream_size(stream_name)
67
+ connection_pool.with do |redis|
68
+ redis.xlen(stream_key_name(stream_name))
69
+ end
70
+ end
71
+
62
72
  # Returns the max id of the specified stream, i.e. the id of the
63
73
  # last/newest message added. Returns nil for empty streams.
64
74
  #
@@ -69,7 +79,7 @@ module Redstream
69
79
  connection_pool.with do |redis|
70
80
  message = redis.xrevrange(stream_key_name(stream_name), "+", "-", count: 1).first
71
81
 
72
- return unless message
82
+ return nil unless message
73
83
 
74
84
  message[0]
75
85
  end
@@ -131,4 +141,3 @@ module Redstream
131
141
  [namespace, "redstream"].compact.join(":")
132
142
  end
133
143
  end
134
-
@@ -1,4 +1,3 @@
1
-
2
1
  require "thread"
3
2
 
4
3
  module Redstream
@@ -92,7 +91,7 @@ module Redstream
92
91
  end
93
92
 
94
93
  sleep(5) unless got_lock
95
- rescue => e
94
+ rescue StandardError => e
96
95
  @logger.error e
97
96
 
98
97
  sleep 5
@@ -112,4 +111,3 @@ module Redstream
112
111
  end
113
112
  end
114
113
  end
115
-
@@ -1,4 +1,3 @@
1
-
2
1
  module Redstream
3
2
  # The Redstream::Delayer class is responsible for reading messages from
4
3
  # special delay streams which are used to fix inconsistencies resulting from
@@ -68,7 +67,7 @@ module Redstream
68
67
 
69
68
  deliver
70
69
  end
71
- rescue => e
70
+ rescue StandardError => e
72
71
  @logger.error e
73
72
 
74
73
  sleep 5
@@ -97,4 +96,3 @@ module Redstream
97
96
  end
98
97
  end
99
98
  end
100
-
@@ -1,4 +1,3 @@
1
-
2
1
  require "securerandom"
3
2
 
4
3
  module Redstream
@@ -55,7 +54,7 @@ module Redstream
55
54
  end
56
55
 
57
56
  def get_lock
58
- @get_lock_script =<<-EOF
57
+ @get_lock_script = <<~GET_LOCK_SCRIPT
59
58
  local lock_key_name, id = ARGV[1], ARGV[2]
60
59
 
61
60
  local cur = redis.call('get', lock_key_name)
@@ -71,10 +70,9 @@ module Redstream
71
70
  end
72
71
 
73
72
  return false
74
- EOF
73
+ GET_LOCK_SCRIPT
75
74
 
76
75
  Redstream.connection_pool.with { |redis| redis.eval(@get_lock_script, argv: [Redstream.lock_key_name(@name), @id]) }
77
76
  end
78
77
  end
79
78
  end
80
-
@@ -1,4 +1,3 @@
1
-
2
1
  module Redstream
3
2
  # The Redstream::Message class wraps a raw redis stream message to allow hash
4
3
  # and id/offset access as well as convenient parsing of the json payload.
@@ -49,4 +48,3 @@ module Redstream
49
48
  end
50
49
  end
51
50
  end
52
-
@@ -1,4 +1,3 @@
1
-
2
1
  module Redstream
3
2
  # Include Redstream::Model in your model to stream the model's updates via
4
3
  # redis streams.
@@ -13,9 +12,11 @@ module Redstream
13
12
  # end
14
13
 
15
14
  module Model
15
+ IVAR_DELAY_MESSAGE_ID = :@__redstream_delay_message_id__
16
+
16
17
  def self.included(base)
17
18
  base.extend(ClassMethods)
18
- end
19
+ end
19
20
 
20
21
  module ClassMethods
21
22
  # Adds after_save, after_touch, after_destroy and, most importantly,
@@ -30,11 +31,22 @@ module Redstream
30
31
  # responsible for writing to a redis stream
31
32
 
32
33
  def redstream_callbacks(producer: Producer.new)
33
- after_save { |object| producer.delay object }
34
- after_touch { |object| producer.delay object }
35
- after_destroy { |object| producer.delay object }
36
- after_commit { |object| producer.queue object }
37
- end
34
+ after_save { |object| instance_variable_set(IVAR_DELAY_MESSAGE_ID, producer.delay(object)) if object.saved_changes.present? }
35
+ after_touch { |object| instance_variable_set(IVAR_DELAY_MESSAGE_ID, producer.delay(object)) }
36
+ after_destroy { |object| instance_variable_set(IVAR_DELAY_MESSAGE_ID, producer.delay(object)) }
37
+
38
+ after_commit(on: [:create, :update]) do |object|
39
+ if object.saved_changes.present?
40
+ producer.queue(object, delay_message_id: instance_variable_get(IVAR_DELAY_MESSAGE_ID))
41
+ instance_variable_set(IVAR_DELAY_MESSAGE_ID, nil)
42
+ end
43
+ end
44
+
45
+ after_commit(on: :destroy) do |object|
46
+ producer.queue(object, delay_message_id: instance_variable_get(IVAR_DELAY_MESSAGE_ID))
47
+ instance_variable_set(IVAR_DELAY_MESSAGE_ID, nil)
48
+ end
49
+ end
38
50
 
39
51
  def redstream_name
40
52
  name.pluralize.underscore
@@ -52,6 +64,5 @@ module Redstream
52
64
  def redstream_payload
53
65
  { id: id }
54
66
  end
55
- end
67
+ end
56
68
  end
57
-
@@ -1,4 +1,3 @@
1
-
2
1
  module Redstream
3
2
  # A Redstream::Producer is responsible for writing the actual messages to
4
3
  # redis. This includes the delay messages as well as the messages for
@@ -53,11 +52,11 @@ module Redstream
53
52
  def bulk(records)
54
53
  records_array = Array(records)
55
54
 
56
- bulk_delay(records_array)
55
+ delay_message_ids = bulk_delay(records_array)
57
56
 
58
57
  yield
59
58
 
60
- bulk_queue(records_array)
59
+ bulk_queue(records_array, delay_message_ids: delay_message_ids)
61
60
  end
62
61
 
63
62
  # @api private
@@ -65,13 +64,15 @@ module Redstream
65
64
  # Writes delay messages to a delay stream in redis.
66
65
  #
67
66
  # @param records [#to_a] The object/objects that will be updated or deleted
67
+ #
68
+ # @return The redis message ids
68
69
 
69
70
  def bulk_delay(records)
70
- records.each_slice(250) do |slice|
71
+ res = records.each_slice(250).flat_map do |slice|
71
72
  Redstream.connection_pool.with do |redis|
72
73
  redis.pipelined do
73
- slice.map do |object|
74
- redis.xadd Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload)
74
+ slice.each do |object|
75
+ redis.xadd(Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload))
75
76
  end
76
77
  end
77
78
  end
@@ -81,7 +82,7 @@ module Redstream
81
82
  redis.wait(@wait, 0) if @wait
82
83
  end
83
84
 
84
- true
85
+ res
85
86
  end
86
87
 
87
88
  # @api private
@@ -89,13 +90,15 @@ module Redstream
89
90
  # Writes messages to a stream in redis for immediate retrieval.
90
91
  #
91
92
  # @param records [#to_a] The object/objects that will be updated deleted
93
+ # @param delay_message_ids [#to_a] The delay message ids to delete
92
94
 
93
- def bulk_queue(records)
94
- records.each_slice(250) do |slice|
95
+ def bulk_queue(records, delay_message_ids: nil)
96
+ records.each_with_index.each_slice(250) do |slice|
95
97
  Redstream.connection_pool.with do |redis|
96
98
  redis.pipelined do
97
- slice.each do |object|
98
- redis.xadd Redstream.stream_key_name(stream_name(object)), payload: JSON.dump(object.redstream_payload)
99
+ slice.each do |object, index|
100
+ redis.xadd(Redstream.stream_key_name(stream_name(object)), payload: JSON.dump(object.redstream_payload))
101
+ redis.xdel(Redstream.stream_key_name("#{stream_name(object)}.delay"), delay_message_ids[index]) if delay_message_ids
99
102
  end
100
103
  end
101
104
  end
@@ -108,15 +111,16 @@ module Redstream
108
111
  #
109
112
  # Writes a single delay message to a delay stream in redis.
110
113
  #
111
- # @param object The object hat will be updated, deleted, etc.
114
+ # @param object The object that will be updated, deleted, etc.
115
+ #
116
+ # @return The redis message id
112
117
 
113
118
  def delay(object)
114
119
  Redstream.connection_pool.with do |redis|
115
- redis.xadd Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload)
120
+ res = redis.xadd(Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload))
116
121
  redis.wait(@wait, 0) if @wait
122
+ res
117
123
  end
118
-
119
- true
120
124
  end
121
125
 
122
126
  # @api private
@@ -124,10 +128,14 @@ module Redstream
124
128
  # Writes a single message to a stream in redis for immediate retrieval.
125
129
  #
126
130
  # @param object The object hat will be updated, deleted, etc.
131
+ # @param delay_message_id The delay message id to delete
127
132
 
128
- def queue(object)
133
+ def queue(object, delay_message_id: nil)
129
134
  Redstream.connection_pool.with do |redis|
130
- redis.xadd Redstream.stream_key_name(stream_name(object)), payload: JSON.dump(object.redstream_payload)
135
+ redis.pipelined do
136
+ redis.xadd(Redstream.stream_key_name(stream_name(object)), payload: JSON.dump(object.redstream_payload))
137
+ redis.xdel(Redstream.stream_key_name("#{stream_name(object)}.delay"), delay_message_id) if delay_message_id
138
+ end
131
139
  end
132
140
 
133
141
  true
@@ -142,4 +150,3 @@ module Redstream
142
150
  end
143
151
  end
144
152
  end
145
-
@@ -1,4 +1,3 @@
1
-
2
1
  module Redstream
3
2
  # The Redstream::Trimmer class is neccessary to clean up messsages after all
4
3
  # consumers have successfully processed and committed them. Otherwise they
@@ -79,7 +78,7 @@ module Redstream
79
78
  end
80
79
 
81
80
  sleep(5) unless got_lock
82
- rescue => e
81
+ rescue StandardError => e
83
82
  @logger.error e
84
83
 
85
84
  sleep 5
@@ -88,4 +87,3 @@ module Redstream
88
87
  end
89
88
  end
90
89
  end
91
-
@@ -1,5 +1,3 @@
1
-
2
1
  module Redstream
3
- VERSION = "0.0.1"
2
+ VERSION = "0.4.0"
4
3
  end
5
-
data/redstream.gemspec CHANGED
@@ -1,15 +1,14 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'redstream/version'
3
+ require "redstream/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "redstream"
8
7
  spec.version = Redstream::VERSION
9
8
  spec.authors = ["Benjamin Vetter"]
10
9
  spec.email = ["vetter@plainpicture.de"]
11
- spec.summary = %q{Using redis streams to keep your primary database in sync with secondary datastores}
12
- spec.description = %q{Using redis streams to keep your primary database in sync with secondary datastores}
10
+ spec.summary = "Using redis streams to keep your primary database in sync with secondary datastores"
11
+ spec.description = "Using redis streams to keep your primary database in sync with secondary datastores"
13
12
  spec.homepage = "https://github.com/mrkamel/redstream"
14
13
  spec.license = "MIT"
15
14
 
@@ -18,21 +17,21 @@ Gem::Specification.new do |spec|
18
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
18
  spec.require_paths = ["lib"]
20
19
 
21
- spec.add_development_dependency "bundler"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
24
20
  spec.add_development_dependency "activerecord"
21
+ spec.add_development_dependency "bundler"
22
+ spec.add_development_dependency "concurrent-ruby"
25
23
  spec.add_development_dependency "database_cleaner"
26
- spec.add_development_dependency "sqlite3", "1.3.13"
27
24
  spec.add_development_dependency "factory_bot"
28
- spec.add_development_dependency "timecop"
29
- spec.add_development_dependency "concurrent-ruby"
30
- spec.add_development_dependency "rspec-instafail"
31
25
  spec.add_development_dependency "mocha"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "rspec-instafail"
29
+ spec.add_development_dependency "rubocop"
30
+ spec.add_development_dependency "sqlite3"
31
+ spec.add_development_dependency "timecop"
32
32
 
33
- spec.add_dependency "connection_pool"
34
33
  spec.add_dependency "activesupport"
35
- spec.add_dependency "redis", ">= 4.1.0"
34
+ spec.add_dependency "connection_pool"
36
35
  spec.add_dependency "json"
36
+ spec.add_dependency "redis", ">= 4.1.0"
37
37
  end
38
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Consumer do
@@ -6,7 +5,7 @@ RSpec.describe Redstream::Consumer do
6
5
  it "doesn't call the block without messages" do
7
6
  called = false
8
7
 
9
- Redstream::Consumer.new(name: "consumer", stream_name: "products", batch_size: 5).run_once do |batch|
8
+ Redstream::Consumer.new(name: "consumer", stream_name: "products", batch_size: 5).run_once do |_batch|
10
9
  called = true
11
10
  end
12
11
 
@@ -18,9 +17,9 @@ RSpec.describe Redstream::Consumer do
18
17
 
19
18
  calls = Concurrent::AtomicFixnum.new(0)
20
19
 
21
- threads = Array.new(2) do |i|
20
+ threads = Array.new(2) do |_i|
22
21
  Thread.new do
23
- Redstream::Consumer.new(name: "consumer", stream_name: "products", batch_size: 5).run_once do |batch|
22
+ Redstream::Consumer.new(name: "consumer", stream_name: "products", batch_size: 5).run_once do |_batch|
24
23
  calls.increment
25
24
 
26
25
  sleep 1
@@ -55,7 +54,7 @@ RSpec.describe Redstream::Consumer do
55
54
  end
56
55
 
57
56
  it "yields messages in batches" do
58
- products = create_list(:product, 15)
57
+ create_list(:product, 15)
59
58
 
60
59
  consumer = Redstream::Consumer.new(name: "consumer", stream_name: "products", batch_size: 10)
61
60
 
@@ -81,10 +80,11 @@ RSpec.describe Redstream::Consumer do
81
80
 
82
81
  all_messages = redis.xrange(Redstream.stream_key_name("products"), "-", "+")
83
82
 
84
- Redstream::Consumer.new(name: "consumer", stream_name: "products").run_once {}
83
+ Redstream::Consumer.new(name: "consumer", stream_name: "products").run_once do
84
+ # nothing
85
+ end
85
86
 
86
87
  expect(redis.get(Redstream.offset_key_name(stream_name: "products", consumer_name: "consumer"))).to eq(all_messages.last[0])
87
88
  end
88
89
  end
89
90
  end
90
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Delayer do
@@ -50,4 +49,3 @@ RSpec.describe Redstream::Delayer do
50
49
  end
51
50
  end
52
51
  end
53
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Lock do
@@ -7,7 +6,7 @@ RSpec.describe Redstream::Lock do
7
6
  lock_results = Concurrent::Array.new
8
7
  calls = Concurrent::AtomicFixnum.new(0)
9
8
 
10
- threads = Array.new(2) do |i|
9
+ threads = Array.new(2) do |_i|
11
10
  Thread.new do
12
11
  lock_results << Redstream::Lock.new(name: "lock").acquire do
13
12
  calls.increment
@@ -65,4 +64,3 @@ RSpec.describe Redstream::Lock do
65
64
  end
66
65
  end
67
66
  end
68
-
@@ -1,57 +1,136 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Model do
5
- it "adds a delay message after save" do
6
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
4
+ describe "after_save" do
5
+ it "adds a delay message after_save" do
6
+ Product.transaction do
7
+ expect { create(:product) }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
8
+ end
9
+ end
7
10
 
8
- time = Time.now
11
+ it "assigns the delay message id" do
12
+ Product.transaction do
13
+ expect(create(:product).instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_present
14
+ end
15
+ end
9
16
 
10
- product = Timecop.freeze(time) do
11
- create(:product)
17
+ it "adds the correct payload for the delay message" do
18
+ Product.transaction do
19
+ product = create(:product)
20
+
21
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").first[1]).to eq("payload" => JSON.dump(product.redstream_payload))
22
+ end
12
23
  end
13
24
 
14
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(1)
15
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").first[1]).to eq("payload" => JSON.dump(product.redstream_payload))
16
- end
25
+ it "adds a queue message after_save on commit" do
26
+ expect { create(:product) }.to change { redis.xlen(Redstream.stream_key_name("products")) }
27
+ end
17
28
 
18
- it "adds a delay message after touch" do
19
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
29
+ it "deletes the delay message on commit" do
30
+ product = create(:product)
20
31
 
21
- product = create(:product)
32
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
33
+ expect(product.instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_nil
34
+ end
22
35
 
23
- time = Time.now
36
+ it "does not add a delay message after_save if there are no changes" do
37
+ product = create(:product)
24
38
 
25
- Timecop.freeze(time) do
26
- product.touch
39
+ expect { product.save }.not_to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
27
40
  end
28
41
 
29
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(2)
30
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
42
+ it "does not add a queue messages after_save on commit if there are no changes" do
43
+ product = create(:product)
44
+
45
+ expect { product.save }.not_to change { redis.xlen(Redstream.stream_key_name("products")) }
46
+ end
31
47
  end
32
48
 
33
- it "adds a delay message after destroy" do
34
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
49
+ describe "after_touch" do
50
+ it "adds a delay message after touch" do
51
+ product = create(:product)
35
52
 
36
- product = create(:product)
53
+ Product.transaction do
54
+ expect { product.touch }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
55
+ end
56
+ end
37
57
 
38
- time = Time.now
58
+ it "assigns the delay message id" do
59
+ product = create(:product)
39
60
 
40
- Timecop.freeze(time) do
41
- product.touch
61
+ Product.transaction do
62
+ product.touch
63
+
64
+ expect(product.instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_present
65
+ end
66
+ end
67
+
68
+ it "sets the correct payload for the delay message" do
69
+ product = create(:product)
70
+
71
+ Product.transaction do
72
+ product.touch
73
+
74
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
75
+ end
76
+ end
77
+
78
+ it "adds a queue message after touch on commit" do
79
+ product = create(:product)
80
+
81
+ expect { product.touch }.to change { redis.xlen(Redstream.stream_key_name("products")) }
42
82
  end
43
83
 
44
- expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(2)
45
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
84
+ it "deletes the delay message after touch on commit" do
85
+ product = create(:product)
86
+ product.touch
87
+
88
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
89
+ expect(product.instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_nil
90
+ end
46
91
  end
47
92
 
48
- it "adds a queue message after commit" do
49
- expect(redis.xlen(Redstream.stream_key_name("products"))).to eq(0)
93
+ describe "after_destroy" do
94
+ it "adds a delay message after destroy" do
95
+ product = create(:product)
96
+
97
+ Product.transaction do
98
+ expect { product.destroy }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
99
+ end
100
+ end
101
+
102
+ it "assigns the delay message id" do
103
+ product = create(:product)
104
+
105
+ Product.transaction do
106
+ product.destroy
107
+
108
+ expect(product.instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_present
109
+ end
110
+ end
50
111
 
51
- product = create(:product)
112
+ it "sets the correct payload for the delay message" do
113
+ product = create(:product)
52
114
 
53
- expect(redis.xlen(Redstream.stream_key_name("products"))).to eq(1)
54
- expect(redis.xrange(Redstream.stream_key_name("products"), "-", "+").first[1]).to eq("payload" => JSON.dump(product.redstream_payload))
115
+ Product.transaction do
116
+ product.destroy
117
+
118
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
119
+ end
120
+ end
121
+
122
+ it "adds a queue messages after destroy on commit" do
123
+ product = create(:product)
124
+
125
+ expect { product.destroy }.to change { redis.xlen(Redstream.stream_key_name("products")) }.by(1)
126
+ end
127
+
128
+ it "deletes the delay message after destroy on commit" do
129
+ product = create(:product)
130
+ product.destroy
131
+
132
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
133
+ expect(product.instance_variable_get(Redstream::Model::IVAR_DELAY_MESSAGE_ID)).to be_nil
134
+ end
55
135
  end
56
136
  end
57
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Producer do
@@ -11,6 +10,17 @@ RSpec.describe Redstream::Producer do
11
10
  expect { Redstream::Producer.new.queue(product) }.to change { redis.xlen(stream_key_name) }.by(1)
12
11
  expect(redis.xrange(stream_key_name, "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
13
12
  end
13
+
14
+ it "deletes the delay message when given" do
15
+ product = create(:product)
16
+
17
+ producer = Redstream::Producer.new
18
+
19
+ id = producer.delay(product)
20
+ producer.queue(product, delay_message_id: id)
21
+
22
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
23
+ end
14
24
  end
15
25
 
16
26
  describe "#delay" do
@@ -32,6 +42,57 @@ RSpec.describe Redstream::Producer do
32
42
  end
33
43
  end
34
44
 
45
+ describe "#bulk" do
46
+ it "adds bulk delay messages for scopes" do
47
+ products = create_list(:product, 2)
48
+
49
+ stream_key_name = Redstream.stream_key_name("products")
50
+
51
+ expect(redis.xlen("#{stream_key_name}.delay")).to eq(0)
52
+
53
+ Redstream::Producer.new.bulk(Product.all) do
54
+ messages = redis.xrange("#{stream_key_name}.delay", "-", "+").last(2).map { |message| message[1] }
55
+
56
+ expect(messages).to eq([
57
+ { "payload" => JSON.dump(products[0].redstream_payload) },
58
+ { "payload" => JSON.dump(products[1].redstream_payload) }
59
+ ])
60
+ end
61
+ end
62
+
63
+ it "adds bulk queue messages for scopes" do
64
+ products = create_list(:product, 2)
65
+
66
+ stream_key_name = Redstream.stream_key_name("products")
67
+
68
+ expect do
69
+ Redstream::Producer.new.bulk(Product.all) do
70
+ # nothing
71
+ end
72
+ end.to change { redis.xlen(stream_key_name) }.by(2)
73
+
74
+ messages = redis.xrange(stream_key_name, "-", "+").last(2).map { |message| message[1] }
75
+
76
+ expect(messages).to eq([
77
+ { "payload" => JSON.dump(products[0].redstream_payload) },
78
+ { "payload" => JSON.dump(products[1].redstream_payload) }
79
+ ])
80
+ end
81
+
82
+ it "deletes the delay messages after the queue messages have been sent" do
83
+ products = create_list(:product, 2)
84
+ producer = Redstream::Producer.new
85
+
86
+ other_delay_message_id = producer.delay(create(:product))
87
+
88
+ producer.bulk(products) do
89
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(3)
90
+ end
91
+
92
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").map(&:first)).to eq([other_delay_message_id])
93
+ end
94
+ end
95
+
35
96
  describe "#bulk_queue" do
36
97
  it "adds bulk queue messages for scopes" do
37
98
  products = create_list(:product, 2)
@@ -47,6 +108,18 @@ RSpec.describe Redstream::Producer do
47
108
  { "payload" => JSON.dump(products[1].redstream_payload) }
48
109
  ])
49
110
  end
111
+
112
+ it "deletes the delay messages after the queue messages have been sent" do
113
+ products = create_list(:product, 2)
114
+ producer = Redstream::Producer.new
115
+
116
+ delay_message_ids = producer.bulk_delay(products)
117
+ other_delay_message_id = producer.delay(create(:product))
118
+
119
+ producer.bulk_queue(products, delay_message_ids: delay_message_ids)
120
+
121
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").map(&:first)).to eq([other_delay_message_id])
122
+ end
50
123
  end
51
124
 
52
125
  describe "#bulk_delay" do
@@ -65,8 +138,8 @@ RSpec.describe Redstream::Producer do
65
138
  ])
66
139
  end
67
140
 
68
- it "should resepect wait for delay" do
69
- product = create(:product)
141
+ it "should respect wait for delay" do
142
+ create(:product)
70
143
 
71
144
  stream_key_name = Redstream.stream_key_name("products.delay")
72
145
 
@@ -76,4 +149,3 @@ RSpec.describe Redstream::Producer do
76
149
  end
77
150
  end
78
151
  end
79
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream::Trimmer do
@@ -29,4 +28,3 @@ RSpec.describe Redstream::Trimmer do
29
28
  end
30
29
  end
31
30
  end
32
-
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("spec_helper", __dir__)
3
2
 
4
3
  RSpec.describe Redstream do
@@ -31,11 +30,21 @@ RSpec.describe Redstream do
31
30
  end
32
31
  end
33
32
 
33
+ describe ".stream_size" do
34
+ it "returns the stream's size" do
35
+ expect(Redstream.stream_size("products")).to eq(0)
36
+
37
+ redis.xadd("redstream:stream:products", key: "value")
38
+
39
+ expect(Redstream.stream_size("products")).to eq(1)
40
+ end
41
+ end
42
+
34
43
  describe ".max_stream_id" do
35
44
  it "returns the stream's max id" do
36
45
  expect(Redstream.max_stream_id("products")).to be_nil
37
46
 
38
- id1 = redis.xadd("redstream:stream:products", key: "value")
47
+ _id1 = redis.xadd("redstream:stream:products", key: "value")
39
48
  id2 = redis.xadd("redstream:stream:products", key: "value")
40
49
 
41
50
  expect(Redstream.max_stream_id("products")).to eq(id2)
@@ -46,7 +55,7 @@ RSpec.describe Redstream do
46
55
  it "returns the consumer's max id" do
47
56
  expect(Redstream.max_consumer_id(stream_name: "products", consumer_name: "consumer")).to be_nil
48
57
 
49
- id1 = redis.xadd("redstream:stream:products", key: "value")
58
+ _id1 = redis.xadd("redstream:stream:products", key: "value")
50
59
  id2 = redis.xadd("redstream:stream:products", key: "value")
51
60
 
52
61
  Redstream::Consumer.new(name: "consumer", stream_name: "products").run_once do |messages|
@@ -114,4 +123,3 @@ RSpec.describe Redstream do
114
123
  end
115
124
  end
116
125
  end
117
-
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  require File.expand_path("../lib/redstream", __dir__)
3
2
  require "active_record"
4
3
  require "factory_bot"
@@ -18,7 +17,7 @@ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS products"
18
17
 
19
18
  ActiveRecord::Base.connection.create_table :products do |t|
20
19
  t.string :title
21
- t.timestamps
20
+ t.timestamps null: false
22
21
  end
23
22
 
24
23
  class Product < ActiveRecord::Base
@@ -62,5 +61,3 @@ RSpec.configure do |config|
62
61
  Redis.new.flushdb
63
62
  end
64
63
  end
65
-
66
-
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.4.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: 2019-04-26 00:00:00.000000000 Z
11
+ date: 2021-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: concurrent-ruby
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: activerecord
56
+ name: database_cleaner
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: database_cleaner
70
+ name: factory_bot
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,21 +81,21 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: sqlite3
84
+ name: mocha
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 1.3.13
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 1.3.13
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: factory_bot
98
+ name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: timecop
112
+ name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -123,7 +123,7 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: concurrent-ruby
126
+ name: rspec-instafail
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
@@ -137,7 +137,7 @@ dependencies:
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
- name: rspec-instafail
140
+ name: rubocop
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - ">="
@@ -151,7 +151,7 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: mocha
154
+ name: sqlite3
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
@@ -165,13 +165,13 @@ dependencies:
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
167
  - !ruby/object:Gem::Dependency
168
- name: connection_pool
168
+ name: timecop
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
171
  - - ">="
172
172
  - !ruby/object:Gem::Version
173
173
  version: '0'
174
- type: :runtime
174
+ type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
@@ -193,19 +193,19 @@ dependencies:
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
195
  - !ruby/object:Gem::Dependency
196
- name: redis
196
+ name: connection_pool
197
197
  requirement: !ruby/object:Gem::Requirement
198
198
  requirements:
199
199
  - - ">="
200
200
  - !ruby/object:Gem::Version
201
- version: 4.1.0
201
+ version: '0'
202
202
  type: :runtime
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - ">="
207
207
  - !ruby/object:Gem::Version
208
- version: 4.1.0
208
+ version: '0'
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: json
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +220,20 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: redis
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: 4.1.0
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: 4.1.0
223
237
  description: Using redis streams to keep your primary database in sync with secondary
224
238
  datastores
225
239
  email:
@@ -228,8 +242,11 @@ executables: []
228
242
  extensions: []
229
243
  extra_rdoc_files: []
230
244
  files:
245
+ - ".github/workflows/test.yml"
231
246
  - ".gitignore"
247
+ - ".rubocop.yml"
232
248
  - ".travis.yml"
249
+ - CHANGELOG.md
233
250
  - Gemfile
234
251
  - LICENSE.txt
235
252
  - README.md
@@ -272,8 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
272
289
  - !ruby/object:Gem::Version
273
290
  version: '0'
274
291
  requirements: []
275
- rubyforge_project:
276
- rubygems_version: 2.7.3
292
+ rubygems_version: 3.0.3
277
293
  signing_key:
278
294
  specification_version: 4
279
295
  summary: Using redis streams to keep your primary database in sync with secondary