start_her 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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