redis-stream 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e51c4737c9df5802350f4542ff2174d94b1c04d2822e7cfa8a0228add8b5064
4
+ data.tar.gz: f6943fea98b8deb03c2308e12c7b361e751c58f178830f7e572cfbfb9c0715ff
5
+ SHA512:
6
+ metadata.gz: 79236d75161bb94c395021424287f6d8d980f580d1d24fb841ccae07b5afc5fd43054dc0d8f766db032eaca517075ced1c0dbff824818a34fa49e90c89356d8c
7
+ data.tar.gz: a89ed5e218e74e1a0688ec7f587466f3818d92d5f36c09e7df795ae1bf7b36a2f1a36666091d21b2b044ff6acf7d36155167185ac6838490901df367e83d1c31
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .idea
10
+ .idea/*
11
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.0
7
+ before_install: gem install bundler -v 2.0.1
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at mehmet@celik.be. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in redis-stream.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ redis-stream (0.1.0)
5
+ moneta (~> 1.2)
6
+ multi_json (~> 1.14)
7
+ redis (= 4.1.3)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ minitest (5.13.0)
13
+ moneta (1.2.1)
14
+ multi_json (1.14.1)
15
+ rake (10.5.0)
16
+ redis (4.1.3)
17
+
18
+ PLATFORMS
19
+ java
20
+
21
+ DEPENDENCIES
22
+ bundler (~> 2.0)
23
+ minitest (~> 5.0)
24
+ rake (~> 10.0)
25
+ redis-stream!
26
+
27
+ BUNDLED WITH
28
+ 2.1.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Mehmet Celik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Redis::Stream
2
+ ### !!!Use jRuby for now. It has a weird bug in cruby
3
+
4
+ Sugar coating Redis Streams
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'redis-stream'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install redis-stream
21
+
22
+ ## Usage
23
+
24
+ #### A simple non-blocking example
25
+ ```ruby
26
+ require 'redis-stream'
27
+ s1 = Redis::Stream::Client.new("test", "LIST", 't1')
28
+ s2 = Redis::Stream::Client.new("test", "MANIFEST", 't2')
29
+
30
+ s2.on_message do |message|
31
+ m = message['payload']
32
+ puts "Hello #{m}"
33
+ s1.stop
34
+ s2.stop
35
+ end
36
+
37
+ s1.start(false)
38
+ s2.start(false)
39
+
40
+ id = s1.add("World!", "to" => "*", "group" => "MANIFEST", "type" => Redis::Stream::Type::ACTION)
41
+
42
+ Timeout::timeout(10) do
43
+ loop do
44
+ break unless s1.running? || s2.running?
45
+ sleep 1
46
+ puts "checkin if still active #{s1.running?}, #{s2.running?}"
47
+ end
48
+ end
49
+ ```
50
+
51
+ #### Micro services example
52
+
53
+ 1. Sinatra as a point of entry
54
+ 2. microservice for processing
55
+
56
+ # http.rb
57
+ ```ruby
58
+ require 'sinatra'
59
+ require 'redis-stream'
60
+
61
+ configure do
62
+ set :redis_stream, Redis::Stream::Client.new("greetings", "HTTP", "http_client", "sync_start" => true)
63
+
64
+ get '/:name' do
65
+ halt 500, 'name parameter not found' unless params.include?(:name)
66
+ result = settings.redis_stream.sync_add(params[:name], "time_out" => 60)
67
+ @name = params[:name]
68
+ @reversed_name = result[payload]
69
+ erb :index
70
+ end
71
+
72
+ __END__
73
+ @@index
74
+
75
+ <p>Hello, <%= @name %>!</p>
76
+ <p><%= @reversed_name</p>
77
+
78
+ ```
79
+
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
84
+
85
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
86
+
87
+ ## Contributing
88
+
89
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mehmetc/redis-stream. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
90
+
91
+ ## License
92
+
93
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
94
+
95
+ ## Code of Conduct
96
+
97
+ Everyone interacting in the Redis::Stream project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/redis-stream/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "redis/stream"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.yml ADDED
@@ -0,0 +1 @@
1
+ data_cache: "/Users/mehmetc/Tmp/resolver/data_cache"
@@ -0,0 +1,319 @@
1
+ #encoding: UTF-8
2
+ require "redis"
3
+ require "logger"
4
+ require "json"
5
+ require "thread"
6
+ require "redis/stream/inspect"
7
+ require "redis/stream/config"
8
+ require "redis/stream/data_cache"
9
+
10
+ class Redis
11
+ module Stream
12
+ class Client
13
+ include Redis::Stream::Inspect
14
+
15
+ attr_reader :logger, :name, :group, :consumer_id, :cache, :redis, :non_blocking
16
+
17
+ # Initialize: setup rstream
18
+ # @param [String] stream_name name of the rstream
19
+ # @param [String] group name of the rstream group
20
+ # @param [Object] options options can contain redis[host, port, db] and logger keys
21
+ #
22
+ # Example: Redis::Stream::Client.new("resolver", "stream", {"logger" => Logger.new(STDOUT)})
23
+ # if group is nil or not supplied then no rstream group will be setup
24
+ def initialize(stream_name, group_name = nil, name = rand(36 ** 7).to_s(36), options = {})
25
+ default_options = {"host" => "127.0.0.1", "port" => 6379, "db" => 0, "logger" => Logger.new(STDOUT)}
26
+ options = default_options.merge(options)
27
+
28
+ host = options["host"]
29
+ port = options["port"]
30
+ db = options["db"]
31
+ @logger = options["logger"]
32
+ @cache = Redis::Stream::DataCache.new
33
+
34
+ @name = name
35
+ @state = Redis::Stream::State::IDLE
36
+ @stream = stream_name
37
+ @group = group_name
38
+ if options.include?('redis')
39
+ @redis = options['redis']
40
+ else
41
+ @redis = Redis.new(host: host, port: port, db: db)
42
+ end
43
+ @consumer_id = "#{@name}-#{@group}-#{Process.pid}"
44
+ @non_blocking = nil
45
+ # @send_queue = []
46
+
47
+ raise "No redis" if @redis.nil?
48
+
49
+ @state = Redis::Stream::State::RUNNING if options.include?("sync_start") && options["sync_start"]
50
+ setup_stream
51
+
52
+ @last_id = info['last-generated-id'] rescue '0'
53
+ @logger.info "#{@consumer_id} - Last ID = #{@last_id}"
54
+ end
55
+
56
+
57
+ # add: add a message to the stream
58
+ # @param [Object] data Any data you want to transmit
59
+ # @param [String] to Name of the consumer can be "*" or "" or nil for any consumer
60
+ # @param [String] group Name of the consumer group can be "*" or "" or nil for any group
61
+ # @param [Stream::Type] type Type of message
62
+ #
63
+ # no passthrough variable here. The passthrough is available in the start method
64
+ def add(data = {}, options = {})
65
+ raise "Client isn't running" unless @state.eql?(Redis::Stream::State::RUNNING)
66
+
67
+ default_options = {"to" => "*", "group" => "*", "type" => Redis::Stream::Type::ACTION, "cache_key" => nil}
68
+ options = default_options.merge(options)
69
+
70
+ type = options["type"]
71
+ to = options["to"]
72
+ group = options["group"]
73
+ payload = build_payload(data, options)
74
+ add_id = @redis.xadd(@stream, payload)
75
+ # @send_queue << add_id
76
+
77
+ @logger.info("#{@consumer_id} - send to '#{to}' in group '#{group}' with id '#{add_id}' of type '#{type}'")
78
+ add_id
79
+ end
80
+
81
+ # sync_add: same as add command but synchronous. Blocks call until a message arrives
82
+ # @param [Object] data Any data you want to transmit
83
+ # @param [String] to Name of the consumer can be "*" or "" or nil for any consumer
84
+ # @param [String] group Name of the consumer group can be "*" or "" or nil for any group
85
+ # @param [Stream::Type] type Type of message
86
+ # @param [Integer] time_out Time out after x seconds
87
+ # @param [Boolean] passthrough Receive all messages also the ones intended for other consumers
88
+ def sync_add(data = {}, options = {})
89
+ raise "Client isn't running" unless @state.eql?(Redis::Stream::State::RUNNING)
90
+
91
+ default_options = {"to" => "*", "group" => "*", "type" => Redis::Stream::Type::ACTION, "time_out" => 5, "passthrough" => false, "cache_key" => nil}
92
+ options = default_options.merge(options)
93
+
94
+ to = options["to"]
95
+ group = options["group"]
96
+ passthrough = options["passthrough"]
97
+ time_out = options["time_out"]
98
+
99
+ #@state = Redis::Stream::State::RUNNING
100
+ data_out = nil
101
+ add_id = add(data, "to" => to, "group" => group, "type" => options["type"], "cache_key" => options["cache_key"])
102
+
103
+ time = Time.now
104
+
105
+ loop do
106
+ timing = ((Time.now - time)).to_i
107
+ if timing > time_out
108
+ @logger.info("#{@consumer_id} - Time out(#{time_out}) for '#{to}' in group '#{group}'")
109
+ #@send_queue.delete(add_id) if @send_queue.include?(add_id)
110
+ break
111
+ end
112
+ break if (data_out = read_next_message_from_stream(false, passthrough))
113
+ end
114
+ #@state = Redis::Stream::State::STOPPED
115
+ data_out
116
+ end
117
+
118
+ # on_message: execute this block everytime a new message is received
119
+ def on_message(&block)
120
+ @on_message_callback = block
121
+ end
122
+
123
+ # start: start listening for stream messages
124
+ #
125
+ # @param [Boolean] block Should the thread be blocked.
126
+ # @param [Boolean] passthrough Receive all messages also the ones intended for other consumers
127
+ def start(block = true, passthrough = false)
128
+ raise "#{@consumer_id} already running" if @state == Redis::Stream::State::RUNNING
129
+ @state = Redis::Stream::State::RUNNING
130
+ #sanitize
131
+ if block
132
+ while @state == Redis::Stream::State::RUNNING
133
+ read_next_message_from_stream(true, passthrough)
134
+ end
135
+ else
136
+ @non_blocking = Thread.new do
137
+ while @state == Redis::Stream::State::RUNNING
138
+ read_next_message_from_stream(true, passthrough)
139
+ end
140
+ @logger.info("#{@consumer_id} - ending thread")
141
+ end
142
+ end
143
+ end
144
+
145
+ #stop: stop listening for new messages
146
+ def stop
147
+ @state = Redis::Stream::State::STOPPED
148
+ @logger.info("#{@consumer_id} - stopping")
149
+ @non_blocking.join unless @non_blocking.nil?
150
+ ensure
151
+ del_consumer
152
+ del_group
153
+ end
154
+
155
+ #running?: Are we still in the running state
156
+ def running?
157
+ t = @non_blocking.nil? ? true : @non_blocking.alive?
158
+ t && @state.eql?(Redis::Stream::State::RUNNING)
159
+ end
160
+
161
+ #remove dead and non existing consumers and groups
162
+ def sanitize
163
+ groups.each do |group|
164
+ consumers(group["name"]).each do |consumer|
165
+ if @consumer_id != consumer["name"]
166
+ result = sync_add({}, "to" => consumer["name"], "group" => group["name"], "type" => Redis::Stream::Type::PING, "time_out" => 1)
167
+ if result.nil?
168
+ del_consumer(group['name'], consumer['name'])
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+
176
+ private
177
+
178
+ def build_payload(data, options)
179
+ to = options['to']
180
+ group = options['group']
181
+ type = options['type']
182
+
183
+ payload = nil
184
+
185
+ if options["cache_key"].nil?
186
+ cache_key = @cache.build_key(data)
187
+ if @cache.include?(cache_key)
188
+ if data && data.include?('from_cache') && data['from_cache'].eql?(0)
189
+ @cache.delete(cache_key)
190
+ @logger.info("#{@consumer_id} - invalidating cache with key #{cache_key}")
191
+ else
192
+ payload = {
193
+ type: type,
194
+ from: to,
195
+ from_group: group,
196
+ to: @consumer_id,
197
+ to_group: @group,
198
+ payload: @cache[cache_key].to_json
199
+ }
200
+ @logger.info("#{@consumer_id} - fetching from cache with key #{cache_key}")
201
+ end
202
+
203
+ end
204
+ else
205
+ @cache[options["cache_key"]] = data
206
+ end
207
+
208
+ if payload.nil?
209
+ payload = {
210
+ type: type,
211
+ from: @consumer_id,
212
+ from_group: @group,
213
+ to: to,
214
+ to_group: group,
215
+ payload: data.to_json
216
+ }
217
+ end
218
+ payload
219
+ end
220
+
221
+ #setup stream
222
+ def setup_stream
223
+ if @group
224
+ begin
225
+ @redis.xgroup(:create, @stream, @group, '$', mkstream: true)
226
+ @logger.info("#{@consumer_id} - Group #{@group} created")
227
+ rescue Redis::CommandError => e
228
+ @logger.error("#{@consumer_id} - Group #{@group} exists")
229
+ @logger.error("#{@consumer_id} - #{e.message}")
230
+ end
231
+ end
232
+
233
+ Signal.trap('INT') {
234
+ @logger.info("#{@consumer_id} - Caught CTRL+c")
235
+ stop
236
+ }
237
+
238
+ at_exit do
239
+ stop if @state == Redis::Stream::State::RUNNING
240
+ @logger.info("#{@consumer_id} - Done")
241
+ end
242
+
243
+ @logger.info("#{@consumer_id} - Listening for incoming requests")
244
+ end
245
+
246
+ #handle_incoming: process incoming message
247
+ # @param [Object] message
248
+ def handle_incoming(message)
249
+ if callback = @on_message_callback
250
+ timing = Time.now
251
+ begin
252
+ callback.call(message)
253
+ rescue Exception => e
254
+ @logger.error("#{@consumer_id} - #{e.message} - #{message["payload"].to_json}")
255
+ ensure
256
+ @logger.info("#{@consumer_id} - message from '#{message["from"]}' handled in #{((Time.now.to_f - timing.to_f).to_f * 1000.0).to_i}ms")
257
+ end
258
+ end
259
+ end
260
+
261
+ #read message from the stream
262
+ # @param [Boolean] async return the message if synchronous else call handle_incoming
263
+ # @param [Boolean] passthrough Receive all messages also the ones intended for other consumers
264
+ def read_next_message_from_stream(async = true, passthrough = false)
265
+ if @state == Redis::Stream::State::RUNNING
266
+ result = @redis.xread(@stream, @lastid, block: 1000, count: 1) if @group.nil?
267
+ result = @redis.xreadgroup(@group, @consumer_id, @stream, '>', block: 1000, count: 1) if @group
268
+
269
+ unless result.empty?
270
+ id, data_out = result[@stream][0]
271
+ ack_count = @redis.xack(@stream, @group, id) if @group
272
+
273
+ begin
274
+ data_out["payload"] = JSON.parse(data_out["payload"])
275
+ rescue Exception => e
276
+ @logger.error("#{@consumer_id} error parsing payload: #{e.message}")
277
+ end
278
+
279
+ # if @send_queue.include?(id)
280
+ # @send_queue.delete(id)
281
+ # @logger.warning("#{@consumer_id} - send queue is not empty: #{@send_queue.join(',')}") if @send_queue.length > 0
282
+ # unless passthrough
283
+ # #@logger.info("#{@consumer_id} - ignoring self")
284
+ # return false
285
+ # end
286
+ # end
287
+
288
+ if (data_out["from"].eql?(@consumer_id))
289
+ return false
290
+ end
291
+
292
+ unless (data_out["to"].nil? || data_out["to"].eql?('') || data_out["to"].eql?('*') || data_out["to"].eql?(@consumer_id)) &&
293
+ (data_out["to_group"].nil? || data_out["to_group"].eql?('') || data_out["to_group"].eql?('*') || data_out["to_group"].eql?(@group))
294
+ @logger.info("#{@consumer_id} - ignoring message from '#{data_out["from"]}' to '#{data_out["to"]}-#{data_out["to_group"]}'")
295
+
296
+ return false
297
+ end
298
+
299
+ @logger.info("#{@consumer_id} - received from '#{data_out["from"]}' of type '#{data_out['type']}' to '#{data_out["to"]}' in group '#{data_out["to_group"]}' with message id '#{id}' - with ack #{ack_count}")
300
+
301
+ if data_out["type"].eql?(Redis::Stream::Type::PING)
302
+ add(data_out["payload"].to_s, "to" => data_out["from"], "group" => "*", "type" => Redis::Stream::Type::PONG)
303
+ return false
304
+ end
305
+
306
+ if data_out["type"].eql?(Redis::Stream::Type::PONG)
307
+ return false
308
+ end
309
+
310
+ return data_out unless async
311
+ handle_incoming(data_out)
312
+ end
313
+ end
314
+ rescue Exception => e
315
+ return false
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,90 @@
1
+ #encoding: UTF-8
2
+ require 'yaml'
3
+ class Redis
4
+ module Stream
5
+ # Simple way to read and manage a config.yml file
6
+ class Config
7
+ @config = {}
8
+ @config_file_path = ""
9
+
10
+ # return the current location of the config.yml file
11
+ # @return [String] path of config.yml
12
+ def self.path
13
+ @config_file_path
14
+ end
15
+
16
+ # set path to config file
17
+ # @param [String] config_file_path path to config.yml file
18
+ def self.path=(config_file_path)
19
+ @config_file_path = config_file_path
20
+ end
21
+
22
+ # get the value for a key
23
+ # @param [String] key key of key/value
24
+ # @return [Object] value of key/value pair
25
+ def self.[](key)
26
+ init
27
+ @config[key]
28
+ end
29
+
30
+ # set a value into the config.yml file
31
+ # @param [String] key
32
+ # @param [Object] value
33
+ # @return [Object]
34
+ def self.[]=(key, value)
35
+ init
36
+ @config[key] = value
37
+ File.open("#{path}/config.yml", 'w') do |f|
38
+ f.puts @config.to_yaml
39
+ end
40
+ end
41
+
42
+ #is key available in config store
43
+ # @param [String] key key to lookup
44
+ # @return [Boolean]
45
+ def self.include?(key)
46
+ init
47
+ @config.include?(key)
48
+ end
49
+
50
+ private
51
+
52
+ # load and prepare config.yml
53
+ def self.init
54
+ discover_config_file_path
55
+ if @config.empty?
56
+ config = YAML::load_file("#{path}/config.yml")
57
+ @config = process(config)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ #determine location of config.yml file
64
+ def self.discover_config_file_path
65
+ if @config_file_path.nil? || @config_file_path.empty?
66
+ if File.exist?('config.yml')
67
+ @config_file_path = '.'
68
+ elsif File.exist?("config/config.yml")
69
+ @config_file_path = 'config'
70
+ end
71
+ end
72
+ end
73
+
74
+ #process config.yml file
75
+ # @param [Object] config yaml data
76
+ # @return [Object]
77
+ def self.process(config)
78
+ new_config = {}
79
+ config.each do |k, v|
80
+ if config[k].is_a?(Hash)
81
+ v = process(v)
82
+ end
83
+ new_config.store(k.to_sym, v)
84
+ end
85
+
86
+ new_config
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,46 @@
1
+ #encoding: UTF-8
2
+ require 'moneta'
3
+ require 'redis/stream/config'
4
+
5
+ class Redis
6
+ module Stream
7
+ class DataCache
8
+ def initialize
9
+ @cache = Moneta.new(:HashFile, dir: Redis::Stream::Config[:data_cache] || "/tmp/cache", serializer: :json)
10
+ end
11
+
12
+ def []=(key, value)
13
+ @cache.store(key, value)
14
+ end
15
+
16
+ def [](key)
17
+ @cache.fetch(key)
18
+ end
19
+
20
+ def include?(key)
21
+ @cache.key?(key)
22
+ end
23
+
24
+ def delete(key)
25
+ @cache.delete(key)
26
+ end
27
+
28
+ def build_key(data)
29
+ key = ""
30
+ if data && data.include?('payload')
31
+ payload_data = data['payload']
32
+ else
33
+ payload_data = data
34
+ end
35
+
36
+ if payload_data.include?("id")
37
+ action = payload_data["action"].downcase
38
+ id = payload_data["id"].downcase
39
+ key = "#{action}_#{id}"
40
+ end
41
+ key
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,28 @@
1
+ #encoding: UTF-8
2
+ class Redis
3
+ module Stream
4
+ module Group
5
+ THUMBNAIL = "THUMBNAIL".freeze
6
+ STREAM = "STREAM".freeze
7
+ REPRESENTATION = "REPRESENTATION".freeze
8
+ METADATA = "METADATA".freeze
9
+ CACHE = "CACHE".freeze
10
+ LIST = "LIST".freeze
11
+ MANIFEST = "MANIFEST".freeze
12
+
13
+ def self.exists?(group)
14
+ self.constants.include?(group.upcase.to_sym)
15
+ end
16
+
17
+ def self.to_s
18
+ self.constants.map { |m| m.to_s.downcase }.compact.join(', ')
19
+ end
20
+
21
+ def self.lookup(group)
22
+ self.constants.each { |e| return e if e.to_s.downcase.eql?(group.downcase) }
23
+
24
+ return '*'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ #encoding: UTF-8
2
+ class Redis
3
+ module Stream
4
+ module Inspect
5
+ def groups
6
+ @redis.xinfo("groups", @stream)
7
+ rescue Exception => e
8
+ @logger.error("#{@consumer_id} - #{e.message}")
9
+ {}
10
+ end
11
+
12
+ def info
13
+ @redis.xinfo("stream", @stream)
14
+ end
15
+
16
+ def consumers(group = @group)
17
+ @redis.xinfo("consumers", @stream, group)
18
+ end
19
+
20
+ def del_consumer(group = @group, consumer = @consumer_id)
21
+ @logger.info("#{@consumer_id} - deleting consumer #{group}-#{consumer}")
22
+ @redis.xgroup('DELCONSUMER', @stream, group, consumer)
23
+ end
24
+
25
+ def del_group(group = @group)
26
+ if consumers(group).length == 0 && groups.map { |m| m["name"] }.include?(group)
27
+ @logger.info("#{@consumer_id} - deleting group #{group}")
28
+ @redis.xgroup('DESTROY', @stream, group)
29
+ end
30
+ end
31
+
32
+ def pending_messages
33
+ @redis.xrange(@stream)
34
+ end
35
+ end #Inspect
36
+ end #Stream
37
+ end # Redis
@@ -0,0 +1,19 @@
1
+ #encoding: UTF-8
2
+ class Redis
3
+ module Stream
4
+ module State
5
+ ERROR = -1
6
+ IDLE = 0
7
+ RUNNING = 1
8
+ STOPPED = 2
9
+
10
+ def self.exists?(state)
11
+ self.constants.include?(state.upcase.to_sym)
12
+ end
13
+
14
+ def self.to_s
15
+ self.constants.map { |m| m.to_s.downcase }.join(', ')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ #encoding: UTF-8
2
+ class Redis
3
+ module Stream
4
+ module Type
5
+ PING = "PING".freeze
6
+ PONG = "PONG".freeze
7
+ ACTION = "ACTION".freeze
8
+
9
+ def self.exists?(type)
10
+ self.constants.include?(type.upcase.to_sym)
11
+ end
12
+
13
+ def self.to_s
14
+ self.constants.map { |m| m.to_s.downcase }.join(', ')
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ #encoding: UTF-8
2
+ class Redis
3
+ module Stream
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ #encoding: UTF-8
2
+ require "redis/stream/version"
3
+ require "redis/stream/state"
4
+ require "redis/stream/type"
5
+ require "redis/stream/group"
6
+ require "redis/stream/client"
7
+ require "redis/stream/config"
8
+ require "redis/stream/data_cache"
@@ -0,0 +1,45 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "redis/stream/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "redis-stream"
8
+ spec.version = Redis::Stream::VERSION
9
+ spec.authors = ["Mehmet Celik"]
10
+ spec.email = ["mehmet@celik.be"]
11
+
12
+ spec.summary = %q{Sugar coating Redis Streams }
13
+ spec.description = %q{Simple stream library using Redis Streams}
14
+ spec.homepage = "https://github.com/mehmetc/redis-stream"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/mehmetc/redis-stream"
24
+ spec.metadata["changelog_uri"] = "https://github.com/mehmetc/redis-stream"
25
+ else
26
+ raise "RubyGems 2.0 or newer is required to protect against " \
27
+ "public gem pushes."
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "minitest", "~> 5.0"
42
+ spec.add_dependency "redis", "4.1.3"
43
+ spec.add_dependency "moneta", "~> 1.2"
44
+ spec.add_dependency "multi_json", "~> 1.14"
45
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-stream
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mehmet Celik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 4.1.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 4.1.3
69
+ - !ruby/object:Gem::Dependency
70
+ name: moneta
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: multi_json
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.14'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.14'
97
+ description: Simple stream library using Redis Streams
98
+ email:
99
+ - mehmet@celik.be
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".travis.yml"
106
+ - CODE_OF_CONDUCT.md
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - config.yml
115
+ - lib/redis/stream.rb
116
+ - lib/redis/stream/client.rb
117
+ - lib/redis/stream/config.rb
118
+ - lib/redis/stream/data_cache.rb
119
+ - lib/redis/stream/group.rb
120
+ - lib/redis/stream/inspect.rb
121
+ - lib/redis/stream/state.rb
122
+ - lib/redis/stream/type.rb
123
+ - lib/redis/stream/version.rb
124
+ - redis-stream.gemspec
125
+ homepage: https://github.com/mehmetc/redis-stream
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ allowed_push_host: https://rubygems.org
130
+ homepage_uri: https://github.com/mehmetc/redis-stream
131
+ source_code_uri: https://github.com/mehmetc/redis-stream
132
+ changelog_uri: https://github.com/mehmetc/redis-stream
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.0.6
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Sugar coating Redis Streams
152
+ test_files: []