pallets 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 +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTING.md +19 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +91 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/pallets +7 -0
- data/bin/setup +8 -0
- data/lib/pallets.rb +53 -0
- data/lib/pallets/backends/base.rb +38 -0
- data/lib/pallets/backends/redis.rb +109 -0
- data/lib/pallets/backends/scripts/discard.lua +3 -0
- data/lib/pallets/backends/scripts/give_up.lua +6 -0
- data/lib/pallets/backends/scripts/reschedule_all.lua +22 -0
- data/lib/pallets/backends/scripts/retry.lua +6 -0
- data/lib/pallets/backends/scripts/run_workflow.lua +11 -0
- data/lib/pallets/backends/scripts/save.lua +18 -0
- data/lib/pallets/cli.rb +102 -0
- data/lib/pallets/configuration.rb +44 -0
- data/lib/pallets/dsl/workflow.rb +32 -0
- data/lib/pallets/errors.rb +4 -0
- data/lib/pallets/graph.rb +44 -0
- data/lib/pallets/manager.rb +57 -0
- data/lib/pallets/pool.rb +26 -0
- data/lib/pallets/scheduler.rb +53 -0
- data/lib/pallets/serializers/base.rb +13 -0
- data/lib/pallets/serializers/json.rb +15 -0
- data/lib/pallets/task.rb +12 -0
- data/lib/pallets/version.rb +3 -0
- data/lib/pallets/worker.rb +109 -0
- data/lib/pallets/workflow.rb +58 -0
- data/pallets.gemspec +28 -0
- metadata +177 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
-- Queue reliability queue jobs that are ready to be retried (their score is
|
2
|
+
-- below given value) and remove jobs from sorted set and list
|
3
|
+
-- TODO: Add limit of items to get
|
4
|
+
local count = redis.call("ZCOUNT", KEYS[1], "-inf", ARGV[1])
|
5
|
+
if count > 0 then
|
6
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
|
7
|
+
redis.call("LPUSH", KEYS[4], unpack(work))
|
8
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
|
9
|
+
for _, job in pairs(work) do
|
10
|
+
redis.call("LREM", KEYS[2], 0, job)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
-- Queue jobs that are ready to be retried (their score is below given value) and
|
15
|
+
-- remove jobs from sorted set
|
16
|
+
-- TODO: Add limit of items to get
|
17
|
+
local count = redis.call("ZCOUNT", KEYS[3], "-inf", ARGV[1])
|
18
|
+
if count > 0 then
|
19
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[3], "-inf", ARGV[1])
|
20
|
+
redis.call("LPUSH", KEYS[4], unpack(work))
|
21
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[3], "-inf", ARGV[1])
|
22
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
-- Add all jobs to sorted set
|
2
|
+
redis.call("ZADD", KEYS[1], unpack(ARGV))
|
3
|
+
|
4
|
+
-- Queue jobs that are ready to be processed (their score is 0) and
|
5
|
+
-- remove queued jobs from the sorted set
|
6
|
+
local count = redis.call("ZCOUNT", KEYS[1], 0, 0)
|
7
|
+
if count > 0 then
|
8
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
9
|
+
redis.call("LPUSH", KEYS[2], unpack(work))
|
10
|
+
redis.call("ZREM", KEYS[1], unpack(work))
|
11
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
-- Remove job from reliability queue
|
2
|
+
redis.call("LREM", KEYS[3], 0, ARGV[1])
|
3
|
+
redis.call("ZREM", KEYS[4], ARGV[1])
|
4
|
+
|
5
|
+
-- Decrement all jobs from the sorted set
|
6
|
+
local all_pending = redis.call("ZRANGE", KEYS[1], 0, -1)
|
7
|
+
for score, task in pairs(all_pending) do
|
8
|
+
redis.call("ZINCRBY", KEYS[1], -1, task)
|
9
|
+
end
|
10
|
+
|
11
|
+
-- Queue jobs that are ready to be processed (their score is 0) and
|
12
|
+
-- remove queued jobs from sorted set
|
13
|
+
local count = redis.call("ZCOUNT", KEYS[1], 0, 0)
|
14
|
+
if count > 0 then
|
15
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
16
|
+
redis.call("LPUSH", KEYS[2], unpack(work))
|
17
|
+
redis.call("ZREM", KEYS[1], unpack(work))
|
18
|
+
end
|
data/lib/pallets/cli.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Pallets
|
4
|
+
class CLI
|
5
|
+
def initialize
|
6
|
+
parse_options
|
7
|
+
setup_signal_handlers
|
8
|
+
|
9
|
+
@manager = Manager.new
|
10
|
+
@signal_queue = Queue.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
Pallets.logger.info 'Starting the awesomeness of Pallets <3'
|
15
|
+
|
16
|
+
@manager.start
|
17
|
+
|
18
|
+
loop do
|
19
|
+
# This blocks until signals are received
|
20
|
+
handle_signal(@signal_queue.pop)
|
21
|
+
end
|
22
|
+
rescue Interrupt
|
23
|
+
Pallets.logger.info 'Shutting down...'
|
24
|
+
@manager.shutdown
|
25
|
+
Pallets.logger.info 'Buh-bye!'
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def handle_signal(signal)
|
32
|
+
case signal
|
33
|
+
when 'INT'
|
34
|
+
raise Interrupt
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_options
|
39
|
+
OptionParser.new do |opts|
|
40
|
+
opts.banner = 'Usage: pallets [options]'
|
41
|
+
|
42
|
+
opts.on('-b', '--backend NAME', 'Backend to use') do |backend|
|
43
|
+
Pallets.configuration.backend = backend
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('-c', '--concurrency NUM', Integer, 'Number of workers to start') do |concurrency|
|
47
|
+
Pallets.configuration.concurrency = concurrency
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-f', '--max-failures NUM', Integer, 'Maximum allowed number of failures per task') do |max_failures|
|
51
|
+
Pallets.configuration.max_failures = max_failures
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('-n', '--namespace NAME', 'Namespace to use for backend') do |namespace|
|
55
|
+
Pallets.configuration.namespace = namespace
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on('-p', '--pool-size NUM', Integer, 'Size of backend pool') do |pool_size|
|
59
|
+
Pallets.configuration.pool_size = pool_size
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('-q', '--quiet', 'Output less logs') do
|
63
|
+
Pallets.logger.level = Logger::ERROR
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on('-r', '--require PATH', 'Path containing workflow definitions') do |path|
|
67
|
+
require(path)
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('-s', '--serializer NAME', 'Serializer to use') do |serializer|
|
71
|
+
Pallets.configuration.serializer = serializer
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('-u', '--blocking-timeout NUM', Integer, 'Seconds to block while waiting for work') do |blocking_timeout|
|
75
|
+
Pallets.configuration.blocking_timeout = blocking_timeout
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on('-v', '--verbose', 'Output more logs') do
|
79
|
+
Pallets.logger.level = Logger::DEBUG
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on('--version', 'Version of Pallets') do
|
83
|
+
puts "Pallets v#{Pallets::VERSION}"
|
84
|
+
exit
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
88
|
+
puts opts
|
89
|
+
exit
|
90
|
+
end
|
91
|
+
end.parse!
|
92
|
+
end
|
93
|
+
|
94
|
+
def setup_signal_handlers
|
95
|
+
%w(INT).each do |signal|
|
96
|
+
trap signal do
|
97
|
+
@signal_queue.push signal
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Pallets
|
2
|
+
class Configuration
|
3
|
+
# Backend to use for handling workflows
|
4
|
+
attr_accessor :backend
|
5
|
+
|
6
|
+
# Arguments used to initialize the backend
|
7
|
+
attr_accessor :backend_args
|
8
|
+
|
9
|
+
# Number of seconds to block while waiting for jobs
|
10
|
+
attr_accessor :blocking_timeout
|
11
|
+
|
12
|
+
# Number of workers to process jobs
|
13
|
+
attr_accessor :concurrency
|
14
|
+
|
15
|
+
# Number of seconds allowed for a job to be processed. If a job exceeds this
|
16
|
+
# period, it is considered failed, and scheduled to be processed again
|
17
|
+
attr_accessor :job_timeout
|
18
|
+
|
19
|
+
# Maximum number of failures allowed per job. Can also be configured on a
|
20
|
+
# per task basis
|
21
|
+
attr_accessor :max_failures
|
22
|
+
|
23
|
+
# Namespace used by the backend to store information
|
24
|
+
attr_accessor :namespace
|
25
|
+
|
26
|
+
# Number of connections to the backend
|
27
|
+
attr_accessor :pool_size
|
28
|
+
|
29
|
+
# Serializer used for jobs
|
30
|
+
attr_accessor :serializer
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@backend = :redis
|
34
|
+
@backend_args = {}
|
35
|
+
@blocking_timeout = 5
|
36
|
+
@concurrency = 2
|
37
|
+
@job_timeout = 1800 # 30 minutes
|
38
|
+
@max_failures = 3
|
39
|
+
@namespace = 'pallets'
|
40
|
+
@pool_size = 5
|
41
|
+
@serializer = :json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module Pallets
|
4
|
+
module DSL
|
5
|
+
module Workflow
|
6
|
+
def task(*args, &block)
|
7
|
+
options = args.extract_options!
|
8
|
+
name, depends_on = if args.any?
|
9
|
+
[args.first, options[:depends_on]]
|
10
|
+
else
|
11
|
+
options.first
|
12
|
+
end
|
13
|
+
raise ArgumentError, "A task must have a name" unless name
|
14
|
+
|
15
|
+
# Handle nils, symbols or arrays consistently
|
16
|
+
name = name.to_sym
|
17
|
+
dependencies = Array(depends_on).compact.map(&:to_sym)
|
18
|
+
graph.add(name, dependencies)
|
19
|
+
|
20
|
+
class_name = options[:class_name] || name.to_s.camelize
|
21
|
+
max_failures = options[:max_failures] || Pallets.configuration.max_failures
|
22
|
+
|
23
|
+
task_config[name] = {
|
24
|
+
'class_name' => class_name,
|
25
|
+
'max_failures' => max_failures
|
26
|
+
}
|
27
|
+
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Pallets
|
4
|
+
class Graph
|
5
|
+
include TSort
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@nodes = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(node, dependencies)
|
12
|
+
@nodes[node] = dependencies
|
13
|
+
end
|
14
|
+
|
15
|
+
def parents(node)
|
16
|
+
@nodes[node]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns nodes topologically sorted, together with their order (number of
|
20
|
+
# nodes that have to be executed prior)
|
21
|
+
def sorted_with_order
|
22
|
+
# Identify groups of nodes that can be executed concurrently
|
23
|
+
groups = tsort_each.slice_when { |a, b| parents(a) != parents(b) }
|
24
|
+
|
25
|
+
# Assign order to each node
|
26
|
+
i = 0
|
27
|
+
groups.flat_map do |group|
|
28
|
+
group_with_order = group.product([i])
|
29
|
+
i += group.size
|
30
|
+
group_with_order
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def tsort_each_node(&block)
|
37
|
+
@nodes.each_key(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def tsort_each_child(node, &block)
|
41
|
+
@nodes.fetch(node).each(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Pallets
|
2
|
+
class Manager
|
3
|
+
attr_reader :workers, :timeout
|
4
|
+
|
5
|
+
def initialize(concurrency: Pallets.configuration.concurrency)
|
6
|
+
@workers = concurrency.times.map { Worker.new(self) }
|
7
|
+
@scheduler = Scheduler.new(self)
|
8
|
+
@lock = Mutex.new
|
9
|
+
@needs_to_stop = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
@workers.each(&:start)
|
14
|
+
@scheduler.start
|
15
|
+
end
|
16
|
+
|
17
|
+
# Attempt to gracefully shutdown every worker. If any is still busy after
|
18
|
+
# the given timeout, hard shutdown it. We don't need to worry about lost
|
19
|
+
# jobs caused by the hard shutdown; there is a reliability list that
|
20
|
+
# contains all active jobs, which will be automatically requeued upon next
|
21
|
+
# start
|
22
|
+
def shutdown
|
23
|
+
@needs_to_stop = true
|
24
|
+
|
25
|
+
@workers.reverse_each(&:graceful_shutdown)
|
26
|
+
@scheduler.shutdown
|
27
|
+
|
28
|
+
Pallets.logger.info 'Waiting for workers to finish their jobs...'
|
29
|
+
# Wait for 10 seconds at most
|
30
|
+
10.times do
|
31
|
+
return if @workers.empty?
|
32
|
+
sleep 1
|
33
|
+
end
|
34
|
+
|
35
|
+
@workers.reverse_each(&:hard_shutdown)
|
36
|
+
# Ensure Pallets::Shutdown got propagated and workers finished; if not,
|
37
|
+
# their threads will be killed anyway when the manager quits
|
38
|
+
sleep 0.5
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_worker(worker)
|
42
|
+
@lock.synchronize { @workers.delete(worker) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def replace_worker(worker)
|
46
|
+
@lock.synchronize do
|
47
|
+
@workers.delete(worker)
|
48
|
+
|
49
|
+
return if @needs_to_stop
|
50
|
+
|
51
|
+
worker = Worker.new(self)
|
52
|
+
@workers << worker
|
53
|
+
worker.start
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/pallets/pool.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Pallets
|
2
|
+
class Pool
|
3
|
+
def initialize(size)
|
4
|
+
raise ArgumentError, 'Pool needs a block to initialize' unless block_given?
|
5
|
+
|
6
|
+
@queue = Queue.new
|
7
|
+
@size = size
|
8
|
+
size.times { @queue << yield }
|
9
|
+
end
|
10
|
+
|
11
|
+
def size
|
12
|
+
@queue.size
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
raise ArgumentError, 'Pool needs a block to execute' unless block_given?
|
17
|
+
|
18
|
+
begin
|
19
|
+
item = @queue.pop
|
20
|
+
yield item
|
21
|
+
ensure
|
22
|
+
@queue << item
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Pallets
|
2
|
+
class Scheduler
|
3
|
+
def initialize(manager)
|
4
|
+
@manager = manager
|
5
|
+
@needs_to_stop = false
|
6
|
+
@thread = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
@thread ||= Thread.new { work }
|
11
|
+
end
|
12
|
+
|
13
|
+
def shutdown
|
14
|
+
@needs_to_stop = true
|
15
|
+
|
16
|
+
return unless @thread
|
17
|
+
@thread.join
|
18
|
+
end
|
19
|
+
|
20
|
+
def needs_to_stop?
|
21
|
+
@needs_to_stop
|
22
|
+
end
|
23
|
+
|
24
|
+
def id
|
25
|
+
"S#{@thread.object_id.to_s(36)}".upcase if @thread
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def work
|
31
|
+
loop do
|
32
|
+
break if needs_to_stop?
|
33
|
+
|
34
|
+
backend.reschedule_all(Time.now.to_f)
|
35
|
+
wait_a_bit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def wait_a_bit
|
40
|
+
# Wait for roughly 10 seconds
|
41
|
+
# We don't want to block the entire polling interval, since we want to
|
42
|
+
# deal with shutdowns synchronously and as fast as possible
|
43
|
+
10.times do
|
44
|
+
break if needs_to_stop?
|
45
|
+
sleep 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def backend
|
50
|
+
@backend ||= Pallets.backend
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|