que 0.4.0 → 0.5.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 +4 -4
- data/.travis.yml +15 -0
- data/CHANGELOG.md +23 -1
- data/README.md +17 -13
- data/docs/advanced_setup.md +13 -11
- data/docs/customizing_que.md +113 -0
- data/docs/error_handling.md +1 -1
- data/docs/logging.md +42 -0
- data/docs/managing_workers.md +6 -0
- data/docs/using_sequel.md +1 -1
- data/lib/generators/que/templates/add_que.rb +4 -2
- data/lib/que.rb +37 -11
- data/lib/que/adapters/active_record.rb +26 -1
- data/lib/que/adapters/base.rb +8 -15
- data/lib/que/job.rb +9 -23
- data/lib/que/migrations.rb +78 -0
- data/lib/que/migrations/1-down.sql +1 -0
- data/lib/que/migrations/1-up.sql +12 -0
- data/lib/que/migrations/2-down.sql +1 -0
- data/lib/que/migrations/2-up.sql +3 -0
- data/lib/que/railtie.rb +8 -3
- data/lib/que/rake_tasks.rb +10 -6
- data/lib/que/sql.rb +1 -29
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +49 -46
- data/que.gemspec +0 -2
- data/spec/adapters/active_record_spec.rb +38 -8
- data/spec/adapters/connection_pool_spec.rb +10 -1
- data/spec/adapters/sequel_spec.rb +19 -8
- data/spec/spec_helper.rb +6 -2
- data/spec/support/shared_examples/adapter.rb +3 -1
- data/spec/travis.rb +13 -0
- data/spec/unit/helper_spec.rb +0 -8
- data/spec/unit/logging_spec.rb +75 -0
- data/spec/unit/migrations_spec.rb +114 -0
- data/spec/unit/pool_spec.rb +22 -40
- data/spec/unit/queue_spec.rb +7 -7
- data/spec/unit/states_spec.rb +3 -5
- data/spec/unit/work_spec.rb +63 -47
- data/spec/unit/worker_spec.rb +20 -52
- data/tasks/safe_shutdown.rb +6 -6
- metadata +17 -17
@@ -2,7 +2,32 @@ module Que
|
|
2
2
|
module Adapters
|
3
3
|
class ActiveRecord < Base
|
4
4
|
def checkout
|
5
|
-
|
5
|
+
checkout_activerecord_adapter { |conn| yield conn.raw_connection }
|
6
|
+
end
|
7
|
+
|
8
|
+
def wake_worker_after_commit
|
9
|
+
# Works with ActiveRecord 3.2 and 4 (possibly earlier, didn't check)
|
10
|
+
if in_transaction?
|
11
|
+
checkout_activerecord_adapter { |adapter| adapter.add_transaction_record(CommittedCallback.new) }
|
12
|
+
else
|
13
|
+
Que.wake!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class CommittedCallback
|
18
|
+
def has_transactional_callbacks?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def committed!
|
23
|
+
Que.wake!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def checkout_activerecord_adapter(&block)
|
30
|
+
::ActiveRecord::Base.connection_pool.with_connection(&block)
|
6
31
|
end
|
7
32
|
end
|
8
33
|
end
|
data/lib/que/adapters/base.rb
CHANGED
@@ -7,7 +7,7 @@ module Que
|
|
7
7
|
|
8
8
|
class Base
|
9
9
|
def initialize(thing = nil)
|
10
|
-
@
|
10
|
+
@prepared_statements = {}
|
11
11
|
end
|
12
12
|
|
13
13
|
# The only method that adapters really need to implement. Should lock a
|
@@ -18,7 +18,7 @@ module Que
|
|
18
18
|
raise NotImplementedError
|
19
19
|
end
|
20
20
|
|
21
|
-
# Called after a job is queued in async mode, to
|
21
|
+
# Called after a job is queued in async mode, to prompt a worker to
|
22
22
|
# wake up after the current transaction commits. Not all adapters will
|
23
23
|
# implement this.
|
24
24
|
def wake_worker_after_commit
|
@@ -31,26 +31,19 @@ module Que
|
|
31
31
|
|
32
32
|
def execute_prepared(name, params = [])
|
33
33
|
checkout do |conn|
|
34
|
-
|
34
|
+
statements = @prepared_statements[conn] ||= {}
|
35
|
+
|
36
|
+
unless statements[name]
|
35
37
|
conn.prepare("que_#{name}", SQL[name])
|
36
|
-
|
38
|
+
statements[name] = true
|
37
39
|
end
|
38
40
|
|
39
41
|
conn.exec_prepared("que_#{name}", params)
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
# Each adapter needs to remember which of its connections have prepared
|
46
|
-
# which statements. This is a shared data structure, so protect it. We
|
47
|
-
# assume that the hash of statements for a particular connection is only
|
48
|
-
# being accessed by the thread that's checked it out, though.
|
49
|
-
def statements_prepared(conn)
|
50
|
-
@statement_mutex.synchronize do
|
51
|
-
@statements_prepared ||= {}
|
52
|
-
@statements_prepared[conn] ||= {}
|
53
|
-
end
|
45
|
+
def in_transaction?
|
46
|
+
checkout { |conn| conn.transaction_status != ::PG::PQTRANS_IDLE }
|
54
47
|
end
|
55
48
|
end
|
56
49
|
end
|
data/lib/que/job.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
require 'multi_json'
|
2
|
-
|
3
1
|
module Que
|
4
2
|
class Job
|
5
3
|
attr_reader :attrs
|
6
4
|
|
7
5
|
def initialize(attrs)
|
8
6
|
@attrs = attrs
|
9
|
-
@attrs[:args] = Que.indifferentiate
|
7
|
+
@attrs[:args] = Que.indifferentiate JSON_MODULE.load(@attrs[:args])
|
10
8
|
end
|
11
9
|
|
12
10
|
# Subclasses should define their own run methods, but keep an empty one
|
@@ -15,10 +13,8 @@ module Que
|
|
15
13
|
end
|
16
14
|
|
17
15
|
def _run
|
18
|
-
time = Time.now
|
19
16
|
run *attrs[:args]
|
20
17
|
destroy unless @destroyed
|
21
|
-
Que.log :info, "Worked job in #{((Time.now - time) * 1000).round(1)} ms: #{inspect}"
|
22
18
|
end
|
23
19
|
|
24
20
|
private
|
@@ -37,7 +33,7 @@ module Que
|
|
37
33
|
args << options if options.any?
|
38
34
|
end
|
39
35
|
|
40
|
-
attrs = {:job_class => to_s, :args =>
|
36
|
+
attrs = {:job_class => to_s, :args => JSON_MODULE.dump(args)}
|
41
37
|
|
42
38
|
if time = run_at || @default_run_at && @default_run_at.call
|
43
39
|
attrs[:run_at] = time
|
@@ -57,12 +53,6 @@ module Que
|
|
57
53
|
end
|
58
54
|
|
59
55
|
def work
|
60
|
-
# Job.work is typically called in a loop, where we sleep when there's
|
61
|
-
# no more work to be done, so its return value should reflect whether
|
62
|
-
# we should look for another job or not. So, return truthy if we
|
63
|
-
# worked a job or encountered a typical error while working a job, and
|
64
|
-
# falsy if we found nothing to do or hit a connection error.
|
65
|
-
|
66
56
|
# Since we're taking session-level advisory locks, we have to hold the
|
67
57
|
# same connection throughout the process of getting a job, working it,
|
68
58
|
# deleting it, and removing the lock.
|
@@ -79,12 +69,13 @@ module Que
|
|
79
69
|
# Note that there is currently no spec for this behavior, since
|
80
70
|
# I'm not sure how to reliably commit a transaction that deletes
|
81
71
|
# the job in a separate thread between lock_job and check_job.
|
82
|
-
|
83
|
-
|
84
|
-
|
72
|
+
if Que.execute(:check_job, job.values_at(:priority, :run_at, :job_id)).none?
|
73
|
+
{:event => :job_race_condition}
|
74
|
+
else
|
75
|
+
{:event => :job_worked, :job => run_job(job).attrs}
|
76
|
+
end
|
85
77
|
else
|
86
|
-
|
87
|
-
nil
|
78
|
+
{:event => :job_unavailable}
|
88
79
|
end
|
89
80
|
rescue => error
|
90
81
|
begin
|
@@ -105,12 +96,7 @@ module Que
|
|
105
96
|
Que.error_handler.call(error) rescue nil
|
106
97
|
end
|
107
98
|
|
108
|
-
|
109
|
-
# another job, no big deal. If it's a PG::Error, though, assume
|
110
|
-
# it's a disconnection or something and that we shouldn't just hit
|
111
|
-
# the database again right away. We could be a lot more
|
112
|
-
# sophisticated about what errors we delay for, though.
|
113
|
-
return !error.is_a?(PG::Error)
|
99
|
+
return {:event => :job_errored, :error => error, :job => job}
|
114
100
|
ensure
|
115
101
|
# Clear the advisory lock we took when locking the job. Important
|
116
102
|
# to do this so that they don't pile up in the database. Again, if
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Que
|
2
|
+
module Migrations
|
3
|
+
# In order to ship a schema change, add the relevant up and down sql files
|
4
|
+
# to the migrations directory, and bump the version both here and in the
|
5
|
+
# add_que generator template.
|
6
|
+
CURRENT_VERSION = 2
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def migrate!(options = {:version => CURRENT_VERSION})
|
10
|
+
transaction do
|
11
|
+
version = options[:version]
|
12
|
+
|
13
|
+
if (current = db_version) == version
|
14
|
+
return
|
15
|
+
elsif current < version
|
16
|
+
direction = 'up'
|
17
|
+
steps = ((current + 1)..version).to_a
|
18
|
+
elsif current > version
|
19
|
+
direction = 'down'
|
20
|
+
steps = ((version + 1)..current).to_a.reverse
|
21
|
+
end
|
22
|
+
|
23
|
+
steps.each do |step|
|
24
|
+
Que.execute File.read("#{File.dirname(__FILE__)}/migrations/#{step}-#{direction}.sql")
|
25
|
+
end
|
26
|
+
|
27
|
+
set_db_version(version)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def db_version
|
32
|
+
result = Que.execute <<-SQL
|
33
|
+
SELECT relname, description
|
34
|
+
FROM pg_class
|
35
|
+
LEFT JOIN pg_description ON pg_description.objoid = pg_class.oid
|
36
|
+
WHERE relname = 'que_jobs'
|
37
|
+
SQL
|
38
|
+
|
39
|
+
if result.none?
|
40
|
+
# No table in the database at all.
|
41
|
+
0
|
42
|
+
elsif (d = result.first[:description]).nil?
|
43
|
+
# There's a table, it was just created before the migration system existed.
|
44
|
+
1
|
45
|
+
else
|
46
|
+
d.to_i
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_db_version(version)
|
51
|
+
i = version.to_i
|
52
|
+
Que.execute "COMMENT ON TABLE que_jobs IS '#{i}'" unless i.zero?
|
53
|
+
end
|
54
|
+
|
55
|
+
def transaction
|
56
|
+
Que.adapter.checkout do
|
57
|
+
if Que.adapter.in_transaction?
|
58
|
+
yield
|
59
|
+
else
|
60
|
+
begin
|
61
|
+
Que.execute "BEGIN"
|
62
|
+
yield
|
63
|
+
rescue => error
|
64
|
+
raise
|
65
|
+
ensure
|
66
|
+
# Handle a raised error or a killed thread.
|
67
|
+
if error || Thread.current.status == 'aborting'
|
68
|
+
Que.execute "ROLLBACK"
|
69
|
+
else
|
70
|
+
Que.execute "COMMIT"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
DROP TABLE que_jobs;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
CREATE TABLE que_jobs
|
2
|
+
(
|
3
|
+
priority integer NOT NULL DEFAULT 1,
|
4
|
+
run_at timestamptz NOT NULL DEFAULT now(),
|
5
|
+
job_id bigserial NOT NULL,
|
6
|
+
job_class text NOT NULL,
|
7
|
+
args json NOT NULL DEFAULT '[]'::json,
|
8
|
+
error_count integer NOT NULL DEFAULT 0,
|
9
|
+
last_error text,
|
10
|
+
|
11
|
+
CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
|
12
|
+
);
|
@@ -0,0 +1 @@
|
|
1
|
+
ALTER TABLE que_jobs ALTER COLUMN priority SET DEFAULT 1;
|
data/lib/que/railtie.rb
CHANGED
@@ -3,7 +3,7 @@ module Que
|
|
3
3
|
config.que = Que
|
4
4
|
|
5
5
|
Que.mode = :sync if Rails.env.test?
|
6
|
-
Que.connection = ::ActiveRecord if defined?
|
6
|
+
Que.connection = ::ActiveRecord if defined? ::ActiveRecord
|
7
7
|
|
8
8
|
rake_tasks do
|
9
9
|
load 'que/rake_tasks.rb'
|
@@ -16,8 +16,13 @@ module Que
|
|
16
16
|
# Only start up the worker pool if running as a server.
|
17
17
|
Que.mode ||= :async if defined? Rails::Server
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
at_exit do
|
20
|
+
if Que.mode == :async
|
21
|
+
$stdout.puts "Finishing Que's current jobs before exiting..."
|
22
|
+
Que.mode = :off
|
23
|
+
$stdout.puts "Que's jobs finished, exiting..."
|
24
|
+
end
|
25
|
+
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
data/lib/que/rake_tasks.rb
CHANGED
@@ -3,19 +3,23 @@ namespace :que do
|
|
3
3
|
task :work => :environment do
|
4
4
|
require 'logger'
|
5
5
|
|
6
|
-
Que.logger
|
7
|
-
Que.
|
6
|
+
Que.logger = Logger.new(STDOUT)
|
7
|
+
Que.logger.level = Logger.const_get((ENV['QUE_LOG_LEVEL'] || 'INFO').upcase)
|
8
|
+
Que.worker_count = (ENV['QUE_WORKER_COUNT'] || 4).to_i
|
9
|
+
Que.wake_interval = (ENV['QUE_WAKE_INTERVAL'] || 0.1).to_f
|
8
10
|
|
9
11
|
# When changing how signals are caught, be sure to test the behavior with
|
10
12
|
# the rake task in tasks/safe_shutdown.rb.
|
11
|
-
at_exit do
|
12
|
-
puts "Stopping Que..."
|
13
|
-
Que.stop!
|
14
|
-
end
|
15
13
|
|
16
14
|
stop = false
|
17
15
|
trap('INT'){stop = true}
|
18
16
|
|
17
|
+
at_exit do
|
18
|
+
$stdout.puts "Finishing Que's current jobs before exiting..."
|
19
|
+
Que.mode = :off
|
20
|
+
$stdout.puts "Que's jobs finished, exiting..."
|
21
|
+
end
|
22
|
+
|
19
23
|
loop do
|
20
24
|
sleep 0.01
|
21
25
|
break if stop
|
data/lib/que/sql.rb
CHANGED
@@ -55,7 +55,7 @@ module Que
|
|
55
55
|
INSERT INTO que_jobs
|
56
56
|
(priority, run_at, job_class, args)
|
57
57
|
VALUES
|
58
|
-
(coalesce($1,
|
58
|
+
(coalesce($1, 100)::integer, coalesce($2, 'now')::timestamptz, $3::text, coalesce($4, '[]')::json)
|
59
59
|
RETURNING *
|
60
60
|
}.freeze,
|
61
61
|
|
@@ -99,34 +99,6 @@ module Que
|
|
99
99
|
JOIN pg_stat_activity USING (pid)
|
100
100
|
WHERE locktype = 'advisory'
|
101
101
|
) pg USING (job_id)
|
102
|
-
}.freeze,
|
103
|
-
|
104
|
-
:create_table => %{
|
105
|
-
CREATE TABLE que_jobs
|
106
|
-
(
|
107
|
-
priority integer NOT NULL DEFAULT 1,
|
108
|
-
run_at timestamptz NOT NULL DEFAULT now(),
|
109
|
-
job_id bigserial NOT NULL,
|
110
|
-
job_class text NOT NULL,
|
111
|
-
args json NOT NULL DEFAULT '[]'::json,
|
112
|
-
error_count integer NOT NULL DEFAULT 0,
|
113
|
-
last_error text,
|
114
|
-
|
115
|
-
CONSTRAINT que_jobs_pkey PRIMARY KEY (priority, run_at, job_id)
|
116
|
-
)
|
117
102
|
}.freeze
|
118
|
-
|
119
|
-
# Note: if schema changes to the que_jobs table become necessary later on,
|
120
|
-
# a simple versioning scheme would be:
|
121
|
-
|
122
|
-
# Set version:
|
123
|
-
# COMMENT ON TABLE que_jobs IS '2'
|
124
|
-
|
125
|
-
# Get version:
|
126
|
-
# SELECT description
|
127
|
-
# FROM pg_description
|
128
|
-
# JOIN pg_class
|
129
|
-
# ON pg_description.objoid = pg_class.oid
|
130
|
-
# WHERE relname = 'que_jobs'
|
131
103
|
}
|
132
104
|
end
|
data/lib/que/version.rb
CHANGED
data/lib/que/worker.rb
CHANGED
@@ -8,9 +8,6 @@ module Que
|
|
8
8
|
# synchronize access to it.
|
9
9
|
include MonitorMixin
|
10
10
|
|
11
|
-
# A custom exception to immediately kill a worker and its current job.
|
12
|
-
class Stop < Interrupt; end
|
13
|
-
|
14
11
|
attr_reader :thread, :state
|
15
12
|
|
16
13
|
def initialize
|
@@ -24,15 +21,7 @@ module Que
|
|
24
21
|
end
|
25
22
|
|
26
23
|
def sleeping?
|
27
|
-
synchronize
|
28
|
-
if @state == :sleeping
|
29
|
-
# There's a very small period of time between when the Worker marks
|
30
|
-
# itself as sleeping and when it actually goes to sleep. Only report
|
31
|
-
# true when we're certain the thread is sleeping.
|
32
|
-
wait until @thread.status == 'sleep'
|
33
|
-
true
|
34
|
-
end
|
35
|
-
end
|
24
|
+
synchronize { _sleeping? }
|
36
25
|
end
|
37
26
|
|
38
27
|
def working?
|
@@ -51,20 +40,10 @@ module Que
|
|
51
40
|
end
|
52
41
|
end
|
53
42
|
|
54
|
-
#
|
55
|
-
# while #stop! kills the job and worker immediately. #stop! is bad news
|
56
|
-
# because its results are unpredictable (it can leave the DB connection
|
57
|
-
# in an unusable state), so it should only be used when we're shutting
|
58
|
-
# down the whole process anyway and side effects aren't a big deal.
|
43
|
+
# This needs to be called when trapping a signal, so it can't lock the monitor.
|
59
44
|
def stop
|
60
|
-
|
61
|
-
|
62
|
-
@thread.wakeup if sleeping?
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def stop!
|
67
|
-
@thread.raise Stop
|
45
|
+
@stop = true
|
46
|
+
@thread.wakeup if _sleeping?
|
68
47
|
end
|
69
48
|
|
70
49
|
def wait_until_stopped
|
@@ -78,15 +57,47 @@ module Que
|
|
78
57
|
sleep 0.0001
|
79
58
|
end
|
80
59
|
|
60
|
+
def _sleeping?
|
61
|
+
if @state == :sleeping
|
62
|
+
# There's a very small period of time between when the Worker marks
|
63
|
+
# itself as sleeping and when it actually goes to sleep. Only report
|
64
|
+
# true when we're certain the thread is sleeping.
|
65
|
+
wait until @thread.status == 'sleep'
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
81
70
|
def work_loop
|
82
71
|
loop do
|
83
|
-
|
84
|
-
|
72
|
+
time = Time.now
|
73
|
+
cycle = nil
|
74
|
+
result = Job.work
|
75
|
+
|
76
|
+
case result[:event]
|
77
|
+
when :job_unavailable
|
78
|
+
cycle = false
|
79
|
+
result[:level] = :debug
|
80
|
+
when :job_race_condition
|
81
|
+
cycle = true
|
82
|
+
result[:level] = :debug
|
83
|
+
when :job_worked
|
84
|
+
cycle = true
|
85
|
+
result[:elapsed] = (Time.now - time).round(5)
|
86
|
+
when :job_errored
|
87
|
+
# For PG::Errors, assume we had a problem reaching the database, and
|
88
|
+
# don't hit it again right away.
|
89
|
+
cycle = !result[:error].is_a?(PG::Error)
|
90
|
+
result[:error] = {:class => result[:error].class.to_s, :message => result[:error].message}
|
91
|
+
else
|
92
|
+
raise "Unknown Event: #{result[:event].inspect}"
|
93
|
+
end
|
94
|
+
|
95
|
+
Que.log(result)
|
96
|
+
|
97
|
+
synchronize { @state = :sleeping unless cycle || @stop }
|
85
98
|
sleep if @state == :sleeping
|
86
99
|
break if @stop
|
87
100
|
end
|
88
|
-
rescue Stop
|
89
|
-
# This process is shutting down - let it.
|
90
101
|
ensure
|
91
102
|
@state = :stopped
|
92
103
|
end
|
@@ -97,7 +108,12 @@ module Que
|
|
97
108
|
# a worker, and make sure to wake up the wrangler when @wake_interval is
|
98
109
|
# changed in Que.wake_interval= below.
|
99
110
|
@wake_interval = 5
|
100
|
-
@wrangler = Thread.new
|
111
|
+
@wrangler = Thread.new do
|
112
|
+
loop do
|
113
|
+
sleep *@wake_interval
|
114
|
+
wake! if @wake_interval
|
115
|
+
end
|
116
|
+
end
|
101
117
|
|
102
118
|
class << self
|
103
119
|
attr_reader :mode, :wake_interval
|
@@ -129,19 +145,6 @@ module Que
|
|
129
145
|
@wrangler.wakeup
|
130
146
|
end
|
131
147
|
|
132
|
-
def stop!
|
133
|
-
# The behavior of Worker#stop! is unpredictable - what it does is
|
134
|
-
# dependent on what the Worker is currently doing. Sometimes it won't
|
135
|
-
# work the first time, so we need to try again, but sometimes it'll
|
136
|
-
# never do anything, so we can't repeat indefinitely. So, compromise.
|
137
|
-
5.times do
|
138
|
-
break if workers.select(&:alive?).each(&:stop!).none?
|
139
|
-
sleep 0.001
|
140
|
-
end
|
141
|
-
|
142
|
-
workers.clear
|
143
|
-
end
|
144
|
-
|
145
148
|
def wake!
|
146
149
|
workers.find &:wake!
|
147
150
|
end
|
@@ -154,17 +157,17 @@ module Que
|
|
154
157
|
|
155
158
|
def set_mode(mode)
|
156
159
|
if mode != @mode
|
157
|
-
Que.log :
|
160
|
+
Que.log :event => 'mode_change', :value => mode.to_s
|
158
161
|
@mode = mode
|
159
162
|
end
|
160
163
|
end
|
161
164
|
|
162
165
|
def set_worker_count(count)
|
163
166
|
if count != worker_count
|
164
|
-
Que.log :
|
167
|
+
Que.log :event => 'worker_count_change', :value => count.to_s
|
165
168
|
|
166
169
|
if count > worker_count
|
167
|
-
workers.push *(count - worker_count).times.map
|
170
|
+
workers.push *(count - worker_count).times.map{new}
|
168
171
|
elsif count < worker_count
|
169
172
|
workers.pop(worker_count - count).each(&:stop).each(&:wait_until_stopped)
|
170
173
|
end
|