manana 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts CHANGED
@@ -5,4 +5,4 @@
5
5
  --readme README.md
6
6
  -
7
7
  LICENSE.txt
8
- CHANGELOG.md
8
+ CHANGELOG.md
data/Gemfile CHANGED
@@ -5,10 +5,26 @@ gemspec
5
5
 
6
6
  # these aren't strictly needed for development but are nice to have
7
7
  unless ENV["TRAVIS"] == "1"
8
- gem 'yard'
9
- gem 'redcarpet'
10
- gem 'simplecov'
11
- gem 'pry'
8
+
9
+ group :development do
10
+ gem 'yard'
11
+ gem 'redcarpet'
12
+
13
+ gem 'pry'
14
+ gem 'pry-debugger'
15
+ gem 'pry-rescue'
16
+ gem 'pry-stack_explorer'
17
+ end
18
+
19
+ group :samples do
20
+ gem 'savon', '~> 2.3'
21
+ gem 'webmock'
22
+ end
23
+
24
+ group :test do
25
+ gem 'simplecov'
26
+ end
27
+
12
28
  end
13
29
 
14
30
  # for travis.cl
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 coldnebo
1
+ Copyright (c) 2013 Larry Kyrala
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,12 +1,42 @@
1
1
  # Manana
2
2
 
3
- *Manana* lets you defer the initialization of any class to when an instance method is called.
3
+ *Manana* lets you defer the initialization of an object until its methods are called.
4
4
 
5
- This can be useful in cases where class initialization may take a long time or fail due to errors (i.e. a network service).
6
- In these situations, you don't want to tie the initialization of service adapters to the initialization of your application,
7
- because if the service init fails, then your app fails to init and start (i.e. you want your app to be self-healing and
8
- retry initialization when the method is called later)... also it can unncessarily increase the startup
9
- time of your app for testcases, etc.
5
+ This can be useful in cases where initialization may take a long time or fail due to errors. For example, in rails, if
6
+ the database specified in `database.yml` doesn't exist at start time, the rails application will fail initialization. Likewise
7
+ if you tie configuration of SOAP services (e.g. using [Savon](http://savonrb.com/version2/)) to your application initialization,
8
+ and the service wsdl isn't up, your application will fail to start.
9
+
10
+ Instead, it is an established best practice in service-oriented architectures to make your application or service *startup order independent*:
11
+ i.e. your app starts fast and initializes other dependencies afterwards, providing fault detection and *self-healing* properties for the app.
12
+
13
+ *Manana* is a simple approach that allows you to keep your configuration and initialization code in the same place, while deferring it to method calls.
14
+
15
+ ### Pros
16
+
17
+ * Reduces the startup time for your app. (i.e. moves the startup cost from initialization to first use)
18
+
19
+ * Once initialization is succesful, it stores the object instance for reuse.
20
+
21
+ * Until the initialization is successful, it will retry every time a method is called.
22
+
23
+ * You can layer more complex retry semantics such as [exponential backoff](http://en.wikipedia.org/wiki/Exponential_backoff) using this wrapper. See [samples/exponential_backoff.rb](https://github.com/coldnebo/manana/blob/master/samples/exponential_backoff.rb)
24
+
25
+ ### Cons
26
+
27
+ * If your initialization takes a very long time, (i.e. a cache) you may want to pre-warm it instead of taking the hit on the first use of the object.
28
+
29
+ * Very simple approach. You may want more complex retry semantics, or pooling.
30
+
31
+ ### Similar ideas:
32
+
33
+ * [Connection pool](http://en.wikipedia.org/wiki/Connection_pool) of one?
34
+
35
+ * [Avoid Start Order Dependencies](http://wiki.osgi.org/wiki/Avoid_Start_Order_Dependencies)
36
+
37
+ * [Data Centers need shutdown/startup order](http://www.boche.net/blog/index.php/2009/01/01/datacenters-need-shutdownstartup-order/)
38
+
39
+ * 'self-healing' initialization faults from the practice of [Autonomic computing](http://en.wikipedia.org/wiki/Autonomic_computing)
10
40
 
11
41
  ## Installation
12
42
 
@@ -24,7 +54,7 @@ Or install it yourself as:
24
54
 
25
55
  ## Usage
26
56
 
27
- TODO: Write usage instructions here
57
+ See the [samples](https://github.com/coldnebo/manana/blob/master/samples) for examples of use.
28
58
 
29
59
  ## Contributing
30
60
 
data/Rakefile CHANGED
@@ -6,3 +6,10 @@ Rake::TestTask.new(:test) do |t|
6
6
  end
7
7
 
8
8
  task :default => :test
9
+
10
+ desc "install gems for running samples"
11
+ task :samples do
12
+ Bundler.with_clean_env do
13
+ sh "bundle install --gemfile=samples/common/samples.gemfile"
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  class Manana
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/manana.rb CHANGED
@@ -1,12 +1,42 @@
1
1
  require "manana/version"
2
2
 
3
+
4
+ # *Manana* lets you defer the initialization of an object until its methods are called.
5
+ # @example basic usage - see {https://github.com/coldnebo/manana/blob/master/samples/self_healing.rb samples/self_healing.rb}
6
+ # # initialization...
7
+ # client = Manana.wrap {
8
+ # Weather.setup
9
+ # Weather
10
+ # }
11
+ #
12
+ # runtime_loop {
13
+ # # wait for next interval
14
+ # weather = client.city_weather("02201") # deferred initialization happens here once
15
+ # puts "At %s the temperature is currently %s F and the humidity is %s." % [weather.city, weather.temperature, weather.relative_humidity]
16
+ # }
17
+ #
3
18
  class Manana
4
- attr_reader :deferred_initialization, :instance
5
19
 
6
- def self.wrap(&block)
7
- Manana.new(&block)
20
+ # wraps an object initialization block so that it can be deferred to a later time when object methods are called.
21
+ # @example wrap an object - see {https://github.com/coldnebo/manana/blob/master/samples/self_healing.rb samples/self_healing.rb}
22
+ # client = Manana.wrap {
23
+ # Weather.setup # initialize the class
24
+ # Weather # return the Weather class
25
+ # }
26
+ #
27
+ # @param initialization_block [Proc] object initialization. the block must return the object to be wrapped.
28
+ # @return [Manana] a wrapped version of the object.
29
+ def self.wrap(&initialization_block)
30
+ Manana.new(&initialization_block)
8
31
  end
9
32
 
33
+ # passes any method call through to the wrapped object after ensuring that the initialization block has
34
+ # successfully completed once (setting a valid instance of the object).
35
+ # @note Once the initialization block succeeds, it keeps the resulting object instance for subsequent method calls.
36
+ #
37
+ # @example calling a wrapped object - see {https://github.com/coldnebo/manana/blob/master/samples/self_healing.rb samples/self_healing.rb}
38
+ # weather = client.city_weather("02201")
39
+ #
10
40
  def method_missing(method, *args, &block)
11
41
  instance = get_instance
12
42
  instance.send(method, *args, &block);
@@ -14,8 +44,8 @@ class Manana
14
44
 
15
45
  private
16
46
 
17
- def initialize(&block)
18
- @deferred_initialization = block
47
+ def initialize(&initialization_block)
48
+ @deferred_initialization = initialization_block
19
49
  end
20
50
 
21
51
  def get_instance
data/manana.gemspec CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Manana::VERSION
9
9
  spec.authors = ["coldnebo"]
10
10
  spec.email = ["larry.kyrala@gmail.com"]
11
- spec.description = %q{provides a simple way to defer initialization of an object to action on an object}
12
- spec.summary = %q{I'll get to your initialization tomorrow...}
13
- spec.homepage = ""
11
+ spec.description = %q{provides a simple way to defer initialization of an object until its methods are called}
12
+ spec.summary = %q{provides a simple way to defer initialization of an object until its methods are called}
13
+ spec.homepage = "https://github.com/coldnebo/manana"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
@@ -0,0 +1,10 @@
1
+
2
+ source 'https://rubygems.org'
3
+
4
+ # these are gems used in the samples.
5
+ # install with:
6
+ # $ rake samples
7
+
8
+ gem 'rack'
9
+ gem 'savon', '~> 2.3'
10
+ gem 'webmock'
@@ -0,0 +1,42 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.3.5)
5
+ akami (1.2.0)
6
+ gyoku (>= 0.4.0)
7
+ nokogiri (>= 1.4.0)
8
+ builder (3.2.2)
9
+ crack (0.4.1)
10
+ safe_yaml (~> 0.9.0)
11
+ gyoku (1.1.0)
12
+ builder (>= 2.1.2)
13
+ httpi (2.1.0)
14
+ rack
15
+ rubyntlm (~> 0.3.2)
16
+ nokogiri (1.5.10)
17
+ nori (2.3.0)
18
+ rack (1.5.2)
19
+ rubyntlm (0.3.4)
20
+ safe_yaml (0.9.7)
21
+ savon (2.3.0)
22
+ akami (~> 1.2.0)
23
+ builder (>= 2.1.2)
24
+ gyoku (~> 1.1.0)
25
+ httpi (~> 2.1.0)
26
+ nokogiri (>= 1.4.0, < 1.6)
27
+ nori (~> 2.3.0)
28
+ wasabi (~> 3.2.0)
29
+ wasabi (3.2.0)
30
+ httpi (~> 2.0)
31
+ nokogiri (>= 1.4.0, < 1.6)
32
+ webmock (1.15.2)
33
+ addressable (>= 2.2.7)
34
+ crack (>= 0.3.2)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ rack
41
+ savon (~> 2.3)
42
+ webmock
@@ -0,0 +1,7 @@
1
+ # sample helper takes care of loading the gem from source without requiring it to be rebuilt and installed.
2
+ # this is useful in allowing the samples in this directory to evolve the behavior of the actual gem.
3
+
4
+ lp = File.expand_path(File.join(*%w[.. .. lib]), File.dirname(__FILE__))
5
+ unless $LOAD_PATH.include?(lp)
6
+ $LOAD_PATH.unshift(lp)
7
+ end
@@ -0,0 +1,65 @@
1
+ # This sample demonstrates how exponential backoff can be layered on top of Manana
2
+ #
3
+ # run from the gem dir with:
4
+ # $ rake samples
5
+ # $ ruby samples/exponential_backoff.rb
6
+
7
+ require_relative 'common/samples_helper.rb'
8
+
9
+ # ------------------------- sample code starts here -------------------------
10
+
11
+ require 'manana'
12
+ require 'benchmark'
13
+
14
+
15
+ class PoorService
16
+ @@instance = 0
17
+
18
+ def initialize
19
+ @@instance += 1
20
+ if @@instance <= 3
21
+ raise "failed to init"
22
+ end
23
+ puts "successfully inited service."
24
+ end
25
+
26
+ def process
27
+ puts "successfully called service!"
28
+ end
29
+
30
+ end
31
+
32
+ # ------------ initialization code
33
+
34
+ client = Manana.wrap {
35
+ # exponential backkoff logic in the initialization block
36
+ retries = 0
37
+ obj = nil
38
+
39
+ puts "trying to create service object..."
40
+ begin
41
+ obj = PoorService.new
42
+ rescue RuntimeError => e
43
+ puts "couldn't create object, sleeping for #{2**retries} seconds before next try..."
44
+ sleep(2**retries)
45
+ retries += 1
46
+
47
+ if retries <= 3
48
+ retry
49
+ else
50
+ puts "giving up."
51
+ raise e
52
+ end
53
+ end
54
+ obj
55
+ }
56
+
57
+
58
+ # ------------ runtime code
59
+
60
+ client.process
61
+ client.process
62
+ client.process
63
+ client.process
64
+
65
+
@@ -0,0 +1,83 @@
1
+ # This sample demonstrates how Manana may be used to defer a long initialization
2
+ # to the first method call.
3
+ #
4
+ # run from the gem dir with:
5
+ # $ rake samples
6
+ # $ ruby samples/fast_startup.rb
7
+
8
+ require_relative 'common/samples_helper.rb'
9
+
10
+ # ------------------------- sample code starts here -------------------------
11
+
12
+ require 'manana'
13
+ require 'benchmark'
14
+
15
+
16
+ class DeathStar
17
+
18
+ def initialize
19
+ puts " DeathStar initialization sequence commencing..."
20
+ super_complex_startup_sequence
21
+ puts " ...sequence complete."
22
+ end
23
+
24
+ def navigate_to(planet)
25
+ puts "setting course for the planet '#{planet}'"
26
+ end
27
+
28
+ private
29
+
30
+ def super_complex_startup_sequence
31
+ puts " 1. press the 'start' button."
32
+ puts " 2. snooze while Vader isn't looking."
33
+ sleep(5)
34
+ puts " 3. ???"
35
+ puts " 4. profit!"
36
+ end
37
+
38
+ end
39
+
40
+ # ------------ initialization code
41
+
42
+ mini_death_star = nil
43
+
44
+ puts "[Vader]: requsition a new DeathStar and make it snappy!"
45
+ # boss said requisitions have to be faster*! Manana to the rescue!
46
+ puts Benchmark.measure {
47
+ mini_death_star = Manana.wrap {
48
+ DeathStar.new
49
+ }
50
+ }
51
+ puts "[Expendable Commander]: completed, sir!"
52
+
53
+
54
+ puts "-------------------------------------------"
55
+
56
+
57
+ # ------------ runtime code
58
+
59
+ puts "[Vader]: ok, send it towards some rebels..."
60
+
61
+ # * this first method eats the hidden startup cost...
62
+ puts Benchmark.measure {
63
+ mini_death_star.navigate_to("Yavin")
64
+ }
65
+ puts "[Vader raises an eyebrow and starts to reach towards the commander] \n# first method call was long, but subsequent calls are faster..."
66
+
67
+ # * subsequent method calls are fast though... maybe tradeoff is good enough for Vader?
68
+ puts Benchmark.measure {
69
+ mini_death_star.navigate_to("Bespin")
70
+ }
71
+
72
+ puts Benchmark.measure {
73
+ mini_death_star.navigate_to("Tatooine")
74
+ }
75
+
76
+ puts "[Vader shrugs]: Well done Expendable Commander!"
77
+ puts "[Expendable Commander sweatly profusely]: Thank you Lord Vader!"
78
+
79
+
80
+
81
+
82
+
83
+
@@ -0,0 +1,104 @@
1
+ # This sample demonstrates how Manana may be used to create a self-healing soap adapter
2
+ # by wrapping an existing Savon::Model client.
3
+ #
4
+ # run from the gem dir with:
5
+ # $ rake samples
6
+ # $ ruby samples/self_healing.rb
7
+
8
+ require_relative 'common/samples_helper.rb'
9
+
10
+ # ------------------------- sample code starts here -------------------------
11
+
12
+ require 'manana'
13
+ require 'savon'
14
+ require 'ostruct'
15
+
16
+ # used to simulate conn up/down
17
+ require 'webmock'
18
+ include WebMock::API
19
+ # fake a failure to get the wsdl (service down temporarily)
20
+ stub_request(:any, /wsf.cdyne.com\/.*/).
21
+ to_return(:status => 500, :body => "Internal Server Error", :headers => {})
22
+
23
+
24
+ LOGGING_ENABLED = false
25
+ HTTPI.log = false unless LOGGING_ENABLED
26
+
27
+ class Weather < OpenStruct
28
+ extend Savon::Model
29
+
30
+ client wsdl: "http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL", convert_request_keys_to: :none, log: LOGGING_ENABLED
31
+
32
+ def self.setup
33
+ # if this were outside of setup() the class would fail to load if the service was down.
34
+ # instead we dynamically initialize the available operations from withing a class method to be used by the Manana wrapper.
35
+ operations *client.operations
36
+ end
37
+
38
+ def self.city_weather(zip)
39
+ resp = get_city_weather_by_zip(message: {"ZIP" => zip})
40
+ weather = resp.body[:get_city_weather_by_zip_response][:get_city_weather_by_zip_result]
41
+ Weather.new(weather)
42
+ end
43
+ end
44
+
45
+
46
+ # ------------ initialization code
47
+
48
+ # Before we start, we use Manana to wrap the client setup so that method calls on the client can be self-healing in case of failure.
49
+ # NOTE: that the caller doesn't have to deal with whether or not this initialization succeeded, they can just call the client methods repeatedly.
50
+ client = Manana.wrap {
51
+ Weather.setup
52
+ Weather
53
+ }
54
+
55
+
56
+ # ------------ runtime code
57
+
58
+ # first call fails...
59
+ # a) Weather.setup() called, but raises because the server is 500.
60
+ begin
61
+ weather = client.city_weather("02201")
62
+ rescue Wasabi::Resolver::HTTPError => e
63
+ puts "# 1. first call failed as expected, because Weather.setup() couldn't connect to provide the operations for the client."
64
+ end
65
+
66
+ # server restored
67
+ WebMock.disable!
68
+
69
+ # second call ok...
70
+ # a) Weather.setup() succeeds, caches instance for future use.
71
+ # b) city_weather() succeeds.
72
+ weather = client.city_weather("02201")
73
+
74
+ puts "# 2. second call should succeed; Weather.setup() runs successfully and returns a successfully intialized Weather class."
75
+ puts " > At %s the temperature is currently %s F and the humidity is %s." % [weather.city, weather.temperature, weather.relative_humidity]
76
+
77
+ # server down again
78
+ WebMock.enable!
79
+
80
+ # third call; service down again...
81
+ # remember, we already have a valid instance, so Weather.setup() is not called this time.
82
+ # a) city_weather() fails because the server is 500.
83
+ begin
84
+ weather = client.city_weather("02201")
85
+ rescue Savon::HTTPError => e
86
+ puts "# 3. third call fails; the connection is down again, but this time the failure is in the API call since Weather has successfully intialized opertaions from call #2."
87
+ end
88
+
89
+
90
+ # server restored
91
+ WebMock.disable!
92
+
93
+ # fourth call ok...
94
+ # a) city_weather() succeeds.
95
+ weather = client.city_weather("02201")
96
+
97
+ puts "# 4. fourth call should succeed; connection is up again, still using cached operations from the first time service was up (call #2)."
98
+ puts " > At %s the temperature is currently %s F and the humidity is %s." % [weather.city, weather.temperature, weather.relative_humidity]
99
+
100
+
101
+ puts "\nThis concludes the demonstration of how Manana provides self-healing capability to a web-service adapter."
102
+
103
+
104
+
data/test/test_manana.rb CHANGED
@@ -22,11 +22,7 @@ class TestManana < MiniTest::Unit::TestCase
22
22
  end
23
23
 
24
24
  def raise_something
25
- raise "kablewey!" # simulate a call that raises an exception
26
- end
27
-
28
- def self.do_another_thing
29
- "I did something else" # simulate a class method call
25
+ raise "kablooey!" # simulate a call that raises an exception
30
26
  end
31
27
  }
32
28
  end
@@ -43,6 +39,14 @@ class TestManana < MiniTest::Unit::TestCase
43
39
  assert_raises RuntimeError do
44
40
  obj.raise_something
45
41
  end
42
+
43
+ begin
44
+ obj.raise_something
45
+ rescue Exception => e
46
+ # make sure we get an appropriate stack trace at the point of raise.
47
+ assert_match(/test_manana.rb:25:in .raise_something./, e.backtrace.first)
48
+ end
49
+
46
50
  end
47
51
 
48
52
  def test_deferred_init
@@ -68,6 +72,14 @@ class TestManana < MiniTest::Unit::TestCase
68
72
  assert_instance_of(String, result)
69
73
 
70
74
 
75
+ begin
76
+ handle.raise_something
77
+ rescue Exception => e
78
+ # make sure we get an appropriate stack trace at the point of raise.
79
+ assert_match(/test_manana.rb:25:in .raise_something./, e.backtrace.first)
80
+ end
81
+
82
+
71
83
  end
72
84
 
73
85
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: manana
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-08 00:00:00.000000000 Z
12
+ date: 2013-11-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -59,8 +59,8 @@ dependencies:
59
59
  - - ! '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
- description: provides a simple way to defer initialization of an object to action
63
- on an object
62
+ description: provides a simple way to defer initialization of an object until its
63
+ methods are called
64
64
  email:
65
65
  - larry.kyrala@gmail.com
66
66
  executables: []
@@ -77,9 +77,15 @@ files:
77
77
  - lib/manana.rb
78
78
  - lib/manana/version.rb
79
79
  - manana.gemspec
80
+ - samples/common/samples.gemfile
81
+ - samples/common/samples.gemfile.lock
82
+ - samples/common/samples_helper.rb
83
+ - samples/exponential_backoff.rb
84
+ - samples/fast_startup.rb
85
+ - samples/self_healing.rb
80
86
  - test/minitest_helper.rb
81
87
  - test/test_manana.rb
82
- homepage: ''
88
+ homepage: https://github.com/coldnebo/manana
83
89
  licenses:
84
90
  - MIT
85
91
  post_install_message:
@@ -94,7 +100,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
100
  version: '0'
95
101
  segments:
96
102
  - 0
97
- hash: -3461867159210393723
103
+ hash: -1234298163661349463
98
104
  required_rubygems_version: !ruby/object:Gem::Requirement
99
105
  none: false
100
106
  requirements:
@@ -103,13 +109,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
109
  version: '0'
104
110
  segments:
105
111
  - 0
106
- hash: -3461867159210393723
112
+ hash: -1234298163661349463
107
113
  requirements: []
108
114
  rubyforge_project:
109
115
  rubygems_version: 1.8.25
110
116
  signing_key:
111
117
  specification_version: 3
112
- summary: I'll get to your initialization tomorrow...
118
+ summary: provides a simple way to defer initialization of an object until its methods
119
+ are called
113
120
  test_files:
114
121
  - test/minitest_helper.rb
115
122
  - test/test_manana.rb