attention 0.0.1 → 0.0.2
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/.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
|
+
[](https://travis-ci.org/parrish/Attention)
|
4
|
+
[](https://codeclimate.com/github/parrish/Attention)
|
5
|
+
[](https://codeclimate.com/github/parrish/Attention)
|
6
|
+
[](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:
|