simpleci-worker 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aa2239b17e25fbb44d9c225fff71f7c12ba00405
4
+ data.tar.gz: fe00bb5bd7b03a3ede9fe94452832ef0c7a6da46
5
+ SHA512:
6
+ metadata.gz: e5a188555947734aaac6fdba54b7f6f8f3f3a769c7f2ae959bbf39b11a9279fdaf65822e7b356de716f9eef2d48f26cd03cb2d478d775c8986e3cc97fbd3fd02
7
+ data.tar.gz: 2917b225da806903c91db0b2e3da1a7c4aacead239c9b71a42daf965492c42acfa99c0e54390c60fa2bcdc8ae589baaccc628650904a02c18ce97fa484f272a5
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in worker.gemspec
4
+ gemspec
5
+
6
+ gem 'sqlite3'
7
+ gem 'activerecord'
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Worker
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/worker`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'worker'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install worker
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/worker.
36
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ t.warning = false
9
+ end
10
+
11
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "worker"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/worker/job.rb ADDED
@@ -0,0 +1,61 @@
1
+ require "json"
2
+ require "active_support"
3
+ require "active_support/hash_with_indifferent_access"
4
+
5
+ module Worker
6
+ class Job
7
+
8
+ def initialize(data={})
9
+ @data = data
10
+ end
11
+
12
+ def self.queue(name=nil)
13
+ @queue = name if name
14
+ @queue || 'default'
15
+ end
16
+
17
+ def self.priority(num=nil)
18
+ @priority = num if num
19
+ @priority || 0
20
+ end
21
+
22
+ def queue
23
+ self.class.queue
24
+ end
25
+
26
+ def priority
27
+ self.class.priority
28
+ end
29
+
30
+ def self.deserialize(string)
31
+ self.new(Hash[*JSON.parse(string).map { |k, v| [k.to_sym, v] }.flatten])
32
+ end
33
+
34
+ def before
35
+ end
36
+
37
+ def on_success
38
+ end
39
+
40
+ def on_failure
41
+ end
42
+
43
+ def perform
44
+ sleep 5
45
+ puts "performing"
46
+ end
47
+
48
+ def run_at
49
+ Time.now
50
+ end
51
+
52
+ def serialize
53
+ JSON.generate(Hash[*instance_variables.map { |attr| [attr.to_s.sub('@', ''), instance_variable_get(attr)] }.flatten])
54
+ end
55
+
56
+ def enqueue
57
+ Worker.queue.enqueue(self)
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,168 @@
1
+ require "sequel"
2
+ require "logger"
3
+
4
+ module Worker
5
+
6
+ class Queue
7
+ attr_reader :id
8
+ attr_accessor :jobs_per_worker, :max_job_time, :queue_name, :max_attempts, :logger, :poll_interval, :log_backtrace
9
+
10
+ def initialize(opts={})
11
+ @logger = Logger.new(STDOUT)
12
+
13
+ db_config = opts[:db_config] || Worker.db_config
14
+
15
+ db_config.merge(logger: logger) if opts[:log_queries]
16
+
17
+ @db = Sequel.connect(db_config)
18
+
19
+ name = `hostname`.chomp("\n")
20
+
21
+ @id = "#{name}.#{Process.pid}.#{rand(0..1000)}-#{opts[:queue_name] || 'all'}"
22
+
23
+ @jobs_per_worker = opts[:jobs_per_worker] || 5
24
+ @max_job_time = opts[:max_job_time] || 5 * 60 # in seconds
25
+ @queue_name = opts[:queue_name]
26
+ @max_attempts = opts[:max_attempts] || 20
27
+ @poll_interval = opts[:poll_interval] || 5 # 5 second polling
28
+ @log_backtrace = !!opts[:log_backtrace]
29
+
30
+ create_table
31
+ end
32
+
33
+ def create_table
34
+ db.create_table? :worker_jobs do
35
+ primary_key :id
36
+ Time :locked_at, null: true
37
+ Time :run_at, null: true
38
+ String :locked_by, null: true
39
+ String :handler, null: false, text: true
40
+ String :last_error, null: true, text: true
41
+ String :queue, null: false, default: 'default'
42
+ String :job_class, null: false
43
+ Integer :attempts, null: false, default: 0
44
+ Integer :priority, null: false, default: 0
45
+ end
46
+ end
47
+
48
+ def set_error(job, error)
49
+ db[:worker_jobs]
50
+ .where(id: job[:id])
51
+ .update(last_error: error.backtrace.join("\n"), run_at: Time.now + 10)
52
+ end
53
+
54
+ def remove(id)
55
+ db[:worker_jobs].where(id: id).delete
56
+ end
57
+
58
+ # cleans locked by jobs that have been dormant for a while
59
+ def clean_locks
60
+ db[:worker_jobs].where('locked_at < ?', Time.now - max_job_time).update(locked_by: nil)
61
+ end
62
+
63
+ # locks a set of jobs for this worker
64
+ def dequeue
65
+ query = db[:worker_jobs]
66
+ .where(
67
+ '`run_at` < ? AND `locked_by` IS NULL AND `id` in (SELECT `id` FROM `worker_jobs` ORDER BY `priority` DESC LIMIT ?)',
68
+ Time.now, jobs_per_worker)
69
+
70
+ if queue_name
71
+ query = query.where(queue: queue_name)
72
+ end
73
+
74
+ query.update(locked_by: id, locked_at: Time.now)
75
+ db[:worker_jobs].where(locked_by: id)
76
+ end
77
+
78
+ def enqueue(job)
79
+ db[:worker_jobs].insert(
80
+ job_class: job.class.name,
81
+ queue: job.queue,
82
+ priority: job.priority,
83
+ handler: job.serialize,
84
+ run_at: job.run_at,
85
+ )
86
+ end
87
+
88
+ def unlock_all
89
+ db[:worker_jobs].where(locked_by: id).update(locked_by: nil)
90
+ end
91
+
92
+ def db
93
+ @db
94
+ end
95
+
96
+ def run(number_to_run=nil)
97
+ begin
98
+ while true
99
+
100
+ logger.debug("Polling")
101
+
102
+ t1 = Time.now
103
+ failures = 0
104
+ successes = 0
105
+
106
+ dequeue.each do |item|
107
+
108
+ if number_to_run
109
+ number_to_run -= 1
110
+ end
111
+
112
+ logger.info("Performing #{item[:job_class]}.#{item[:id]} attempts=#{item[:attempts]} priority=#{item[:priority]} run_at=#{item[:run_at]}")
113
+
114
+ job = nil
115
+ begin
116
+ job = eval(item[:job_class]).deserialize(item[:handler])
117
+
118
+ Timeout::timeout(max_job_time) do
119
+ job.before
120
+ job.perform
121
+ job.on_success
122
+ end
123
+
124
+ remove(item[:id])
125
+
126
+ successes += 1
127
+ logger.info("Successful #{item[:job_class]}.#{item[:id]}")
128
+
129
+ rescue Exception => e
130
+
131
+ job.on_failure if job
132
+
133
+ if item[:attempts] + 1 > max_attempts
134
+ remove(item[:id])
135
+ else
136
+ set_error(item, e)
137
+ end
138
+
139
+ failures += 1
140
+ logger.info("Failed #{item[:job_class]}.#{item[:id]} err: #{e.class.name} #{e.message}")
141
+ if log_backtrace
142
+ puts e.backtrace
143
+ end
144
+
145
+ end
146
+ end
147
+
148
+ t2 = Time.now
149
+
150
+ if failures + successes > 0
151
+ logger.info("Performed #{failures + successes} jobs in #{t2 - t1} seconds")
152
+ end
153
+
154
+ return if number_to_run <= 0
155
+ sleep(poll_interval)
156
+ end
157
+ rescue StandardError => e
158
+ logger.warn("Exiting due to #{e}")
159
+ return
160
+ ensure
161
+ logger.info("Unlocking")
162
+ unlock_all
163
+ end
164
+
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,3 @@
1
+ module Worker
2
+ VERSION = "0.1.0"
3
+ end
data/lib/worker.rb ADDED
@@ -0,0 +1,25 @@
1
+ require "worker/version"
2
+ require "worker/queue"
3
+ require "worker/job"
4
+ require "logger"
5
+
6
+ module Worker
7
+ class << self
8
+ attr_accessor :queue, :logger, :db, :env
9
+
10
+ def db_config
11
+ db[env]
12
+ end
13
+
14
+ end
15
+ end
16
+
17
+ Worker.env = :test
18
+ Worker.db = {
19
+ test: {
20
+ adapter: 'sqlite',
21
+ }
22
+ }
23
+
24
+ Worker.queue = Worker::Queue.new
25
+ Worker.logger = Logger.new(STDOUT)
data/worker.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'worker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "simpleci-worker"
8
+ spec.version = Worker::VERSION
9
+ spec.authors = ["Colin Walker"]
10
+ spec.email = ["colinwalker270@gmail.com"]
11
+
12
+ spec.summary = %q{Database based job execution. Rails independent.}
13
+ spec.homepage = "https://github.com/simpleci-worker"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.12"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "minitest", "~> 5.0"
23
+
24
+ spec.add_dependency "sequel", "~> 4.40"
25
+ spec.add_dependency "activesupport", ">= 4.0"
26
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simpleci-worker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Colin Walker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-11-14 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.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sequel
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.40'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.40'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ description:
84
+ email:
85
+ - colinwalker270@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - README.md
94
+ - Rakefile
95
+ - bin/console
96
+ - bin/setup
97
+ - lib/worker.rb
98
+ - lib/worker/job.rb
99
+ - lib/worker/queue.rb
100
+ - lib/worker/version.rb
101
+ - worker.gemspec
102
+ homepage: https://github.com/simpleci-worker
103
+ licenses: []
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.5.1
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Database based job execution. Rails independent.
125
+ test_files: []