fluent-plugin-kinesis 0.4.1 → 1.0.0

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -18
  3. data/.travis.yml +9 -9
  4. data/CHANGELOG.md +9 -0
  5. data/CONTRIBUTORS.txt +1 -1
  6. data/Gemfile +12 -9
  7. data/LICENSE.txt +39 -201
  8. data/Makefile +40 -0
  9. data/NOTICE.txt +1 -1
  10. data/README-v0.4.md +348 -0
  11. data/README.md +398 -183
  12. data/Rakefile +20 -14
  13. data/benchmark/dummer.conf +13 -0
  14. data/benchmark/firehose.conf +24 -0
  15. data/benchmark/producer.conf +28 -0
  16. data/benchmark/streams.conf +24 -0
  17. data/fluent-plugin-kinesis.gemspec +34 -23
  18. data/gemfiles/Gemfile.fluentd-0.10.58 +20 -0
  19. data/lib/fluent/plugin/kinesis_helper.rb +30 -0
  20. data/lib/fluent/plugin/kinesis_helper/api.rb +164 -0
  21. data/lib/fluent/plugin/kinesis_helper/class_methods.rb +120 -0
  22. data/lib/fluent/plugin/kinesis_helper/client.rb +36 -0
  23. data/lib/fluent/plugin/kinesis_helper/credentials.rb +51 -0
  24. data/lib/fluent/plugin/kinesis_helper/error.rb +38 -0
  25. data/lib/fluent/plugin/kinesis_helper/format.rb +85 -0
  26. data/lib/fluent/plugin/kinesis_helper/initialize.rb +58 -0
  27. data/lib/fluent/plugin/kinesis_helper/kpl.rb +81 -0
  28. data/lib/fluent/plugin/out_kinesis.rb +13 -11
  29. data/lib/fluent/plugin/out_kinesis_firehose.rb +44 -0
  30. data/lib/fluent/plugin/out_kinesis_producer.rb +38 -0
  31. data/lib/fluent/plugin/out_kinesis_streams.rb +47 -0
  32. data/lib/fluent/plugin/patched_detach_process_impl.rb +103 -0
  33. data/lib/fluent_plugin_kinesis/version.rb +17 -0
  34. data/lib/kinesis_producer.rb +24 -0
  35. data/lib/kinesis_producer/binary.rb +10 -0
  36. data/lib/kinesis_producer/daemon.rb +238 -0
  37. data/lib/kinesis_producer/library.rb +122 -0
  38. data/lib/kinesis_producer/protobuf/config.pb.rb +66 -0
  39. data/lib/kinesis_producer/protobuf/messages.pb.rb +151 -0
  40. data/lib/kinesis_producer/tasks/binary.rake +73 -0
  41. metadata +196 -36
  42. data/lib/fluent/plugin/version.rb +0 -16
  43. data/test/helper.rb +0 -32
  44. data/test/plugin/test_out_kinesis.rb +0 -641
data/Rakefile CHANGED
@@ -1,23 +1,29 @@
1
- # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
1
  #
3
- # Licensed under the Apache License, Version 2.0 (the "License"). You
4
- # may not use this file except in compliance with the License. A copy of
5
- # the License is located at
2
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
6
3
  #
7
- # http://www.apache.org/licenses/LICENSE-2.0
4
+ # Licensed under the Amazon Software License (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
8
7
  #
9
- # or in the "license" file accompanying this file. This file is
10
- # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
- # ANY KIND, either express or implied. See the License for the specific
12
- # language governing permissions and limitations under the License.
8
+ # http://aws.amazon.com/asl/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
13
14
 
14
15
  require "bundler/gem_tasks"
15
16
 
16
17
  require 'rake/testtask'
17
- Rake::TestTask.new(:test) do |test|
18
- test.libs << 'lib' << 'test'
19
- test.pattern = 'test/**/test_*.rb'
20
- test.verbose = true
18
+
19
+ task default: [:test]
20
+ Rake::TestTask.new do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.test_files = FileList['test/**/test_*.rb']
23
+ test.options = '-v'
21
24
  end
22
25
 
23
- task :default => :test
26
+ load 'kinesis_producer/tasks/binary.rake'
27
+
28
+ Rake::Task[:build].enhance [:binaries]
29
+ Rake::Task[:test].enhance [:binaries]
@@ -0,0 +1,13 @@
1
+ configure 'sample' do
2
+ output "/tmp/dummy.log"
3
+ rate 5000
4
+ delimiter "\t"
5
+ labeled true
6
+ field :id, type: :integer, countup: true, format: "%04d"
7
+ field :time, type: :datetime, format: "[%Y-%m-%d %H:%M:%S]", random: false
8
+ field :level, type: :string, any: %w[DEBUG INFO WARN ERROR]
9
+ field :method, type: :string, any: %w[GET POST PUT]
10
+ field :uri, type: :string, any: %w[/api/v1/people /api/v1/textdata /api/v1/messages]
11
+ field :reqtime, type: :float, range: 0.1..5.0
12
+ field :foobar, type: :string, length: 8
13
+ end
@@ -0,0 +1,24 @@
1
+ <source>
2
+ @type tail
3
+ path /tmp/dummy.log
4
+ format none
5
+ tag dummy
6
+ </source>
7
+
8
+ <source>
9
+ @type forward
10
+ </source>
11
+
12
+ <match dummy>
13
+ @type kinesis_firehose
14
+ flush_interval 1
15
+ buffer_chunk_limit 1m
16
+ try_flush_interval 0.1
17
+ queued_chunk_flush_interval 0.01
18
+ num_threads 15
19
+ detach_process 5
20
+ log_level debug
21
+
22
+ region us-west-2
23
+ delivery_stream_name fluent-plugin-test
24
+ </match>
@@ -0,0 +1,28 @@
1
+ <source>
2
+ @type tail
3
+ path /tmp/dummy.log
4
+ format none
5
+ tag dummy
6
+ </source>
7
+
8
+ <source>
9
+ @type forward
10
+ </source>
11
+
12
+ <match dummy>
13
+ @type kinesis_producer
14
+ flush_interval 1
15
+ buffer_chunk_limit 1m
16
+ try_flush_interval 0.1
17
+ queued_chunk_flush_interval 0.01
18
+ num_threads 15
19
+ detach_process 5
20
+ log_level debug
21
+
22
+ region ap-northeast-1
23
+ stream_name fluent-plugin-test
24
+ debug true
25
+ <kinesis_producer>
26
+ record_max_buffered_time 1000
27
+ </kinesis_producer>
28
+ </match>
@@ -0,0 +1,24 @@
1
+ <source>
2
+ @type tail
3
+ path /tmp/dummy.log
4
+ format none
5
+ tag dummy
6
+ </source>
7
+
8
+ <source>
9
+ @type forward
10
+ </source>
11
+
12
+ <match dummy>
13
+ @type kinesis_streams
14
+ flush_interval 1
15
+ buffer_chunk_limit 1m
16
+ try_flush_interval 0.1
17
+ queued_chunk_flush_interval 0.01
18
+ num_threads 15
19
+ detach_process 5
20
+ log_level debug
21
+
22
+ region ap-northeast-1
23
+ stream_name fluent-plugin-test
24
+ </match>
@@ -1,21 +1,22 @@
1
- # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
1
+ # coding: utf-8
2
+ #
3
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
4
  #
3
- # Licensed under the Apache License, Version 2.0 (the "License"). You
4
- # may not use this file except in compliance with the License. A copy of
5
- # the License is located at
5
+ # Licensed under the Amazon Software License (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # A copy of the License is located at
6
8
  #
7
- # http://www.apache.org/licenses/LICENSE-2.0
9
+ # http://aws.amazon.com/asl/
8
10
  #
9
- # or in the "license" file accompanying this file. This file is
10
- # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
- # ANY KIND, either express or implied. See the License for the specific
12
- # language governing permissions and limitations under the License.
11
+ # or in the "license" file accompanying this file. This file is distributed
12
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13
+ # express or implied. See the License for the specific language governing
14
+ # permissions and limitations under the License.
13
15
 
14
- # coding: utf-8
15
16
  lib = File.expand_path('../lib', __FILE__)
16
17
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
17
-
18
- require "fluent/plugin/version"
18
+ require 'fluent_plugin_kinesis/version'
19
+ require 'kinesis_producer/binary'
19
20
 
20
21
  Gem::Specification.new do |spec|
21
22
  spec.name = "fluent-plugin-kinesis"
@@ -23,19 +24,29 @@ Gem::Specification.new do |spec|
23
24
  spec.author = 'Amazon Web Services'
24
25
  spec.summary = %q{Fluentd output plugin that sends events to Amazon Kinesis.}
25
26
  spec.homepage = "https://github.com/awslabs/aws-fluent-plugin-kinesis"
26
- spec.license = "Apache License, Version 2.0"
27
+ spec.license = "Amazon Software License"
27
28
 
28
- spec.files = `git ls-files`.split($/)
29
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
29
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ spec.files += KinesisProducer::Binary::Files.values
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
32
  spec.require_paths = ["lib"]
32
- spec.required_ruby_version = '>= 1.9.3'
33
+ spec.required_ruby_version = '>= 2.0.0'
33
34
 
34
- spec.add_development_dependency "bundler", "~> 1.3"
35
- spec.add_development_dependency "rake", "~> 10.0"
36
- spec.add_development_dependency "test-unit-rr", "~> 1.0"
35
+ spec.add_dependency "fluentd", ">= 0.10.58", "< 2"
36
+ spec.add_dependency "protobuf", ">= 3.5.5"
37
+ spec.add_dependency "aws-sdk", "~> 2"
38
+ spec.add_dependency "concurrent-ruby", "~> 1"
39
+ spec.add_dependency "os", ">= 0.9.6"
37
40
 
38
- spec.add_dependency "fluentd", ">= 0.10.53", "< 2"
39
- spec.add_dependency "aws-sdk-core", ">= 2.0.12", "< 3.0"
40
- spec.add_dependency "msgpack", ">= 0.5.8"
41
+ spec.add_development_dependency "bundler", "~> 1.10"
42
+ spec.add_development_dependency "rake", "~> 10.0"
43
+ spec.add_development_dependency "test-unit", ">= 3.0.8"
44
+ spec.add_development_dependency "test-unit-rr", ">= 1.0.3"
45
+ spec.add_development_dependency "pry", ">= 0.10.1"
46
+ spec.add_development_dependency "pry-byebug", ">= 3.3.0"
47
+ spec.add_development_dependency "pry-stack_explorer", ">= 0.4.9.2"
48
+ spec.add_development_dependency "net-empty_port", ">= 0.0.2"
49
+ spec.add_development_dependency "dummer", ">= 0.4.0"
50
+ spec.add_development_dependency "rubyzip", ">= 1.0.0"
51
+ spec.add_development_dependency "mocha", ">= 1.1.0"
41
52
  end
@@ -0,0 +1,20 @@
1
+ #
2
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Amazon Software License (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/asl/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+
15
+ source 'https://rubygems.org'
16
+
17
+ # Specify your gem's dependencies in fluent-plugin-kinesis.gemspec
18
+ gemspec path: ".."
19
+
20
+ gem "fluentd", "0.10.58"
@@ -0,0 +1,30 @@
1
+ #
2
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Amazon Software License (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/asl/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+
15
+ require 'fluent/plugin/kinesis_helper/class_methods'
16
+ require 'fluent/plugin/kinesis_helper/initialize'
17
+ require 'fluent/plugin/kinesis_helper/error'
18
+
19
+ module Fluent
20
+ module KinesisHelper
21
+ include Fluent::SetTimeKeyMixin
22
+ include Fluent::SetTagKeyMixin
23
+ include Fluent::DetachMultiProcessMixin
24
+
25
+ def self.included(klass)
26
+ klass.extend ClassMethods
27
+ end
28
+ include Initialize
29
+ end
30
+ end
@@ -0,0 +1,164 @@
1
+ #
2
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Amazon Software License (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/asl/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+
15
+ require 'fluent_plugin_kinesis/version'
16
+
17
+ module Fluent
18
+ module KinesisHelper
19
+ module API
20
+ def configure(conf)
21
+ super
22
+ if @batch_request_max_count > self.class::BatchRequestLimitCount
23
+ raise ConfigError, "batch_request_max_count can't be grater than #{self.class::BatchRequestLimitCount}."
24
+ end
25
+ if @batch_request_max_size > self.class::BatchRequestLimitSize
26
+ raise ConfigError, "batch_request_max_size can't be grater than #{self.class::BatchRequestLimitSize}."
27
+ end
28
+ @region = client.config.region if @region.nil?
29
+ end
30
+
31
+ def start
32
+ detach_multi_process do
33
+ super
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def client_options
40
+ options = {
41
+ user_agent_suffix: "fluent-plugin-kinesis/#{request_type}/#{FluentPluginKinesis::VERSION}",
42
+ credentials: credentials,
43
+ }
44
+ options.update(region: @region) unless @region.nil?
45
+ options.update(http_proxy: @http_proxy) unless @http_proxy.nil?
46
+ options.update(endpoint: @endpoint) unless @endpoint.nil?
47
+ options.update(ssl_verify_peer: @ssl_verify_peer) unless @ssl_verify_peer.nil?
48
+ if @debug
49
+ options.update(logger: Logger.new(log.out))
50
+ options.update(log_level: :debug)
51
+ end
52
+ options
53
+ end
54
+
55
+ def split_to_batches(records)
56
+ batch_by_limit(records, @batch_request_max_count, @batch_request_max_size)
57
+ end
58
+
59
+ def batch_by_limit(records, max_count, max_size)
60
+ result, buf, size = records.inject([[],[],0]){|(result, buf, size), record|
61
+ if buf.size >= max_count or size >= max_size
62
+ result << buf
63
+ buf = []
64
+ size = 0
65
+ end
66
+ buf << record
67
+ size += size_of_values(record)
68
+ [result, buf, size]
69
+ }
70
+ result << buf
71
+ end
72
+
73
+ def size_of_values(record)
74
+ record.values_at(:data, :partition_key).compact.map(&:size).inject(:+) || 0
75
+ end
76
+
77
+ def batch_request_with_retry(batch, retry_count=0, backoff: nil)
78
+ backoff ||= Backoff.new
79
+ res = batch_request(batch)
80
+ if failed_count(res) > 0
81
+ failed_records = collect_failed_records(batch, res)
82
+ if retry_count < @retries_on_batch_request
83
+ backoff.reset if @reset_backoff_if_success and any_records_shipped?(res)
84
+ sleep(backoff.next)
85
+ log.warn(truncate 'Retrying to request batch. Retry count: %d, Retry records: %d' % [retry_count, failed_records.size])
86
+ retry_batch = failed_records.map{|r| r[:original] }
87
+ batch_request_with_retry(retry_batch, retry_count + 1, backoff: backoff)
88
+ else
89
+ give_up_retries(failed_records)
90
+ end
91
+ end
92
+ end
93
+
94
+ def any_records_shipped?(res)
95
+ results(res).size > failed_count(res)
96
+ end
97
+
98
+ def collect_failed_records(records, res)
99
+ failed_records = []
100
+ results(res).each_with_index do |record, index|
101
+ next unless record[:error_code]
102
+ failed_records.push(
103
+ original: records[index],
104
+ error_code: record[:error_code],
105
+ error_message: record[:error_message]
106
+ )
107
+ end
108
+ failed_records
109
+ end
110
+
111
+ def failed_count(res)
112
+ failed_field = case request_type
113
+ when :streams; :failed_record_count
114
+ when :firehose; :failed_put_count
115
+ end
116
+ res[failed_field]
117
+ end
118
+
119
+ def results(res)
120
+ result_field = case request_type
121
+ when :streams; :records
122
+ when :firehose; :request_responses
123
+ end
124
+ res[result_field]
125
+ end
126
+
127
+ def give_up_retries(failed_records)
128
+ failed_records.each {|record|
129
+ log.error(truncate 'Could not put record, Error: %s/%s, Record: %s' % [
130
+ record[:error_code],
131
+ record[:error_message],
132
+ record[:original]
133
+ ])
134
+ }
135
+ end
136
+
137
+ class Backoff
138
+ def initialize
139
+ @count = 0
140
+ end
141
+
142
+ def next
143
+ value = calc(@count)
144
+ @count += 1
145
+ value
146
+ end
147
+
148
+ def reset
149
+ @count = 0
150
+ end
151
+
152
+ private
153
+
154
+ def calc(count)
155
+ (2 ** count) * scaling_factor
156
+ end
157
+
158
+ def scaling_factor
159
+ 0.3 + (0.5-rand) * 0.1
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,120 @@
1
+ #
2
+ # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ #
4
+ # Licensed under the Amazon Software License (the "License").
5
+ # You may not use this file except in compliance with the License.
6
+ # A copy of the License is located at
7
+ #
8
+ # http://aws.amazon.com/asl/
9
+ #
10
+ # or in the "license" file accompanying this file. This file is distributed
11
+ # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12
+ # express or implied. See the License for the specific language governing
13
+ # permissions and limitations under the License.
14
+
15
+ module Fluent
16
+ module KinesisHelper
17
+ module ClassMethods
18
+ def config_param_for_streams
19
+ const_set(:RequestType, :streams)
20
+ const_set(:BatchRequestLimitCount, 500)
21
+ const_set(:BatchRequestLimitSize, 5 * 1024 * 1024)
22
+ config_param :stream_name, :string
23
+ config_param :region, :string, default: nil
24
+ config_param :partition_key, :string, default: nil
25
+ config_param_for_sdk
26
+ config_param_for_credentials
27
+ config_param_for_format
28
+ config_param_for_batch_request
29
+ config_param_for_debug
30
+ end
31
+
32
+ def config_param_for_firehose
33
+ const_set(:RequestType, :firehose)
34
+ const_set(:BatchRequestLimitCount, 500)
35
+ const_set(:BatchRequestLimitSize, 4 * 1024 * 1024)
36
+ config_param :delivery_stream_name, :string
37
+ config_param :region, :string, default: nil
38
+ config_param :append_new_line, :bool, default: true
39
+ config_param_for_sdk
40
+ config_param_for_credentials
41
+ config_param_for_format
42
+ config_param_for_batch_request
43
+ config_param_for_debug
44
+ end
45
+
46
+ def config_param_for_producer
47
+ const_set(:RequestType, :producer)
48
+ config_param :stream_name, :string
49
+ config_param :region, :string, default: nil
50
+ config_param :partition_key, :string, default: nil
51
+ config_param_for_credentials
52
+ config_param_for_format
53
+ config_param_for_debug
54
+
55
+ config_section :kinesis_producer, multi: false do
56
+ require 'kinesis_producer'
57
+ type_map = {
58
+ Protobuf::Field::BoolField => :bool,
59
+ Protobuf::Field::Uint64Field => :integer,
60
+ Protobuf::Field::StringField => :string,
61
+ }
62
+ KinesisProducer::ConfigurationFields.each do |field|
63
+ next if field.name == 'region'
64
+ type = type_map[field.type_class]
65
+ config_param field.name, type, default: field.default_value
66
+ end
67
+ config_param :credentials_refresh_delay, :integer, default: 5000
68
+ end
69
+ end
70
+
71
+ def config_param_for_sdk
72
+ config_param :http_proxy, :string, default: nil
73
+ config_param :endpoint, :string, default: nil
74
+ config_param :ssl_verify_peer, :bool, default: true
75
+ end
76
+
77
+ def config_param_for_credentials
78
+ config_param :aws_key_id, :string, default: nil, secret: true
79
+ config_param :aws_sec_key, :string, default: nil, secret: true
80
+ config_section :shared_credentials, multi: false do
81
+ config_param :profile_name, :string, default: nil
82
+ config_param :path, :string, default: nil
83
+ end
84
+ config_section :assume_role_credentials, multi: false do
85
+ config_param :role_arn, :string, secret: true
86
+ config_param :external_id, :string, default: nil, secret: true
87
+ end
88
+ end
89
+
90
+ def config_param_for_format
91
+ config_param :formatter, :string, default: 'json'
92
+ config_param :data_key, :string, default: nil
93
+ config_param :log_truncate_max_size, :integer, default: 0
94
+ end
95
+
96
+ def config_param_for_batch_request
97
+ config_param :retries_on_batch_request, :integer, default: 3
98
+ config_param :reset_backoff_if_success, :bool, default: true
99
+ config_param :batch_request_max_count, :integer, default: const_get(:BatchRequestLimitCount)
100
+ config_param :batch_request_max_size, :integer, default: const_get(:BatchRequestLimitSize)
101
+ end
102
+
103
+ def config_param_for_debug
104
+ config_param :debug, :bool, default: false
105
+ end
106
+
107
+ def request_type
108
+ const_get(:RequestType)
109
+ end
110
+
111
+ def api?
112
+ [:streams, :firehose].include?(request_type)
113
+ end
114
+
115
+ def kpl?
116
+ [:producer].include?(request_type)
117
+ end
118
+ end
119
+ end
120
+ end