graphite-api 0.0.3.beta3 → 0.0.3

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.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # GraphiteAPI ( [![Gem Version](https://fury-badge.herokuapp.com/rb/graphite-api.png)](http://badge.fury.io/rb/graphite-api) )
1
+ # GraphiteAPI
2
2
  A Ruby API toolkit for [Graphite](http://graphite.wikidot.com/).
3
3
 
4
4
  ## Description
@@ -7,12 +7,27 @@ A Ruby API toolkit for [Graphite](http://graphite.wikidot.com/).
7
7
  ## Package Content
8
8
  * Includes a **simple** client for ruby.
9
9
  * Ships with a **GraphiteAPI-Middleware**, which is a lightweight, event-driven, aggregator daemon.
10
- * only one dependency (EventMachine).
10
+ * Only one gem dependency ( EventMachine ).
11
11
  * Utilities like scheduling and caching.
12
12
 
13
13
  ## Key Features
14
- * **Multiple Graphite Servers Support** - GraphiteAPI-Middleware supports sending aggregated data to multiple graphite servers, useful for large data centers and backup purposes
14
+ * **Multiple Graphite Servers Support** - GraphiteAPI-Middleware supports sending aggregated data to multiple graphite servers, in a multiplex fashion, useful for large data centers and backup purposes
15
15
  * **Reanimation mode** - support cases which the same keys (same timestamps as well) can be received simultaneously and asynchronously from multiple input sources, in these cases GraphiteAPI-Middleware will "reanimate" old records (records that were already sent to Graphite server), and will send the sum of the reanimated record value + the value of the record that was just received to the graphite server; this new summed record should override the key with the new value on Graphite database.
16
+ * **non-blocking I/O** ( EventMachine aware ).
17
+ * **Thread-Safe** client.
18
+
19
+ ## Status
20
+ <table>
21
+ <tr>
22
+ <td> Version </td>
23
+ <td><a href="https://rubygems.org/gems/graphite-api"><img src=https://fury-badge.herokuapp.com/rb/graphite-api.png></a> </td>
24
+ </tr>
25
+ <tr>
26
+ <td> Build </td>
27
+ <td><a href="https://travis-ci.org/kontera-technologies/graphite-api"><img src=https://travis-ci.org/kontera-technologies/graphite-api.png?branch=master></a>
28
+ </td>
29
+ </tr>
30
+ </table>
16
31
 
17
32
  ## Installation
18
33
  Install stable version
@@ -30,55 +45,125 @@ rake install
30
45
  ```
31
46
 
32
47
  ## Client Usage
48
+ Creating a new client instance
49
+
50
+ ```ruby
51
+ require 'graphite-api'
52
+
53
+ GraphiteAPI::Client.new(
54
+ graphite: "graphite.example.com:2003", # not optional
55
+ prefix: ["example","prefix"], # add example.prefix to each key
56
+ slice: 60 # results are aggregated in 60 seconds slices
57
+ interval: 60 # send to graphite every 60 seconds
58
+ cache: 4 * 60 * 60 # set the max age in seconds for records reanimation
59
+ )
60
+ ```
61
+
62
+ Adding simple metrics
33
63
  ```ruby
34
- require 'graphite-api'
35
- require 'logger'
36
-
37
- # Turn on the logging ( optional )
38
- GraphiteAPI::Logger.logger = ::Logger.new(STDOUT)
39
- GraphiteAPI::Logger.logger.level = ::Logger::DEBUG
40
-
41
- # Setup client
42
- client = GraphiteAPI::Client.new(
43
- :graphite => "graphite.example.com:2003",
44
- :prefix => ["example","prefix"], # add example.prefix to each key
45
- :slice => 60.seconds # results are aggregated in 60 seconds slices
46
- :interval => 60.seconds # send to graphite every 60 seconds
47
- )
48
-
49
- # Simple
50
- client.webServer.web01.loadAvg 10.7
51
- # => example.prefix.webServer.web01.loadAvg 10.7 time.now.to_i
52
-
53
- # "Same Same But Different" ( http://en.wikipedia.org/wiki/Tinglish )
54
- client.metrics "webServer.web01.loadAvg" => 10.7
55
- # => example.prefix.webServer.web01.loadAvg 10.7 time.now.to_i
56
-
57
- # With event time
58
- client.webServer.web01.blaBlaBla(29.1, Time.at(9999999999))
59
- # => example.prefix.webServer.web01.blaBlaBla 29.1 9999999999
60
-
61
- # Multiple with event time
62
- client.metrics({
63
- "webServer.web01.loadAvg" => 10.7,
64
- "webServer.web01.memUsage" => 40
65
- },Time.at(1326067060))
66
- # => example.prefix.webServer.web01.loadAvg 10.7 1326067060
67
- # => example.prefix.webServer.web01.memUsage 40 1326067060
68
-
69
- # Timers
70
- client.every 10.seconds do |c|
71
- c.webServer.web01.uptime `uptime`.split.first.to_i
72
- # => example.prefix.webServer.web01.uptime 40 1326067060
73
- end
74
-
75
- client.every 52.minutes do |c|
76
- c.abcd.efghi.jklmnop.qrst 12
77
- # => example.prefix.abcd.efghi.jklmnop.qrst 12 1326067060
78
- end
79
-
80
- client.join # wait...
81
- ```
64
+ require 'graphite-api'
65
+
66
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
67
+
68
+ client.metrics "webServer.web01.loadAvg" => 10.7
69
+ # => webServer.web01.loadAvg 10.7 time.now.to_i
70
+
71
+ client.metrics(
72
+ "webServer.web01.loadAvg" => 10.7,
73
+ "webServer.web01.memUsage" => 40
74
+ )
75
+ # => webServer.web01.loadAvg 10.7 1326067060
76
+ # => webServer.web01.memUsage 40 1326067060
77
+ ```
78
+
79
+ Adding metrics with timestamp
80
+ ```ruby
81
+ require 'graphite-api'
82
+
83
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
84
+
85
+ client.metrics({
86
+ "webServer.web01.loadAvg" => 10.7,
87
+ "webServer.web01.memUsage" => 40
88
+ },Time.at(1326067060))
89
+ # => webServer.web01.loadAvg 10.7 1326067060
90
+ # => webServer.web01.memUsage 40 1326067060
91
+ ```
92
+
93
+ Some DSL sweetness
94
+ ```ruby
95
+ require 'graphite-api'
96
+
97
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
98
+
99
+ client.webServer.web01.loadAvg 10.7
100
+ # => webServer.web01.loadAvg 10.7 time.now.to_i
101
+
102
+ client.webServer.web01.blaBlaBla(29.1, Time.at(9999999999))
103
+ # => webServer.web01.blaBlaBla 29.1 9999999999
104
+ ```
105
+
106
+ Built-in timers support
107
+ ```ruby
108
+ require 'graphite-api'
109
+
110
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
111
+
112
+ # lets send the metric every 120 seconds
113
+ client.every(120) do |c|
114
+ c.webServer.web01.uptime `uptime`.split.first.to_i
115
+ end
116
+ ```
117
+
118
+ Built-in extension for time declarations stuff, like 2.minutes, 3.hours etc...
119
+ ```ruby
120
+ require 'graphite-api'
121
+ require 'graphite-api/core_ext/numeric'
122
+
123
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
124
+
125
+ client.every 10.seconds do |c|
126
+ c.webServer.web01.uptime `uptime`.split.first.to_i
127
+ end
128
+
129
+ client.every 52.minutes do |c|
130
+ c.just.fake 12
131
+ end
132
+ ```
133
+
134
+ Make your own custom metrics daemons, using `client#join`
135
+ ```ruby
136
+ require 'graphite-api'
137
+ require 'graphite-api/core_ext/numeric'
138
+
139
+ client = GraphiteAPI::Client.new( graphite: 'graphite:2003' )
140
+
141
+ client.every 26.minutes do |c|
142
+ c.webServer.shuki.stats 10
143
+ c.webServer.shuki.x 97
144
+ c.webServer.shuki.y 121
145
+ end
146
+
147
+ client.join # wait for ever...
148
+ ```
149
+
150
+ Logging support
151
+
152
+ ```ruby
153
+ # Provide an external logger
154
+ require 'graphite-api'
155
+ require 'logger'
156
+
157
+ GraphiteAPI::Logger.logger = ::Logger.new(STDOUT)
158
+ GraphiteAPI::Logger.logger.level = ::Logger::DEBUG
159
+
160
+ # Or use the built-in one
161
+ GraphiteAPI::Logger.init(
162
+ :level => :debug,
163
+ :std => 'logger.out' # or STDOUT | STDERR
164
+ )
165
+ ```
166
+
82
167
  > more examples can be found [here](https://github.com/kontera-technologies/graphite-api/tree/master/examples).
83
168
 
84
169
  ## GraphiteAPI-Middleware Usage
@@ -142,21 +227,13 @@ client.bla.bla.value2 27
142
227
  > more examples can be found [here](https://github.com/kontera-technologies/graphite-api/tree/master/examples).
143
228
 
144
229
 
145
- ## Recommended Topologies
230
+ ## Example Setup
146
231
  <br/>
147
-
148
- <img src="https://raw.github.com/kontera-technologies/graphite-api/master/examples/graphite-middleware-star.jpg" align="center">
149
-
150
- <hr/>
151
- <br/>
152
-
153
- <img src="https://raw.github.com/kontera-technologies/graphite-api/master/examples/graphite-middleware-mesh.jpg" align="center">
154
-
155
- <hr/>
232
+ <img src="https://raw.github.com/kontera-technologies/graphite-api/master/examples/middleware_t1.png" align="center">
156
233
 
157
234
  ## TODO:
158
- * Documentation
159
- * Use Redis
235
+ * Better documentation
236
+ * Use Redis for caching
160
237
  * Multiple backends via client as well
161
238
 
162
239
  ## Bugs
data/Rakefile CHANGED
@@ -3,4 +3,8 @@ Dir.chdir File.dirname __FILE__
3
3
 
4
4
  require 'graphite-api'
5
5
 
6
+ def message msg
7
+ puts "*** #{msg} ***"
8
+ end
9
+
6
10
  Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -19,5 +19,4 @@ module GraphiteAPI
19
19
  GraphiteAPI::Version::VERSION
20
20
  end
21
21
 
22
- Dir.glob( "#{ROOT}/core-extensions/**" ).each &method( :require )
23
22
  end
@@ -8,11 +8,11 @@ module GraphiteAPI
8
8
  end
9
9
 
10
10
  def get time, key
11
- cache[time][key]
11
+ cache[time.to_i][key]
12
12
  end
13
13
 
14
14
  def set time, key, value
15
- cache[time][key] = value.to_f
15
+ cache[time.to_i][key] = value.to_f
16
16
  end
17
17
 
18
18
  def incr time, key, value
@@ -22,12 +22,12 @@ module GraphiteAPI
22
22
  private
23
23
 
24
24
  def cache
25
- @cache ||= Hash.new {|h,k| h[k] = Hash.new {|h1,k1| h1[k1] = 0}}
25
+ @cache ||= nested_zero_hash
26
26
  end
27
27
 
28
- def clean age
28
+ def clean max_age
29
29
  debug [:MemoryCache, :before_clean, cache]
30
- cache.delete_if {|t,k| Time.now.to_i - t > age}
30
+ cache.delete_if {|t,k| Time.now.to_i - t > max_age }
31
31
  debug [:MemoryCache, :after_clean, cache]
32
32
  end
33
33
 
@@ -11,15 +11,12 @@
11
11
  # :interval => 60.seconds # send to graphite every 60 seconds
12
12
  # )
13
13
  #
14
- # # Simple
15
14
  # client.webServer.web01.loadAvg 10.7
16
15
  # # => example.prefix.webServer.web01.loadAvg 10.7 time.now.to_i
17
16
 
18
- # # "Same Same But Different" ( http://en.wikipedia.org/wiki/Tinglish )
19
17
  # client.metrics "webServer.web01.loadAvg" => 10.7
20
18
  # # => example.prefix.webServer.web01.loadAvg 10.7 time.now.to_i
21
19
  #
22
- # # Multiple with event time
23
20
  # client.metrics({
24
21
  # "webServer.web01.loadAvg" => 10.7,
25
22
  # "webServer.web01.memUsage" => 40
@@ -23,8 +23,7 @@ module GraphiteAPI
23
23
 
24
24
  def puts message
25
25
  begin
26
- Logger.debug [:connector,:puts,[host,port].join(":"),message]
27
- return
26
+ debug [:connector,:puts,[host,port].join(":"),message]
28
27
  socket.puts message + "\n"
29
28
  rescue Errno::EPIPE, Errno::EINVAL
30
29
  @socket = nil
@@ -16,7 +16,6 @@
16
16
  # ["mem.usage", 190.0, 1326842520]
17
17
  # ["shuki.tuki", 999.0, 1326842520]
18
18
  # -----------------------------------------------------
19
-
20
19
  require 'thread'
21
20
  require 'set'
22
21
 
@@ -48,7 +47,7 @@ module GraphiteAPI
48
47
  streamer[client_id] += char
49
48
 
50
49
  if closed_stream? streamer[client_id]
51
- if valid_stream_message streamer[client_id]
50
+ if valid_stream_message? streamer[client_id]
52
51
  push stream_message_to_obj streamer[client_id]
53
52
  end
54
53
  streamer.delete client_id
@@ -62,16 +61,16 @@ module GraphiteAPI
62
61
  debug [:buffer,:add, obj]
63
62
  queue.push obj
64
63
  nil
65
- end
64
+ end
66
65
 
67
66
  alias_method :<<, :push
68
67
 
69
68
  def pull format = nil
70
- data = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = 0} }
69
+ data = nested_zero_hash
71
70
 
72
71
  counter = 0
73
72
  while new_records?
74
- break if ( counter += 1 ) > 10_000
73
+ break if ( counter += 1 ) > 1_000_000 # TODO: fix this
75
74
  hash = queue.pop
76
75
  time = normalize_time(hash[:time],options[:slice])
77
76
  hash[:metric].each { |k,v| data[time][k] += v.to_f }
@@ -91,7 +90,8 @@ module GraphiteAPI
91
90
  end
92
91
 
93
92
  def inspect
94
- "#<GraphiteAPI::SafeBuffer:#{object_id} @quque#size=#{queue.size} @streamer=#{streamer.inspect}>"
93
+ "#<GraphiteAPI::SafeBuffer:%s @quque#size=%s @streamer=%s>" %
94
+ [object_id,queue.size,streamer]
95
95
  end
96
96
 
97
97
  private
@@ -109,7 +109,7 @@ module GraphiteAPI
109
109
  string[-1,1] == END_OF_STREAM
110
110
  end
111
111
 
112
- def valid_stream_message message
112
+ def valid_stream_message? message
113
113
  message =~ VALID_MESSAGE
114
114
  end
115
115
 
@@ -28,7 +28,11 @@ module GraphiteAPI
28
28
  slice = 60 if slice.nil?
29
29
  ((time || Time.now).to_i / slice * slice).to_i
30
30
  end
31
-
31
+
32
+ def nested_zero_hash
33
+ Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = 0} }
34
+ end
35
+
32
36
  module_function
33
37
 
34
38
  def expand_host host
@@ -1,5 +1,5 @@
1
1
  module GraphiteAPI
2
2
  class Version
3
- VERSION = "0.0.3.beta3"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
@@ -19,7 +19,8 @@ GraphiteAPI::GemSpec = Gem::Specification.new do |s|
19
19
  s.add_dependency 'eventmachine','>= 0.3.3'
20
20
  end
21
21
 
22
- task :gem => [:clobber_package]
22
+ task :gem => [:test,:clobber_package]
23
+
23
24
  Gem::PackageTask.new(GraphiteAPI::GemSpec) do |p|
24
25
  p.gem_spec = GraphiteAPI::GemSpec
25
26
  end
@@ -1,10 +1,42 @@
1
1
  require "rake/testtask"
2
2
 
3
- task(:test) { ENV['with_coverage'] = "true" }
3
+ task(:test => :functional) { ENV['with_coverage'] = "true" }
4
4
 
5
5
  Rake::TestTask.new(:test) do |t|
6
6
  t.libs << "tests"
7
7
  t.pattern = "tests/**/*_test.rb"
8
8
  end
9
9
 
10
+
11
+ task :functional do
12
+ some_failed = false
13
+
14
+ next unless ENV['SKIP_FUNC'].nil?
15
+
16
+ unless RUBY_COPYRIGHT.end_with?("Matsumoto")
17
+ puts("Functional tests are enabled only on MRI...")
18
+ next
19
+ end
20
+
21
+ message "Executing GraphiteAPI Functional Tests"
22
+ message "( You can skip them by passing SKIP_FUNC=true )"
23
+
24
+ Dir[File.expand_path("../../tests/functional/*",__FILE__)].each do |file|
25
+ next unless file.end_with?(".rb")
26
+ now = Time.now.to_i
27
+ name = File.basename(file)
28
+ message "Executing #{name}"
29
+ Process.waitpid(Process.spawn("ruby", File.expand_path(file)))
30
+ took = "took #{Time.now.to_i - now} seconds"
31
+ if $?.success?
32
+ message "[PASS] #{name}, #{took}"
33
+ else
34
+ message "[FAIL] #{name}, #{took}"
35
+ some_failed = true
36
+ end
37
+ end
38
+ message "Done Executing GraphiteAPI Functional Tests"
39
+ abort "Some functional tests failed..." if some_failed
40
+ end
41
+
10
42
  task :default => :test
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphite-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3.beta3
5
- prerelease: 6
4
+ version: 0.0.3
5
+ prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Eran Barak Levi
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-06 00:00:00.000000000 Z
12
+ date: 2013-03-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
@@ -37,14 +37,13 @@ files:
37
37
  - README.md
38
38
  - Rakefile
39
39
  - bin/graphite-middleware
40
- - lib/core-extensions/numeric.rb
41
- - lib/graphite-api/buffer.rb
42
40
  - lib/graphite-api/cache/memory.rb
43
41
  - lib/graphite-api/cache.rb
44
42
  - lib/graphite-api/cli.rb
45
43
  - lib/graphite-api/client.rb
46
44
  - lib/graphite-api/connector.rb
47
45
  - lib/graphite-api/connector_group.rb
46
+ - lib/graphite-api/core_ext/numeric.rb
48
47
  - lib/graphite-api/logger.rb
49
48
  - lib/graphite-api/middleware.rb
50
49
  - lib/graphite-api/reactor.rb
@@ -70,9 +69,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
69
  required_rubygems_version: !ruby/object:Gem::Requirement
71
70
  none: false
72
71
  requirements:
73
- - - ! '>'
72
+ - - ! '>='
74
73
  - !ruby/object:Gem::Version
75
- version: 1.3.1
74
+ version: '0'
76
75
  requirements: []
77
76
  rubyforge_project: graphite-api
78
77
  rubygems_version: 1.8.25
@@ -1,137 +0,0 @@
1
- # -----------------------------------------------------
2
- # Buffer Object
3
- # Handle Socket & Client data streams
4
- # -----------------------------------------------------
5
- # Usage:
6
- # buff = GraphiteAPI::Buffer.new(GraphiteAPI::Utils.default_options)
7
- # buff << {:metric => {"load_avg" => 10},:time => Time.now}
8
- # buff << {:metric => {"load_avg" => 30},:time => Time.now}
9
- # buff.stream "mem.usage 1"
10
- # buff.stream "90 1326842563\n"
11
- # buff.stream "shuki.tuki 999 1326842563\n"
12
- # buff.pull.each {|o| p o}
13
- #
14
- # Produce:
15
- # ["load_avg", 40.0, 1326881160]
16
- # ["mem.usage", 190.0, 1326842520]
17
- # ["shuki.tuki", 999.0, 1326842520]
18
- # -----------------------------------------------------
19
- require 'set'
20
-
21
- module GraphiteAPI
22
- class Buffer
23
- include Utils
24
-
25
- CLOSING_STREAM = "\n" # end of message - when streaming to buffer obj
26
- CHARS_TO_IGNORE = %w(\r) # skip these chars when parsing new message
27
- FLOATS_ROUND_BY = 2 # round(x) after summing floats
28
-
29
- VALID_MESSAGE = /^[\w|\.]+ \d+(?:\.|\d)* \d+$/
30
-
31
- def initialize options
32
- @options = options
33
- @keys_to_sync = Hash.new { |h,k| h[k] = Set.new }
34
- @streamer_buff = Hash.new {|h,k| h[k] = ""}
35
- @reanimation_mode = !options[:cache].nil?
36
- start_cleaner if reanimation_mode
37
- end
38
-
39
- private_reader :options, :keys_to_sync, :reanimation_mode, :streamer_buff
40
-
41
- def push hash
42
- debug [:buffer,:add, hash]
43
- time = normalize_time(hash[:time],options[:slice])
44
- hash[:metric].each { |k,v| cache_set(time,k,v) }
45
- end
46
-
47
- alias_method :<<, :push
48
-
49
- def stream message, client_id = nil
50
- message.gsub(/\t/,' ').each_char do |char|
51
- next if invalid_char? char
52
- streamer_buff[client_id] += char
53
-
54
- if closed_stream? streamer_buff[client_id]
55
- if valid streamer_buff[client_id]
56
- push build_metric *streamer_buff[client_id].split
57
- end
58
- streamer_buff.delete client_id
59
- end
60
- end
61
- end
62
-
63
- def pull as = nil
64
- Array.new.tap do |data|
65
- keys_to_sync.each do |time,keys|
66
- keys.each do |key|
67
- data.push cache_get(time, key, as)
68
- end
69
- end
70
- clear
71
- end
72
-
73
- end
74
-
75
- def new_records?
76
- !keys_to_sync.empty?
77
- end
78
-
79
- private
80
-
81
- def closed_stream? string
82
- string[-1,1] == CLOSING_STREAM
83
- end
84
-
85
- def invalid_char? char
86
- CHARS_TO_IGNORE.include? char
87
- end
88
-
89
- def cache_set time, key, value
90
- buffer_cache[time][key] = sum buffer_cache[time][key], value.to_f
91
- keys_to_sync[time].add key
92
- end
93
-
94
- def sum float1, float2
95
- ("%.#{FLOATS_ROUND_BY}f" % (float1 + float2)).to_f
96
- end
97
-
98
- def cache_get time, key, as
99
- metric = [prefix + key,buffer_cache[time][key],time]
100
- as == :string ? metric.join(" ") : metric
101
- end
102
-
103
- def build_metric key, value, time
104
- { :metric => { key => value },:time => Time.at(time.to_i) }
105
- end
106
-
107
- def clear
108
- keys_to_sync.clear
109
- buffer_cache.clear unless reanimation_mode
110
- end
111
-
112
- def valid message
113
- message =~ VALID_MESSAGE
114
- end
115
-
116
- def prefix
117
- @prefix ||= options[:prefix].empty? ? '' : prefix_to_s
118
- end
119
-
120
- def prefix_to_s
121
- Array(options[:prefix]).join('.') << '.'
122
- end
123
-
124
- def buffer_cache
125
- @buffer_cache ||= Hash.new {|h,k| h[k] = Hash.new {|h1,k1| h1[k1] = 0}}
126
- end
127
-
128
- def clean age
129
- [buffer_cache,keys_to_sync].each {|o| o.delete_if {|t,k| Time.now.to_i - t > age}}
130
- end
131
-
132
- def start_cleaner
133
- Reactor::every(options[:cleaner_interval]) { clean(options[:cache]) }
134
- end
135
-
136
- end
137
- end