skyrunner 0.0.1

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
+ 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: