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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b48770e4d6245a630343d966765eac85af863ea0
4
- data.tar.gz: 2be0077411ce85b73a2cf2f4aac3438dad9b657d
3
+ metadata.gz: 7a3c5fb2f9ae19b5dcfb4eb7fff6d17dc33cdc4a
4
+ data.tar.gz: d7adfc321e1f26471f6f1f0d4099b06136ae92fe
5
5
  SHA512:
6
- metadata.gz: 48bfb49370698125f8cddac048e6fb7416377c146e26ec17ade58dd2ec182ec1758459dd1a94432267c1734ccbe10df74ee000a9ea754645ca54ac3f88947977
7
- data.tar.gz: 3707fda7d1a9d014db0f4314ff0c8dd02929d4dff7efb1662cc289a371496d4539b6bc0d4d951f643d1d9c782093822de906eb4f8679256e903069ba5fd296fc
6
+ metadata.gz: b53f4dde91d7de78f85e826d92dce285b79e8536e29af80df9b31f77befa88e54d6962bc8413225cdc510f2b62d7bcd3c89ae923d77176cc32dd6bb2685b7f48
7
+ data.tar.gz: 6beac8bb89eaf9b221b31f4cb220da1c125950462037c089110dafbb319e7a7cbd0239b04e27f70ca763ba345c7883ccd77882f017c031d7b3b9d30e3b33333e
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .DS_Store
11
+
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
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'rspec-its', '~> 1.2'
30
30
  spec.add_development_dependency 'pry', '~> 0.10'
31
31
  spec.add_development_dependency 'guard-rspec', '~> 4.5'
32
+ spec.add_development_dependency 'yard'
32
33
  end
@@ -0,0 +1,11 @@
1
+ require 'bundler/setup'
2
+ require 'attention'
3
+
4
+ # Listen to changes from other servers
5
+ Attention.on_change do |change, instances|
6
+ p change, instances
7
+ end
8
+
9
+ Attention.activate port: ARGV[0]
10
+
11
+ sleep
@@ -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],
@@ -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, :publisher
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 'instance'
38
+ @publisher = Publisher.new
14
39
  end
15
40
 
41
+ # Publishes this server and starts the {#heartbeat}
16
42
  def publish
17
- redis = Attention.redis.call
18
- redis.setex "instance_#{ @id }", Attention.options[:ttl], JSON.dump(info)
19
- publisher.publish added: info
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
- Attention.redis.call.del "instance_#{ @id }"
26
- publisher.publish removed: info
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?
@@ -1,17 +1,19 @@
1
1
  module Attention
2
+ # Uses Redis pub/sub to publish events
2
3
  class Publisher
3
- attr_reader :key
4
-
5
- def initialize(key)
6
- @key = key
7
- end
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 key, payload_for(value)
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
@@ -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],
@@ -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
- attr_reader :key, :redis
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
- def initialize(key, &callback)
13
- @key = key
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(key) do |on|
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 key, data
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(key)
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
@@ -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, :callback, :lock, :thread
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
@@ -1,3 +1,3 @@
1
1
  module Attention
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
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, # Heartbeat TTL in seconds
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.1
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-29 00:00:00.000000000 Z
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: