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 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: