rightscale-nanite 0.4.1 → 0.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/lib/nanite.rb +71 -0
  2. data/lib/nanite/actor.rb +60 -0
  3. data/lib/nanite/actor_registry.rb +24 -0
  4. data/lib/nanite/admin.rb +153 -0
  5. data/lib/nanite/agent.rb +250 -0
  6. data/lib/nanite/amqp.rb +47 -0
  7. data/lib/nanite/cluster.rb +203 -0
  8. data/lib/nanite/config.rb +102 -0
  9. data/lib/nanite/console.rb +39 -0
  10. data/lib/nanite/daemonize.rb +13 -0
  11. data/lib/nanite/dispatcher.rb +90 -0
  12. data/lib/nanite/identity.rb +16 -0
  13. data/lib/nanite/job.rb +104 -0
  14. data/lib/nanite/local_state.rb +34 -0
  15. data/lib/nanite/log.rb +64 -0
  16. data/lib/nanite/log/formatter.rb +39 -0
  17. data/lib/nanite/mapper.rb +277 -0
  18. data/lib/nanite/mapper_proxy.rb +56 -0
  19. data/lib/nanite/packets.rb +231 -0
  20. data/lib/nanite/pid_file.rb +52 -0
  21. data/lib/nanite/reaper.rb +38 -0
  22. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  23. data/lib/nanite/security/certificate.rb +55 -0
  24. data/lib/nanite/security/certificate_cache.rb +66 -0
  25. data/lib/nanite/security/distinguished_name.rb +34 -0
  26. data/lib/nanite/security/encrypted_document.rb +46 -0
  27. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  28. data/lib/nanite/security/secure_serializer.rb +67 -0
  29. data/lib/nanite/security/signature.rb +40 -0
  30. data/lib/nanite/security/static_certificate_store.rb +35 -0
  31. data/lib/nanite/security_provider.rb +47 -0
  32. data/lib/nanite/serializer.rb +52 -0
  33. data/lib/nanite/state.rb +164 -0
  34. data/lib/nanite/streaming.rb +125 -0
  35. data/lib/nanite/util.rb +51 -0
  36. data/spec/actor_registry_spec.rb +62 -0
  37. data/spec/actor_spec.rb +59 -0
  38. data/spec/agent_spec.rb +235 -0
  39. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  40. data/spec/certificate_cache_spec.rb +49 -0
  41. data/spec/certificate_spec.rb +27 -0
  42. data/spec/cluster_spec.rb +300 -0
  43. data/spec/dispatcher_spec.rb +136 -0
  44. data/spec/distinguished_name_spec.rb +24 -0
  45. data/spec/encrypted_document_spec.rb +21 -0
  46. data/spec/job_spec.rb +219 -0
  47. data/spec/local_state_spec.rb +112 -0
  48. data/spec/packet_spec.rb +218 -0
  49. data/spec/rsa_key_pair_spec.rb +33 -0
  50. data/spec/secure_serializer_spec.rb +41 -0
  51. data/spec/serializer_spec.rb +107 -0
  52. data/spec/signature_spec.rb +30 -0
  53. data/spec/spec_helper.rb +23 -0
  54. data/spec/static_certificate_store_spec.rb +30 -0
  55. data/spec/util_spec.rb +63 -0
  56. metadata +62 -1
@@ -0,0 +1,71 @@
1
+ require 'rubygems'
2
+ require 'amqp'
3
+ require 'mq'
4
+ require 'json'
5
+ require 'logger'
6
+ require 'yaml'
7
+ require 'openssl'
8
+
9
+ $:.unshift File.dirname(__FILE__)
10
+ require 'nanite/amqp'
11
+ require 'nanite/util'
12
+ require 'nanite/config'
13
+ require 'nanite/packets'
14
+ require 'nanite/identity'
15
+ require 'nanite/console'
16
+ require 'nanite/daemonize'
17
+ require 'nanite/pid_file'
18
+ require 'nanite/job'
19
+ require 'nanite/mapper'
20
+ require 'nanite/actor'
21
+ require 'nanite/actor_registry'
22
+ require 'nanite/streaming'
23
+ require 'nanite/dispatcher'
24
+ require 'nanite/agent'
25
+ require 'nanite/cluster'
26
+ require 'nanite/reaper'
27
+ require 'nanite/log'
28
+ require 'nanite/mapper_proxy'
29
+ require 'nanite/security_provider'
30
+ require 'nanite/security/cached_certificate_store_proxy'
31
+ require 'nanite/security/certificate'
32
+ require 'nanite/security/certificate_cache'
33
+ require 'nanite/security/distinguished_name'
34
+ require 'nanite/security/encrypted_document'
35
+ require 'nanite/security/rsa_key_pair'
36
+ require 'nanite/security/secure_serializer'
37
+ require 'nanite/security/signature'
38
+ require 'nanite/security/static_certificate_store'
39
+ require 'nanite/serializer'
40
+
41
+ module Nanite
42
+ VERSION = '0.4.0' unless defined?(Nanite::VERSION)
43
+
44
+ class MapperNotRunning < StandardError; end
45
+
46
+ class << self
47
+ attr_reader :mapper, :agent
48
+
49
+ def start_agent(options = {})
50
+ @agent = Nanite::Agent.start(options)
51
+ end
52
+
53
+ def start_mapper(options = {})
54
+ @mapper = Nanite::Mapper.start(options)
55
+ end
56
+
57
+ def request(*args, &blk)
58
+ ensure_mapper
59
+ @mapper.request(*args, &blk)
60
+ end
61
+
62
+ def push(*args)
63
+ ensure_mapper
64
+ @mapper.push(*args)
65
+ end
66
+
67
+ def ensure_mapper
68
+ raise MapperNotRunning.new('A mapper needs to be started via Nanite.start_mapper') unless @mapper
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,60 @@
1
+ module Nanite
2
+ # This mixin provides Nanite actor functionality.
3
+ #
4
+ # To use it simply include it your class containing the functionality to be exposed:
5
+ #
6
+ # class Foo
7
+ # include Nanite::Actor
8
+ # expose :bar
9
+ #
10
+ # def bar(payload)
11
+ # # ...
12
+ # end
13
+ #
14
+ # end
15
+ module Actor
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ include Nanite::Actor::InstanceMethods
20
+ extend Nanite::Actor::ClassMethods
21
+ end # base.class_eval
22
+ end # self.included
23
+
24
+ module ClassMethods
25
+ def default_prefix
26
+ to_s.to_const_path
27
+ end
28
+
29
+ def expose(*meths)
30
+ @exposed ||= []
31
+ meths.each do |meth|
32
+ @exposed << meth unless @exposed.include?(meth)
33
+ end
34
+ end
35
+
36
+ def provides_for(prefix)
37
+ return [] unless @exposed
38
+ @exposed.map {|meth| "/#{prefix}/#{meth}".squeeze('/')}
39
+ end
40
+
41
+ def on_exception(proc = nil, &blk)
42
+ raise 'No callback provided for on_exception' unless proc || blk
43
+ @exception_callback = proc || blk
44
+ end
45
+
46
+ def exception_callback
47
+ @exception_callback
48
+ end
49
+
50
+ end # ClassMethods
51
+
52
+ module InstanceMethods
53
+ # send nanite request to another agent (through the mapper)
54
+ def request(*args, &blk)
55
+ MapperProxy.instance.request(*args, &blk)
56
+ end
57
+ end # InstanceMethods
58
+
59
+ end # Actor
60
+ end # Nanite
@@ -0,0 +1,24 @@
1
+ module Nanite
2
+ class ActorRegistry
3
+ attr_reader :actors
4
+
5
+ def initialize
6
+ @actors = {}
7
+ end
8
+
9
+ def register(actor, prefix)
10
+ raise ArgumentError, "#{actor.inspect} is not a Nanite::Actor subclass instance" unless Nanite::Actor === actor
11
+ Nanite::Log.info("Registering #{actor.inspect} with prefix #{prefix.inspect}")
12
+ prefix ||= actor.class.default_prefix
13
+ actors[prefix.to_s] = actor
14
+ end
15
+
16
+ def services
17
+ actors.map {|prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
18
+ end
19
+
20
+ def actor_for(prefix)
21
+ actor = actors[prefix]
22
+ end
23
+ end # ActorRegistry
24
+ end # Nanite
@@ -0,0 +1,153 @@
1
+ require 'rack'
2
+
3
+ module Nanite
4
+ # This is a Rack app for nanite-admin. You need to have an async capable
5
+ # version of Thin installed for this to work. See bin/nanite-admin for install
6
+ # instructions.
7
+ class Admin
8
+ def initialize(mapper)
9
+ @mapper = mapper
10
+ end
11
+
12
+ AsyncResponse = [-1, {}, []].freeze
13
+
14
+ def call(env)
15
+ req = Rack::Request.new(env)
16
+ if cmd = req.params['command']
17
+ @command = cmd
18
+ @selection = req.params['type'] if req.params['type']
19
+
20
+ options = {}
21
+ case @selection
22
+ when 'least_loaded', 'random', 'all', 'rr'
23
+ options[:selector] = @selection
24
+ else
25
+ options[:target] = @selection
26
+ end
27
+
28
+ @mapper.request(cmd, req.params['payload'], options) do |response, responsejob|
29
+ env['async.callback'].call [200, {'Content-Type' => 'text/html'}, [layout(ul(response, responsejob))]]
30
+ end
31
+ AsyncResponse
32
+ else
33
+ [200, {'Content-Type' => 'text/html'}, layout]
34
+ end
35
+ end
36
+
37
+ def services
38
+ buf = "<select name='command'>"
39
+ @mapper.cluster.nanites.all_services.each do |srv|
40
+ buf << "<option value='#{srv}' #{@command == srv ? 'selected="true"' : ''}>#{srv}</option>"
41
+ end
42
+ buf << "</select>"
43
+ buf
44
+ end
45
+
46
+ def ul(hash, job)
47
+ buf = "<ul>"
48
+ hash.each do |k,v|
49
+ buf << "<li><div class=\"nanite\">#{k}:</div><div class=\"response\">#{v.inspect}</div>"
50
+ if job.intermediate_state && job.intermediate_state[k]
51
+ buf << "<div class=\"intermediatestates\"><span class=\"statenote\">intermediate state:</span> #{job.intermediate_state[k].inspect}</div>"
52
+ end
53
+ buf << "</li>"
54
+ end
55
+ buf << "</ul>"
56
+ buf
57
+ end
58
+
59
+ def layout(content=nil)
60
+ %Q{
61
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
62
+ <html xmlns='http://www.w3.org/1999/xhtml'>
63
+ <head>
64
+ <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
65
+ <meta content='en' http-equiv='Content-Language' />
66
+ <meta content='Engineyard' name='author' />
67
+ <title>Nanite Control Tower</title>
68
+
69
+ <!-- Google AJAX Libraries API -->
70
+ <script src="http://www.google.com/jsapi"></script>
71
+ <script type="text/javascript">
72
+ google.load("jquery", "1");
73
+ </script>
74
+
75
+ <script type="text/javascript">
76
+ $(document).ready(function(){
77
+
78
+ // set the focus to the payload field
79
+ $("#payload").focus();
80
+
81
+ });
82
+ </script>
83
+
84
+ <style>
85
+ body {margin: 0; font-family: verdana; background-color: #fcfcfc;}
86
+ ul {margin: 0; padding: 0; margin-left: 10px}
87
+ li {list-style-type: none; margin-bottom: 6px}
88
+ li .nanite {font-weight: bold; font-size: 12px}
89
+ li .response {padding: 8px}
90
+ li .intermediatestates {padding: 8px; font-size: 10px;}
91
+ li .intermediatestates span.statenote {font-style: italic;}
92
+ h1, h2, h3 {margin-top: none; padding: none; margin-left: 40px;}
93
+ h1 {font-size: 22px; margin-top: 40px; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 6px;
94
+ margin-right: 40px}
95
+ h2 {font-size: 16px;}
96
+ h3 {margin-left: 0; font-size: 14px}
97
+ .section {border: 1px solid #ccc; background-color: #fefefe; padding: 10px; margin: 20px 40px; padding: 20px;
98
+ font-size: 14px}
99
+ #footer {text-align: center; color: #AAA; font-size: 12px}
100
+ </style>
101
+
102
+ </head>
103
+
104
+ <body>
105
+ <div id="header">
106
+ <h1>Nanite Control Tower</h1>
107
+ </div>
108
+
109
+ <h2>#{@mapper.options[:vhost]}</h2>
110
+ <div class="section">
111
+ <form method="post" action="/">
112
+ <input type="hidden" value="POST" name="_method"/>
113
+
114
+ <label>Send</label>
115
+ <select name="type">
116
+ <option #{@selection == 'least_loaded' ? 'selected="true"' : ''} value="least_loaded">the least loaded nanite</option>
117
+ <option #{@selection == 'random' ? 'selected="true"' : ''} value="random">a random nanite</option>
118
+ <option #{@selection == 'all' ? 'selected="true"' : ''} value="all">all nanites</option>
119
+ <option #{@selection == 'rr' ? 'selected="true"' : ''} value="rr">a nanite chosen by round robin</option>
120
+ #{@mapper.cluster.nanites.map {|k,v| "<option #{@selection == k ? 'selected="true"' : ''} value='#{k}'>#{k}</option>" }.join}
121
+ </select>
122
+
123
+ <label>providing service</label>
124
+ #{services}
125
+
126
+ <label>the payload</label>
127
+ <input type="text" class="text" name="payload" id="payload"/>
128
+
129
+ <input type="submit" class="submit" value="Go!" name="submit"/>
130
+ </form>
131
+
132
+ #{"<h3>Responses</h3>" if content}
133
+ #{content}
134
+ </div>
135
+
136
+ <h2>Running nanites</h2>
137
+ <div class="section">
138
+ #{"No nanites online." if @mapper.cluster.nanites.size == 0}
139
+ <ul>
140
+ #{@mapper.cluster.nanites.map {|k,v| "<li>identity : #{k}<br />load : #{v[:status]}<br />services : #{v[:services].to_a.inspect}</li>" }.join}
141
+ </ul>
142
+ </div>
143
+ <div id="footer">
144
+ Nanite #{Nanite::VERSION}
145
+ <br />
146
+ &copy; 2009 a bunch of random geeks
147
+ </div>
148
+ </body>
149
+ </html>
150
+ }
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,250 @@
1
+ module Nanite
2
+ class Agent
3
+ include AMQPHelper
4
+ include FileStreaming
5
+ include ConsoleHelper
6
+ include DaemonizeHelper
7
+
8
+ attr_reader :identity, :options, :serializer, :dispatcher, :registry, :amq, :tags
9
+ attr_accessor :status_proc
10
+
11
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({:user => 'nanite', :ping_time => 15,
12
+ :default_services => []}) unless defined?(DEFAULT_OPTIONS)
13
+
14
+ # Initializes a new agent and establishes AMQP connection.
15
+ # This must be used inside EM.run block or if EventMachine reactor
16
+ # is already started, for instance, by a Thin server that your Merb/Rails
17
+ # application runs on.
18
+ #
19
+ # Agent options:
20
+ #
21
+ # identity : identity of this agent, may be any string
22
+ #
23
+ # status_proc : a callable object that returns agent load as a string,
24
+ # defaults to load averages string extracted from `uptime`
25
+ # format : format to use for packets serialization. One of the three:
26
+ # :marshall, :json, or :yaml. Defaults to
27
+ # Ruby's Marshall format. For interoperability with
28
+ # AMQP clients implemented in other languages, use JSON.
29
+ #
30
+ # Note that Nanite uses JSON gem,
31
+ # and ActiveSupport's JSON encoder may cause clashes
32
+ # if ActiveSupport is loaded after JSON gem.
33
+ #
34
+ # root : application root for this agent, defaults to Dir.pwd
35
+ #
36
+ # log_dir : path to directory where agent stores it's log file
37
+ # if not given, app_root is used.
38
+ #
39
+ # file_root : path to directory to files this agent provides
40
+ # defaults to app_root/files
41
+ #
42
+ # ping_time : time interval in seconds between two subsequent heartbeat messages
43
+ # this agent broadcasts. Default value is 15.
44
+ #
45
+ # console : true tells Nanite to start interactive console
46
+ #
47
+ # daemonize : true tells Nanite to daemonize
48
+ #
49
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
50
+ # defaults to the root or the current working directory.
51
+ #
52
+ # services : list of services provided by this agent, by default
53
+ # all methods exposed by actors are listed
54
+ #
55
+ # single_threaded: Run all operations in one thread
56
+ #
57
+ # Connection options:
58
+ #
59
+ # vhost : AMQP broker vhost that should be used
60
+ #
61
+ # user : AMQP broker user
62
+ #
63
+ # pass : AMQP broker password
64
+ #
65
+ # host : host AMQP broker (or node of interest) runs on,
66
+ # defaults to 0.0.0.0
67
+ #
68
+ # port : port AMQP broker (or node of interest) runs on,
69
+ # this defaults to 5672, port used by some widely
70
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
71
+ #
72
+ # On start Nanite reads config.yml, so it is common to specify
73
+ # options in the YAML file. However, when both Ruby code options
74
+ # and YAML file specify option, Ruby code options take precedence.
75
+ def self.start(options = {})
76
+ agent = new(options)
77
+ agent.run
78
+ agent
79
+ end
80
+
81
+ def initialize(opts)
82
+ set_configuration(opts)
83
+ @tags = []
84
+ @tags << opts[:tag]
85
+ @tags.flatten!
86
+ @options.freeze
87
+ end
88
+
89
+ def run
90
+ log_path = false
91
+ if @options[:daemonize]
92
+ log_path = (@options[:log_dir] || @options[:root] || Dir.pwd)
93
+ end
94
+ Log.init(@identity, log_path)
95
+ Log.level = @options[:log_level] if @options[:log_level]
96
+ @serializer = Serializer.new(@options[:format])
97
+ @status_proc = lambda { parse_uptime(`uptime`) rescue 'no status' }
98
+ pid_file = PidFile.new(@identity, @options)
99
+ pid_file.check
100
+ if @options[:daemonize]
101
+ daemonize
102
+ pid_file.write
103
+ at_exit { pid_file.remove }
104
+ end
105
+ @amq = start_amqp(@options)
106
+ @registry = ActorRegistry.new
107
+ @dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @options)
108
+ setup_mapper_proxy
109
+ load_actors
110
+ setup_traps
111
+ setup_queue
112
+ advertise_services
113
+ setup_heartbeat
114
+ at_exit { un_register } unless $TESTING
115
+ start_console if @options[:console] && !@options[:daemonize]
116
+ end
117
+
118
+ def register(actor, prefix = nil)
119
+ registry.register(actor, prefix)
120
+ end
121
+
122
+ # Can be used in agent's initialization file to register a security module
123
+ # This security module 'authorize' method will be called back whenever the
124
+ # agent receives a request and will be given the corresponding deliverable.
125
+ # It should return 'true' for the request to proceed.
126
+ # Requests will return 'deny_token' or the string "Denied" by default when
127
+ # 'authorize' does not return 'true'.
128
+ def register_security(security, deny_token = "Denied")
129
+ @security = security
130
+ @deny_token = deny_token
131
+ end
132
+
133
+ protected
134
+
135
+ def set_configuration(opts)
136
+ @options = DEFAULT_OPTIONS.clone
137
+ root = opts[:root] || @options[:root]
138
+ custom_config = if root
139
+ file = File.expand_path(File.join(root, 'config.yml'))
140
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
141
+ else
142
+ {}
143
+ end
144
+ opts.delete(:identity) unless opts[:identity]
145
+ @options.update(custom_config.merge(opts))
146
+ @options[:file_root] ||= File.join(@options[:root], 'files')
147
+ return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
148
+ token = Identity.generate
149
+ @identity = "nanite-#{token}"
150
+ File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
151
+ fd.write(YAML.dump(custom_config.merge(:identity => token)))
152
+ end
153
+ end
154
+
155
+ def load_actors
156
+ return unless options[:root]
157
+ actors_dir = @options[:actors_dir] || "#{@options[:root]}/actors"
158
+ actors = @options[:actors]
159
+ Dir["#{actors_dir}/*.rb"].each do |actor|
160
+ next if actors && !actors.include?(File.basename(actor, ".rb"))
161
+ Nanite::Log.info("loading actor: #{actor}")
162
+ require actor
163
+ end
164
+ init_path = @options[:initrb] || File.join(options[:root], 'init.rb')
165
+ instance_eval(File.read(init_path), init_path) if File.exist?(init_path)
166
+ end
167
+
168
+ def receive(packet)
169
+ case packet
170
+ when Advertise
171
+ Nanite::Log.debug("handling Advertise: #{packet.inspect}")
172
+ advertise_services
173
+ when Request, Push
174
+ Nanite::Log.debug("handling Request: #{packet.inspect}")
175
+ if @security && !@security.authorize(packet)
176
+ if packet.kind_of?(Request)
177
+ r = Result.new(packet.token, packet.reply_to, @deny_token, identity)
178
+ amq.queue(packet.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
179
+ end
180
+ else
181
+ dispatcher.dispatch(packet)
182
+ end
183
+ when Result
184
+ Nanite::Log.debug("handling Result: #{packet.inspect}")
185
+ @mapper_proxy.handle_result(packet)
186
+ when IntermediateMessage
187
+ Nanite::Log.debug("handling Intermediate Result: #{packet.inspect}")
188
+ @mapper_proxy.handle_intermediate_result(packet)
189
+ end
190
+ end
191
+
192
+ def tag(*tags)
193
+ tags.each {|t| @tags << t}
194
+ @tags.uniq!
195
+ end
196
+
197
+ def setup_queue
198
+ amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
199
+ begin
200
+ info.ack
201
+ packet = serializer.load(msg)
202
+ receive(packet)
203
+ rescue Exception => e
204
+ Nanite::Log.error("Error handling packet: #{e.message}")
205
+ end
206
+ end
207
+ end
208
+
209
+ def setup_heartbeat
210
+ EM.add_periodic_timer(options[:ping_time]) do
211
+ amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
212
+ end
213
+ end
214
+
215
+ def setup_mapper_proxy
216
+ @mapper_proxy = MapperProxy.new(identity, options)
217
+ end
218
+
219
+ def setup_traps
220
+ ['INT', 'TERM'].each do |sig|
221
+ old = trap(sig) do
222
+ un_register
223
+ amq.instance_variable_get('@connection').close do
224
+ EM.stop
225
+ old.call if old.is_a? Proc
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ def un_register
232
+ unless @unregistered
233
+ @unregistered = true
234
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(UnRegister.new(identity)))
235
+ end
236
+ end
237
+
238
+ def advertise_services
239
+ Nanite::Log.debug("advertise_services: #{registry.services.inspect}")
240
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(Register.new(identity, registry.services, status_proc.call, self.tags)))
241
+ end
242
+
243
+ def parse_uptime(up)
244
+ if up =~ /load averages?: (.*)/
245
+ a,b,c = $1.split(/\s+|,\s+/)
246
+ (a.to_f + b.to_f + c.to_f) / 3
247
+ end
248
+ end
249
+ end
250
+ end