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 +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
|
[](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
|