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 +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +4 -4
- data/README.md +102 -2
- data/lib/start_her/configuration.rb +2 -1
- data/lib/start_her/heartbeat.rb +35 -0
- data/lib/start_her/publisher.rb +37 -0
- data/lib/start_her/refinements/string.rb +9 -0
- data/lib/start_her/refinements.rb +1 -0
- data/lib/start_her/subscriber.rb +67 -18
- data/lib/start_her/testing.rb +192 -0
- data/lib/start_her/utils.rb +28 -0
- data/lib/start_her/version.rb +1 -1
- data/lib/start_her.rb +4 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/start_her/configuration_spec.rb +23 -0
- data/spec/start_her/heartbeat_spec.rb +57 -0
- data/spec/start_her/publisher_spec.rb +47 -0
- data/spec/start_her/redis_client_spec.rb +2 -0
- data/spec/start_her/refinements/string_spec.rb +14 -0
- data/spec/start_her/subscriber_spec.rb +51 -3
- data/spec/start_her/utils_spec.rb +20 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 415d14c2937cc1dcedbbf57b32ab27d028c5cded
|
4
|
+
data.tar.gz: 7ff178f1d99806898d1d68cfbf4db0341d8cfc06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac9f60be168c0e9d38b00b5c7f41e9168c6a7cc064f0a081164b0b8c8f5aa72248e8704fc9633484b8112bb4a3db4e57b6beb5bca9869ed42f321567337d8620
|
7
|
+
data.tar.gz: b9963c219ddd67f2e51cfdd94d337f19ec2617aa103c85090f2633c068cf5dbe7816b700f7d371fc496196b0bafc7c050a8ffb915f408b2d53a1a43368f399f2
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
start_her (0.0.
|
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.
|
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.
|
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.
|
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
|
-
|
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 @@
|
|
1
|
+
require 'start_her/refinements/string'
|
data/lib/start_her/subscriber.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
module StartHer
|
2
2
|
module Subscriber
|
3
3
|
include StartHer::RedisClient
|
4
|
+
include StartHer::Utils
|
4
5
|
|
5
|
-
|
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.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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 =
|
52
|
-
|
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
|
data/lib/start_her/version.rb
CHANGED
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
|
@@ -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(
|
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.
|
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-
|
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.
|
189
|
+
rubygems_version: 2.4.7
|
179
190
|
signing_key:
|
180
191
|
specification_version: 4
|
181
192
|
summary: StartHer
|