employer 0.0.1 → 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.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ config/
data/.pryrc ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler"
2
+ Bundler.require
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format documentation -I. -rspec_helper
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Employer
2
2
 
3
- TODO: Write a gem description
3
+ There comes a time in the life of an application that async job processing
4
+ becomes a requirement. If you want something flexible that you can easily adapt
5
+ to fit in with your application's infrastucture, then Employer may be what you
6
+ are looking for.
4
7
 
5
8
  ## Installation
6
9
 
@@ -18,7 +21,135 @@ Or install it yourself as:
18
21
 
19
22
  ## Usage
20
23
 
21
- TODO: Write usage instructions here
24
+ To use Employer to run jobs you need to do the following:
25
+
26
+ - Define your jobs as classes that include the Employer::Job module
27
+ - Hook up Employer with a backend to manage the jobs
28
+
29
+ ### Defining your own jobs
30
+
31
+ Implementing your own jobs is simple, here's a silly example:
32
+
33
+ ```ruby
34
+ class NamePutsJob
35
+ include Employer::Job
36
+
37
+ attribute :first_name
38
+ attribute :last_name
39
+ attribute :tries
40
+
41
+ def initialize
42
+ tries ||= 0
43
+ end
44
+
45
+ def try_again?
46
+ true if tries < 3
47
+ end
48
+
49
+ def perform
50
+ puts "#{first_name} #{last_name}"
51
+ end
52
+ end
53
+ ```
54
+
55
+ The attribute class method will define an attr_accessor and will ensure that the
56
+ attribute is part of the serialized data that is sent to the backend when a job
57
+ is enqueued.
58
+
59
+ The perform method is what will get executed when Employer picks up a job for
60
+ processing.
61
+
62
+ If a job fails the try_again? method will determine whether or not the job gets
63
+ tried again, if this method returns false the job will be marked as failed and
64
+ won't be attempted again.
65
+
66
+ ### Hooking up a backend
67
+
68
+ Employer manages its jobs through its pipeline, in order to feed jobs into the
69
+ pipeline and to get jobs out of the pipeline you need to connect a backend to
70
+ it. You can either use a backend that someone has built already (if you're using
71
+ Mongoid 3 you can use the employer-mongoid gem), or implement your own. A valid
72
+ pipeline backend must implement the methods shown in the below code snippet:
73
+
74
+ ```ruby
75
+ class CustomPipelineBackend
76
+ # job_hash is a Hash with the following keys:
77
+ # - class: The class of the Job object
78
+ # - attributes: A Hash with attribute values set on the Job object
79
+ def enqueue(job_hash)
80
+ end
81
+
82
+ # dequeue must return a job_hash in the same format as is passed into
83
+ # enqueue, except that it must add the key id with the Job's unique
84
+ # identifier (such as a record id)
85
+ def dequeue
86
+ end
87
+
88
+ # complete accepts a Job object, using its id the pipeline backend should
89
+ # mark the job as complete
90
+ def complete(job)
91
+ end
92
+
93
+ # fail accepts a Job object, using its id the pipeline backend should
94
+ # mark the job as failed
95
+ def fail(job)
96
+ end
97
+
98
+ # reset accepts a Job object, using its id the pipeline backend should
99
+ # reset the job by marking it as free
100
+ def reset(job)
101
+ end
102
+ end
103
+ ```
104
+
105
+ To hook up the backend to Employer you must generate and edit a config file by
106
+ running `employer config` (or more likely `bundle exec employer config`). If you
107
+ don't specify a custom path (with -c /path/to/employer\_config.rb) this will
108
+ generate config/employer.rb, the file will look something like this:
109
+
110
+ ```ruby
111
+ # If you're using Rails the below line requires config/environment to setup the
112
+ # Rails environment. If you're not using Rails you'll want to require something
113
+ # here that sets up Employer's environment appropriately (making available the
114
+ # classes that your jobs need to do their work, providing the connection to
115
+ # your database, etc.)
116
+ # require "./config/environment"
117
+
118
+ require "employer-mongoid"
119
+
120
+ # Setup the backend for the pipeline, this is where the boss gets the jobs to
121
+ # process. See the documentation for details on writing your own pipeline
122
+ # backend.
123
+ pipeline_backend Employer::Mongoid::Pipeline.new
124
+
125
+ # Use employees that fork subprocesses to perform jobs. You cannot use these
126
+ # with JRuby, because JRuby doesn't support Process#fork.
127
+ forking_employees 4
128
+
129
+ # Use employees that run their jobs in threads, you can use these when using
130
+ # JRuby. While threaded employees also work with MRI they are limited by the
131
+ # GIL (this may or may not be a problem depending on the type of work your jobs
132
+ # need to do).
133
+ # threading_employees 4
134
+ ```
135
+
136
+ The comments in the file pretty much explain how you should edit it.
137
+
138
+ When setup properly you can start processing jobs by running `employer` (or
139
+ `employer -c /path/to/employer\_config.rb`, likely prepended with `bundle exec`)
140
+
141
+ In your application code you can obtain a pipeline to enqueue jobs with like so:
142
+
143
+ ```ruby
144
+ # Obtain the pipeline
145
+ pipeline = Employer::Workshop.enqueue("/path/to/employer\_config.rb")
146
+
147
+ # Enqueue a job
148
+ job = NamePutsJob.new
149
+ job.first_name = "Mark"
150
+ job.last_name = "Kremer"
151
+ pipeline.enqueue(job)
152
+ ```
22
153
 
23
154
  ## Contributing
24
155
 
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "employer"
5
+ require "employer/cli"
6
+
7
+ Employer::CLI.start(ARGV)
@@ -9,10 +9,16 @@ Gem::Specification.new do |gem|
9
9
  gem.authors = ["Mark Kremer"]
10
10
  gem.email = ["mark@without-brains.net"]
11
11
  gem.summary = %q{Job processing made easy}
12
- gem.homepage = ""
12
+ gem.homepage = "https://github.com/mkremer/employer"
13
+ gem.license = "MIT"
13
14
 
14
15
  gem.files = `git ls-files`.split($/)
15
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
18
  gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency "thor", "~> 0.17"
21
+
22
+ gem.add_development_dependency "rspec"
23
+ gem.add_development_dependency "pry"
18
24
  end
@@ -1,5 +1,7 @@
1
1
  require "employer/version"
2
-
3
- module Employer
4
- # Your code goes here...
5
- end
2
+ require "employer/errors"
3
+ require "employer/pipeline"
4
+ require "employer/job"
5
+ require "employer/employees"
6
+ require "employer/boss"
7
+ require "employer/workshop"
@@ -0,0 +1,110 @@
1
+ require_relative "errors"
2
+
3
+ module Employer
4
+ class Boss
5
+ attr_reader :pipeline, :employees, :keep_going, :sleep_time
6
+
7
+ def initialize
8
+ @pipeline = nil
9
+ @employees = []
10
+ @sleep_time_index = 0
11
+ end
12
+
13
+ def pipeline=(pipeline)
14
+ @pipeline = pipeline
15
+ end
16
+
17
+ def allocate_employee(employee)
18
+ employees << employee
19
+ end
20
+
21
+ def stop_managing
22
+ @keep_going = false
23
+ end
24
+
25
+ def manage
26
+ @keep_going = true
27
+
28
+ while keep_going
29
+ delegate_work
30
+ progress_update
31
+ end
32
+
33
+ wait_on_employees
34
+ end
35
+
36
+ def delegate_work
37
+ while free_employee? && job = get_work
38
+ delegate_job(job)
39
+ end
40
+ end
41
+
42
+ def get_work
43
+ sleep_times = [0.1, 0.5, 1, 2.5, 5]
44
+ if job = pipeline.dequeue
45
+ @sleep_time_index = 0
46
+ else
47
+ @sleep_time_index += 1 unless @sleep_time_index == (sleep_times.count - 1)
48
+ end
49
+ @sleep_time = sleep_times[@sleep_time_index]
50
+ sleep(sleep_time)
51
+ job
52
+ end
53
+
54
+ def progress_update
55
+ busy_employees.each do |employee|
56
+ update_job_status(employee)
57
+ end
58
+ end
59
+
60
+ def update_job_status(employee)
61
+ return if employee.work_in_progress?
62
+
63
+ job = employee.job
64
+
65
+ if employee.work_completed?
66
+ pipeline.complete(job)
67
+ elsif employee.work_failed?
68
+ if job.try_again?
69
+ pipeline.reset(job)
70
+ else
71
+ pipeline.fail(job)
72
+ end
73
+ end
74
+
75
+ employee.free
76
+ end
77
+
78
+ def wait_on_employees
79
+ busy_employees.each do |employee|
80
+ employee.wait_for_completion
81
+ update_job_status(employee)
82
+ end
83
+ end
84
+
85
+ def stop_employees
86
+ busy_employees.each do |employee|
87
+ employee.stop_working
88
+ update_job_status(employee)
89
+ employee.free
90
+ end
91
+ end
92
+
93
+ def delegate_job(job)
94
+ raise Employer::Errors::NoEmployeeFree unless employee = free_employee
95
+ employee.work(job)
96
+ end
97
+
98
+ def busy_employees
99
+ employees.select { |employee| !employee.free? }
100
+ end
101
+
102
+ def free_employee
103
+ employees.find(&:free?)
104
+ end
105
+
106
+ def free_employee?
107
+ free_employee
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,70 @@
1
+ require "thor"
2
+ require "fileutils"
3
+
4
+ module Employer
5
+ class CLI < Thor
6
+ default_task :work
7
+
8
+ desc "work", "Process jobs"
9
+ option :config, default: "config/employer.rb", desc: "Config file to use"
10
+ def work
11
+ unless File.exists?(options[:config])
12
+ STDERR.puts "#{options[:config]} does not exist."
13
+ exit 1
14
+ end
15
+
16
+ int_count = 0
17
+ workshop = Employer::Workshop.setup(File.read(options[:config]))
18
+
19
+ Signal.trap("INT") do
20
+ int_count += 1
21
+ if int_count == 1
22
+ workshop.stop
23
+ else
24
+ workshop.stop_now
25
+ end
26
+ end
27
+
28
+ workshop.run
29
+ end
30
+
31
+ desc "config", "Generate config file"
32
+ option :config, default: "config/employer.rb", desc: "Path to config file"
33
+ def config
34
+ if File.exists?(options[:config])
35
+ STDERR.puts "#{options[:config]} already exists."
36
+ exit 1
37
+ end
38
+
39
+ FileUtils.mkdir("config") unless File.directory?("config")
40
+
41
+ File.open(options[:config], "w") do |file|
42
+ file.write <<CONFIG
43
+ # If you're using Rails the below line requires config/environment to setup the
44
+ # Rails environment. If you're not using Rails you'll want to require something
45
+ # here that sets up Employer's environment appropriately (making available the
46
+ # classes that your jobs need to do their work, providing the connection to
47
+ # your database, etc.)
48
+ # require "./config/environment"
49
+
50
+ require "employer-mongoid"
51
+
52
+ # Setup the backend for the pipeline, this is where the boss gets the jobs to
53
+ # process. See the documentation for details on writing your own pipeline
54
+ # backend.
55
+ pipeline_backend Employer::Mongoid::Pipeline.new
56
+
57
+ # Use employees that fork subprocesses to perform jobs. You cannot use these
58
+ # with JRuby, because JRuby doesn't support Process#fork.
59
+ forking_employees 4
60
+
61
+ # Use employees that run their jobs in threads, you can use these when using
62
+ # JRuby. While threaded employees also work with MRI they are limited by the
63
+ # GIL (this may or may not be a problem depending on the type of work your jobs
64
+ # need to do).
65
+ # threading_employees 4
66
+ CONFIG
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "employees/abstract_employee"
2
+ require_relative "employees/forking_employee"
3
+ require_relative "employees/threading_employee"
@@ -0,0 +1,55 @@
1
+ require_relative "../errors"
2
+
3
+ module Employer
4
+ module Employees
5
+ class AbstractEmployee
6
+ attr_reader :job
7
+
8
+ def work(job)
9
+ raise Employer::Errors::EmployeeBusy unless free?
10
+ @job = job
11
+ end
12
+
13
+ def perform_job
14
+ job.perform
15
+ end
16
+
17
+ def wait_for_completion
18
+ work_state(true)
19
+ end
20
+
21
+ def stop_working
22
+ return if work_completed? || work_failed?
23
+ force_work_stop
24
+ end
25
+
26
+ def free?
27
+ job.nil?
28
+ end
29
+
30
+ def work_in_progress?
31
+ true if work_state == :busy
32
+ end
33
+
34
+ def work_completed?
35
+ true if work_state == :complete
36
+ end
37
+
38
+ def work_failed?
39
+ true if work_state == :failed
40
+ end
41
+
42
+ def free
43
+ return unless work_completed? || work_failed?
44
+ @work_state = nil
45
+ @job = nil
46
+ end
47
+
48
+ def work_state(wait = false)
49
+ end
50
+
51
+ def force_work_stop
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,52 @@
1
+ require_relative "abstract_employee"
2
+
3
+ module Employer
4
+ module Employees
5
+ class ForkingEmployee < AbstractEmployee
6
+ def work(job)
7
+ super
8
+
9
+ @job_pid = fork do
10
+ state = nil
11
+
12
+ begin
13
+ perform_job
14
+ state = 0
15
+ ensure
16
+ state = 1 if state.nil?
17
+ exit(state)
18
+ end
19
+ end
20
+ end
21
+
22
+ def free
23
+ super
24
+ @job_pid = nil
25
+ end
26
+
27
+ def work_state(wait = false)
28
+ return @work_state if [:complete, :failed].include?(@work_state)
29
+
30
+ @work_state = :busy
31
+
32
+ flags = wait == false ? Process::WNOHANG : 0
33
+ pid, status = Process.waitpid2(@job_pid, flags)
34
+ if pid
35
+ if status.exitstatus == 0
36
+ @work_state = :complete
37
+ else
38
+ @work_state = :failed
39
+ end
40
+ end
41
+
42
+ @work_state
43
+ end
44
+
45
+ def force_work_stop
46
+ return if free?
47
+ Process.kill("KILL", @job_pid)
48
+ work_state(true)
49
+ end
50
+ end
51
+ end
52
+ end