redstream 0.1.1 → 0.2.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: 6be694edcad63b15c2e7a6630047080556a412fc1b52f874e4f793e8f07f80b9
4
- data.tar.gz: f140446d6fbebc97533de509ff622fe32b121cd89af2c5f0cc2b856f5e820e02
3
+ metadata.gz: ba9468ab2f4b93d53a2f063b3b841137c32ded698a46b0faf1d30aac19baaf1e
4
+ data.tar.gz: a2cd5dd4067687ad9e3f768b26be016978126037578a8489d6bc8f00d3b55f62
5
5
  SHA512:
6
- metadata.gz: 2b38daa5a07f1761fa47340c4d479b974b30937479a6a73897ad8d8e47c9d45328ef976d20b8a74a7cb0488d35985864d162b3adbb66133e56195cb016b09ab4
7
- data.tar.gz: 0b625e0f6fea967e69ba319115dd9bfd727b68aec369a83a0842bb39805b0389ebef33bae5cf2559314d54724a9fd28d5a517ab8cbdc88f14db1ec5a601c12ee
6
+ metadata.gz: b2d36e3a49fde99a438da2648164248f42ed376764073b3b8c2d935854fd8402058a724001a2409aca5cd9e0d0d560aebb381c504aa5fc2148664a989f9b7601
7
+ data.tar.gz: be66802bd2052f762b4a02c1bfca39f25c4497716fb30a8ba9046df6ba281e59ad75ed6901d90fce9b945dfcdfc8d2dfa2a77b07688cccbd8ed56bfedfcd4297
@@ -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 CHANGED
@@ -1,3 +1,12 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Lint/AssignmentInCondition:
5
+ Enabled: false
6
+
7
+ Gemspec/RequiredRubyVersion:
8
+ Enabled: false
9
+
1
10
  Lint/AmbiguousBlockAssociation:
2
11
  Enabled: false
3
12
 
@@ -13,9 +22,6 @@ Layout/ArgumentAlignment:
13
22
  Layout/FirstArrayElementIndentation:
14
23
  EnforcedStyle: consistent
15
24
 
16
- Style/BracesAroundHashParameters:
17
- EnforcedStyle: no_braces
18
-
19
25
  Style/PercentLiteralDelimiters:
20
26
  Enabled: false
21
27
 
@@ -52,7 +58,7 @@ Style/SymbolArray:
52
58
  Layout/RescueEnsureAlignment:
53
59
  Enabled: false
54
60
 
55
- Metrics/LineLength:
61
+ Layout/LineLength:
56
62
  Enabled: false
57
63
 
58
64
  Metrics/MethodLength:
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
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
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.2.0
4
+ * Delete delay messages after queue messages are sent
5
+
3
6
  ## v0.1.1
4
7
  * Fix missing queue message in `after_commit on: :destroy`
5
8
 
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
@@ -12,6 +12,8 @@ module Redstream
12
12
  # end
13
13
 
14
14
  module Model
15
+ IVAR_DELAY_MESSAGE_ID = :@__redstream_delay_message_id__
16
+
15
17
  def self.included(base)
16
18
  base.extend(ClassMethods)
17
19
  end
@@ -29,11 +31,29 @@ module Redstream
29
31
  # responsible for writing to a redis stream
30
32
 
31
33
  def redstream_callbacks(producer: Producer.new)
32
- after_save { |object| producer.delay(object) if object.saved_changes.present? }
33
- after_touch { |object| producer.delay(object) }
34
- after_destroy { |object| producer.delay(object) }
35
- after_commit(on: [:create, :update]) { |object| producer.queue(object) if object.saved_changes.present? }
36
- after_commit(on: :destroy) { |object| producer.queue(object) }
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)
41
+
42
+ if id = instance_variable_get(IVAR_DELAY_MESSAGE_ID)
43
+ producer.delete(object, id)
44
+ remove_instance_variable(IVAR_DELAY_MESSAGE_ID)
45
+ end
46
+ end
47
+ end
48
+
49
+ after_commit(on: :destroy) do |object|
50
+ producer.queue(object)
51
+
52
+ if id = instance_variable_get(IVAR_DELAY_MESSAGE_ID)
53
+ producer.delete(object, id)
54
+ remove_instance_variable(IVAR_DELAY_MESSAGE_ID)
55
+ end
56
+ end
37
57
  end
38
58
 
39
59
  def redstream_name
@@ -52,11 +52,12 @@ module Redstream
52
52
  def bulk(records)
53
53
  records_array = Array(records)
54
54
 
55
- bulk_delay(records_array)
55
+ message_ids = bulk_delay(records_array)
56
56
 
57
57
  yield
58
58
 
59
59
  bulk_queue(records_array)
60
+ bulk_delete(records_array, message_ids)
60
61
  end
61
62
 
62
63
  # @api private
@@ -64,12 +65,14 @@ module Redstream
64
65
  # Writes delay messages to a delay stream in redis.
65
66
  #
66
67
  # @param records [#to_a] The object/objects that will be updated or deleted
68
+ #
69
+ # @return The redis message ids
67
70
 
68
71
  def bulk_delay(records)
69
- records.each_slice(250) do |slice|
72
+ res = records.each_slice(250).flat_map do |slice|
70
73
  Redstream.connection_pool.with do |redis|
71
74
  redis.pipelined do
72
- slice.map do |object|
75
+ slice.each do |object|
73
76
  redis.xadd Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload)
74
77
  end
75
78
  end
@@ -80,7 +83,26 @@ module Redstream
80
83
  redis.wait(@wait, 0) if @wait
81
84
  end
82
85
 
83
- true
86
+ res
87
+ end
88
+
89
+ # @api private
90
+ #
91
+ # Deletes delay message from a delay stream in redis.
92
+ #
93
+ # @param records [#to_a] The object/objects that have beeen updated or deleted
94
+ # @param ids [#to_a] The ids of the respective delay messages
95
+
96
+ def bulk_delete(records, ids)
97
+ records.each_with_index.each_slice(250) do |slice|
98
+ Redstream.connection_pool.with do |redis|
99
+ redis.pipelined do
100
+ slice.each do |object, index|
101
+ redis.xdel Redstream.stream_key_name("#{stream_name(object)}.delay"), ids[index]
102
+ end
103
+ end
104
+ end
105
+ end
84
106
  end
85
107
 
86
108
  # @api private
@@ -107,15 +129,29 @@ module Redstream
107
129
  #
108
130
  # Writes a single delay message to a delay stream in redis.
109
131
  #
110
- # @param object The object hat will be updated, deleted, etc.
132
+ # @param object The object that will be updated, deleted, etc.
133
+ #
134
+ # @return The redis message id
111
135
 
112
136
  def delay(object)
113
137
  Redstream.connection_pool.with do |redis|
114
- redis.xadd Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload)
138
+ res = redis.xadd(Redstream.stream_key_name("#{stream_name(object)}.delay"), payload: JSON.dump(object.redstream_payload))
115
139
  redis.wait(@wait, 0) if @wait
140
+ res
116
141
  end
142
+ end
117
143
 
118
- true
144
+ # @api private
145
+ #
146
+ # Deletes a single delay message from a delay stream in redis.
147
+ #
148
+ # @param object The object that has been updated, deleted, ect.
149
+ # @param id The redis message id
150
+
151
+ def delete(object, id)
152
+ Redstream.connection_pool.with do |redis|
153
+ redis.xdel Redstream.stream_key_name("#{stream_name(object)}.delay"), id
154
+ end
119
155
  end
120
156
 
121
157
  # @api private
@@ -1,3 +1,3 @@
1
1
  module Redstream
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -80,7 +80,9 @@ RSpec.describe Redstream::Consumer do
80
80
 
81
81
  all_messages = redis.xrange(Redstream.stream_key_name("products"), "-", "+")
82
82
 
83
- 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
84
86
 
85
87
  expect(redis.get(Redstream.offset_key_name(stream_name: "products", consumer_name: "consumer"))).to eq(all_messages.last[0])
86
88
  end
@@ -3,19 +3,36 @@ require File.expand_path("../spec_helper", __dir__)
3
3
  RSpec.describe Redstream::Model do
4
4
  describe "after_save" do
5
5
  it "adds a delay message after_save" do
6
- expect { create(:product) }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
6
+ Product.transaction do
7
+ expect { create(:product) }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
8
+ end
9
+ end
10
+
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
7
15
  end
8
16
 
9
17
  it "adds the correct payload for the delay message" do
10
- product = create(:product)
18
+ Product.transaction do
19
+ product = create(:product)
11
20
 
12
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").first[1]).to eq("payload" => JSON.dump(product.redstream_payload))
21
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").first[1]).to eq("payload" => JSON.dump(product.redstream_payload))
22
+ end
13
23
  end
14
24
 
15
25
  it "adds a queue message after_save on commit" do
16
26
  expect { create(:product) }.to change { redis.xlen(Redstream.stream_key_name("products")) }
17
27
  end
18
28
 
29
+ it "deletes the delay message on commit" do
30
+ product = create(:product)
31
+
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
35
+
19
36
  it "does not add a delay message after_save if there are no changes" do
20
37
  product = create(:product)
21
38
 
@@ -33,35 +50,73 @@ RSpec.describe Redstream::Model do
33
50
  it "adds a delay message after touch" do
34
51
  product = create(:product)
35
52
 
36
- expect { product.touch }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
53
+ Product.transaction do
54
+ expect { product.touch }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
55
+ end
56
+ end
57
+
58
+ it "assigns the delay message id" do
59
+ product = create(:product)
60
+
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
37
66
  end
38
67
 
39
68
  it "sets the correct payload for the delay message" do
40
69
  product = create(:product)
41
- product.touch
42
70
 
43
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[1]).to eq("payload" => JSON.dump(product.redstream_payload))
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
44
76
  end
45
77
 
46
- it "adds a queue message after touch commit" do
78
+ it "adds a queue message after touch on commit" do
47
79
  product = create(:product)
48
80
 
49
81
  expect { product.touch }.to change { redis.xlen(Redstream.stream_key_name("products")) }
50
82
  end
83
+
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
51
91
  end
52
92
 
53
93
  describe "after_destroy" do
54
94
  it "adds a delay message after destroy" do
55
95
  product = create(:product)
56
96
 
57
- expect { product.destroy }.to change { redis.xlen(Redstream.stream_key_name("products.delay")) }
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
58
110
  end
59
111
 
60
112
  it "sets the correct payload for the delay message" do
61
113
  product = create(:product)
62
- product.destroy
63
114
 
64
- expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").last[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
65
120
  end
66
121
 
67
122
  it "adds a queue messages after destroy on commit" do
@@ -69,5 +124,13 @@ RSpec.describe Redstream::Model do
69
124
 
70
125
  expect { product.destroy }.to change { redis.xlen(Redstream.stream_key_name("products")) }.by(1)
71
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
72
135
  end
73
136
  end
@@ -31,6 +31,19 @@ RSpec.describe Redstream::Producer do
31
31
  end
32
32
  end
33
33
 
34
+ describe "#destroy" do
35
+ it "deletes the delay message for the object" do
36
+ product = create(:product)
37
+
38
+ producer = Redstream::Producer.new
39
+
40
+ id = producer.delay(product)
41
+ producer.delete(product, id)
42
+
43
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(0)
44
+ end
45
+ end
46
+
34
47
  describe "#bulk_queue" do
35
48
  it "adds bulk queue messages for scopes" do
36
49
  products = create_list(:product, 2)
@@ -46,6 +59,19 @@ RSpec.describe Redstream::Producer do
46
59
  { "payload" => JSON.dump(products[1].redstream_payload) }
47
60
  ])
48
61
  end
62
+
63
+ it "deletes the delay messages after the queue messages have been sent" do
64
+ products = create_list(:product, 2)
65
+ producer = Redstream::Producer.new
66
+
67
+ other_id = producer.delay(create(:product))
68
+
69
+ producer.bulk_queue(products) do
70
+ expect(redis.xlen(Redstream.stream_key_name("products.delay"))).to eq(2)
71
+ end
72
+
73
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").map(&:first)).to eq([other_id])
74
+ end
49
75
  end
50
76
 
51
77
  describe "#bulk_delay" do
@@ -64,7 +90,7 @@ RSpec.describe Redstream::Producer do
64
90
  ])
65
91
  end
66
92
 
67
- it "should resepect wait for delay" do
93
+ it "should respect wait for delay" do
68
94
  create(:product)
69
95
 
70
96
  stream_key_name = Redstream.stream_key_name("products.delay")
@@ -74,4 +100,18 @@ RSpec.describe Redstream::Producer do
74
100
  expect { Redstream::Producer.new(wait: 0).bulk_delay(products) }.to change { redis.xlen(stream_key_name) }.by(2)
75
101
  end
76
102
  end
103
+
104
+ describe "#bulk_delete" do
105
+ it "deletes delay messages for scopes" do
106
+ products = create_list(:product, 2)
107
+ producer = Redstream::Producer.new
108
+
109
+ other_id = producer.delay(create(:product))
110
+
111
+ ids = producer.bulk_delay(products)
112
+ producer.bulk_delete(products, ids)
113
+
114
+ expect(redis.xrange(Redstream.stream_key_name("products.delay"), "-", "+").map(&:first)).to eq([other_id])
115
+ end
116
+ end
77
117
  end
data/spec/spec_helper.rb CHANGED
@@ -17,7 +17,7 @@ ActiveRecord::Base.connection.execute "DROP TABLE IF EXISTS products"
17
17
 
18
18
  ActiveRecord::Base.connection.create_table :products do |t|
19
19
  t.string :title
20
- t.timestamps
20
+ t.timestamps null: false
21
21
  end
22
22
 
23
23
  class Product < ActiveRecord::Base
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redstream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.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: 2020-04-20 00:00:00.000000000 Z
11
+ date: 2021-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -242,6 +242,7 @@ executables: []
242
242
  extensions: []
243
243
  extra_rdoc_files: []
244
244
  files:
245
+ - ".github/workflows/test.yml"
245
246
  - ".gitignore"
246
247
  - ".rubocop.yml"
247
248
  - ".travis.yml"