skyrunner 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8928e9988b2964b41819825a1b47adf1a458338d
4
+ data.tar.gz: cfbd389efd794094ce4be1f465dfff0252b976d4
5
+ SHA512:
6
+ metadata.gz: befca88f0e29654d83dafb5539a973c5b9f39fd52db3e72edf34e2ce55e529e738691835701c308dc62ebb9057133998c41ffd710c806e72e238df53bf8e3400
7
+ data.tar.gz: e180a3849b4f5b1891eb681e1786826464612be54dbc523328b2c442c9390e23b9bacc869ba9049ac4bf9fbbf2ca7ab54353fc35fb7cdbd01b245fe304bfca8f
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in skyrunner.gemspec
4
+ gemspec
5
+
6
+ gem "aws-sdk"
7
+ gem "activesupport"
8
+ gem "log4r"
9
+ gem "trollop"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Greg Fodor
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ skyrunner
2
+ =========
3
+
4
+ Skyrunner lets you run logical jobs using AWS SQS and DynamoDB
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/skyrunner ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.dirname(__FILE__) + "/../lib"
4
+
5
+ require "skyrunner"
6
+ require "trollop"
7
+
8
+ logger = Log4r::Logger.new("sky")
9
+ logger.outputters << Log4r::StdoutOutputter.new("out")
10
+ logger.level = Log4r::DEBUG
11
+
12
+ SkyRunner.logger = logger
13
+
14
+ trap("INT") do
15
+ SkyRunner.stop_consuming!
16
+ end
17
+
18
+ trap("TERM") do
19
+ SkyRunner.stop_consuming!
20
+ end
21
+
22
+ opts = Trollop::options do
23
+ banner <<-END
24
+ Runs logical jobs (made up of tasks) coordinated via AWS SQS & DynamoDB.
25
+
26
+ Usage:
27
+ skyrunner [options] <command>
28
+
29
+ Valid commands:
30
+
31
+ consume - Starts consuming tasks.
32
+ init - Creates DynamoDB table and SQS queue for SkyRunner.
33
+ purge - Purges and re-creates DynamoDB table and SQS queue for SkyRunner. (Destructive!)
34
+ END
35
+
36
+ opt :dynamo_db_table_name, "DynamoDB table to use for job state.", default: "skyrunner_jobs", type: :string
37
+ opt :sqs_queue_name, "SQS queue use for tasks.", default: "skyrunner_tasks", type: :string
38
+ opt :namespace, "Namespace of jobs to consume.", default: "default", type: :string
39
+ opt :batch_size, "Number of tasks to consume per batch.", default: 10
40
+ end
41
+
42
+ SkyRunner.dynamo_db_table_name = opts[:dynamo_db_table_name]
43
+ SkyRunner.sqs_queue_name = opts[:sqs_queue_name]
44
+ SkyRunner.job_namespace = opts[:namespace]
45
+ SkyRunner.consumer_batch_size = opts[:batch_size].to_i
46
+
47
+ COMMANDS = ["init", "purge", "consume"]
48
+
49
+ Trollop::die "Must specify command" unless COMMANDS.include?(ARGV[0])
50
+
51
+ command = ARGV[0]
52
+
53
+ case command
54
+ when "init"
55
+ SkyRunner.init!
56
+ when "purge"
57
+ SkyRunner.init!(purge: true)
58
+ when "consume"
59
+ SkyRunner.consume!
60
+ end
@@ -0,0 +1,28 @@
1
+ require "skyrunner"
2
+
3
+ module ExampleJobModule
4
+ class ExampleJob
5
+ include SkyRunner::Job
6
+
7
+ on_completed :print_completed
8
+ on_failed :print_failed
9
+
10
+ def run(number_of_tasks: nil)
11
+ 1.upto(number_of_tasks).each do |n|
12
+ yield :print_number, task_number: n
13
+ end
14
+ end
15
+
16
+ def print_number(task_number: nil)
17
+ puts "Ran rask #{task_number}"
18
+ end
19
+
20
+ def print_completed(number_of_tasks: nil)
21
+ puts "Completed with #{number_of_tasks}"
22
+ end
23
+
24
+ def print_failed(number_of_tasks: nil)
25
+ puts "Failed with #{number_of_tasks}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,145 @@
1
+ module SkyRunner::Job
2
+ attr_accessor :skyrunner_job_id
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def on_completed(methods)
10
+ add_job_event_methods(methods, :completed)
11
+ end
12
+
13
+ def on_failed(methods)
14
+ add_job_event_methods(methods, :failed)
15
+ end
16
+
17
+ def job_event_methods
18
+ @_job_event_methods ||= {}
19
+ end
20
+
21
+ private
22
+
23
+ def add_job_event_methods(methods, type)
24
+ methods = Array(methods)
25
+ job_event_methods[type] ||= []
26
+ job_event_methods[type].concat(methods.map(&:to_sym))
27
+ end
28
+ end
29
+
30
+ def execute!(args = {})
31
+ job_id = SecureRandom.hex
32
+ self.skyrunner_job_id = job_id
33
+
34
+ table = SkyRunner.dynamo_db_table
35
+ queue = SkyRunner.sqs_queue
36
+
37
+ record = table.items.put(job_id: job_id, namespace: SkyRunner.job_namespace, class: self.class.name, args: args.to_json, total_tasks: 1, completed_tasks: 0, done: 0, failed: 0)
38
+
39
+ pending_args = []
40
+
41
+ flush = lambda do
42
+ messages = pending_args.map do |task_args|
43
+ { job_id: job_id, task_id: SecureRandom.hex, task_args: task_args }.to_json
44
+ end
45
+
46
+ dropped_message_count = 0
47
+ pending_args.clear
48
+
49
+ begin
50
+ queue.batch_send(messages)
51
+ rescue AWS::SQS::Errors::BatchSendError => e
52
+ dropped_message_count = e.errors.size
53
+
54
+ # Re-add dropped args
55
+ e.errors.each do |error|
56
+ pending_args << JSON.parse(error[:message_body])["task_args"]
57
+ end
58
+ end
59
+
60
+ record.attributes.add({ total_tasks: messages.size - dropped_message_count })
61
+ end
62
+
63
+ self.run(args) do |*task_args|
64
+ pending_args << task_args
65
+
66
+ if pending_args.size >= SkyRunner::SQS_MAX_BATCH_SIZE
67
+ 1.upto(5) do
68
+ flush.()
69
+ sleep 5 if pending_args.size > 0
70
+ end
71
+ end
72
+ end
73
+
74
+ 1.upto(5) do
75
+ flush.() if pending_args.size > 0
76
+ sleep 5 if pending_args.size > 0
77
+ end
78
+
79
+ handle_task_completed!
80
+ end
81
+
82
+ def consume!(task_args)
83
+ begin
84
+ self.send(task_args[0].to_sym, task_args[1].symbolize_keys)
85
+ handle_task_completed!
86
+ rescue Exception => e
87
+ handle_task_failed! rescue nil
88
+ raise
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def dynamo_db_record
95
+ SkyRunner.dynamo_db_table.items[self.skyrunner_job_id]
96
+ end
97
+
98
+ def handle_task_failed!
99
+ return false unless self.skyrunner_job_id
100
+
101
+ begin
102
+ record = dynamo_db_record
103
+ record.attributes.add({ failed: 1 })
104
+
105
+ (self.class.job_event_methods[:failed] || []).each do |method|
106
+ if self.method(method).arity == 0 && self.method(method).parameters.size == 0
107
+ self.send(method)
108
+ else
109
+ self.send(method, JSON.parse(record.attributes["args"]).symbolize_keys)
110
+ end
111
+ end
112
+ rescue Exception => e
113
+ end
114
+ end
115
+
116
+ def handle_task_completed!
117
+ return false unless self.skyrunner_job_id
118
+
119
+ record = dynamo_db_record
120
+
121
+ new_attributes = record.attributes.add({ completed_tasks: 1 }, return: :all_new)
122
+
123
+ if new_attributes["total_tasks"] == new_attributes["completed_tasks"]
124
+ begin
125
+ if_condition = { completed_tasks: new_attributes["total_tasks"], done: 0 }
126
+
127
+ record.attributes.update(if: if_condition) do |u|
128
+ u.add(done: 1)
129
+ end
130
+
131
+ (self.class.job_event_methods[:completed] || []).each do |method|
132
+ if self.method(method).arity == 0 && self.method(method).parameters.size == 0
133
+ self.send(method)
134
+ else
135
+ self.send(method, JSON.parse(record.attributes["args"]).symbolize_keys)
136
+ end
137
+ end
138
+ rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
139
+ # This is OK, we had a double finisher.
140
+ end
141
+ end
142
+
143
+ true
144
+ end
145
+ end
@@ -0,0 +1,3 @@
1
+ module Skyrunner
2
+ VERSION = "0.0.1"
3
+ end
data/lib/skyrunner.rb ADDED
@@ -0,0 +1,187 @@
1
+ require "skyrunner/version"
2
+ require "aws-sdk"
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+ require "log4r"
6
+ require "json"
7
+ require "set"
8
+
9
+ module SkyRunner
10
+ SQS_MAX_BATCH_SIZE = 10 # Constant defined by AWS
11
+
12
+ def self.setup
13
+ yield self
14
+ end
15
+
16
+ def self.init!(params = {})
17
+ table = self.dynamo_db_table
18
+
19
+ if !table.exists? || params[:purge]
20
+ table_name = SkyRunner.dynamo_db_table_name
21
+
22
+ if table.exists? && params[:purge]
23
+ SkyRunner.log :warn, "Purging DynamoDB table #{table_name}."
24
+ table.delete
25
+
26
+ sleep 1 while table.exists?
27
+ end
28
+
29
+ SkyRunner.log :info, "Creating DynamoDB table #{table_name}."
30
+
31
+ table = dynamo_db.tables.create(table_name,
32
+ SkyRunner.dynamo_db_read_capacity,
33
+ SkyRunner.dynamo_db_write_capacity,
34
+ hash_key: { job_id: :string })
35
+
36
+ sleep 1 while table.status == :creating
37
+ end
38
+
39
+ queue = self.sqs_queue
40
+
41
+ if !queue || params[:purge]
42
+ queue_name = SkyRunner.sqs_queue_name
43
+
44
+ if queue && params[:purge]
45
+ SkyRunner.log :warn, "Purging SQS queue #{queue_name}. Waiting 65 seconds to re-create."
46
+ queue.delete
47
+
48
+ sleep 65
49
+ end
50
+
51
+ SkyRunner.log :info, "Creating SQS queue #{queue_name}."
52
+
53
+ queue = sqs.queues.create(queue_name,
54
+ visibility_timeout: SkyRunner.sqs_visibility_timeout,
55
+ message_retention_period: SkyRunner.sqs_message_retention_period)
56
+ end
57
+
58
+ true
59
+ end
60
+
61
+ def self.consume!(&block)
62
+ queue = sqs_queue
63
+ table = dynamo_db_table
64
+
65
+ raise "Queue #{SkyRunner::sqs_queue_name} not found. Try running 'skyrunner init'" unless queue
66
+ raise "DynamoDB table #{SkyRunner::dynamo_db_table_name} not found. Try running 'skyrunner init'" unless table && table.exists?
67
+
68
+ log :info, "Consumer started."
69
+
70
+ loop do
71
+ return true if stop_consuming
72
+
73
+ received_messages = []
74
+
75
+ queue.receive_messages(limit: [1, [SkyRunner.consumer_batch_size, SQS_MAX_BATCH_SIZE].min].max, wait_time_seconds: 15) do |message|
76
+ received_messages << [message, JSON.parse(message.body)]
77
+ end
78
+
79
+ next unless received_messages.size > 0
80
+
81
+ failed = false
82
+
83
+ table.batch_get(:all, received_messages.map { |m| m[1]["job_id"] }.uniq, consistent_read: true) do |record|
84
+ break if stop_consuming
85
+
86
+ received_messages.select { |m| m[1]["job_id"] == record["job_id"] }.each_with_index do |received_message|
87
+ break if stop_consuming
88
+
89
+ message = received_message[1]
90
+ job_id = message["job_id"]
91
+
92
+ if record["namespace"] == SkyRunner.job_namespace && record["failed"] == 0 && !failed
93
+ start_time = Time.now
94
+
95
+ begin
96
+ klass = Kernel.const_get(record["class"])
97
+
98
+ task_args = message["task_args"]
99
+ log :info, "Run Task: #{task_args} Job: #{job_id}"
100
+
101
+ job = klass.new
102
+ job.skyrunner_job_id = job_id
103
+
104
+ begin
105
+ job.consume!(task_args)
106
+ received_message[0].delete
107
+
108
+ yield false if block_given?
109
+ rescue Exception => e
110
+ failed = true
111
+ log :error, "Task Failed: #{task_args} Job: #{job_id} #{e.message} #{e.backtrace.join("\n")}"
112
+ yield e if block_given?
113
+ end
114
+ rescue NameError => e
115
+ failed = true
116
+ log :error, "Task Failed: No such class #{record["class"]} #{e.message}"
117
+ yield e if block_given?
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def self.dynamo_db_table
126
+ dynamo_db.tables[SkyRunner.dynamo_db_table_name].tap do |table|
127
+ table.load_schema if table && table.exists?
128
+ end
129
+ end
130
+
131
+ def self.sqs_queue
132
+ begin
133
+ sqs.queues.named(SkyRunner.sqs_queue_name)
134
+ rescue AWS::SQS::Errors::NonExistentQueue => e
135
+ return nil
136
+ end
137
+ end
138
+
139
+ def self.log(type, message)
140
+ SkyRunner.logger.send(type, "[SkyRunner] #{message}")
141
+ end
142
+
143
+ mattr_accessor :dynamo_db_table_name
144
+ @@dynamo_db_table_name = "skyrunner_jobs"
145
+
146
+ mattr_accessor :dynamo_db_read_capacity
147
+ @@dynamo_db_read_capacity = 10
148
+
149
+ mattr_accessor :dynamo_db_write_capacity
150
+ @@dynamo_db_write_capacity = 10
151
+
152
+ mattr_accessor :sqs_queue_name
153
+ @@sqs_queue_name = "skyrunner_tasks"
154
+
155
+ mattr_accessor :sqs_visibility_timeout
156
+ @@sqs_visibility_timeout = 90
157
+
158
+ mattr_accessor :sqs_message_retention_period
159
+ @@sqs_message_retention_period = 345600
160
+
161
+ mattr_accessor :job_namespace
162
+ @@job_namespace = "default"
163
+
164
+ mattr_accessor :consumer_batch_size
165
+ @@consumer_batch_size = 10
166
+
167
+ mattr_accessor :logger
168
+ @@logger = Log4r::Logger.new("skyrunner")
169
+
170
+ mattr_accessor :stop_consuming
171
+
172
+ def self.stop_consuming!
173
+ SkyRunner::stop_consuming = true
174
+ end
175
+
176
+ private
177
+
178
+ def self.dynamo_db
179
+ @dynamo_db ||= AWS::DynamoDB.new
180
+ end
181
+
182
+ def self.sqs
183
+ @sqs ||= AWS::SQS.new
184
+ end
185
+ end
186
+
187
+ require "skyrunner/job"
data/skyrunner.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'skyrunner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "skyrunner"
8
+ spec.version = Skyrunner::VERSION
9
+ spec.authors = ["Greg Fodor"]
10
+ spec.email = ["gfodor@gmail.com"]
11
+ spec.description = %q{SkyRunner runs logical jobs that are broken up into tasks via Amazon SQS and DynamoDB.}
12
+ spec.summary = %q{SkyRunner runs logical jobs that are broken up into tasks via Amazon SQS and DynamoDB.}
13
+ spec.homepage = "http://github.com/gfodor/skyrunner"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_runtime_dependency "aws-sdk"
25
+ spec.add_runtime_dependency "activesupport", "~> 4.0"
26
+ spec.add_runtime_dependency "log4r"
27
+ spec.add_runtime_dependency "trollop"
28
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skyrunner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Greg Fodor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '4.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '4.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: log4r
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: trollop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: SkyRunner runs logical jobs that are broken up into tasks via Amazon
98
+ SQS and DynamoDB.
99
+ email:
100
+ - gfodor@gmail.com
101
+ executables:
102
+ - skyrunner
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - .gitignore
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - bin/skyrunner
112
+ - jobs/example_job.rb
113
+ - lib/skyrunner.rb
114
+ - lib/skyrunner/job.rb
115
+ - lib/skyrunner/version.rb
116
+ - skyrunner.gemspec
117
+ homepage: http://github.com/gfodor/skyrunner
118
+ licenses:
119
+ - MIT
120
+ metadata: {}
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 2.0.6
138
+ signing_key:
139
+ specification_version: 4
140
+ summary: SkyRunner runs logical jobs that are broken up into tasks via Amazon SQS
141
+ and DynamoDB.
142
+ test_files: []
143
+ has_rdoc: