status_lib 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p484
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in status_lib.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, notification: false, all_after_pass: true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Kevin McConnell
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # StatusLib
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'status_lib'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install status_lib
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/guard ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'guard' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('guard', 'guard')
@@ -0,0 +1,33 @@
1
+ class StatusLib::Cache
2
+ class << self
3
+ def fetch(item, options={})
4
+ expires_in = options[:expires_in]
5
+ stored = items[item]
6
+ if stored.nil? || expired?(expires_in, timestamps[item])
7
+ stored = items[item] = yield
8
+ timestamps[item] = Time.now
9
+ end
10
+ stored
11
+ end
12
+
13
+ def clear!
14
+ @timestamps = @cached_items = nil
15
+ end
16
+
17
+ private
18
+
19
+ def items
20
+ @cached_items ||= {}
21
+ end
22
+
23
+ def timestamps
24
+ @timestamps ||= {}
25
+ end
26
+
27
+ def expired?(duration, recorded_time)
28
+ return false if duration.nil?
29
+ return true if recorded_time.nil?
30
+ (Time.now - recorded_time) > duration
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,61 @@
1
+ class StatusLib::Config
2
+ DEFAULT_CACHE_TIME = 5
3
+ DEFAULT_API_CALL_TIMEOUT = 1
4
+ DEFAULT_DOWN_DURATION = 0
5
+ DEFAULT_BLOCK_TIMEOUT = 5
6
+
7
+ ATTRIBUTES = [:server,
8
+ :exception_handler, :stats_handler,
9
+ :cache_time, :api_call_timeout,
10
+ :down_duration, :block_timeout]
11
+
12
+ attr_reader *ATTRIBUTES
13
+
14
+ def initialize
15
+ reset_to_defaults
16
+ end
17
+
18
+ def update(args)
19
+ ATTRIBUTES.each do |arg|
20
+ self.instance_variable_set("@#{arg}", args[arg] || defaults[arg]) if args.include?(arg)
21
+ end
22
+ clear_memoized
23
+ end
24
+
25
+ def status_info
26
+ @status_info ||= StatusLib::StatusInfo.new(status_api)
27
+ end
28
+
29
+ def status_api
30
+ @status_api ||= status_api_for_config
31
+ end
32
+
33
+ private
34
+
35
+ def reset_to_defaults
36
+ update(Hash[ATTRIBUTES.map { |attr| [attr, nil] }])
37
+ end
38
+
39
+ def defaults
40
+ {
41
+ cache_time: DEFAULT_CACHE_TIME,
42
+ api_call_timeout: DEFAULT_API_CALL_TIMEOUT,
43
+ down_duration: DEFAULT_DOWN_DURATION,
44
+ block_timeout: DEFAULT_BLOCK_TIMEOUT,
45
+ stats_handler: NullStatsHandler
46
+ }
47
+ end
48
+
49
+ def clear_memoized
50
+ @status_info = nil
51
+ @status_api = nil
52
+ end
53
+
54
+ def status_api_for_config
55
+ if server.nil?
56
+ StatusLib::NullStatusApi.new
57
+ else
58
+ StatusLib::StatusApi.new
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,6 @@
1
+ module NullObject
2
+ def method_missing(meth, *args, &block)
3
+ return meth.call(*args, &block) if respond_to?(meth)
4
+ yield if block_given?
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class NullStatsHandler
2
+ extend NullObject
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'timeout'
4
+ require 'uri'
5
+
6
+ class StatusLib::ApiDownError < Exception; end
7
+
8
+ class StatusLib::NullStatusApi
9
+ def get_status_list
10
+ end
11
+
12
+ def send_status_update(name, status, expires)
13
+ end
14
+ end
15
+
16
+ class StatusLib::StatusApi
17
+ def get_status_list
18
+ response = send_request('GET', "/api/status")
19
+ parse_status_response(response.body)
20
+ end
21
+
22
+ def send_status_update(name, status, expires)
23
+ if expires.kind_of?(Time)
24
+ expires = expires.utc.to_i
25
+ end
26
+
27
+ payload = { status: status, expires: expires }.to_json
28
+
29
+ response = send_request('PUT', "/api/status/#{name}", payload)
30
+ response_successful(response)
31
+ end
32
+
33
+ private
34
+
35
+ def parse_status_response(body)
36
+ Hash[
37
+ JSON.parse(body).map { |name, status| [name.to_sym, status.to_sym ] }
38
+ ]
39
+ end
40
+
41
+ def http_request
42
+ uri = URI.parse(StatusLib.config.server)
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ end
45
+
46
+ def send_request(*args)
47
+ Timeout::timeout(StatusLib.config.api_call_timeout) do
48
+ response = http_request.send_request(*args)
49
+ raise ArgumentError.new("service not found") if response.code == "404"
50
+ raise StatusLib::ApiDownError unless response_successful(response)
51
+ response
52
+ end
53
+ rescue ArgumentError
54
+ raise
55
+ rescue
56
+ raise StatusLib::ApiDownError
57
+ end
58
+
59
+ def response_successful(response)
60
+ response.kind_of? Net::HTTPSuccess
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ class StatusLib::StatusInfo
2
+ def initialize(api)
3
+ @api = api
4
+ @statuses = {}
5
+ @expiries = {}
6
+ @updates = {}
7
+ end
8
+
9
+ def up?(name)
10
+ sync_status_list
11
+ status = @statuses[name]
12
+ expiry = @expiries[name]
13
+
14
+ if status == :down && (expiry.nil? || expiry > now)
15
+ return false
16
+ else
17
+ return true
18
+ end
19
+ end
20
+
21
+ def switch(name, status, options={})
22
+ unless [:up, :down].include?(status)
23
+ raise ArgumentError.new("unknown status")
24
+ end
25
+
26
+ expires = options.fetch(:for, StatusLib.config.down_duration)
27
+ expires = now + expires unless expires.nil?
28
+
29
+ @statuses[name] = status
30
+ @expiries[name] = expires
31
+
32
+ add_pending_update(name, status, expires)
33
+ send_pending_updates
34
+
35
+ StatusLib.config.stats_handler.increment("status.change.#{name}.#{status}")
36
+ end
37
+
38
+ private
39
+
40
+ def now
41
+ Time.now
42
+ end
43
+
44
+ def sync_status_list
45
+ expires_in = StatusLib.config.cache_time
46
+
47
+ @statuses = StatusLib::Cache.fetch('statuses', expires_in: expires_in) do
48
+ send_pending_updates
49
+ begin
50
+ @api.get_status_list || @statuses
51
+ rescue StatusLib::ApiDownError
52
+ # couldn't get the info; just use our local copy
53
+ @statuses
54
+ end
55
+ end
56
+ end
57
+
58
+ def add_pending_update(name, status, expires)
59
+ @updates.delete(name)
60
+ @updates[name] = [status, expires]
61
+ end
62
+
63
+ def send_pending_updates
64
+ @updates.delete_if do |name, args|
65
+ begin
66
+ @api.send_status_update(name, *args)
67
+ rescue StatusLib::ApiDownError
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module StatusLib
2
+ VERSION = "0.0.2"
3
+ end
data/lib/status_lib.rb ADDED
@@ -0,0 +1,119 @@
1
+ require 'status_lib/version'
2
+
3
+ require 'status_lib/cache'
4
+ require 'status_lib/config'
5
+ require 'status_lib/null_object'
6
+ require 'status_lib/null_stats_handler'
7
+ require 'status_lib/status_api'
8
+ require 'status_lib/status_info'
9
+
10
+ require 'timeout'
11
+
12
+ module StatusLib
13
+ class ServiceDownError < StandardError
14
+ attr_reader :original_exception
15
+
16
+ def initialize(original_exception = nil)
17
+ super("Service is down")
18
+ @original_exception = original_exception
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_reader :server
24
+
25
+ # Checks whether a particular service is up or down.
26
+ #
27
+ # Params:
28
+ # +name+:: the name of the service to check
29
+ #
30
+ def up?(name)
31
+ status_info.up?(name)
32
+ end
33
+
34
+ # Sets the status of a service to up or down
35
+ #
36
+ # Params:
37
+ # +name+:: the name of the service to update
38
+ # +status+:: the desired status; either :up or :down
39
+ # +options+:: additional options. can contain:
40
+ # +:for+:: the time to keep the service down, in seconds
41
+ #
42
+ def switch(name, status, options={})
43
+ status_info.switch(name, status, options)
44
+ end
45
+
46
+ # Helper method for guarding a block of code with a curcuit breaker.
47
+ #
48
+ # If the code in the block raises an exception or exceeds its timeout, then
49
+ # the associated service status will be set to down for the specified
50
+ # interval, and a StatusLib::ServiceDownError exception will be raised.
51
+ #
52
+ # If the service is already makred down then the block won't be evaluated at
53
+ # all, and StatusLib::ServiceDownError will be raised immediately.
54
+ #
55
+ # +name+:: the name of the service to update
56
+ # +options+:: additional options. can contain:
57
+ # +:timeout+:: upper limit on how long the block can take to run
58
+ # +:down_for+:: the time to keep the service down, in seconds
59
+ #
60
+ def with_circuit_breaker(name, options={})
61
+ raise ServiceDownError unless up?(name)
62
+
63
+ Timeout::timeout( determine_timeout(options) ) do
64
+ yield
65
+ end
66
+ rescue Timeout::Error => e
67
+ handle_exception(e, name, options)
68
+ raise e
69
+ rescue => e
70
+ handle_exception(e, name, options)
71
+ raise ServiceDownError.new(e)
72
+ end
73
+
74
+ # Configure the gem
75
+ #
76
+ # Params:
77
+ # +settings+:: the configuration settings. A hash that may contain:
78
+ # +:server+:: the URL of the status page service
79
+ # +:cache_time+:: time to cache status reponses from the API
80
+ # +:api_call_timeout+:: maximum time to wait for a call to status API
81
+ # +:down_duration+:: default time to switch a service down for
82
+ # +:block_timeout+:: default time that a `with_circuit_breaker` can take
83
+ #
84
+ def configure(settings)
85
+ config.update(settings)
86
+ end
87
+
88
+ # Read configuration settings
89
+ #
90
+ # The values that are set to #configure are available here.
91
+ #
92
+ def config
93
+ @config ||= Config.new
94
+ end
95
+
96
+ private
97
+
98
+ def handle_exception(e, name, options)
99
+ report_exception(e)
100
+ switch(name, :down, options) if has_down_for?(options)
101
+ end
102
+
103
+ def status_info
104
+ config.status_info
105
+ end
106
+
107
+ def determine_timeout(options)
108
+ options.delete(:timeout) || config.block_timeout
109
+ end
110
+
111
+ def has_down_for?(options)
112
+ (options[:for] || 0) > 0
113
+ end
114
+
115
+ def report_exception(e)
116
+ config.exception_handler.call(e) unless config.exception_handler.nil?
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe StatusLib::Cache do
4
+ let(:target) { double(:target, message: "test") }
5
+
6
+ before do
7
+ StatusLib::Cache.clear!
8
+ end
9
+
10
+ context "#fetch" do
11
+ context "with no prior data" do
12
+ it "fetches the value" do
13
+ value = StatusLib::Cache.fetch('key') { target.message }
14
+
15
+ expect(value).to eq('test')
16
+ end
17
+ end
18
+
19
+ context "when cached" do
20
+ it "does not evalute the block multiple times" do
21
+ value = StatusLib::Cache.fetch('key') { target.message }
22
+ value = StatusLib::Cache.fetch('key') { target.message }
23
+
24
+ expect(value).to eq('test')
25
+ expect(target).to have_received(:message).once
26
+ end
27
+ end
28
+
29
+ context "when cached for a specific amount of time" do
30
+ it "only evaluates the block after the timeout elapses" do
31
+ Timecop.freeze(0)
32
+ StatusLib::Cache.fetch('key', expires_in: 30) { target.message }
33
+ expect(target).to have_received(:message).once
34
+
35
+ Timecop.freeze(15)
36
+ StatusLib::Cache.fetch('key', expires_in: 30) { target.message }
37
+ expect(target).to have_received(:message).once
38
+
39
+ Timecop.freeze(60)
40
+ StatusLib::Cache.fetch('key', expires_in: 30) { target.message }
41
+ expect(target).to have_received(:message).twice
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe StatusLib::Config do
4
+ shared_examples_for "it has default values" do
5
+ it "has no default server address" do
6
+ expect(config.server).to be_nil
7
+ end
8
+
9
+ it "uses a NullStatusApi by default" do
10
+ expect(config.status_api).to be_an_instance_of(StatusLib::NullStatusApi)
11
+ end
12
+
13
+ it "has no default exception handler" do
14
+ expect(config.exception_handler).to be_nil
15
+ end
16
+
17
+ it "has a null stats handler by default" do
18
+ expect(config.stats_handler).to eq(NullStatsHandler)
19
+ end
20
+
21
+ it "has a default cache expiry time" do
22
+ expect(config.cache_time).to eq(5)
23
+ end
24
+
25
+ it "has a default API call timeout" do
26
+ expect(config.api_call_timeout).to eq(1)
27
+ end
28
+
29
+ it "has a default down duration" do
30
+ expect(config.down_duration).to eq(0)
31
+ end
32
+
33
+ it "has a default block timeout" do
34
+ expect(config.block_timeout).to eq(5)
35
+ end
36
+ end
37
+
38
+ context "when initially created" do
39
+ let(:config) { StatusLib::Config.new }
40
+
41
+ it_behaves_like "it has default values"
42
+ end
43
+
44
+ context "updating" do
45
+ let(:config) { StatusLib::Config.new }
46
+ let(:stats) { double(:stats) }
47
+
48
+ it "sets the values form the arguments" do
49
+ config.update(server: "http://example.com:123",
50
+ exception_handler: ->(e) { puts e },
51
+ stats_handler: stats,
52
+ cache_time: 60,
53
+ api_call_timeout: 0.5,
54
+ down_duration: 120,
55
+ block_timeout: 36)
56
+
57
+ expect(config.server).to eq("http://example.com:123")
58
+ expect(config.exception_handler).to be_kind_of(Proc)
59
+ expect(config.stats_handler).to eq(stats)
60
+ expect(config.cache_time).to eq(60)
61
+ expect(config.api_call_timeout).to eq(0.5)
62
+ expect(config.down_duration).to eq(120)
63
+ expect(config.block_timeout).to eq(36)
64
+ expect(config.status_api).to be_an_instance_of(StatusLib::StatusApi)
65
+ end
66
+ end
67
+
68
+ context "setting back to nil" do
69
+ let(:config) { StatusLib::Config.new }
70
+ let(:stats) { double(:stats) }
71
+
72
+ before do
73
+ config.update(server: "http://example.com:123",
74
+ exception_handler: ->(e) { puts e },
75
+ stats_handler: stats,
76
+ cache_time: 60,
77
+ api_call_timeout: 0.5,
78
+ down_duration: 120,
79
+ block_timeout: 36)
80
+
81
+ config.update(server: nil,
82
+ exception_handler: nil,
83
+ stats_handler: nil,
84
+ cache_time: nil,
85
+ api_call_timeout: nil,
86
+ down_duration: nil,
87
+ block_timeout: nil)
88
+ end
89
+
90
+ it_behaves_like "it has default values"
91
+ end
92
+
93
+ context "#status_info" do
94
+ let(:config) { StatusLib::Config.new }
95
+
96
+ it "has an instance of StatusInfo" do
97
+ expect(config.status_info).to be_an_instance_of(StatusLib::StatusInfo)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe NullObject do
4
+ let(:klass) {
5
+ Class.new do
6
+ include NullObject
7
+ end
8
+ }
9
+ let(:instance) { klass.new }
10
+
11
+ it "quietly ignores method calls" do
12
+ expect {
13
+ instance.do_something_i_just_made_up('with', 'args')
14
+ }.to_not raise_error
15
+ end
16
+
17
+ context "with a block" do
18
+ let(:inner) { double(:inner).as_null_object }
19
+
20
+ it "calls the supplied block on ignored methods" do
21
+ instance.do_something_with_a_block do
22
+ inner.block_target('test')
23
+ end
24
+
25
+ expect(inner).to have_received(:block_target).with('test')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe NullStatsHandler do
4
+ it "quietly ignores calls to ::increment" do
5
+ expect {
6
+ NullStatsHandler.increment('my.counter')
7
+ }.to_not raise_error
8
+ end
9
+ end