start_her 0.0.2 → 0.0.3

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
  SHA1:
3
- metadata.gz: ba090c4bbc338ea1efc647a92b4dc2066fdcf30b
4
- data.tar.gz: 8004b2ed21d849d7b8427ebaf786f4a6a8a577d4
3
+ metadata.gz: 415d14c2937cc1dcedbbf57b32ab27d028c5cded
4
+ data.tar.gz: 7ff178f1d99806898d1d68cfbf4db0341d8cfc06
5
5
  SHA512:
6
- metadata.gz: e1baf5d875569065353f05d2c6037bf4fad21a0e29db8378ee710fd5bbbbfdced8fa7470a91d698c7a21a1bda4b7149dcd8b55ef44197e3ea9f2575581c076ef
7
- data.tar.gz: d28d6f9dc204c43d2a0e61c17bb7fd685e14246ae9f4621d3934fa71246c84e9777cd546eb4d3d2f39958d4d7077a5d5d1aa68f525e77389cbbbe8cf35ed1bdb
6
+ metadata.gz: ac9f60be168c0e9d38b00b5c7f41e9168c6a7cc064f0a081164b0b8c8f5aa72248e8704fc9633484b8112bb4a3db4e57b6beb5bca9869ed42f321567337d8620
7
+ data.tar.gz: b9963c219ddd67f2e51cfdd94d337f19ec2617aa103c85090f2633c068cf5dbe7816b700f7d371fc496196b0bafc7c050a8ffb915f408b2d53a1a43368f399f2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.0.3
2
+
3
+ - Realiable push
4
+ - Heartbeat mechanism
5
+
1
6
  ## 0.0.2
2
7
 
3
8
  - Update gemsupport from `0.4.1` to `0.5.0`
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- start_her (0.0.1)
4
+ start_her (0.0.2)
5
5
  gemsupport (~> 0.4)
6
6
  logstash-logger (~> 0.15)
7
7
  msgpack (~> 0.7)
@@ -18,11 +18,11 @@ GEM
18
18
  diff-lcs (1.2.5)
19
19
  gemsupport (0.5.0)
20
20
  logstash-event (1.2.02)
21
- logstash-logger (0.15.1)
21
+ logstash-logger (0.15.2)
22
22
  logstash-event (~> 1.2)
23
23
  stud
24
24
  method_source (0.8.2)
25
- msgpack (0.7.0)
25
+ msgpack (0.7.1)
26
26
  parser (2.2.3.0)
27
27
  ast (>= 1.1, < 3.0)
28
28
  powerpack (0.1.1)
@@ -31,7 +31,7 @@ GEM
31
31
  method_source (~> 0.8.1)
32
32
  slop (~> 3.4)
33
33
  rainbow (2.0.0)
34
- redis (3.2.1)
34
+ redis (3.2.2)
35
35
  redis-namespace (1.5.2)
36
36
  redis (~> 3.0, >= 3.0.4)
37
37
  rspec (3.3.0)
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  [![Circle CI](https://circleci.com/gh/PredicSis/start_her/tree/master.svg?style=shield)](https://circleci.com/gh/PredicSis/start_her/tree/master)
2
2
 
3
3
  # StartHer
4
- Microservices common stuff. Implement redis subscription and logging strategy.
4
+
5
+ Microservices common stuff. It implements Redis PubSub and Logstash logging strategy.
5
6
 
6
7
  ## Requirements
7
8
 
@@ -29,32 +30,131 @@ StartHer.configure do |config|
29
30
  config.logstash_url = 'redis://localhost:6379' # if using default logger in production
30
31
  end
31
32
  ```
33
+ > See `lib/start_her/configuration.rb` for all configuration variables.
32
34
 
33
35
  ## Usage
34
36
 
37
+ ### Publisher
38
+
39
+ ```rb
40
+ class MyClass
41
+ include StartHer::Publisher
42
+
43
+ def do_something
44
+ # ...
45
+ reliable_publish('channel_one', 'succeed') # or publish('channel_one', 'succeed') for non persistent events
46
+ end
47
+ end
48
+ ```
49
+
50
+ By default Redis has no **persistent PubSub**, so a simple mechanism is implemented to attempt to resolve this.
51
+ - Each day a new persistent backlog is created for a given channel
52
+ - Each backlog lives one week according to `StartHer.config.backlog_ttl`
53
+
54
+ ### Subscriber
55
+
35
56
  ```bash
36
57
  bundle exec starther ./lib/path/to/subscriber.rb
37
58
  ```
38
59
 
39
60
  **Note**: You need to put your `subscriber.rb` file into the `lib` directory.
40
61
 
41
- ## Subscriber class
42
62
 
43
63
  ```ruby
44
64
  class Subscriber
45
65
  include StartHer::Subscriber
46
66
 
67
+ # Defaults are defined by StartHer::Subscriber::DEFAULT_OPTS
47
68
  subscriber_options channels: ['channel_one', 'channel_two']
69
+
70
+ # Customization of error
71
+ #
72
+ # This block is not mandatory
48
73
  subscriber_error do |error|
49
74
  # do something with error
50
75
  end
51
76
 
77
+ # Perform an action when Subscriber subscribes on a channel
78
+ #
79
+ # This block is not mandatory
80
+ subscriber_on_psubscribe do |channel|
81
+ # do something at channel subscription time
82
+ end
83
+
84
+ # Customization of heartbeat response
85
+ # Each instance of Subscriber listens `hb_ping' channel and sends a response on `hb_pong'
86
+ # channel for service heartbeat
87
+ #
88
+ # This block is not mandatory
89
+ subscriber_heartbeat do |response|
90
+ # do something with response
91
+ end
92
+
52
93
  def process_message(channel, message)
53
94
  # your custom stuff here
54
95
  end
55
96
  end
56
97
  ```
57
98
 
99
+ Heartbeat ping message format:
100
+ ```json
101
+ {
102
+ "id": "643976ec-fc7c-4cd8-95bb-85a74f1987de",
103
+ "generated_at": "2015-11-23 10:34:05 UTC",
104
+ "data": {
105
+ "id": "058e216d5ce76492b574b39584d0676f359207e05a6a1dd30f238fc5604a66b2",
106
+ "generated_at": "2015-11-23 10:34:05 UTC",
107
+ "service_name": "MyPingService",
108
+ "version": "0.0.1"
109
+ }
110
+ }
111
+ ```
112
+
113
+ ## Testing
114
+
115
+ StartHer provides a few options for testing your micro-service.
116
+
117
+ ### Setup
118
+
119
+ StartHer allows you to dynamically configure the testing harness with the following methods:
120
+ ```rb
121
+ require 'start_her/testing'
122
+ StartHer::Testing.fake! # fake is the default mode
123
+ # StartHer::Testing.disable!
124
+
125
+ RSpec.configure do |config|
126
+ config.before(:each) do
127
+ StartHer::Testing.clear_stubed_redis
128
+ end
129
+ end
130
+ ```
131
+
132
+ To query the current state, use the following methods:
133
+ ```rb
134
+ StartHer::Testing.fake?
135
+ StartHer::Testing.disable?
136
+ ```
137
+
138
+ ### Testing PubSub
139
+ ```rb
140
+ require 'start_her/testing'
141
+
142
+ describe Subscriber do
143
+ let(:publisher) do
144
+ ->(chan, msg) { Object.new.extend(StartHer::Publisher).reliable_publish(chan, msg) }
145
+ end
146
+ let(:chan) { 'channel_one' }
147
+ let(:msg) { 'my message' }
148
+
149
+ describe '#process_message' do
150
+ it 'processes message' do
151
+ expect(subject).to receive(:process_message).with(chan, msg)
152
+ publisher.call(chan, msg)
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
58
158
  ## LICENSE
59
159
 
60
160
  MIT
@@ -1,11 +1,12 @@
1
1
  module StartHer
2
2
  class Configuration
3
- attr_accessor :redis, :logstash_url
3
+ attr_accessor :redis, :logstash_url, :backlog_ttl
4
4
  attr_writer :logger
5
5
 
6
6
  def initialize
7
7
  @redis = {}
8
8
  @logstash_url = ''
9
+ @backlog_ttl = 60 * 60 * 24 * 7 # 1.week
9
10
  end
10
11
 
11
12
  def logger
@@ -0,0 +1,35 @@
1
+ module StartHer
2
+ class Heartbeat
3
+ include StartHer::Publisher
4
+ include StartHer::Utils
5
+
6
+ attr_reader :request, :options
7
+ attr_accessor :response
8
+
9
+ CHANNELS = { in: 'hb_ping', out: 'hb_pong' }
10
+ DEFAULT_OPTS = { heartbeat: CHANNELS }
11
+
12
+ def self.call(request, service_klass, opts = {}, &subscriber_heartbeat_block)
13
+ new(request, service_klass, opts, &subscriber_heartbeat_block).call
14
+ end
15
+
16
+ def initialize(request, service_klass, opts = {}, &subscriber_heartbeat_block)
17
+ @request = request
18
+ @subscriber_heartbeat_block = subscriber_heartbeat_block
19
+ @options = DEFAULT_OPTS.merge opts
20
+ @response = {
21
+ id: msid(service_klass),
22
+ generated_at: Time.now.utc.to_s,
23
+ service_name: service_klass.to_s,
24
+ ping: request['data']
25
+ }
26
+ @response[:version] = service_klass::VERSION if defined? service_klass::VERSION
27
+ end
28
+
29
+ def call
30
+ @subscriber_heartbeat_block.call(response) if @subscriber_heartbeat_block
31
+ publish(options[:heartbeat][:out],
32
+ id: SecureRandom.uuid, generated_at: Time.now.utc.to_s, data: response)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ module StartHer
2
+ module Publisher
3
+ include StartHer::RedisClient
4
+ using StartHer::Refinements
5
+
6
+ def publish(channel, message)
7
+ exponential_backoff({}, ::Redis::BaseConnectionError) do
8
+ client.publish(channel, message.to_msgpack)
9
+ end
10
+ rescue => e
11
+ StartHer.logger.error e
12
+ raise e
13
+ end
14
+
15
+ def reliable_publish(channel, message)
16
+ exponential_backoff({}, ::Redis::BaseConnectionError) do
17
+ client.multi do |multi|
18
+ backlog_ttl(channel)
19
+ multi.lpush(channel.to_current_backlog, message.to_msgpack)
20
+ multi.publish(channel, message.to_msgpack)
21
+ end
22
+ end
23
+ rescue => e
24
+ StartHer.logger.error e
25
+ raise e
26
+ end
27
+
28
+ private
29
+
30
+ # Each day a new persistent backlog is created for a given channel
31
+ # Each backlog lives one week according to `StartHer.config.backlog_ttl'
32
+ def backlog_ttl(channel)
33
+ return if client.ttl(channel.to_current_backlog) >= 0
34
+ client.expire(channel.to_current_backlog, StartHer.config.backlog_ttl)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ module StartHer
2
+ module Refinements
3
+ refine String do
4
+ def to_current_backlog
5
+ self + ":backlog_#{Time.now.utc.strftime('%Y%m%d')}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ require 'start_her/refinements/string'
@@ -1,8 +1,11 @@
1
1
  module StartHer
2
2
  module Subscriber
3
3
  include StartHer::RedisClient
4
+ include StartHer::Utils
4
5
 
5
- DEFAULT_OPTS = { channels: ['*'] }
6
+ attr_reader :channels, :options, :subscriber_heartbeat_block, :subscriber_on_psubscribe_block
7
+
8
+ DEFAULT_OPTS = { channels: ['*'], heartbeat: Heartbeat::CHANNELS }
6
9
  DEFAULT_ERROR_BLOCK = lambda do |e|
7
10
  StartHer.logger.error e
8
11
  end
@@ -12,22 +15,19 @@ module StartHer
12
15
  end
13
16
 
14
17
  module ClassMethods
15
- attr_accessor :subscriber_options_hash, :subscriber_error_block
18
+ attr_accessor :subscriber_options_hash, :subscriber_error_block, :subscriber_heartbeat_block,
19
+ :subscriber_on_psubscribe_block
16
20
 
17
21
  def run!
18
- new.tap do |instance|
19
- instance.psubscribe(subscriber_options_hash[:channels]) do |channel, message|
20
- begin
21
- instance.process_message(channel, message)
22
- rescue => e
23
- if block_given?
24
- subscriber_error_block.call(e)
25
- else
26
- DEFAULT_ERROR_BLOCK.call(e)
27
- end
28
- end
29
- end
30
- end
22
+ new(subscriber_options_hash[:channels]).run!
23
+ end
24
+
25
+ def subscriber_heartbeat(&block)
26
+ self.subscriber_heartbeat_block = block
27
+ end
28
+
29
+ def subscriber_on_psubscribe(&block)
30
+ self.subscriber_on_psubscribe_block = block
31
31
  end
32
32
 
33
33
  def subscriber_error(&block)
@@ -39,17 +39,48 @@ module StartHer
39
39
  end
40
40
  end
41
41
 
42
+ def initialize(channels)
43
+ @channels = channels
44
+ @options = self.class.subscriber_options_hash || DEFAULT_OPTS
45
+ @subscriber_heartbeat_block = self.class.subscriber_heartbeat_block || ->(_response) {}
46
+ @subscriber_on_psubscribe_block = self.class.subscriber_on_psubscribe_block || ->(_chan) {}
47
+ end
48
+
49
+ def run!
50
+ psubscribe(channels) do |channel, message|
51
+ begin
52
+ process_message(channel, message)
53
+ rescue => e
54
+ if block_given?
55
+ subscriber_error_block.call(e)
56
+ else
57
+ DEFAULT_ERROR_BLOCK.call(e)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
42
63
  # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
43
64
  def psubscribe(channels, &block)
44
65
  exponential_backoff({}, ::Redis::BaseConnectionError) do
45
- client.psubscribe(*channels) do |on|
66
+ client.psubscribe(options[:heartbeat][:in], *channels) do |on|
46
67
  on.psubscribe do |channel, _|
47
68
  StartHer.logger.info "PSuscribe on #{channel}"
69
+
70
+ resync_messages(client, channel, &block)
71
+ subscriber_on_psubscribe_block.call(unamespace(channel))
48
72
  end
49
73
 
50
74
  on.pmessage do |_pattern, channel, message|
51
- chan = channel.include?(':') ? channel.split(':').last : channel
52
- block.call(chan, MessagePack.unpack(message))
75
+ chan = unamespace(channel)
76
+ msg = MessagePack.unpack(message)
77
+
78
+ if chan == options[:heartbeat][:in]
79
+ Heartbeat.call(msg, service_klass,
80
+ heartbeat: options[:heartbeat], &subscriber_heartbeat_block)
81
+ else
82
+ block.call(chan, msg)
83
+ end
53
84
  end
54
85
  end
55
86
  end
@@ -58,5 +89,23 @@ module StartHer
58
89
  raise e
59
90
  end
60
91
  # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
92
+
93
+ def resync_messages(client, channel, &block)
94
+ StartHer.logger.info "Re-synchronize message from #{channel}"
95
+
96
+ # Retrieve all backlogs for a given channel
97
+ client.keys(unamespace(channel) + '*').each do |backlog|
98
+ # Retrieve all messages for a given backlog
99
+ client.lrange(backlog, 0, -1).each do |message|
100
+ block.call(backlog.split(':').first, MessagePack.unpack(message))
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def unamespace(channel)
108
+ channel.split(':').last
109
+ end
61
110
  end
62
111
  end
@@ -0,0 +1,192 @@
1
+ module StartHer
2
+ module Testing
3
+ module_function
4
+
5
+ attr_accessor :__test_mode
6
+
7
+ # rubocop:disable Style/GlobalVars
8
+ def clear_stubed_redis
9
+ $stubed_redis.clear_db if $stubed_redis
10
+ end
11
+ # rubocop:enable Style/GlobalVars
12
+
13
+ # rubocop:disable Style/TrivialAccessors
14
+ def __set_test_mode(mode)
15
+ @__test_mode = mode
16
+ end
17
+ # rubocop:enable Style/TrivialAccessors
18
+
19
+ def disable!
20
+ __set_test_mode(:disable)
21
+ end
22
+
23
+ def fake!
24
+ __set_test_mode(:fake)
25
+ end
26
+
27
+ def disable?
28
+ @__test_mode == :disable
29
+ end
30
+
31
+ def fake?
32
+ @__test_mode == :fake
33
+ end
34
+ end
35
+ Testing.fake!
36
+
37
+ class StubedRedis
38
+ attr_reader :namespace, :db
39
+
40
+ def initialize(namespace:)
41
+ @namespace = namespace
42
+ @db = {
43
+ lists: {},
44
+ lists_expiration: {},
45
+ channels: {},
46
+ strings: {}
47
+ }
48
+ end
49
+
50
+ def clear_db
51
+ @db = {
52
+ lists: {},
53
+ lists_expiration: {},
54
+ channels: {},
55
+ strings: {}
56
+ }
57
+ end
58
+
59
+ # Keys command
60
+ def multi
61
+ yield self
62
+ end
63
+
64
+ # Keys command
65
+ def keys(pattern)
66
+ lists.keys.select { |key| key.match(pattern.gsub '**', '*') }
67
+ end
68
+
69
+ # Keys command
70
+ def ttl(key)
71
+ if lists_expiration[key]
72
+ lists_expiration[key]
73
+ elsif lists[key] || channels[key]
74
+ -1
75
+ else
76
+ -2
77
+ end
78
+ end
79
+
80
+ # Keys command
81
+ def expire(key, ttl)
82
+ lists_expiration[key] = ttl
83
+ end
84
+
85
+ # String command
86
+ def get(key)
87
+ strings[key]
88
+ end
89
+
90
+ # String command
91
+ def getset(key, value)
92
+ tmp = strings[key]
93
+ strings[key] = value
94
+ tmp
95
+ end
96
+
97
+ def setnx(key, value)
98
+ if strings[key]
99
+ 0
100
+ else
101
+ strings[key] = value
102
+ 1
103
+ end
104
+ end
105
+
106
+ # List command
107
+ def lpush(list, data)
108
+ (lists[fullkey(list)] ||= []).insert(0, data)
109
+ end
110
+
111
+ # List command
112
+ def lrange(list, min, max)
113
+ lists[fullkey(list)][min..max]
114
+ end
115
+
116
+ # PubSub command
117
+ # rubocop:disable Metric/AbcSize,Style/MultilineOperationIndentation
118
+ def publish(channel, message)
119
+ return unless channels[fullkey(channel)] && channels[fullkey(channel)].callbacks &&
120
+ channels[fullkey(channel)].callbacks[:pmessage]
121
+ channels[fullkey(channel)].callbacks[:pmessage].call(channel, channel, message)
122
+ end
123
+ # rubocop:enable Metric/AbcSize,Style/MultilineOperationIndentation
124
+
125
+ # PubSub command
126
+ # rubocop:disable Metrics/AbcSize
127
+ def psubscribe(*channels)
128
+ channels.each do |channel|
129
+ db[:channels][fullkey(channel)] = Subscription.new
130
+ yield db[:channels][fullkey(channel)]
131
+ db[:channels][fullkey(channel)].callbacks[:psubscribe].call(channel, 0)
132
+ end
133
+ end
134
+ # rubocop:enable Metrics/AbcSize
135
+
136
+ class Subscription
137
+ attr_reader :callbacks
138
+
139
+ def initialize
140
+ @callbacks = {}
141
+ end
142
+
143
+ def psubscribe(&block)
144
+ callbacks[:psubscribe] = block
145
+ end
146
+
147
+ def pmessage(&block)
148
+ callbacks[:pmessage] = block
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def fullkey(key)
155
+ if namespace
156
+ "#{namespace}:#{key}"
157
+ else
158
+ key
159
+ end
160
+ end
161
+
162
+ def lists
163
+ db[:lists]
164
+ end
165
+
166
+ def lists_expiration
167
+ db[:lists_expiration]
168
+ end
169
+
170
+ def channels
171
+ db[:channels]
172
+ end
173
+
174
+ def strings
175
+ db[:strings]
176
+ end
177
+ end
178
+
179
+ module RedisClient
180
+ alias_method :real_client, :client
181
+ # rubocop:disable Style/GlobalVars
182
+ def client(options = {})
183
+ if Testing.fake?
184
+ options = client_opts(options)
185
+ $stubed_redis ||= StubedRedis.new(namespace: options[:namespace])
186
+ else
187
+ real_client(options)
188
+ end
189
+ end
190
+ # rubocop:enable Style/GlobalVars
191
+ end
192
+ end
@@ -0,0 +1,28 @@
1
+ module StartHer
2
+ module Utils
3
+ def msid(*args)
4
+ if StartHer.env == 'development'
5
+ # Easy understanding identifier in development environment
6
+ "#{args.first || service_klass}:#{object_id}"
7
+ else
8
+ Digest::SHA256.hexdigest(fingerprint(*args))
9
+ end
10
+ end
11
+
12
+ def fingerprint(*args)
13
+ args ||= []
14
+ [Socket.gethostname, service_klass].concat(args).map(&:to_s).join('')
15
+ end
16
+
17
+ # If the current class is `MyService::Subscriber'
18
+ # It returns `MyService' object
19
+ def service_klass
20
+ if (klass = Object.const_get(self.class.name.split('::').first)) == StartHer
21
+ # Find the class name of the caller if `klass' is StartHer
22
+ binding.receiver.class.name.split('::').first
23
+ else
24
+ klass
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module StartHer
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
data/lib/start_her.rb CHANGED
@@ -3,6 +3,8 @@ require 'gemsupport'
3
3
  require 'start_her/configuration'
4
4
  require 'start_her/logger'
5
5
  require 'start_her/retry_policies'
6
+ require 'start_her/refinements'
7
+ require 'start_her/utils'
6
8
 
7
9
  module StartHer
8
10
  module_function
@@ -29,4 +31,6 @@ module StartHer
29
31
  # Safe loading for stuff that needs MailHer#config
30
32
  autoload :RedisClient, 'start_her/redis_client'
31
33
  autoload :Subscriber, 'start_her/subscriber'
34
+ autoload :Publisher, 'start_her/publisher'
35
+ autoload :Heartbeat, 'start_her/heartbeat'
32
36
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'start_her'
2
+ require 'start_her/testing'
2
3
 
3
4
  ENV['RACK_ENV'] ||= 'test'
4
5
 
@@ -8,6 +9,10 @@ Dir[File.join(ROOT_SPEC, 'spec', 'support', '**', '*.rb')].each { |f| require f
8
9
  RSpec.configure do |config|
9
10
  config.include Gemsupport::Error
10
11
 
12
+ config.before(:each) do
13
+ StartHer::Testing.clear_stubed_redis
14
+ end
15
+
11
16
  config.order = 'random'
12
17
  config.run_all_when_everything_filtered = true
13
18
  config.filter_run :focus
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ module StartHer
4
+ describe Configuration do
5
+ describe '#initialize' do
6
+ it 'contains default Redis options' do
7
+ expect(subject.redis).to eq({})
8
+ end
9
+
10
+ it 'contains default Logstash URL' do
11
+ expect(subject.logstash_url).to eq('')
12
+ end
13
+
14
+ it 'contains default logger' do
15
+ expect(subject.logger).to be_a(::LogStashLogger)
16
+ end
17
+
18
+ it 'contains default backlog TTL' do
19
+ expect(subject.backlog_ttl).to eq(604_800)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ module StartHer
4
+ describe Heartbeat do
5
+ include Utils
6
+
7
+ let(:uuid) { 'bb8dd4f2-5e3c-420e-9b18-535a335052a2' }
8
+
9
+ before do
10
+ allow(SecureRandom).to receive(:uuid).and_return(uuid)
11
+ end
12
+
13
+ let(:request) { { 'data' => { service: 'ping_service' } } }
14
+ let(:block) { ->(response) { response[:custom_key] = 'custom' } }
15
+ subject { Heartbeat.new(request, StartHer, &block) }
16
+ let(:raw_response) do
17
+ {
18
+ id: subject.msid(StartHer),
19
+ generated_at: Time.now.utc.to_s,
20
+ service_name: 'StartHer',
21
+ version: StartHer::VERSION,
22
+ ping: request['data']
23
+ }
24
+ end
25
+ let(:response) do
26
+ {
27
+ id: uuid,
28
+ generated_at: Time.now.utc.to_s,
29
+ data: raw_response
30
+ }
31
+ end
32
+ let(:raw_response_2) { raw_response.merge(custom_key: 'custom') }
33
+ let(:response_2) { response.merge(data: raw_response_2) }
34
+
35
+ describe '.call' do
36
+ it 'publishes the pong message' do
37
+ expect_any_instance_of(Heartbeat).to receive(:publish)
38
+ .with('hb_pong', response_2)
39
+ Heartbeat.call(request, StartHer, &block)
40
+ end
41
+ end
42
+
43
+ describe '#initialize' do
44
+ it 'formats response' do
45
+ expect(subject.response).to eq(raw_response)
46
+ end
47
+ end
48
+
49
+ describe '#call' do
50
+ it 'publishes the pong message' do
51
+ expect(subject).to receive(:publish)
52
+ .with('hb_pong', response_2)
53
+ subject.call
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ module StartHer
4
+ describe Publisher do
5
+ class TestPublisher
6
+ include Publisher
7
+ end
8
+ subject { TestPublisher.new }
9
+ let(:channel) { 'channel' }
10
+ let(:message) { 'message' }
11
+
12
+ describe '#publish' do
13
+ using StartHer::Refinements
14
+
15
+ it 'publishes the message' do
16
+ expect_any_instance_of(StubedRedis).to receive(:publish)
17
+ .with(channel, message.to_msgpack)
18
+
19
+ subject.publish(channel, message)
20
+ end
21
+ end
22
+
23
+ describe '#reliable_publish' do
24
+ using StartHer::Refinements
25
+
26
+ it 'backlogs the message' do
27
+ expect_any_instance_of(StubedRedis).to receive(:lpush)
28
+ .with(channel.to_current_backlog, message.to_msgpack)
29
+
30
+ subject.reliable_publish(channel, message)
31
+ end
32
+
33
+ it 'adds TTL on backlog' do
34
+ subject.reliable_publish(channel, message)
35
+
36
+ expect(subject.send(:client).ttl(channel.to_current_backlog)).to eq(604_800)
37
+ end
38
+
39
+ it 'publishes the message' do
40
+ expect_any_instance_of(StubedRedis).to receive(:publish)
41
+ .with(channel, message.to_msgpack)
42
+
43
+ subject.reliable_publish(channel, message)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -7,6 +7,8 @@ module StartHer
7
7
  end
8
8
 
9
9
  subject { RedisClientTest.new }
10
+ before(:each) { StartHer::Testing.disable! }
11
+ after(:each) { StartHer::Testing.fake! }
10
12
 
11
13
  describe '#client' do
12
14
  context 'with all options' do
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ module StartHer
4
+ describe 'String refinements' do
5
+ using Refinements
6
+
7
+ describe '#to_current_backlog' do
8
+ it 'defines backlogs pattern' do
9
+ expect('channel'.to_current_backlog)
10
+ .to eq("channel:backlog_#{Time.now.utc.strftime('%Y%m%d')}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -2,22 +2,70 @@ require 'spec_helper'
2
2
 
3
3
  module StartHer
4
4
  describe Subscriber do
5
- let(:channels) { ['channel'] }
5
+ let(:channels) { ['channel.1', 'channel.2'] }
6
6
 
7
7
  class TestSubscriber
8
8
  include Subscriber
9
9
 
10
- subscriber_options channels: ['channel']
10
+ subscriber_options channels: ['channel.*']
11
11
 
12
12
  def process_message(_channel, _message)
13
13
  end
14
14
  end
15
+ subject { TestSubscriber.new(['channel.*']) }
15
16
 
16
17
  describe '.run!' do
17
18
  it 'subscribes to channels' do
18
- expect_any_instance_of(Subscriber).to receive(:psubscribe).with(channels)
19
+ expect_any_instance_of(Subscriber).to receive(:psubscribe).with(['channel.*'])
19
20
  TestSubscriber.run!
20
21
  end
21
22
  end
23
+
24
+ describe '#resync_message' do
25
+ let(:message) { 'my message' }
26
+ let(:publisher) do
27
+ ->(chan, msg) { Object.new.extend(StartHer::Publisher).reliable_publish(chan, msg) }
28
+ end
29
+
30
+ before do
31
+ publisher.call(channels.first, message)
32
+ publisher.call(channels.last, message)
33
+ end
34
+
35
+ it 'resynchronizes messages' do
36
+ expect(subject).to receive(:process_message)
37
+ .with(channels.first, message).ordered
38
+ expect(subject).to receive(:process_message)
39
+ .with(channels.last, message).ordered
40
+
41
+ subject.run!
42
+ end
43
+ end
44
+
45
+ describe '#psubscribe' do
46
+ context 'when it receives a heartbeat' do
47
+ let(:ping) do
48
+ {
49
+ 'id' => Object.new.extend(StartHer::Utils).msid,
50
+ 'timestamp' => Time.now.utc.to_s,
51
+ 'service' => 'KeepHer'
52
+ }
53
+ end
54
+ let(:publisher) do
55
+ ->(chan, msg) { Object.new.extend(StartHer::Publisher).publish(chan, msg) }
56
+ end
57
+
58
+ before do
59
+ subject.run!
60
+ end
61
+
62
+ it 'calls the heartbeat responder' do
63
+ expect(Heartbeat).to receive(:call)
64
+ .with(ping, 'StartHer', heartbeat: subject.options[:heartbeat])
65
+
66
+ publisher.call(subject.options[:heartbeat][:in], ping)
67
+ end
68
+ end
69
+ end
22
70
  end
23
71
  end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ module StartHer
4
+ describe Utils do
5
+ subject { Object.new.extend(Utils) }
6
+
7
+ describe '#msid' do
8
+ it 'returns a microservice id' do
9
+ expect(subject.msid('StartHer')).to match(/[a-f0-9]+/)
10
+ end
11
+ end
12
+
13
+ describe '#fingerprint' do
14
+ it 'returns a microservice fingerprint' do
15
+ expect(subject.fingerprint('StartHer'))
16
+ .to eq([Socket.gethostname, 'Object', 'StartHer'].map(&:to_s).join(''))
17
+ end
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: start_her
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - abourquin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-11-03 00:00:00.000000000 Z
12
+ date: 2015-12-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -143,15 +143,26 @@ files:
143
143
  - circle.yml
144
144
  - lib/start_her.rb
145
145
  - lib/start_her/configuration.rb
146
+ - lib/start_her/heartbeat.rb
146
147
  - lib/start_her/logger.rb
148
+ - lib/start_her/publisher.rb
147
149
  - lib/start_her/redis_client.rb
150
+ - lib/start_her/refinements.rb
151
+ - lib/start_her/refinements/string.rb
148
152
  - lib/start_her/retry_policies.rb
149
153
  - lib/start_her/subscriber.rb
154
+ - lib/start_her/testing.rb
155
+ - lib/start_her/utils.rb
150
156
  - lib/start_her/version.rb
151
157
  - spec/spec_helper.rb
158
+ - spec/start_her/configuration_spec.rb
159
+ - spec/start_her/heartbeat_spec.rb
160
+ - spec/start_her/publisher_spec.rb
152
161
  - spec/start_her/redis_client_spec.rb
162
+ - spec/start_her/refinements/string_spec.rb
153
163
  - spec/start_her/retry_policies_spec.rb
154
164
  - spec/start_her/subscriber_spec.rb
165
+ - spec/start_her/utils_spec.rb
155
166
  - spec/start_her_spec.rb
156
167
  - spec/support/custom_helpers.rb
157
168
  - start_her.gemspec
@@ -175,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
186
  version: '0'
176
187
  requirements: []
177
188
  rubyforge_project:
178
- rubygems_version: 2.2.2
189
+ rubygems_version: 2.4.7
179
190
  signing_key:
180
191
  specification_version: 4
181
192
  summary: StartHer