resque-scheduler 2.5.5 → 3.0.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.

Potentially problematic release.


This version of resque-scheduler might be problematic. Click here for more details.

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -6
  3. data/.rubocop.yml +18 -113
  4. data/.rubocop_todo.yml +29 -0
  5. data/.simplecov +3 -1
  6. data/.travis.yml +12 -4
  7. data/.vagrant-provision-as-vagrant.sh +15 -0
  8. data/.vagrant-provision.sh +23 -0
  9. data/.vagrant-skel/bash_profile +7 -0
  10. data/.vagrant-skel/bashrc +7 -0
  11. data/AUTHORS.md +5 -0
  12. data/Gemfile +1 -2
  13. data/HISTORY.md +18 -0
  14. data/README.md +39 -11
  15. data/ROADMAP.md +0 -6
  16. data/Rakefile +11 -19
  17. data/Vagrantfile +14 -0
  18. data/bin/resque-scheduler +2 -2
  19. data/examples/Rakefile +1 -1
  20. data/examples/config/initializers/resque-web.rb +2 -2
  21. data/examples/dynamic-scheduling/app/jobs/fix_schedules_job.rb +2 -4
  22. data/examples/dynamic-scheduling/app/jobs/send_email_job.rb +1 -1
  23. data/examples/dynamic-scheduling/app/models/user.rb +2 -2
  24. data/examples/dynamic-scheduling/lib/tasks/resque.rake +2 -2
  25. data/lib/resque-scheduler.rb +3 -1
  26. data/lib/resque/scheduler.rb +112 -168
  27. data/lib/resque/scheduler/cli.rb +144 -0
  28. data/lib/resque/scheduler/configuration.rb +73 -0
  29. data/lib/resque/scheduler/delaying_extensions.rb +278 -0
  30. data/lib/resque/scheduler/env.rb +61 -0
  31. data/lib/resque/scheduler/extension.rb +13 -0
  32. data/lib/resque/scheduler/lock.rb +2 -1
  33. data/lib/resque/scheduler/lock/base.rb +6 -2
  34. data/lib/resque/scheduler/lock/basic.rb +4 -5
  35. data/lib/resque/scheduler/lock/resilient.rb +30 -37
  36. data/lib/resque/scheduler/locking.rb +94 -0
  37. data/lib/resque/scheduler/logger_builder.rb +72 -0
  38. data/lib/resque/scheduler/plugin.rb +31 -0
  39. data/lib/resque/scheduler/scheduling_extensions.rb +150 -0
  40. data/lib/resque/scheduler/server.rb +246 -0
  41. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed.erb +2 -1
  42. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_schedules.erb +0 -0
  43. data/lib/{resque_scheduler → resque/scheduler}/server/views/delayed_timestamp.erb +0 -0
  44. data/lib/{resque_scheduler → resque/scheduler}/server/views/requeue-params.erb +0 -0
  45. data/lib/{resque_scheduler → resque/scheduler}/server/views/scheduler.erb +16 -1
  46. data/lib/{resque_scheduler → resque/scheduler}/server/views/search.erb +2 -1
  47. data/lib/{resque_scheduler → resque/scheduler}/server/views/search_form.erb +0 -0
  48. data/lib/resque/scheduler/signal_handling.rb +40 -0
  49. data/lib/{resque_scheduler → resque/scheduler}/tasks.rb +3 -5
  50. data/lib/resque/scheduler/util.rb +41 -0
  51. data/lib/resque/scheduler/version.rb +7 -0
  52. data/resque-scheduler.gemspec +21 -19
  53. data/script/migrate_to_timestamps_set.rb +5 -3
  54. data/tasks/resque_scheduler.rake +1 -1
  55. data/test/cli_test.rb +26 -69
  56. data/test/delayed_queue_test.rb +262 -169
  57. data/test/env_test.rb +41 -0
  58. data/test/resque-web_test.rb +169 -48
  59. data/test/scheduler_args_test.rb +73 -41
  60. data/test/scheduler_hooks_test.rb +9 -8
  61. data/test/scheduler_locking_test.rb +55 -36
  62. data/test/scheduler_setup_test.rb +52 -15
  63. data/test/scheduler_task_test.rb +15 -10
  64. data/test/scheduler_test.rb +215 -114
  65. data/test/support/redis_instance.rb +32 -33
  66. data/test/test_helper.rb +33 -36
  67. data/test/util_test.rb +11 -0
  68. metadata +113 -57
  69. data/lib/resque/scheduler_locking.rb +0 -91
  70. data/lib/resque_scheduler.rb +0 -386
  71. data/lib/resque_scheduler/cli.rb +0 -160
  72. data/lib/resque_scheduler/logger_builder.rb +0 -72
  73. data/lib/resque_scheduler/plugin.rb +0 -28
  74. data/lib/resque_scheduler/server.rb +0 -183
  75. data/lib/resque_scheduler/util.rb +0 -34
  76. data/lib/resque_scheduler/version.rb +0 -5
  77. data/test/redis-test.conf +0 -108
@@ -0,0 +1,61 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ class Env
6
+ def initialize(options)
7
+ @options = options
8
+ end
9
+
10
+ def setup
11
+ require 'resque'
12
+ require 'resque/scheduler'
13
+
14
+ setup_backgrounding
15
+ setup_pid_file
16
+ setup_scheduler_configuration
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :options
22
+
23
+ def setup_backgrounding
24
+ # Need to set this here for conditional Process.daemon redirect of
25
+ # stderr/stdout to /dev/null
26
+ Resque::Scheduler.quiet = !!options[:quiet]
27
+
28
+ if options[:background]
29
+ unless Process.respond_to?('daemon')
30
+ abort 'background option is set, which requires ruby >= 1.9'
31
+ end
32
+
33
+ Process.daemon(true, !Resque::Scheduler.quiet)
34
+ Resque.redis.client.reconnect
35
+ end
36
+ end
37
+
38
+ def setup_pid_file
39
+ File.open(options[:pidfile], 'w') do |f|
40
+ f.puts $PROCESS_ID
41
+ end if options[:pidfile]
42
+ end
43
+
44
+ def setup_scheduler_configuration
45
+ Resque::Scheduler.configure do |c|
46
+ # These settings are somewhat redundant given the defaults present
47
+ # in the attr reader methods. They are left here for clarity and
48
+ # to serve as an example of how to use `.configure`.
49
+
50
+ c.app_name = options[:app_name]
51
+ c.dynamic = !!options[:dynamic]
52
+ c.env = options[:env]
53
+ c.logfile = options[:logfile]
54
+ c.logformat = options[:logformat]
55
+ c.poll_sleep_amount = Float(options[:poll_sleep_amount] || '5')
56
+ c.verbose = !!options[:verbose]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require_relative 'scheduling_extensions'
4
+ require_relative 'delaying_extensions'
5
+
6
+ module Resque
7
+ module Scheduler
8
+ module Extension
9
+ include SchedulingExtensions
10
+ include DelayingExtensions
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,4 @@
1
- %w[base basic resilient].each do |file|
1
+ # vim:fileencoding=utf-8
2
+ %w(base basic resilient).each do |file|
2
3
  require "resque/scheduler/lock/#{file}"
3
4
  end
@@ -1,5 +1,7 @@
1
+ # vim:fileencoding=utf-8
2
+
1
3
  module Resque
2
- class Scheduler
4
+ module Scheduler
3
5
  module Lock
4
6
  class Base
5
7
  attr_reader :key
@@ -40,7 +42,9 @@ module Resque
40
42
 
41
43
  def hostname
42
44
  local_hostname = Socket.gethostname
43
- Socket.gethostbyname(local_hostname).first rescue local_hostname
45
+ Socket.gethostbyname(local_hostname).first
46
+ rescue
47
+ local_hostname
44
48
  end
45
49
 
46
50
  def process_id
@@ -1,7 +1,8 @@
1
- require 'resque/scheduler/lock/base'
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'base'
2
3
 
3
4
  module Resque
4
- class Scheduler
5
+ module Scheduler
5
6
  module Lock
6
7
  class Basic < Base
7
8
  def acquire!
@@ -15,9 +16,7 @@ module Resque
15
16
  if Resque.redis.get(key) == value
16
17
  extend_lock!
17
18
 
18
- if Resque.redis.get(key) == value
19
- return true
20
- end
19
+ return true if Resque.redis.get(key) == value
21
20
  end
22
21
 
23
22
  false
@@ -1,22 +1,23 @@
1
- require 'resque/scheduler/lock/base'
1
+ # vim:fileencoding=utf-8
2
+ require_relative 'base'
2
3
 
3
4
  module Resque
4
- class Scheduler
5
+ module Scheduler
5
6
  module Lock
6
- class Resilient < Base # rubocop:disable TrailingComma
7
+ class Resilient < Base
7
8
  def acquire!
8
9
  Resque.redis.evalsha(
9
10
  acquire_sha,
10
- :keys => [key],
11
- :argv => [value]
11
+ keys: [key],
12
+ argv: [value]
12
13
  ).to_i == 1
13
14
  end
14
15
 
15
16
  def locked?
16
17
  Resque.redis.evalsha(
17
18
  locked_sha,
18
- :keys => [key],
19
- :argv => [value]
19
+ keys: [key],
20
+ argv: [value]
20
21
  ).to_i == 1
21
22
  end
22
23
 
@@ -34,43 +35,35 @@ module Resque
34
35
  def locked_sha(refresh = false)
35
36
  @locked_sha = nil if refresh
36
37
 
37
- @locked_sha ||= begin
38
- Resque.redis.script(
39
- :load,
40
- <<-EOF
41
- if redis.call('GET', KEYS[1]) == ARGV[1]
42
- then
43
- redis.call('EXPIRE', KEYS[1], #{timeout})
38
+ @locked_sha ||=
39
+ Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
40
+ if redis.call('GET', KEYS[1]) == ARGV[1]
41
+ then
42
+ redis.call('EXPIRE', KEYS[1], #{timeout})
44
43
 
45
- if redis.call('GET', KEYS[1]) == ARGV[1]
46
- then
47
- return 1
48
- end
49
- end
44
+ if redis.call('GET', KEYS[1]) == ARGV[1]
45
+ then
46
+ return 1
47
+ end
48
+ end
50
49
 
51
- return 0
52
- EOF
53
- )
54
- end
50
+ return 0
51
+ EOF
55
52
  end
56
53
 
57
54
  def acquire_sha(refresh = false)
58
55
  @acquire_sha = nil if refresh
59
56
 
60
- @acquire_sha ||= begin
61
- Resque.redis.script(
62
- :load,
63
- <<-EOF
64
- if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
65
- then
66
- redis.call('EXPIRE', KEYS[1], #{timeout})
67
- return 1
68
- else
69
- return 0
70
- end
71
- EOF
72
- )
73
- end
57
+ @acquire_sha ||=
58
+ Resque.redis.script(:load, <<-EOF.gsub(/^ {14}/, ''))
59
+ if redis.call('SETNX', KEYS[1], ARGV[1]) == 1
60
+ then
61
+ redis.call('EXPIRE', KEYS[1], #{timeout})
62
+ return 1
63
+ else
64
+ return 0
65
+ end
66
+ EOF
74
67
  end
75
68
  end
76
69
  end
@@ -0,0 +1,94 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ # ### Locking the scheduler process
4
+ #
5
+ # There are two places in resque-scheduler that need to be synchonized in order
6
+ # to be able to run redundant scheduler processes while ensuring jobs don't get
7
+ # queued multiple times when the master process changes.
8
+ #
9
+ # 1) Processing the delayed queues (jobs that are created from
10
+ # enqueue_at/enqueue_in, etc) 2) Processing the scheduled (cron-like) jobs from
11
+ # rufus-scheduler
12
+ #
13
+ # Protecting the delayed queues (#1) is relatively easy. A simple SETNX in
14
+ # redis would suffice. However, protecting the scheduled jobs is trickier
15
+ # because the clocks on machines could be slightly off or actual firing times
16
+ # could vary slightly due to load. If scheduler A's clock is slightly ahead of
17
+ # scheduler B's clock (since they are on different machines), when scheduler A
18
+ # dies, we need to ensure that scheduler B doesn't queue jobs that A already
19
+ # queued before it's death. (This all assumes that it is better to miss a few
20
+ # scheduled jobs than it is to run them multiple times for the same iteration.)
21
+ #
22
+ # To avoid queuing multiple jobs in the case of master fail-over, the master
23
+ # should remain the master as long as it can rather than a simple SETNX which
24
+ # would result in the master roll being passed around frequently.
25
+ #
26
+ # Locking Scheme: Each resque-scheduler process attempts to get the master lock
27
+ # via SETNX. Once obtained, it sets the expiration for 3 minutes
28
+ # (configurable). The master process continually updates the timeout on the
29
+ # lock key to be 3 minutes in the future in it's loop(s) (see `run`) and when
30
+ # jobs come out of rufus-scheduler (see `load_schedule_job`). That ensures
31
+ # that a minimum of 3 minutes must pass since the last queuing operation before
32
+ # a new master is chosen. If, for whatever reason, the master fails to update
33
+ # the expiration for 3 minutes, the key expires and the lock is up for grabs.
34
+ # If miraculously the original master comes back to life, it will realize it is
35
+ # no longer the master and stop processing jobs.
36
+ #
37
+ # The clocks on the scheduler machines can then be up to 3 minutes off from
38
+ # each other without the risk of queueing the same scheduled job twice during a
39
+ # master change. The catch is, in the event of a master change, no scheduled
40
+ # jobs will be queued during those 3 minutes. So, there is a trade off: the
41
+ # higher the timeout, the less likely scheduled jobs will be fired twice but
42
+ # greater chances of missing scheduled jobs. The lower the timeout, less
43
+ # likely jobs will be missed, greater the chances of jobs firing twice. If you
44
+ # don't care about jobs firing twice or are certain your machines' clocks are
45
+ # well in sync, a lower timeout is preferable. One thing to keep in mind: this
46
+ # only effects *scheduled* jobs - delayed jobs will never be lost or skipped
47
+ # since eventually a master will come online and it will process everything
48
+ # that is ready (no matter how old it is). Scheduled jobs work like cron - if
49
+ # you stop cron, no jobs fire while it's stopped and it doesn't fire jobs that
50
+ # were missed when it starts up again.
51
+
52
+ require_relative 'lock'
53
+
54
+ module Resque
55
+ module Scheduler
56
+ module Locking
57
+ def master_lock
58
+ @master_lock ||= build_master_lock
59
+ end
60
+
61
+ def supports_lua?
62
+ redis_master_version >= 2.5
63
+ end
64
+
65
+ def master?
66
+ master_lock.acquire! || master_lock.locked?
67
+ end
68
+
69
+ def release_master_lock!
70
+ master_lock.release!
71
+ end
72
+
73
+ private
74
+
75
+ def build_master_lock
76
+ if supports_lua?
77
+ Resque::Scheduler::Lock::Resilient.new(master_lock_key)
78
+ else
79
+ Resque::Scheduler::Lock::Basic.new(master_lock_key)
80
+ end
81
+ end
82
+
83
+ def master_lock_key
84
+ lock_prefix = ENV['RESQUE_SCHEDULER_MASTER_LOCK_PREFIX'] || ''
85
+ lock_prefix += ':' if lock_prefix != ''
86
+ "#{Resque.redis.namespace}:#{lock_prefix}resque_scheduler_master_lock"
87
+ end
88
+
89
+ def redis_master_version
90
+ Resque.redis.info['redis_version'].to_f
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,72 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'mono_logger'
4
+
5
+ module Resque
6
+ module Scheduler
7
+ # Just builds a logger, with specified verbosity and destination.
8
+ # The simplest example:
9
+ #
10
+ # Resque::Scheduler::LoggerBuilder.new.build
11
+ class LoggerBuilder
12
+ # Initializes new instance of the builder
13
+ #
14
+ # Pass :opts Hash with
15
+ # - :quiet if logger needs to be silent for all levels. Default - false
16
+ # - :verbose if there is a need in debug messages. Default - false
17
+ # - :log_dev to output logs into a desired file. Default - STDOUT
18
+ # - :format log format, either 'text' or 'json'. Default - 'text'
19
+ #
20
+ # Example:
21
+ #
22
+ # LoggerBuilder.new(
23
+ # :quiet => false, :verbose => true, :log_dev => 'log/scheduler.log'
24
+ # )
25
+ def initialize(opts = {})
26
+ @quiet = !!opts[:quiet]
27
+ @verbose = !!opts[:verbose]
28
+ @log_dev = opts[:log_dev] || $stdout
29
+ @format = opts[:format] || 'text'
30
+ end
31
+
32
+ # Returns an instance of MonoLogger
33
+ def build
34
+ logger = MonoLogger.new(@log_dev)
35
+ logger.level = level
36
+ logger.formatter = send(:"#{@format}_formatter")
37
+ logger
38
+ end
39
+
40
+ private
41
+
42
+ def level
43
+ if @verbose && !@quiet
44
+ MonoLogger::DEBUG
45
+ elsif !@quiet
46
+ MonoLogger::INFO
47
+ else
48
+ MonoLogger::FATAL
49
+ end
50
+ end
51
+
52
+ def text_formatter
53
+ proc do |severity, datetime, _progname, msg|
54
+ "resque-scheduler: [#{severity}] #{datetime.iso8601}: #{msg}\n"
55
+ end
56
+ end
57
+
58
+ def json_formatter
59
+ proc do |severity, datetime, progname, msg|
60
+ require 'json'
61
+ JSON.dump(
62
+ name: 'resque-scheduler',
63
+ progname: progname,
64
+ level: severity,
65
+ timestamp: datetime.iso8601,
66
+ msg: msg
67
+ ) + "\n"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module Plugin
6
+ def self.hooks(job, pattern)
7
+ job.methods.grep(/^#{pattern}/).sort
8
+ end
9
+
10
+ def self.run_hooks(job, pattern, *args)
11
+ results = hooks(job, pattern).map do |hook|
12
+ job.send(hook, *args)
13
+ end
14
+
15
+ results.all? { |result| result != false }
16
+ end
17
+
18
+ def self.run_before_delayed_enqueue_hooks(klass, *args)
19
+ run_hooks(klass, 'before_delayed_enqueue', *args)
20
+ end
21
+
22
+ def self.run_before_schedule_hooks(klass, *args)
23
+ run_hooks(klass, 'before_schedule', *args)
24
+ end
25
+
26
+ def self.run_after_schedule_hooks(klass, *args)
27
+ run_hooks(klass, 'after_schedule', *args)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,150 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ module Resque
4
+ module Scheduler
5
+ module SchedulingExtensions
6
+ # Accepts a new schedule configuration of the form:
7
+ #
8
+ # {
9
+ # "MakeTea" => {
10
+ # "every" => "1m" },
11
+ # "some_name" => {
12
+ # "cron" => "5/* * * *",
13
+ # "class" => "DoSomeWork",
14
+ # "args" => "work on this string",
15
+ # "description" => "this thing works it"s butter off" },
16
+ # ...
17
+ # }
18
+ #
19
+ # Hash keys can be anything and are used to describe and reference
20
+ # the scheduled job. If the "class" argument is missing, the key
21
+ # is used implicitly as "class" argument - in the "MakeTea" example,
22
+ # "MakeTea" is used both as job name and resque worker class.
23
+ #
24
+ # Any jobs that were in the old schedule, but are not
25
+ # present in the new schedule, will be removed.
26
+ #
27
+ # :cron can be any cron scheduling string
28
+ #
29
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every'
30
+ # usage for valid syntax. If :cron is present it will take precedence
31
+ # over :every.
32
+ #
33
+ # :class must be a resque worker class. If it is missing, the job name
34
+ # (hash key) will be used as :class.
35
+ #
36
+ # :args can be any yaml which will be converted to a ruby literal and
37
+ # passed in a params. (optional)
38
+ #
39
+ # :rails_envs is the list of envs where the job gets loaded. Envs are
40
+ # comma separated (optional)
41
+ #
42
+ # :description is just that, a description of the job (optional). If
43
+ # params is an array, each element in the array is passed as a separate
44
+ # param, otherwise params is passed in as the only parameter to
45
+ # perform.
46
+ def schedule=(schedule_hash)
47
+ # clean the schedules as it exists in redis
48
+ clean_schedules
49
+
50
+ schedule_hash = prepare_schedule(schedule_hash)
51
+
52
+ # store all schedules in redis, so we can retrieve them back
53
+ # everywhere.
54
+ schedule_hash.each do |name, job_spec|
55
+ set_schedule(name, job_spec)
56
+ end
57
+
58
+ # ensure only return the successfully saved data!
59
+ reload_schedule!
60
+ end
61
+
62
+ # Returns the schedule hash
63
+ def schedule
64
+ @schedule ||= all_schedules
65
+ @schedule || {}
66
+ end
67
+
68
+ # reloads the schedule from redis
69
+ def reload_schedule!
70
+ @schedule = all_schedules
71
+ end
72
+
73
+ # gets the schedules as it exists in redis
74
+ def all_schedules
75
+ return nil unless redis.exists(:schedules)
76
+
77
+ redis.hgetall(:schedules).tap do |h|
78
+ h.each do |name, config|
79
+ h[name] = decode(config)
80
+ end
81
+ end
82
+ end
83
+
84
+ # clean the schedules as it exists in redis, useful for first setup?
85
+ def clean_schedules
86
+ if redis.exists(:schedules)
87
+ redis.hkeys(:schedules).each do |key|
88
+ remove_schedule(key) unless schedule_persisted?(key)
89
+ end
90
+ end
91
+ @schedule = nil
92
+ true
93
+ end
94
+
95
+ # Create or update a schedule with the provided name and configuration.
96
+ #
97
+ # Note: values for class and custom_job_class need to be strings,
98
+ # not constants.
99
+ #
100
+ # Resque.set_schedule('some_job', {:class => 'SomeJob',
101
+ # :every => '15mins',
102
+ # :queue => 'high',
103
+ # :args => '/tmp/poop'})
104
+ def set_schedule(name, config)
105
+ existing_config = fetch_schedule(name)
106
+ persist = config.delete(:persist) || config.delete('persist')
107
+ unless existing_config && existing_config == config
108
+ redis.pipelined do
109
+ redis.hset(:schedules, name, encode(config))
110
+ redis.sadd(:schedules_changed, name)
111
+ redis.sadd(:persisted_schedules, name) if persist
112
+ end
113
+ end
114
+ config
115
+ end
116
+
117
+ # retrive the schedule configuration for the given name
118
+ def fetch_schedule(name)
119
+ decode(redis.hget(:schedules, name))
120
+ end
121
+
122
+ def schedule_persisted?(name)
123
+ redis.sismember(:persisted_schedules, name)
124
+ end
125
+
126
+ # remove a given schedule by name
127
+ def remove_schedule(name)
128
+ redis.pipelined do
129
+ redis.hdel(:schedules, name)
130
+ redis.srem(:persisted_schedules, name)
131
+ redis.sadd(:schedules_changed, name)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def prepare_schedule(schedule_hash)
138
+ prepared_hash = {}
139
+ schedule_hash.each do |name, job_spec|
140
+ job_spec = job_spec.dup
141
+ unless job_spec.key?('class') || job_spec.key?(:class)
142
+ job_spec['class'] = name
143
+ end
144
+ prepared_hash[name] = job_spec
145
+ end
146
+ prepared_hash
147
+ end
148
+ end
149
+ end
150
+ end