pd-blender 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +14 -0
- data/README.md +342 -0
- data/Rakefile +21 -0
- data/bin/blend +20 -0
- data/blender.gemspec +36 -0
- data/lib/blender.rb +67 -0
- data/lib/blender/cli.rb +71 -0
- data/lib/blender/configuration.rb +45 -0
- data/lib/blender/discovery.rb +41 -0
- data/lib/blender/drivers/base.rb +40 -0
- data/lib/blender/drivers/compound.rb +29 -0
- data/lib/blender/drivers/ruby.rb +55 -0
- data/lib/blender/drivers/shellout.rb +63 -0
- data/lib/blender/drivers/ssh.rb +93 -0
- data/lib/blender/drivers/ssh_multi.rb +102 -0
- data/lib/blender/event_dispatcher.rb +45 -0
- data/lib/blender/exceptions.rb +26 -0
- data/lib/blender/handlers/base.rb +39 -0
- data/lib/blender/handlers/doc.rb +73 -0
- data/lib/blender/job.rb +73 -0
- data/lib/blender/lock/flock.rb +64 -0
- data/lib/blender/log.rb +24 -0
- data/lib/blender/rspec.rb +68 -0
- data/lib/blender/rspec/stub_registry.rb +45 -0
- data/lib/blender/scheduled_job.rb +66 -0
- data/lib/blender/scheduler.rb +114 -0
- data/lib/blender/scheduler/dsl.rb +160 -0
- data/lib/blender/scheduling_strategies/base.rb +30 -0
- data/lib/blender/scheduling_strategies/default.rb +37 -0
- data/lib/blender/scheduling_strategies/per_host.rb +38 -0
- data/lib/blender/scheduling_strategies/per_task.rb +37 -0
- data/lib/blender/tasks/base.rb +72 -0
- data/lib/blender/tasks/ruby.rb +31 -0
- data/lib/blender/tasks/shell_out.rb +30 -0
- data/lib/blender/tasks/ssh.rb +25 -0
- data/lib/blender/timer.rb +54 -0
- data/lib/blender/utils/refinements.rb +45 -0
- data/lib/blender/utils/thread_pool.rb +54 -0
- data/lib/blender/utils/ui.rb +51 -0
- data/lib/blender/version.rb +20 -0
- data/spec/blender/blender_rspec.rb +31 -0
- data/spec/blender/discovery_spec.rb +16 -0
- data/spec/blender/drivers/ssh_multi_spec.rb +16 -0
- data/spec/blender/drivers/ssh_spec.rb +17 -0
- data/spec/blender/dsl_spec.rb +19 -0
- data/spec/blender/event_dispatcher_spec.rb +17 -0
- data/spec/blender/job_spec.rb +42 -0
- data/spec/blender/lock_spec.rb +129 -0
- data/spec/blender/scheduled_job_spec.rb +30 -0
- data/spec/blender/scheduler_spec.rb +140 -0
- data/spec/blender/scheduling_strategies/default_spec.rb +75 -0
- data/spec/blender/utils/refinements_spec.rb +16 -0
- data/spec/blender/utils/thread_pool_spec.rb +16 -0
- data/spec/blender_spec.rb +37 -0
- data/spec/data/example.rb +12 -0
- data/spec/spec_helper.rb +35 -0
- metadata +304 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
|
19
|
+
require 'blender/log'
|
20
|
+
require 'blender/job'
|
21
|
+
|
22
|
+
module Blender
|
23
|
+
module SchedulingStrategy
|
24
|
+
class Base
|
25
|
+
def compute_jobs(tasks)
|
26
|
+
raise RuntimeError, 'Must be overridden'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'blender/scheduling_strategies/base'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module SchedulingStrategy
|
22
|
+
class Default < Base
|
23
|
+
def compute_jobs(tasks)
|
24
|
+
Log.debug("Computing jobs from #{tasks.size} tasks")
|
25
|
+
pairs = tasks.map{|t| [t].product(t.hosts)}.flatten(1)
|
26
|
+
job_id = 0
|
27
|
+
jobs = pairs.map do |task, host|
|
28
|
+
Log.debug("Creating job (#{host}|#{task.name})")
|
29
|
+
job_id += 1
|
30
|
+
Job.new(job_id, task.driver, [task], [host])
|
31
|
+
end
|
32
|
+
Log.debug("Total jobs : #{jobs.size}")
|
33
|
+
jobs
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'blender/drivers/compound'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module SchedulingStrategy
|
22
|
+
class PerHost < Base
|
23
|
+
def compute_jobs(tasks)
|
24
|
+
Log.debug("Computing jobs from #{tasks.size} tasks")
|
25
|
+
hosts_list = tasks.map(&:hosts).uniq
|
26
|
+
if hosts_list.size != 1
|
27
|
+
raise UnsupportedFeature, 'PerHost strategy does not support scheduling tasks with different memebers'
|
28
|
+
end
|
29
|
+
job_id = 1
|
30
|
+
jobs = hosts_list.first.map do |host|
|
31
|
+
Job.new(job_id, Blender::Driver::Compound.new, tasks, [host])
|
32
|
+
end
|
33
|
+
Log.debug("Total jobs : #{jobs.size}")
|
34
|
+
jobs
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'blender/scheduling_strategies/base'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module SchedulingStrategy
|
22
|
+
class PerTask < Base
|
23
|
+
def compute_jobs(tasks)
|
24
|
+
Log.debug("Computing jobs from #{tasks.size} tasks")
|
25
|
+
job_id = 0
|
26
|
+
jobs = tasks.map do |task|
|
27
|
+
hosts = task.hosts
|
28
|
+
Log.debug("Creating job (#{hosts.size}|#{task.name})")
|
29
|
+
job_id += 1
|
30
|
+
Job.new(job_id, task.driver, [task] , hosts)
|
31
|
+
end
|
32
|
+
Log.debug("Total jobs : #{jobs.size}")
|
33
|
+
jobs
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
require 'blender/discovery'
|
18
|
+
|
19
|
+
module Blender
|
20
|
+
module Task
|
21
|
+
class Base
|
22
|
+
include Blender::Discovery
|
23
|
+
|
24
|
+
attr_reader :guards
|
25
|
+
attr_reader :metadata
|
26
|
+
attr_reader :name
|
27
|
+
attr_reader :hosts
|
28
|
+
attr_reader :driver
|
29
|
+
attr_reader :command
|
30
|
+
attr_reader :driver_opts
|
31
|
+
|
32
|
+
def initialize(name, metadata = {})
|
33
|
+
@name = name
|
34
|
+
@metadata = default_metadata.merge(metadata)
|
35
|
+
@hosts = []
|
36
|
+
@command = name
|
37
|
+
@driver = nil
|
38
|
+
@driver_opts = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_driver(driver)
|
42
|
+
@driver = driver
|
43
|
+
end
|
44
|
+
|
45
|
+
def ignore_failure(value)
|
46
|
+
@metadata[:ignore_failure] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def driver_options(opts)
|
50
|
+
@driver_opts = opts
|
51
|
+
end
|
52
|
+
|
53
|
+
def execute(cmd)
|
54
|
+
@command = cmd
|
55
|
+
end
|
56
|
+
|
57
|
+
def members(hosts)
|
58
|
+
@hosts = hosts
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_metadata(opts = {})
|
62
|
+
opts.keys.each do |k|
|
63
|
+
@metadata[k] = opts[k]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_metadata
|
68
|
+
{ ignore_failure: false }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'blender/tasks/base'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module Task
|
22
|
+
class Ruby < Blender::Task::Base
|
23
|
+
|
24
|
+
attr_reader :code_block
|
25
|
+
|
26
|
+
def execute(&block)
|
27
|
+
@command = block
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require 'blender/tasks/base'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module Task
|
22
|
+
class ShellOut < Base
|
23
|
+
def initialize(name, metadata ={})
|
24
|
+
super
|
25
|
+
@command = name
|
26
|
+
@members = ['localhost']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'blender/tasks/ssh'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module Task
|
22
|
+
class Ssh < Blender::Task::Base
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require 'rufus-scheduler'
|
19
|
+
require 'blender/scheduled_job'
|
20
|
+
|
21
|
+
module Blender
|
22
|
+
# Timer class provides a simple dsl for running blender jobs periodically.
|
23
|
+
# It uses Rufus::Scheduler for scheduling jobs
|
24
|
+
class Timer
|
25
|
+
def initialize
|
26
|
+
@scheduled_jobs = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def schedule(name, &block)
|
30
|
+
job = Blender::ScheduledJob.new(name)
|
31
|
+
job.instance_eval(&block)
|
32
|
+
@scheduled_jobs << job
|
33
|
+
end
|
34
|
+
|
35
|
+
def join
|
36
|
+
scheduler = Rufus::Scheduler.new
|
37
|
+
@scheduled_jobs.each do |job|
|
38
|
+
case job.schedule.first
|
39
|
+
when :every
|
40
|
+
scheduler.every(job.schedule[1]) do
|
41
|
+
job.run
|
42
|
+
end
|
43
|
+
when :cron
|
44
|
+
scheduler.cron(job.schedule[1]) do
|
45
|
+
job.run
|
46
|
+
end
|
47
|
+
else
|
48
|
+
raise UnsupportedFeature, "Unsupported scheduling: '#{job.schedule.first}'"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
scheduler.join
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
module Blender
|
18
|
+
module Utils
|
19
|
+
module Refinements
|
20
|
+
def camelcase(string)
|
21
|
+
str = string.dup
|
22
|
+
str.gsub!(/[^A-Za-z0-9_]/,'_')
|
23
|
+
rname = nil
|
24
|
+
regexp = %r{^(.+?)(_(.+))?$}
|
25
|
+
mn = str.match(regexp)
|
26
|
+
if mn
|
27
|
+
rname = mn[1].capitalize
|
28
|
+
while mn && mn[3]
|
29
|
+
mn = mn[3].match(regexp)
|
30
|
+
rname << mn[1].capitalize if mn
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rname
|
34
|
+
end
|
35
|
+
|
36
|
+
def symbolize(hash)
|
37
|
+
res = {}
|
38
|
+
hash.keys.each do |k|
|
39
|
+
res[k.to_sym] = hash[k]
|
40
|
+
end
|
41
|
+
res
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ranjib Dey (<ranjib@pagerduty.com>)
|
3
|
+
# Copyright:: Copyright (c) 2014 PagerDuty, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
require 'thread'
|
18
|
+
require 'blender/log'
|
19
|
+
|
20
|
+
module Blender
|
21
|
+
module Utils
|
22
|
+
class ThreadPool
|
23
|
+
|
24
|
+
def initialize(size)
|
25
|
+
@size = size
|
26
|
+
@queue = Queue.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_job(&blk)
|
30
|
+
@queue << blk
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_till_done
|
34
|
+
num = @size > @queue.size ? @queue.size : @size
|
35
|
+
threads = Array.new(num) do
|
36
|
+
Thread.new do
|
37
|
+
Thread.current.abort_on_exception = true
|
38
|
+
@queue.pop.call while true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
until @queue.empty?
|
42
|
+
sleep 0.2
|
43
|
+
end
|
44
|
+
until @queue.num_waiting == num
|
45
|
+
sleep 0.2
|
46
|
+
end
|
47
|
+
threads.each do |thread|
|
48
|
+
thread.join(0.02)
|
49
|
+
end
|
50
|
+
threads.map(&:kill)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|