logstash-filter-empowclassifier 0.3.15

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f2b80bd475e12f3e0e88a74a1fa718782e83b26f07b4a89a5c88b625e9adf863
4
+ data.tar.gz: d4a09d4b8c26ab17e64f3a99ff5dbaf12f6fc18ef7e84acc198b13aa9563ca87
5
+ SHA512:
6
+ metadata.gz: 95b93a9f5b54a078d0723c5472513a3f50fbb4e52b16893838f2f69901f982f71f3e9b96297014d29dd3b0664d65090f3030e1e35d64d95579ad357ce10ff81f
7
+ data.tar.gz: 2c5330ff79e5f2b2f819957a16892459bb48cd6907bd2388101e77d565bf1a4ad6f2b038731c098ef0f1feab7933d33429a8a413ed078b7d66637b6f2504d4f7
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ - Plugin created with the logstash plugin generator
data/CONTRIBUTORS ADDED
@@ -0,0 +1,11 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ Assaf Abulafia
6
+ Rami Cohen
7
+
8
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
9
+ Logstash, and you aren't on the list above and want to be, please let us know
10
+ and we'll make sure you're here. Contributions from folks like you are what make
11
+ open source awesome.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # empow classification plugin
2
+
3
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
4
+
5
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
+
7
+ <a href="https://badge.fury.io/rb/logstash-filter-empowclassifier"><img src="https://badge.fury.io/rb/logstash-filter-empowclassifier.svg" alt="Gem Version" height="18"></a>
8
+
9
+ ## Documentation
10
+
11
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
12
+
13
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
14
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
15
+
16
+ ## Need Help?
17
+
18
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
19
+
20
+ ## Developing
21
+
22
+ ### 1. Plugin Developement and Testing
23
+
24
+ #### Code
25
+ - To get started, you'll need JRuby with the Bundler gem installed.
26
+
27
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
28
+
29
+ - Install dependencies
30
+ ```sh
31
+ bundle install
32
+ ```
33
+
34
+ #### Test
35
+
36
+ - Update your dependencies
37
+
38
+ ```sh
39
+ bundle install
40
+ ```
41
+
42
+ - Run tests
43
+
44
+ ```sh
45
+ bundle exec rspec
46
+ ```
47
+
48
+ ### 2. Running your unpublished Plugin in Logstash
49
+
50
+ #### 2.1 Run in a local Logstash clone
51
+
52
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
53
+ ```ruby
54
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
55
+ ```
56
+ - Install plugin
57
+ ```sh
58
+ bin/logstash-plugin install --no-verify
59
+ ```
60
+ - Run Logstash with your plugin
61
+ ```sh
62
+ bin/logstash -e 'filter {awesome {}}'
63
+ ```
64
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
65
+
66
+ #### 2.2 Run in an installed Logstash
67
+
68
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
69
+
70
+ - Build your plugin gem
71
+ ```sh
72
+ gem build logstash-filter-awesome.gemspec
73
+ ```
74
+ - Install the plugin from the Logstash home
75
+ ```sh
76
+ bin/logstash-plugin install /your/local/plugin/logstash-filter-awesome.gem
77
+ ```
78
+ - Start Logstash and proceed to test the plugin
79
+
80
+ ## Contributing
81
+
82
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
83
+
84
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
85
+
86
+ It is more important to the community that you are able to contribute.
87
+
88
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
89
+
90
+ I like rice. Rice is great if you're hungry and want 2000 of something.
@@ -0,0 +1,208 @@
1
+ require "rest-client"
2
+ require "json"
3
+ require 'aws-sdk'
4
+ require_relative 'cognito-client'
5
+ require_relative 'response'
6
+ require_relative 'utils'
7
+
8
+
9
+ module LogStash
10
+ module Filters
11
+ module Empow
12
+ class ClassificationCenterClient
13
+ include LogStash::Util::Loggable
14
+
15
+ def initialize(username, password, aws_client_id, url_base)
16
+ @logger = self.logger
17
+
18
+ @token = nil
19
+ @url_base = url_base
20
+
21
+ aws_region = 'us-east-2'
22
+
23
+ @cognito_client = LogStash::Filters::Empow::CognitoClient.new(username, password, aws_region, aws_client_id)
24
+
25
+ @last_authenticate_minute = 0
26
+ end
27
+
28
+ public
29
+ def authenticate
30
+ # fixme: should check token expiration and throttle connections on failure
31
+
32
+ @token = nil
33
+
34
+ @logger.debug("reconnecting to the classfication center")
35
+
36
+ current_minute = (Time.now.to_i / 60)
37
+ if @last_authenticate_minute < current_minute
38
+ @last_authenticate_minute = current_minute
39
+ @last_minute_failed_login_count = 0
40
+ @last_authentication_error = ''
41
+ end
42
+
43
+ # avoid too many authentication requests
44
+ if @last_minute_failed_login_count < 3
45
+ begin
46
+ @token = @cognito_client.authenticate
47
+ rescue Aws::CognitoIdentityProvider::Errors::NotAuthorizedException, Aws::CognitoIdentityProvider::Errors::UserNotFoundException => e
48
+ @logger.warn("unable to authenticate with classification center", :error => e)
49
+ @last_authentication_error = e.to_s
50
+ inc_unsuccessful_logins()
51
+ rescue StandardError => e
52
+ @logger.warn("unable to authenticate with classification center", :error => e.class.name)
53
+ @last_authentication_error = e.class.name.to_s
54
+ inc_unsuccessful_logins()
55
+ end
56
+ end
57
+
58
+ return (!@token.nil?)
59
+ end
60
+
61
+ private def inc_unsuccessful_logins()
62
+ @last_minute_failed_login_count = @last_minute_failed_login_count + 1
63
+ end
64
+
65
+ public
66
+ def classify(requests)
67
+ authenticate if @token.nil? # try connecting if not already connected
68
+
69
+ res = nil
70
+
71
+ begin
72
+ res = classify_online(requests)
73
+
74
+ rescue RestClient::Unauthorized, RestClient::Forbidden, RestClient::UpgradeRequired => err
75
+ @logger.debug("reconnecting to the empow cloud", :error => err)
76
+
77
+ if !authenticate
78
+ return unauthorized_bulk_response(@last_authentication_error, requests)
79
+ end
80
+
81
+ begin
82
+ res = classify_online(requests)
83
+ rescue StandardError => e
84
+ @logger.debug("encountered an unexpected error on the 2nd attempt. #{e}", :error => e)
85
+
86
+ error_message = rescue_http_error_result(e)
87
+
88
+ return bulk_error(error_message, requests)
89
+ end
90
+
91
+ rescue StandardError => e
92
+ @logger.error("encountered an unexpected error while query the center. #{e}")
93
+
94
+ error_message = rescue_http_error_result(e)
95
+
96
+ return bulk_error(error_message, requests)
97
+ end
98
+
99
+ if res.nil? || res.strip.length == 0
100
+ return bulk_error("no content", requests)
101
+ end
102
+
103
+ parsed_json = nil
104
+
105
+ begin
106
+ parsed_json = JSON.parse(res)
107
+ rescue StandardError => e
108
+ @logger.error("unable to parse json", :json => res)
109
+ return bulk_error("invalid request", requests)
110
+ end
111
+
112
+ return successful_response(requests, parsed_json)
113
+ end
114
+
115
+ private
116
+ def rescue_http_error_result(http_error)
117
+ if http_error.nil? or LogStash::Filters::Empow::Utils.is_blank_string(http_error.http_body)
118
+ return http_error.to_s
119
+ else
120
+ err = http_error.http_body
121
+
122
+ begin
123
+ res = JSON.parse(err)
124
+ msg = res['message']
125
+
126
+ return err if LogStash::Filters::Empow::Utils.is_blank_string(msg)
127
+
128
+ return msg
129
+ rescue StandardError => e
130
+ @logger.debug("unable to read message body", :error => e)
131
+ return http_error.http_body
132
+ end
133
+ end
134
+ end
135
+
136
+ private
137
+ def classify_online(bulk_requests)
138
+ return nil if bulk_requests.nil? or bulk_requests.size == 0
139
+
140
+ payload = Array.new(bulk_requests.size)
141
+
142
+ bulk_size = bulk_requests.size
143
+
144
+ bulk_size.times do |i|
145
+ payload[i] = bulk_requests[i].to_h
146
+ end
147
+
148
+ payload_json = payload.to_json
149
+
150
+ @logger.debug("before online request", :payload => payload_json)
151
+
152
+ return RestClient::Request.execute(
153
+ method: :post,
154
+ url: "#{@url_base}/classification/intent",
155
+ payload: payload_json,
156
+ timeout: 30,
157
+ headers: { content_type: 'application/json', accept: 'application/json', authorization: @token, Bulksize: bulk_size }
158
+ ).body
159
+ end
160
+
161
+ private
162
+ def unauthorized_bulk_response(error_message, requests)
163
+ return bulk_error_by_type(LogStash::Filters::Empow::UnauthorizedReponse, error_message, requests)
164
+ end
165
+
166
+ private
167
+ def bulk_error(error_message, requests)
168
+ return bulk_error_by_type(LogStash::Filters::Empow::FailureResponse, error_message, requests)
169
+ end
170
+
171
+ private
172
+ def bulk_error_by_type(my_type, error_message, requests)
173
+ results = Hash.new
174
+
175
+ requests.each do |req|
176
+ res = my_type.new(error_message)
177
+ results[req] = res
178
+ end
179
+
180
+ return results
181
+ end
182
+
183
+ def successful_response(requests, responses)
184
+
185
+ results = Hash.new
186
+
187
+ responses.each_with_index do |response, i|
188
+ res = nil
189
+
190
+ if response['responseStatus'] == 'SUCCESS'
191
+ res = LogStash::Filters::Empow::SuccessfulResponse.new(response)
192
+ else
193
+ failure_reason = response['failedReason']
194
+ res = LogStash::Filters::Empow::FailureResponse.new(failure_reason)
195
+ end
196
+
197
+ req = requests[i]
198
+
199
+ results[req] = res
200
+ end
201
+
202
+ return results
203
+ end
204
+
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,17 @@
1
+ module LogStash; module Filters; module Empow;
2
+ class LogStash::Filters::Empow::ClassificationRequest < Struct.new(:product_type, :product, :term)
3
+ def initialize(product_type, product, term)
4
+ if product_type.nil?
5
+ raise ArgumentError, 'product type cannot be empty'
6
+ end
7
+
8
+ product_type = product_type.upcase.strip
9
+
10
+ unless product.nil?
11
+ product = product.downcase.strip
12
+ end
13
+
14
+ super(product_type, product, term)
15
+ end
16
+ end
17
+ end; end; end;
@@ -0,0 +1,51 @@
1
+ require 'time'
2
+ require "lru_redux"
3
+
4
+ module LogStash
5
+ module Filters
6
+ module Empow
7
+ class ClassifierCache
8
+ include LogStash::Util::Loggable
9
+
10
+ def initialize(cache_size, ttl)
11
+ @logger ||= self.logger
12
+
13
+ @logger.debug("cache size #{cache_size}")
14
+
15
+ @lru_cache ||= LruRedux::TTL::ThreadSafeCache.new(cache_size, ttl)
16
+ end
17
+
18
+ def classify(key)
19
+ return nil if key.nil?
20
+
21
+ tuple = @lru_cache[key]
22
+
23
+ return nil if tuple.nil?
24
+
25
+ expiration_time = tuple[:expiration_time]
26
+
27
+ if Time.now > expiration_time
28
+ @lru_cache.evict(key)
29
+ return nil
30
+ end
31
+
32
+ res = tuple[:val]
33
+
34
+ return res
35
+ end
36
+
37
+ def put(key, val, expiration_time)
38
+ return if key.nil?
39
+
40
+ @logger.debug("caching new entry", :key => key, :val => val)
41
+
42
+ tuple = {}
43
+ tuple[:val] = val
44
+ tuple[:expiration_time] = expiration_time
45
+
46
+ @lru_cache[key] = tuple
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,325 @@
1
+ require 'thread'
2
+ require 'time'
3
+ java_import java.util.concurrent.ArrayBlockingQueue
4
+ java_import java.util.concurrent.TimeUnit
5
+ java_import java.lang.InterruptedException
6
+
7
+ require_relative 'response'
8
+
9
+ module LogStash; module Filters; module Empow;
10
+ class Classifier
11
+ include LogStash::Util::Loggable
12
+
13
+ MAX_CONCURRENT_REQUESTS = 10000
14
+ BATCH_TIMEOUT = 10
15
+
16
+ def initialize(online_classifer, local_classifier, online_classification_workers, batch_size, batch_interval, max_retries, time_between_queries)
17
+ @logger ||= self.logger
18
+
19
+ @logger.info("initializing classifier")
20
+
21
+ @local_classifier = local_classifier
22
+ @online_classifer = online_classifer
23
+ @batch_interval = batch_interval
24
+ @time_between_queries = time_between_queries
25
+
26
+ @inflight_requests = Concurrent::Hash.new
27
+ @new_request_queue = java.util.concurrent.ArrayBlockingQueue.new(MAX_CONCURRENT_REQUESTS)
28
+
29
+ @bulk_processor = Classification::BulkProcessor.new(max_retries, batch_size, time_between_queries, @inflight_requests, online_classifer, local_classifier, online_classification_workers)
30
+
31
+ @worker_pool = Concurrent::FixedThreadPool.new(1)
32
+
33
+ @worker_pool.post do
34
+ while @worker_pool.running? do
35
+ begin
36
+ management_task()
37
+ rescue StandardError => e
38
+ @logger.error("encountered an error while running the management task", :error => e, :backtrace => e.backtrace)
39
+ end
40
+ end
41
+ end
42
+ @logger.debug("classifier initialized")
43
+
44
+ @last_action_time = Time.now
45
+ end
46
+
47
+ public
48
+ def close
49
+ @logger.info("shutting down empow's classifcation plugin")
50
+
51
+ @inflight_requests.clear()
52
+
53
+ @bulk_processor.close
54
+
55
+ @worker_pool.kill()
56
+ @worker_pool.wait_for_termination(5)
57
+
58
+ @logger.info("empow classifcation plugin closed")
59
+ end
60
+
61
+ private
62
+ def management_task
63
+ begin
64
+ current_time = Time.now
65
+
66
+ diff = (current_time - @bulk_processor.get_last_execution_time()).round
67
+
68
+ sleep_time = @batch_interval - diff
69
+
70
+ sleep_time = 0 if sleep_time < 0 # in case the rounding caused the number to be smaller than zero
71
+
72
+ dequeued_request = nil
73
+ begin
74
+ dequeued_request = @new_request_queue.poll(sleep_time, TimeUnit::SECONDS)
75
+ rescue java.lang.InterruptedException => e
76
+ end
77
+
78
+ # if this is a 'tick'
79
+ if dequeued_request.nil?
80
+ @bulk_processor.flush_current_batch
81
+ else
82
+ @bulk_processor.add_to_batch(dequeued_request)
83
+ end
84
+
85
+ # skip the 'tick' if the timer hasn't expired
86
+ return if current_time - @last_action_time < @time_between_queries
87
+
88
+ @last_action_time = current_time
89
+
90
+ @bulk_processor.retry_queued_requests()
91
+ rescue StandardError => e
92
+ @logger.error("encountered an error while running the management task", :error => e, :backtrace => e.backtrace)
93
+ end
94
+ end
95
+
96
+ public
97
+ def classify(request)
98
+ return nil if request.nil?
99
+
100
+ res = @local_classifier.classify(request)
101
+
102
+ @logger.debug("cached result", :request => request, :res => res)
103
+
104
+ return res if !res.nil?
105
+
106
+ request_online_classifiction(request)
107
+
108
+ return nil
109
+ end
110
+
111
+ private
112
+ def request_online_classifiction(req)
113
+ existing_request = @inflight_requests[req]
114
+
115
+ return if !existing_request.nil? # request already handled by a worker
116
+
117
+ @logger.debug("adding request to online classification queue", :request => req)
118
+
119
+ task = create_task(req)
120
+
121
+ # mark request as in progress
122
+ @inflight_requests[req] = task
123
+
124
+ res = @new_request_queue.offer(req)
125
+
126
+ @logger.warn("queue full, request reject", :request => req) if !res
127
+ end
128
+
129
+ private
130
+ def create_task(request)
131
+ tuple = {}
132
+ tuple[:retries] = 0
133
+ tuple[:request] = request
134
+ tuple[:last_executed] = Time.at(310953600)
135
+
136
+ return tuple
137
+ end
138
+ end # class Classifier
139
+
140
+ module Classification
141
+
142
+ class BulkProcessor
143
+ include LogStash::Util::Loggable
144
+
145
+ ERROR_TTL_SECS = 60
146
+ THREAD_IDLE_TIME = 60
147
+ BATCH_TIMEOUT = 10
148
+
149
+ public
150
+ def initialize(max_retries, batch_size, sec_between_attempts, requests_queue, online_classifer, local_classifier, max_concurrent_threads)
151
+ @logger ||= self.logger
152
+
153
+ @max_retries = max_retries
154
+ @max_batch_size = batch_size
155
+ @sec_between_attempts = sec_between_attempts
156
+ @requests_queue = requests_queue
157
+ @online_classifer = online_classifer
158
+ @local_classifier = local_classifier
159
+
160
+ @online_classification_workers = Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: max_concurrent_threads, idletime: THREAD_IDLE_TIME)
161
+
162
+ clear_batch(Time.now)
163
+ end
164
+
165
+ public
166
+ def close
167
+ @online_classification_workers.kill()
168
+ @online_classification_workers.wait_for_termination(10)
169
+ end
170
+
171
+ public
172
+ def add_to_batch(request)
173
+ # add the new request to the batch
174
+ @current_batch_size = @current_batch_size + 1
175
+ @current_batch << request
176
+
177
+ flush_current_batch
178
+ end
179
+
180
+ public
181
+ def flush_current_batch
182
+ current_time = Time.now
183
+
184
+ # check if the current batch is full or timed out
185
+ if (@current_batch_size == @max_batch_size \
186
+ or (@current_batch_size > 0 and (current_time - @last_execution_time) > BATCH_TIMEOUT))
187
+
188
+ @online_classification_workers.post do
189
+ classify_online(@current_batch)
190
+ end
191
+
192
+ clear_batch(current_time)
193
+ elsif @current_batch_size == 0
194
+ @last_execution_time = current_time
195
+ end
196
+ end
197
+
198
+ public
199
+ def get_last_execution_time
200
+ return @last_execution_time
201
+ end
202
+
203
+ private
204
+ def clear_batch(current_time)
205
+ @current_batch = Array.new
206
+ @current_batch_size = 0
207
+ @last_execution_time = current_time
208
+ end
209
+
210
+ public
211
+ def retry_queued_requests
212
+ @logger.debug("retrying queued requests")
213
+
214
+ current_time = Time.now
215
+ batch_size = 0
216
+ batch = Array.new
217
+
218
+ @requests_queue.each do |k, v|
219
+ last_execution_time = v[:last_executed]
220
+
221
+ if batch_size == @max_batch_size
222
+ @online_classification_workers.post do
223
+ classify_online(batch)
224
+ end
225
+
226
+ batch_size = 0
227
+ batch = Array.new
228
+ end
229
+
230
+ if last_execution_time + @sec_between_attempts > current_time
231
+ next
232
+ end
233
+
234
+ batch << k
235
+
236
+ v[:last_executed] = current_time
237
+ v[:retries] = v[:retries] + 1
238
+
239
+ batch_size = batch_size + 1
240
+ end
241
+
242
+ if batch_size > 0
243
+ @online_classification_workers.post do
244
+ classify_online(batch)
245
+ end
246
+ end
247
+
248
+ # remove requests that were in the queue for too long
249
+ @requests_queue.delete_if {|key, value| value[:retries] >= @max_retries }
250
+ end
251
+
252
+ private
253
+ def classify_online(bulk_request)
254
+
255
+ results = nil
256
+ current_time = Time.now
257
+
258
+ batch = Array.new
259
+
260
+ bulk_request.each do |req|
261
+ task = @requests_queue[req]
262
+
263
+ next if task.nil? # resolved by an earlier thread
264
+
265
+ task[:last_executed] = current_time
266
+ task[:retries] = task[:retries] + 1
267
+
268
+ batch << req
269
+ end
270
+
271
+ begin
272
+ results = @online_classifer.classify(batch)
273
+ rescue StandardError => e
274
+ @logger.error("bulk request ended with a failure, all requests will be removed from queue", :error => e, :backtrace => e.backtrace)
275
+
276
+ batch.each do |req|
277
+ @requests_queue.delete(request)
278
+ end
279
+ end
280
+
281
+ if results.size != batch.size
282
+ @logger.warn("response array isn't the same size as result array. requests: #{batch.size}. results: #{results.size}")
283
+ return
284
+ end
285
+
286
+ results.each do |request, res|
287
+ @logger.debug("processing response", :request => request, :response => res)
288
+
289
+ begin
290
+ expiration_time = Time.now + get_response_ttl(res)
291
+
292
+ if res.is_successful
293
+ # validate the response if needed
294
+ # put the result in memory and in the local db
295
+ @local_classifier.save_to_cache_and_db(request, res, expiration_time)
296
+ else
297
+ @local_classifier.add_to_cache(request, res, expiration_time) # log the failed result for tagging
298
+ end
299
+ rescue StandardError => e
300
+ @logger.error("encountered an error while trying to process result", :request => request, :error => e, :backtrace => e.backtrace)
301
+ end
302
+
303
+ @requests_queue.delete(request)
304
+ end
305
+ end
306
+
307
+ private def get_response_ttl(res)
308
+ return ERROR_TTL_SECS if !res.is_successful
309
+
310
+ responseBody = res.response
311
+
312
+ ttl = responseBody['ttlseconds']
313
+
314
+ if ttl.nil? or ttl < 0
315
+ ttl = 60
316
+ end
317
+
318
+ return ttl
319
+ end
320
+
321
+ end # class BulkProcessor
322
+
323
+ end # module Classification
324
+
325
+ end; end; end