attention 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/README.md +54 -1
- data/attention.gemspec +1 -0
- data/examples/awareness.rb +11 -0
- data/lib/attention/connection.rb +4 -0
- data/lib/attention/instance.rb +46 -7
- data/lib/attention/publisher.rb +10 -8
- data/lib/attention/redis_pool.rb +6 -0
- data/lib/attention/subscriber.rb +30 -7
- data/lib/attention/timer.rb +12 -1
- data/lib/attention/version.rb +1 -1
- data/lib/attention.rb +41 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a3c5fb2f9ae19b5dcfb4eb7fff6d17dc33cdc4a
|
4
|
+
data.tar.gz: d7adfc321e1f26471f6f1f0d4099b06136ae92fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b53f4dde91d7de78f85e826d92dce285b79e8536e29af80df9b31f77befa88e54d6962bc8413225cdc510f2b62d7bcd3c89ae923d77176cc32dd6bb2685b7f48
|
7
|
+
data.tar.gz: 6beac8bb89eaf9b221b31f4cb220da1c125950462037c089110dafbb319e7a7cbd0239b04e27f70ca763ba345c7883ccd77882f017c031d7b3b9d30e3b33333e
|
data/README.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# Attention
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/parrish/Attention.svg?branch=master)](https://travis-ci.org/parrish/Attention)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/parrish/Attention/badges/coverage.svg)](https://codeclimate.com/github/parrish/Attention)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/parrish/Attention/badges/gpa.svg)](https://codeclimate.com/github/parrish/Attention)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/attention.svg)](http://badge.fury.io/rb/attention)
|
7
|
+
|
3
8
|
Redis-based server awareness for distributed applications
|
4
9
|
|
5
10
|
## Installation
|
@@ -20,7 +25,56 @@ Or install it yourself as:
|
|
20
25
|
|
21
26
|
## Usage
|
22
27
|
|
28
|
+
Activate the instance:
|
29
|
+
```ruby
|
30
|
+
# Autodiscover the ip and exclude the port
|
31
|
+
Attention.activate
|
32
|
+
|
33
|
+
# Or specify them explicitly
|
34
|
+
Attention.activate ip: '1.2.3.4', port: 9000
|
35
|
+
```
|
36
|
+
|
37
|
+
The current instance is accessible at:
|
38
|
+
```ruby
|
39
|
+
Attention.instance
|
40
|
+
```
|
41
|
+
|
42
|
+
Deactivate the instance:
|
43
|
+
```ruby
|
44
|
+
Attention.deactivate
|
45
|
+
```
|
46
|
+
|
47
|
+
Subscribe to instance availability changes:
|
48
|
+
```ruby
|
49
|
+
Attention.on_change do |change, instances|
|
50
|
+
# This block is asynchronously called on each change
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
Or get the list of available instances:
|
55
|
+
```ruby
|
56
|
+
Attention.instances
|
57
|
+
```
|
58
|
+
|
59
|
+
## Configuration
|
60
|
+
|
61
|
+
Options can be set on `Attention.options`
|
23
62
|
|
63
|
+
```ruby
|
64
|
+
Attention.options = {
|
65
|
+
namespace: 'attention', # Redis key namespace
|
66
|
+
ttl: 60, # Instance heartbeat TTL in seconds
|
67
|
+
redis_url: 'redis://localhost:6379/0', # Redis connection string
|
68
|
+
pool_size: 5, # Size of the publishing Redis connection pool
|
69
|
+
timeout: 5 # Redis connection timeout
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
## Notes
|
74
|
+
|
75
|
+
The top-level API provides a simple way to keep track of instance availability. More complex schemes of communication could be implemented by using the [`Subscriber`](http://www.rubydoc.info/github/parrish/attention/master/Attention/Subscriber) and [`Publisher`](http://www.rubydoc.info/github/parrish/attention/master/Attention/Publisher) classes directly.
|
76
|
+
|
77
|
+
Instances attempt to deactivate themselves when the program terminates(`at_exit`). If the instance crashes in a dramatic fashion (or a `kill -9`), the instance will continue to be listed as available until the TTL (`Attention.options[:ttl]`) expires.
|
24
78
|
|
25
79
|
## Development
|
26
80
|
|
@@ -36,4 +90,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/parris
|
|
36
90
|
## License
|
37
91
|
|
38
92
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
39
|
-
|
data/attention.gemspec
CHANGED
data/lib/attention/connection.rb
CHANGED
@@ -2,7 +2,11 @@ require 'redis'
|
|
2
2
|
require 'redis-namespace'
|
3
3
|
|
4
4
|
module Attention
|
5
|
+
# Provides a namespaced Redis connection
|
5
6
|
module Connection
|
7
|
+
# Creates a Redis connection
|
8
|
+
# @return [Redis] A namespaced Redis connection with configuration
|
9
|
+
# from Attention.options
|
6
10
|
def self.new
|
7
11
|
connection = Redis.new url: Attention.options[:redis_url],
|
8
12
|
connect_timeout: Attention.options[:timeout],
|
data/lib/attention/instance.rb
CHANGED
@@ -3,31 +3,62 @@ require 'attention/publisher'
|
|
3
3
|
require 'attention/timer'
|
4
4
|
|
5
5
|
module Attention
|
6
|
+
# A publishable representation of the current server
|
7
|
+
#
|
8
|
+
# When an instance is {#publish}ed, an event is sent in the format
|
9
|
+
# {
|
10
|
+
# 'added' => {
|
11
|
+
# 'id' => '123',
|
12
|
+
# 'ip' => '127.0.0.1',
|
13
|
+
# 'port' => 9000
|
14
|
+
# }
|
15
|
+
# }
|
16
|
+
#
|
17
|
+
# When an instance is {#unpublish}ed, an event is sent in the format
|
18
|
+
# {
|
19
|
+
# 'removed' => {
|
20
|
+
# 'id' => '123',
|
21
|
+
# 'ip' => '127.0.0.1',
|
22
|
+
# 'port' => 9000
|
23
|
+
# }
|
24
|
+
# }
|
6
25
|
class Instance
|
7
|
-
attr_reader :id
|
26
|
+
attr_reader :id
|
8
27
|
|
28
|
+
# @!visibility private
|
29
|
+
attr_reader :publisher
|
30
|
+
|
31
|
+
# Creates an Instance
|
32
|
+
# @param ip [String] Optionally override the IP of the server
|
33
|
+
# @param port [Fixnum, Numeric] Optionally specify the port of the server
|
9
34
|
def initialize(ip: nil, port: nil)
|
10
35
|
@id = Attention.redis.call.incr('instances').to_s
|
11
36
|
@ip = ip
|
12
37
|
@port = port
|
13
|
-
@publisher = Publisher.new
|
38
|
+
@publisher = Publisher.new
|
14
39
|
end
|
15
40
|
|
41
|
+
# Publishes this server and starts the {#heartbeat}
|
16
42
|
def publish
|
17
|
-
|
18
|
-
|
19
|
-
|
43
|
+
publisher.publish('instance', added: info) do |redis|
|
44
|
+
redis.setex "instance_#{ @id }", Attention.options[:ttl], JSON.dump(info)
|
45
|
+
end
|
20
46
|
heartbeat
|
21
47
|
end
|
22
48
|
|
49
|
+
# Unpublishes this server and stops the {#heartbeat}
|
23
50
|
def unpublish
|
24
51
|
return unless @heartbeat
|
25
|
-
|
26
|
-
|
52
|
+
publisher.publish('instance', removed: info) do |redis|
|
53
|
+
redis.del "instance_#{ @id }"
|
54
|
+
end
|
27
55
|
@heartbeat.stop
|
28
56
|
@heartbeat = nil
|
29
57
|
end
|
30
58
|
|
59
|
+
# Published information about this instance
|
60
|
+
# @return [Hash<id: Fixnum, ip: String, port: Numeric>]
|
61
|
+
# @option @return [Fixnum] :id The instance id
|
31
62
|
def info
|
32
63
|
{ id: @id, ip: ip }.tap do |h|
|
33
64
|
h[:port] = @port if @port
|
@@ -36,16 +67,24 @@ module Attention
|
|
36
67
|
|
37
68
|
private
|
38
69
|
|
70
|
+
# Uses a {Timer} to periodically tell Redis that this
|
71
|
+
# server is still online
|
72
|
+
# @!visibility public
|
73
|
+
# @api private
|
39
74
|
def heartbeat
|
40
75
|
@heartbeat ||= Timer.new(heartbeat_frequency) do
|
41
76
|
Attention.redis.call.expire "instance_#{ @id }", Attention.options[:ttl]
|
42
77
|
end
|
43
78
|
end
|
44
79
|
|
80
|
+
# The frequency of the {#heartbeat} is based on Attention.options[:ttl]
|
81
|
+
# @!visibility public
|
82
|
+
# @api private
|
45
83
|
def heartbeat_frequency
|
46
84
|
[1, Attention.options[:ttl] - 5].max
|
47
85
|
end
|
48
86
|
|
87
|
+
# Attempts to automatically discover the IP address of the server
|
49
88
|
def ip
|
50
89
|
return @ip if @ip
|
51
90
|
address = Socket.ip_address_list.find &:ipv4_private?
|
data/lib/attention/publisher.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
module Attention
|
2
|
+
# Uses Redis pub/sub to publish events
|
2
3
|
class Publisher
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
def publish(value)
|
4
|
+
# Publishes the value to the channel
|
5
|
+
# @param channel [String] The channel to publish to
|
6
|
+
# @param value [Object] The value to publish
|
7
|
+
# @yield Allows an optional block to use the Redis connection
|
8
|
+
# @yieldparam redis [Redis] The Redis connection
|
9
|
+
def publish(channel, value)
|
10
10
|
redis = Attention.redis.call
|
11
|
-
redis.publish
|
11
|
+
redis.publish channel, payload_for(value)
|
12
12
|
yield redis if block_given?
|
13
13
|
end
|
14
14
|
|
15
|
+
# Converts published values to JSON if possible
|
16
|
+
# @api private
|
15
17
|
def payload_for(value)
|
16
18
|
case value
|
17
19
|
when Array, Hash
|
data/lib/attention/redis_pool.rb
CHANGED
@@ -3,9 +3,12 @@ require 'redis-namespace'
|
|
3
3
|
require 'connection_pool'
|
4
4
|
|
5
5
|
module Attention
|
6
|
+
# A ConnectionPool of Redis connections used by {Publisher}s
|
6
7
|
class RedisPool
|
8
|
+
# @!visibility private
|
7
9
|
attr_reader :pool
|
8
10
|
|
11
|
+
# @return [RedisPool] A singleton instance of the ConnectionPool
|
9
12
|
def self.instance
|
10
13
|
@instance ||= new
|
11
14
|
@pool ||= ->{ @instance.pool.with{ |redis| redis } }
|
@@ -13,6 +16,9 @@ module Attention
|
|
13
16
|
|
14
17
|
private
|
15
18
|
|
19
|
+
# As this is a singleton, +RedisPool.new+ is not public
|
20
|
+
# @!visibility public
|
21
|
+
# @api private
|
16
22
|
def initialize
|
17
23
|
pool_config = {
|
18
24
|
size: Attention.options[:pool_size],
|
data/lib/attention/subscriber.rb
CHANGED
@@ -4,39 +4,62 @@ require 'attention/connection'
|
|
4
4
|
require 'attention/publisher'
|
5
5
|
|
6
6
|
module Attention
|
7
|
+
# Uses Redis pub/sub to asynchronously respond to events
|
8
|
+
#
|
9
|
+
# Each Subscriber uses a Redis connection to listen to a channel for events.
|
7
10
|
class Subscriber
|
8
|
-
|
11
|
+
# The channel subscribed to
|
12
|
+
attr_reader :channel
|
9
13
|
|
14
|
+
# @!visibility private
|
15
|
+
attr_reader :redis
|
16
|
+
|
17
|
+
# Raised when attempting to subscribe multiple times
|
18
|
+
#
|
19
|
+
# Rather than attempting to reuse a subscriber,
|
20
|
+
# unsubscribe and create a new one
|
10
21
|
class AlreadySubscribedError < StandardError; end
|
11
22
|
|
12
|
-
|
13
|
-
|
23
|
+
# Creates a subscription to the given channel
|
24
|
+
# @param channel [String] The channel to listen to
|
25
|
+
# @yield The code to execute on a published event
|
26
|
+
# @yieldparam channel [String] The channel the subscriber is listening to
|
27
|
+
# @yieldparam data [Object] The event published on the channel
|
28
|
+
def initialize(channel, &callback)
|
29
|
+
@channel = channel
|
14
30
|
@redis = Connection.new
|
15
31
|
subscribe &callback
|
16
32
|
end
|
17
33
|
|
34
|
+
# Sets up the Redis pub/sub subscription
|
35
|
+
# @yield The code to execute on a published event
|
36
|
+
# @raise [AlreadySubscribedError] If the subscriber is already subscribed
|
18
37
|
def subscribe(&callback)
|
19
38
|
raise AlreadySubscribedError.new if @thread
|
20
39
|
@thread = Thread.new do
|
21
|
-
redis.subscribe(
|
40
|
+
redis.subscribe(channel) do |on|
|
22
41
|
on.message do |channel, payload|
|
23
42
|
data = JSON.parse(payload) rescue payload
|
24
43
|
if data == 'unsubscribe'
|
25
44
|
redis.unsubscribe
|
26
45
|
else
|
27
|
-
callback.call
|
46
|
+
callback.call channel, data
|
28
47
|
end
|
29
48
|
end
|
30
49
|
end
|
31
50
|
end
|
32
51
|
end
|
33
52
|
|
53
|
+
# The {Publisher} used to send the unsubscribe message
|
54
|
+
# @api private
|
34
55
|
def publisher
|
35
|
-
@publisher ||= Publisher.new
|
56
|
+
@publisher ||= Publisher.new
|
36
57
|
end
|
37
58
|
|
59
|
+
# Unsubscribes from the channel
|
38
60
|
def unsubscribe
|
39
|
-
publisher.publish 'unsubscribe'
|
61
|
+
publisher.publish channel, 'unsubscribe'
|
62
|
+
@thread.kill
|
40
63
|
@thread = nil
|
41
64
|
end
|
42
65
|
end
|
data/lib/attention/timer.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
3
|
module Attention
|
4
|
+
# Periodic asynchronous code execution
|
4
5
|
class Timer
|
5
|
-
attr_reader :frequency
|
6
|
+
attr_reader :frequency
|
6
7
|
|
8
|
+
# @!visibility private
|
9
|
+
attr_reader :callback, :lock, :thread
|
10
|
+
|
11
|
+
# Creates and {#start}s the timer
|
12
|
+
# @param frequency [Numeric] How often to execute
|
13
|
+
# @yield The code to be executed
|
7
14
|
def initialize(frequency, &callback)
|
8
15
|
@frequency = frequency
|
9
16
|
@callback = callback
|
@@ -11,6 +18,7 @@ module Attention
|
|
11
18
|
start
|
12
19
|
end
|
13
20
|
|
21
|
+
# Starts the timer
|
14
22
|
def start
|
15
23
|
@thread ||= Thread.new do
|
16
24
|
loop do
|
@@ -22,10 +30,12 @@ module Attention
|
|
22
30
|
end
|
23
31
|
end
|
24
32
|
|
33
|
+
# @return [Boolean] True if the timer is started
|
25
34
|
def started?
|
26
35
|
!!thread
|
27
36
|
end
|
28
37
|
|
38
|
+
# Stops the timer if it's started
|
29
39
|
def stop
|
30
40
|
return if stopped?
|
31
41
|
lock.synchronize do
|
@@ -34,6 +44,7 @@ module Attention
|
|
34
44
|
end
|
35
45
|
end
|
36
46
|
|
47
|
+
# @return [Boolean] True if the timer is stopped
|
37
48
|
def stopped?
|
38
49
|
!started?
|
39
50
|
end
|
data/lib/attention/version.rb
CHANGED
data/lib/attention.rb
CHANGED
@@ -5,48 +5,85 @@ require 'attention/subscriber'
|
|
5
5
|
require 'attention/instance'
|
6
6
|
require 'attention/timer'
|
7
7
|
|
8
|
+
# The top-level API
|
9
|
+
#
|
10
|
+
# Default options:
|
11
|
+
# {
|
12
|
+
# namespace: 'attention', # Redis key namespace
|
13
|
+
# ttl: 60, # Instance heartbeat TTL in seconds
|
14
|
+
# redis_url: 'redis://localhost:6379/0', # Redis connection string
|
15
|
+
# pool_size: 5, # Size of the publishing Redis pool
|
16
|
+
# timeout: 5 # Redis connection timeout
|
17
|
+
# }
|
8
18
|
module Attention
|
9
19
|
class << self
|
20
|
+
# Configuration options
|
10
21
|
attr_accessor :options
|
22
|
+
|
23
|
+
# The server {Instance}
|
11
24
|
attr_reader :instance
|
12
25
|
end
|
13
26
|
|
14
27
|
self.options = {
|
15
28
|
namespace: 'attention', # Redis key namespace
|
16
|
-
ttl: 60, #
|
29
|
+
ttl: 60, # Instance heartbeat TTL in seconds
|
17
30
|
redis_url: 'redis://localhost:6379/0', # Redis connection string
|
18
31
|
pool_size: 5, # Size of the publishing Redis pool
|
19
32
|
timeout: 5 # Redis connection timeout
|
20
33
|
}
|
21
34
|
|
35
|
+
# Provides access to the {RedisPool} connections
|
36
|
+
# @return [Redis] A Redis connection
|
22
37
|
def self.redis
|
23
38
|
RedisPool.instance
|
24
39
|
end
|
25
40
|
|
41
|
+
# Publishes this server {Instance}
|
42
|
+
# @param ip [String] Optionally override the IP of the server
|
43
|
+
# @param port [Fixnum, Numeric] Optionally specify the port of the server
|
44
|
+
# @see Instance#publish
|
26
45
|
def self.activate(ip: nil, port: nil)
|
27
46
|
return if @instance
|
28
47
|
@instance = Instance.new ip: ip, port: port
|
29
48
|
instance.publish
|
30
49
|
end
|
31
50
|
|
51
|
+
# Unpublishes this server {Instance}
|
52
|
+
# @see Instance#unpublish
|
32
53
|
def self.deactivate
|
33
54
|
@instance.unpublish if @instance
|
34
55
|
end
|
35
56
|
|
57
|
+
# Uses a {Subscriber} to listen to changes to {Instance} statuses
|
58
|
+
# @yield The callback triggered on {Instance} changes
|
59
|
+
# @yieldparam change [Hash] The change event
|
60
|
+
# @yieldparam instances [Array<Hash>] The list of active {Instance}s
|
61
|
+
# @see Instance Format of the change events
|
62
|
+
# @see .instances Format of the instance information
|
36
63
|
def self.on_change(&callback)
|
37
64
|
Subscriber.new('instance') do |channel, change|
|
38
65
|
callback.call change, instances
|
39
66
|
end
|
40
67
|
end
|
41
68
|
|
69
|
+
# A list of the active {Instance}s
|
70
|
+
# @return [Array<Hash>]
|
71
|
+
# [
|
72
|
+
# { 'id' => '1', 'ip' => '127.0.0.1', 'port' => 3000 },
|
73
|
+
# { 'id' => '2', 'ip' => '127.0.0.1', 'port' => 3001 }
|
74
|
+
# ]
|
42
75
|
def self.instances
|
43
76
|
resolve info_for instance_keys
|
44
77
|
end
|
45
78
|
|
79
|
+
# Finds instance keys
|
80
|
+
# @!visibility private
|
46
81
|
def self.instance_keys
|
47
82
|
redis.call.keys 'instance_*'
|
48
83
|
end
|
49
84
|
|
85
|
+
# Maps the Redis key +get+s into a multi operation
|
86
|
+
# @!visibility private
|
50
87
|
def self.info_for(keys)
|
51
88
|
[].tap do |list|
|
52
89
|
redis.call.multi do |multi|
|
@@ -57,11 +94,14 @@ module Attention
|
|
57
94
|
end
|
58
95
|
end
|
59
96
|
|
97
|
+
# Resolves the list of future values from the multi operation
|
98
|
+
# @!visibility private
|
60
99
|
def self.resolve(list)
|
61
100
|
list.map do |future|
|
62
101
|
JSON.parse future.value
|
63
102
|
end
|
64
103
|
end
|
65
104
|
|
105
|
+
# Attempt to remove this instance when the server shuts down
|
66
106
|
at_exit{ deactivate }
|
67
107
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attention
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Parrish
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '4.5'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: yard
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
description: Redis-based server awareness for distributed applications
|
140
154
|
email:
|
141
155
|
- michael@zooniverse.org
|
@@ -155,6 +169,7 @@ files:
|
|
155
169
|
- attention.gemspec
|
156
170
|
- bin/console
|
157
171
|
- bin/setup
|
172
|
+
- examples/awareness.rb
|
158
173
|
- lib/attention.rb
|
159
174
|
- lib/attention/connection.rb
|
160
175
|
- lib/attention/instance.rb
|
@@ -188,3 +203,4 @@ signing_key:
|
|
188
203
|
specification_version: 4
|
189
204
|
summary: Redis-based server awareness
|
190
205
|
test_files: []
|
206
|
+
has_rdoc:
|