pallets 0.6.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.travis.yml +4 -7
- data/CHANGELOG.md +39 -0
- data/LICENSE +1 -1
- data/README.md +6 -6
- data/examples/config_savvy.rb +1 -1
- data/lib/pallets.rb +1 -4
- data/lib/pallets/backends/base.rb +9 -4
- data/lib/pallets/backends/redis.rb +23 -8
- data/lib/pallets/backends/scripts/discard.lua +11 -0
- data/lib/pallets/backends/scripts/give_up.lua +6 -1
- data/lib/pallets/backends/scripts/run_workflow.lua +2 -3
- data/lib/pallets/backends/scripts/save.lua +10 -13
- data/lib/pallets/cli.rb +10 -0
- data/lib/pallets/configuration.rb +12 -0
- data/lib/pallets/dsl/workflow.rb +7 -1
- data/lib/pallets/graph.rb +5 -12
- data/lib/pallets/logger.rb +24 -5
- data/lib/pallets/manager.rb +4 -3
- data/lib/pallets/middleware/job_logger.rb +19 -8
- data/lib/pallets/scheduler.rb +13 -10
- data/lib/pallets/version.rb +1 -1
- data/lib/pallets/worker.rb +14 -17
- data/lib/pallets/workflow.rb +15 -5
- data/pallets.gemspec +3 -3
- metadata +14 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7ee0fbd91594f23bc84b81116f867490620f580223195f69fa9ff4f72be2558
|
4
|
+
data.tar.gz: b681bc765933ce36d3732a04eae3b9b7d60c2695b3d47f5704ebcfe96e3f884b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8dfe6d23000df032d7f65fd81bd7907a3600240eaa6f111a161d1b2fb6d234440c8cc9219203a094ae139847ba00011939b50943e5eaaa04c11dde9128c3bb94
|
7
|
+
data.tar.gz: c054b1abd6d25b0408fa8427550751c8c8d41cd204bed65a968302d2ad5be98ac5ef020a6d096efc52ec58e9fef6f66456528ea72cdf423666916dd3a1394a5f
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
github: linkyndy
|
data/.travis.yml
CHANGED
@@ -4,11 +4,8 @@ services:
|
|
4
4
|
- redis-server
|
5
5
|
cache: bundler
|
6
6
|
rvm:
|
7
|
-
- 2.4.
|
8
|
-
- 2.5.
|
9
|
-
- 2.6.
|
10
|
-
|
11
|
-
# Bundler 2.0 needs a newer RubyGems
|
12
|
-
- gem update --system
|
13
|
-
- gem install bundler
|
7
|
+
- 2.4.10
|
8
|
+
- 2.5.8
|
9
|
+
- 2.6.6
|
10
|
+
- 2.7.1
|
14
11
|
script: bundle exec rspec
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.11.0] - 2020-10-09
|
10
|
+
### Added
|
11
|
+
- JSON logger (#62)
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- fix for leaking Redis connections (#61)
|
15
|
+
|
16
|
+
## [0.10.0] - 2020-08-30
|
17
|
+
### Added
|
18
|
+
- configure polling interval for scheduler (#60)
|
19
|
+
|
20
|
+
### Changed
|
21
|
+
- handle persisting unforseen worker errors more gracefully (#59)
|
22
|
+
- add initial wait to scheduler startup (#60)
|
23
|
+
|
24
|
+
## [0.9.0] - 2020-07-05
|
25
|
+
### Added
|
26
|
+
- limit number of jobs in given up set by number (#56)
|
27
|
+
- job duration and metadata to all task logs (#57)
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
- remove all related workflow keys when giving up on a job (#55)
|
31
|
+
- support redis-rb ~> 4.2 (#58)
|
32
|
+
|
33
|
+
### Removed
|
34
|
+
- support for configuring custom loggers (#57)
|
35
|
+
|
36
|
+
## [0.8.0] - 2020-06-09
|
37
|
+
### Added
|
38
|
+
- sync output in CLI (#49)
|
39
|
+
- support for configuring custom loggers (#50)
|
40
|
+
|
41
|
+
### Changed
|
42
|
+
- improve job scheduling using jobmasks (#52)
|
43
|
+
|
44
|
+
## [0.7.0] - 2020-01-19
|
45
|
+
### Added
|
46
|
+
- support for Ruby 2.7 (#46)
|
47
|
+
|
9
48
|
## [0.6.0] - 2019-09-02
|
10
49
|
### Added
|
11
50
|
- define task aliases in order to reuse tasks within a workflow definition (#44)
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.com/linkyndy/pallets.svg?branch=master)](https://travis-ci.com/linkyndy/pallets)
|
4
4
|
|
5
|
-
|
5
|
+
Simple and reliable workflow engine, written in Ruby
|
6
6
|
|
7
7
|
## It is plain simple!
|
8
8
|
|
@@ -11,10 +11,10 @@ Toy workflow engine, written in Ruby
|
|
11
11
|
require 'pallets'
|
12
12
|
|
13
13
|
class MyWorkflow < Pallets::Workflow
|
14
|
-
task Foo
|
15
|
-
task Bar => Foo
|
16
|
-
task Baz => Foo
|
17
|
-
task Qux => [Bar, Baz]
|
14
|
+
task 'Foo'
|
15
|
+
task 'Bar' => 'Foo'
|
16
|
+
task 'Baz' => 'Foo'
|
17
|
+
task 'Qux' => ['Bar', 'Baz']
|
18
18
|
end
|
19
19
|
|
20
20
|
class Foo < Pallets::Task
|
@@ -120,7 +120,7 @@ end
|
|
120
120
|
|
121
121
|
## Motivation
|
122
122
|
|
123
|
-
The main reason for
|
123
|
+
The main reason for Pallets' existence was the need of a fast, simple and reliable workflow engine, one that is easily extensible with various backends and serializer, one that does not lose your data and one that is intelligent enough to concurrently schedule a workflow's tasks.
|
124
124
|
|
125
125
|
## Status
|
126
126
|
|
data/examples/config_savvy.rb
CHANGED
data/lib/pallets.rb
CHANGED
@@ -38,6 +38,7 @@ module Pallets
|
|
38
38
|
cls.new(
|
39
39
|
blocking_timeout: configuration.blocking_timeout,
|
40
40
|
failed_job_lifespan: configuration.failed_job_lifespan,
|
41
|
+
failed_job_max_count: configuration.failed_job_max_count,
|
41
42
|
job_timeout: configuration.job_timeout,
|
42
43
|
pool_size: configuration.pool_size,
|
43
44
|
**configuration.backend_args
|
@@ -62,8 +63,4 @@ module Pallets
|
|
62
63
|
formatter: Pallets::Logger::Formatters::Pretty.new
|
63
64
|
)
|
64
65
|
end
|
65
|
-
|
66
|
-
def self.logger=(logger)
|
67
|
-
@logger = logger
|
68
|
-
end
|
69
66
|
end
|
@@ -6,12 +6,12 @@ module Pallets
|
|
6
6
|
raise NotImplementedError
|
7
7
|
end
|
8
8
|
|
9
|
-
def get_context(
|
9
|
+
def get_context(wfid)
|
10
10
|
raise NotImplementedError
|
11
11
|
end
|
12
12
|
|
13
13
|
# Saves a job after successfully processing it
|
14
|
-
def save(
|
14
|
+
def save(wfid, jid, job, context_buffer)
|
15
15
|
raise NotImplementedError
|
16
16
|
end
|
17
17
|
|
@@ -20,8 +20,13 @@ module Pallets
|
|
20
20
|
raise NotImplementedError
|
21
21
|
end
|
22
22
|
|
23
|
+
# Discards malformed job
|
24
|
+
def discard(job)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
23
28
|
# Gives up job after repeteadly failing to process it
|
24
|
-
def give_up(job, old_job)
|
29
|
+
def give_up(wfid, job, old_job)
|
25
30
|
raise NotImplementedError
|
26
31
|
end
|
27
32
|
|
@@ -29,7 +34,7 @@ module Pallets
|
|
29
34
|
raise NotImplementedError
|
30
35
|
end
|
31
36
|
|
32
|
-
def run_workflow(
|
37
|
+
def run_workflow(wfid, jobs, jobmasks, context)
|
33
38
|
raise NotImplementedError
|
34
39
|
end
|
35
40
|
end
|
@@ -9,12 +9,15 @@ module Pallets
|
|
9
9
|
RETRY_SET_KEY = 'retry-set'
|
10
10
|
GIVEN_UP_SET_KEY = 'given-up-set'
|
11
11
|
WORKFLOW_QUEUE_KEY = 'workflow-queue:%s'
|
12
|
+
JOBMASKS_KEY = 'jobmasks:%s'
|
13
|
+
JOBMASK_KEY = 'jobmask:%s'
|
12
14
|
CONTEXT_KEY = 'context:%s'
|
13
15
|
REMAINING_KEY = 'remaining:%s'
|
14
16
|
|
15
|
-
def initialize(blocking_timeout:, failed_job_lifespan:, job_timeout:, pool_size:, **options)
|
17
|
+
def initialize(blocking_timeout:, failed_job_lifespan:, failed_job_max_count:, job_timeout:, pool_size:, **options)
|
16
18
|
@blocking_timeout = blocking_timeout
|
17
19
|
@failed_job_lifespan = failed_job_lifespan
|
20
|
+
@failed_job_max_count = failed_job_max_count
|
18
21
|
@job_timeout = job_timeout
|
19
22
|
@pool = Pallets::Pool.new(pool_size) { ::Redis.new(options) }
|
20
23
|
|
@@ -41,11 +44,11 @@ module Pallets
|
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
|
-
def save(wfid, job, context_buffer)
|
47
|
+
def save(wfid, jid, job, context_buffer)
|
45
48
|
@pool.execute do |client|
|
46
49
|
client.evalsha(
|
47
50
|
@scripts['save'],
|
48
|
-
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid],
|
51
|
+
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid, JOBMASK_KEY % jid, JOBMASKS_KEY % wfid],
|
49
52
|
context_buffer.to_a << job
|
50
53
|
)
|
51
54
|
end
|
@@ -61,12 +64,22 @@ module Pallets
|
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
64
|
-
def
|
67
|
+
def discard(job)
|
65
68
|
@pool.execute do |client|
|
66
69
|
client.evalsha(
|
67
|
-
@scripts['
|
70
|
+
@scripts['discard'],
|
68
71
|
[GIVEN_UP_SET_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY],
|
69
|
-
[Time.now.to_f, job,
|
72
|
+
[Time.now.to_f, job, Time.now.to_f - @failed_job_lifespan, @failed_job_max_count]
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def give_up(wfid, job, old_job)
|
78
|
+
@pool.execute do |client|
|
79
|
+
client.evalsha(
|
80
|
+
@scripts['give_up'],
|
81
|
+
[GIVEN_UP_SET_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, JOBMASKS_KEY % wfid, WORKFLOW_QUEUE_KEY % wfid, REMAINING_KEY % wfid, CONTEXT_KEY % wfid],
|
82
|
+
[Time.now.to_f, job, old_job, Time.now.to_f - @failed_job_lifespan, @failed_job_max_count]
|
70
83
|
)
|
71
84
|
end
|
72
85
|
end
|
@@ -81,13 +94,15 @@ module Pallets
|
|
81
94
|
end
|
82
95
|
end
|
83
96
|
|
84
|
-
def run_workflow(wfid,
|
97
|
+
def run_workflow(wfid, jobs, jobmasks, context_buffer)
|
85
98
|
@pool.execute do |client|
|
86
99
|
client.multi do
|
100
|
+
jobmasks.each { |jid, jobmask| client.zadd(JOBMASK_KEY % jid, jobmask) }
|
101
|
+
client.sadd(JOBMASKS_KEY % wfid, jobmasks.map { |jid, _| JOBMASK_KEY % jid }) unless jobmasks.empty?
|
87
102
|
client.evalsha(
|
88
103
|
@scripts['run_workflow'],
|
89
104
|
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, REMAINING_KEY % wfid],
|
90
|
-
|
105
|
+
jobs
|
91
106
|
)
|
92
107
|
client.hmset(CONTEXT_KEY % wfid, *context_buffer.to_a) unless context_buffer.empty?
|
93
108
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
-- Remove job from reliability queue
|
2
|
+
redis.call("LREM", KEYS[2], 0, ARGV[2])
|
3
|
+
redis.call("ZREM", KEYS[3], ARGV[2])
|
4
|
+
|
5
|
+
-- Add job and its fail time (score) to failed sorted set
|
6
|
+
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
|
7
|
+
|
8
|
+
-- Remove any jobs that have been given up long enough ago (their score is
|
9
|
+
-- below given value) and make sure the number of jobs is capped
|
10
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[3])
|
11
|
+
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, -ARGV[4] - 1)
|
@@ -5,6 +5,11 @@ redis.call("ZREM", KEYS[3], ARGV[3])
|
|
5
5
|
-- Add job and its fail time (score) to failed sorted set
|
6
6
|
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
|
7
7
|
|
8
|
+
-- Remove all related workflow keys
|
9
|
+
local keys = redis.call("SMEMBERS", KEYS[4])
|
10
|
+
redis.call("DEL", KEYS[4], KEYS[5], KEYS[6], KEYS[7], unpack(keys))
|
11
|
+
|
8
12
|
-- Remove any jobs that have been given up long enough ago (their score is
|
9
|
-
-- below given value)
|
13
|
+
-- below given value) and make sure the number of jobs is capped
|
10
14
|
redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[4])
|
15
|
+
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, -ARGV[5] - 1)
|
@@ -6,9 +6,8 @@ redis.call("SET", KEYS[3], eta)
|
|
6
6
|
|
7
7
|
-- Queue jobs that are ready to be processed (their score is 0) and
|
8
8
|
-- remove queued jobs from the sorted set
|
9
|
-
local
|
10
|
-
if
|
11
|
-
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
9
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
10
|
+
if #work > 0 then
|
12
11
|
redis.call("LPUSH", KEYS[2], unpack(work))
|
13
12
|
redis.call("ZREM", KEYS[1], unpack(work))
|
14
13
|
end
|
@@ -10,24 +10,21 @@ if #ARGV > 0 then
|
|
10
10
|
redis.call("HMSET", KEYS[5], unpack(ARGV))
|
11
11
|
end
|
12
12
|
|
13
|
-
-- Decrement
|
14
|
-
|
15
|
-
|
16
|
-
redis.call("ZINCRBY", KEYS[1], -1, task)
|
17
|
-
end
|
13
|
+
-- Decrement jobs from the sorted set by applying a jobmask
|
14
|
+
redis.call("ZUNIONSTORE", KEYS[1], 2, KEYS[1], KEYS[7])
|
15
|
+
redis.call("DEL", KEYS[7])
|
18
16
|
|
19
17
|
-- Queue jobs that are ready to be processed (their score is 0) and
|
20
18
|
-- remove queued jobs from sorted set
|
21
|
-
local
|
22
|
-
if
|
23
|
-
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
19
|
+
local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
|
20
|
+
if #work > 0 then
|
24
21
|
redis.call("LPUSH", KEYS[2], unpack(work))
|
25
22
|
redis.call("ZREM", KEYS[1], unpack(work))
|
26
23
|
end
|
27
24
|
|
28
|
-
-- Decrement ETA and remove it together with the context if all
|
29
|
-
-- been processed (ETA is 0)
|
30
|
-
redis.call("DECR", KEYS[6])
|
31
|
-
if
|
32
|
-
redis.call("DEL", KEYS[5], KEYS[6])
|
25
|
+
-- Decrement ETA and remove it together with the context and jobmasks if all
|
26
|
+
-- tasks have been processed (ETA is 0) or if workflow has been given up (ETA is -1)
|
27
|
+
local remaining = redis.call("DECR", KEYS[6])
|
28
|
+
if remaining <= 0 then
|
29
|
+
redis.call("DEL", KEYS[5], KEYS[6], KEYS[8])
|
33
30
|
end
|
data/lib/pallets/cli.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
|
+
$stdout.sync = true
|
4
|
+
|
3
5
|
module Pallets
|
4
6
|
class CLI
|
5
7
|
def initialize
|
@@ -61,10 +63,18 @@ module Pallets
|
|
61
63
|
Pallets.configuration.max_failures = max_failures
|
62
64
|
end
|
63
65
|
|
66
|
+
opts.on('-i', '--scheduler-polling-interval NUM', Integer, 'Seconds between scheduler backend polls') do |scheduler_polling_interval|
|
67
|
+
Pallets.configuration.scheduler_polling_interval = scheduler_polling_interval
|
68
|
+
end
|
69
|
+
|
64
70
|
opts.on('-l', '--failed-job-lifespan NUM', Integer, 'Seconds a job stays in the given up set') do |failed_job_lifespan|
|
65
71
|
Pallets.configuration.failed_job_lifespan = failed_job_lifespan
|
66
72
|
end
|
67
73
|
|
74
|
+
opts.on('-m', '--failed-job-max-count NUM', Integer, 'Maximum number of jobs in the given up set') do |failed_job_max_count|
|
75
|
+
Pallets.configuration.failed_job_max_count = failed_job_max_count
|
76
|
+
end
|
77
|
+
|
68
78
|
opts.on('-p', '--pool-size NUM', Integer, 'Size of backend pool') do |pool_size|
|
69
79
|
Pallets.configuration.pool_size = pool_size
|
70
80
|
end
|
@@ -16,6 +16,10 @@ module Pallets
|
|
16
16
|
# this period, jobs will be permanently deleted
|
17
17
|
attr_accessor :failed_job_lifespan
|
18
18
|
|
19
|
+
# Maximum number of failed jobs that can be in the given up set. When this
|
20
|
+
# number is reached, the oldest jobs will be permanently deleted
|
21
|
+
attr_accessor :failed_job_max_count
|
22
|
+
|
19
23
|
# Number of seconds allowed for a job to be processed. If a job exceeds this
|
20
24
|
# period, it is considered failed, and scheduled to be processed again
|
21
25
|
attr_accessor :job_timeout
|
@@ -27,6 +31,12 @@ module Pallets
|
|
27
31
|
# Number of connections to the backend
|
28
32
|
attr_writer :pool_size
|
29
33
|
|
34
|
+
# Number of seconds at which the scheduler checks whether there are jobs
|
35
|
+
# due to be (re)processed. Note that this interval is per process; it might
|
36
|
+
# require tweaking in case of running multiple Pallets instances, so that
|
37
|
+
# the backend is not polled too often
|
38
|
+
attr_accessor :scheduler_polling_interval
|
39
|
+
|
30
40
|
# Serializer used for jobs
|
31
41
|
attr_accessor :serializer
|
32
42
|
|
@@ -45,8 +55,10 @@ module Pallets
|
|
45
55
|
@blocking_timeout = 5
|
46
56
|
@concurrency = 2
|
47
57
|
@failed_job_lifespan = 7_776_000 # 3 months
|
58
|
+
@failed_job_max_count = 1_000
|
48
59
|
@job_timeout = 1_800 # 30 minutes
|
49
60
|
@max_failures = 3
|
61
|
+
@scheduler_polling_interval = 10
|
50
62
|
@serializer = :json
|
51
63
|
@middleware = default_middleware
|
52
64
|
end
|
data/lib/pallets/dsl/workflow.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
module Pallets
|
2
2
|
module DSL
|
3
3
|
module Workflow
|
4
|
-
def task(arg, as: nil, depends_on: nil, max_failures: nil,
|
4
|
+
def task(arg=nil, as: nil, depends_on: nil, max_failures: nil, **kwargs)
|
5
|
+
# Have to work more to keep Pallets' nice DSL valid in Ruby 2.7
|
6
|
+
arg = !kwargs.empty? ? kwargs : arg
|
7
|
+
raise ArgumentError, 'Task is incorrectly defined. It must receive '\
|
8
|
+
'either a name, or a name => dependencies pair as '\
|
9
|
+
'the first argument' unless arg
|
10
|
+
|
5
11
|
klass, dependencies = case arg
|
6
12
|
when Hash
|
7
13
|
# The `task Foo => Bar` notation
|
data/lib/pallets/graph.rb
CHANGED
@@ -24,18 +24,11 @@ module Pallets
|
|
24
24
|
nodes.empty?
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# Assign order to each node
|
34
|
-
i = 0
|
35
|
-
groups.flat_map do |group|
|
36
|
-
group_with_order = group.product([i])
|
37
|
-
i += group.size
|
38
|
-
group_with_order
|
27
|
+
def each
|
28
|
+
return enum_for(__method__) unless block_given?
|
29
|
+
|
30
|
+
tsort_each do |node|
|
31
|
+
yield(node, parents(node))
|
39
32
|
end
|
40
33
|
end
|
41
34
|
|
data/lib/pallets/logger.rb
CHANGED
@@ -5,18 +5,37 @@ module Pallets
|
|
5
5
|
class Logger < ::Logger
|
6
6
|
# Overwrite severity methods to add metadata capabilities
|
7
7
|
%i[debug info warn error fatal unknown].each do |severity|
|
8
|
-
define_method severity do |message
|
9
|
-
|
8
|
+
define_method severity do |message|
|
9
|
+
metadata = Thread.current[:pallets_log_metadata]
|
10
|
+
return super(message) if metadata.nil?
|
10
11
|
|
11
|
-
|
12
|
-
super(formatted_metadata) { message }
|
12
|
+
super(metadata) { message }
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
def with_metadata(hash)
|
17
|
+
Thread.current[:pallets_log_metadata] = hash
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
Thread.current[:pallets_log_metadata] = nil
|
21
|
+
end
|
22
|
+
|
16
23
|
module Formatters
|
17
24
|
class Pretty < ::Logger::Formatter
|
18
25
|
def call(severity, time, metadata, message)
|
19
|
-
"#{
|
26
|
+
formatted_metadata = ' ' + metadata.map { |k, v| "#{k}=#{v}" }.join(' ') if metadata
|
27
|
+
"#{time.utc.iso8601(4)} pid=#{Process.pid}#{formatted_metadata} #{severity}: #{message}\n"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Json < ::Logger::Formatter
|
32
|
+
def call(severity, time, metadata, message)
|
33
|
+
{
|
34
|
+
timestamp: time.utc.iso8601(4),
|
35
|
+
pid: Process.pid,
|
36
|
+
severity: severity,
|
37
|
+
message: message
|
38
|
+
}.merge(metadata || {}).to_json + "\n"
|
20
39
|
end
|
21
40
|
end
|
22
41
|
end
|
data/lib/pallets/manager.rb
CHANGED
@@ -3,8 +3,9 @@ module Pallets
|
|
3
3
|
attr_reader :workers, :scheduler
|
4
4
|
|
5
5
|
def initialize(concurrency: Pallets.configuration.concurrency)
|
6
|
-
@
|
7
|
-
@
|
6
|
+
@backend = Pallets.backend
|
7
|
+
@workers = concurrency.times.map { Worker.new(self, @backend) }
|
8
|
+
@scheduler = Scheduler.new(self, @backend)
|
8
9
|
@lock = Mutex.new
|
9
10
|
@needs_to_stop = false
|
10
11
|
end
|
@@ -48,7 +49,7 @@ module Pallets
|
|
48
49
|
|
49
50
|
return if @needs_to_stop
|
50
51
|
|
51
|
-
worker = Worker.new(self)
|
52
|
+
worker = Worker.new(self, @backend)
|
52
53
|
@workers << worker
|
53
54
|
worker.start
|
54
55
|
end
|
@@ -2,14 +2,21 @@ module Pallets
|
|
2
2
|
module Middleware
|
3
3
|
class JobLogger
|
4
4
|
def self.call(worker, job, context)
|
5
|
-
|
6
|
-
|
7
|
-
Pallets.logger.
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
start_time = current_time
|
6
|
+
|
7
|
+
Pallets.logger.with_metadata(extract_metadata(worker.id, job)) do
|
8
|
+
begin
|
9
|
+
Pallets.logger.info 'Started'
|
10
|
+
result = yield
|
11
|
+
Pallets.logger.info "Done in #{(current_time - start_time).round(3)}s"
|
12
|
+
result
|
13
|
+
rescue => ex
|
14
|
+
Pallets.logger.warn "Failed after #{(current_time - start_time).round(3)}s"
|
15
|
+
Pallets.logger.warn "#{ex.class.name}: #{ex.message}"
|
16
|
+
Pallets.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
end
|
13
20
|
end
|
14
21
|
|
15
22
|
def self.extract_metadata(wid, job)
|
@@ -21,6 +28,10 @@ module Pallets
|
|
21
28
|
tsk: job['task_class'],
|
22
29
|
}
|
23
30
|
end
|
31
|
+
|
32
|
+
def self.current_time
|
33
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
34
|
+
end
|
24
35
|
end
|
25
36
|
end
|
26
37
|
end
|
data/lib/pallets/scheduler.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
module Pallets
|
2
2
|
class Scheduler
|
3
|
-
def initialize(manager)
|
3
|
+
def initialize(manager, backend)
|
4
4
|
@manager = manager
|
5
|
+
@backend = backend
|
5
6
|
@needs_to_stop = false
|
6
7
|
@thread = nil
|
7
8
|
end
|
8
9
|
|
9
10
|
def start
|
10
|
-
@thread ||= Thread.new { work }
|
11
|
+
@thread ||= Thread.new { wait_initial_bit; work }
|
11
12
|
end
|
12
13
|
|
13
14
|
def shutdown
|
@@ -35,23 +36,25 @@ module Pallets
|
|
35
36
|
loop do
|
36
37
|
break if needs_to_stop?
|
37
38
|
|
38
|
-
backend.reschedule_all(Time.now.to_f)
|
39
|
+
@backend.reschedule_all(Time.now.to_f)
|
39
40
|
wait_a_bit
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
43
|
-
def
|
44
|
-
#
|
44
|
+
def wait_initial_bit
|
45
|
+
# Randomly wait a bit before starting working, so that multiple processes
|
46
|
+
# will not hit the backend all at once
|
47
|
+
wait_a_bit(rand(Pallets.configuration.scheduler_polling_interval))
|
48
|
+
end
|
49
|
+
|
50
|
+
def wait_a_bit(seconds = Pallets.configuration.scheduler_polling_interval)
|
51
|
+
# Wait for roughly the configured number of seconds
|
45
52
|
# We don't want to block the entire polling interval, since we want to
|
46
53
|
# deal with shutdowns synchronously and as fast as possible
|
47
|
-
|
54
|
+
seconds.times do
|
48
55
|
break if needs_to_stop?
|
49
56
|
sleep 1
|
50
57
|
end
|
51
58
|
end
|
52
|
-
|
53
|
-
def backend
|
54
|
-
@backend ||= Pallets.backend
|
55
|
-
end
|
56
59
|
end
|
57
60
|
end
|
data/lib/pallets/version.rb
CHANGED
data/lib/pallets/worker.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
module Pallets
|
2
2
|
class Worker
|
3
|
-
|
4
|
-
|
5
|
-
def initialize(manager)
|
3
|
+
def initialize(manager, backend)
|
6
4
|
@manager = manager
|
5
|
+
@backend = backend
|
7
6
|
@current_job = nil
|
8
7
|
@needs_to_stop = false
|
9
8
|
@thread = nil
|
@@ -40,7 +39,7 @@ module Pallets
|
|
40
39
|
loop do
|
41
40
|
break if needs_to_stop?
|
42
41
|
|
43
|
-
@current_job = backend.pick
|
42
|
+
@current_job = @backend.pick
|
44
43
|
# No need to requeue because of the reliability queue
|
45
44
|
break if needs_to_stop?
|
46
45
|
next if @current_job.nil?
|
@@ -53,8 +52,10 @@ module Pallets
|
|
53
52
|
rescue Pallets::Shutdown
|
54
53
|
@manager.remove_worker(self)
|
55
54
|
rescue => ex
|
56
|
-
Pallets.logger.error "#{ex.class.name}: #{ex.message}"
|
57
|
-
Pallets.logger.error ex.backtrace.join("\n")
|
55
|
+
Pallets.logger.error "#{ex.class.name}: #{ex.message}"
|
56
|
+
Pallets.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
|
57
|
+
# Do not flood the process in case of persisting unforeseen errors
|
58
|
+
sleep 1
|
58
59
|
@manager.replace_worker(self)
|
59
60
|
end
|
60
61
|
|
@@ -64,13 +65,13 @@ module Pallets
|
|
64
65
|
rescue
|
65
66
|
# We ensure only valid jobs are created. If something fishy reaches this
|
66
67
|
# point, just give up on it
|
67
|
-
backend.
|
68
|
-
Pallets.logger.error "Could not deserialize #{job}. Gave up job"
|
68
|
+
@backend.discard(job)
|
69
|
+
Pallets.logger.error "Could not deserialize #{job}. Gave up job"
|
69
70
|
return
|
70
71
|
end
|
71
72
|
|
72
73
|
context = Context[
|
73
|
-
serializer.load_context(backend.get_context(job_hash['wfid']))
|
74
|
+
serializer.load_context(@backend.get_context(job_hash['wfid']))
|
74
75
|
]
|
75
76
|
|
76
77
|
task_class = Pallets::Util.constantize(job_hash["task_class"])
|
@@ -101,9 +102,9 @@ module Pallets
|
|
101
102
|
))
|
102
103
|
if failures < job_hash['max_failures']
|
103
104
|
retry_at = Time.now.to_f + backoff_in_seconds(failures)
|
104
|
-
backend.retry(new_job, job, retry_at)
|
105
|
+
@backend.retry(new_job, job, retry_at)
|
105
106
|
else
|
106
|
-
backend.give_up(new_job, job)
|
107
|
+
@backend.give_up(job_hash['wfid'], new_job, job)
|
107
108
|
end
|
108
109
|
end
|
109
110
|
|
@@ -112,21 +113,17 @@ module Pallets
|
|
112
113
|
'given_up_at' => Time.now.to_f,
|
113
114
|
'reason' => 'returned_false'
|
114
115
|
))
|
115
|
-
backend.give_up(new_job, job)
|
116
|
+
@backend.give_up(job_hash['wfid'], new_job, job)
|
116
117
|
end
|
117
118
|
|
118
119
|
def handle_job_success(context, job, job_hash)
|
119
|
-
backend.save(job_hash['wfid'], job, serializer.dump_context(context.buffer))
|
120
|
+
@backend.save(job_hash['wfid'], job_hash['jid'], job, serializer.dump_context(context.buffer))
|
120
121
|
end
|
121
122
|
|
122
123
|
def backoff_in_seconds(count)
|
123
124
|
count ** 4 + rand(6..10)
|
124
125
|
end
|
125
126
|
|
126
|
-
def backend
|
127
|
-
@backend ||= Pallets.backend
|
128
|
-
end
|
129
|
-
|
130
127
|
def serializer
|
131
128
|
@serializer ||= Pallets.serializer
|
132
129
|
end
|
data/lib/pallets/workflow.rb
CHANGED
@@ -20,7 +20,7 @@ module Pallets
|
|
20
20
|
raise WorkflowError, "#{self.class.name} has no tasks. Workflows "\
|
21
21
|
"must contain at least one task" if self.class.graph.empty?
|
22
22
|
|
23
|
-
backend.run_workflow(id,
|
23
|
+
backend.run_workflow(id, *prepare_jobs, serializer.dump_context(context.buffer))
|
24
24
|
id
|
25
25
|
end
|
26
26
|
|
@@ -30,11 +30,21 @@ module Pallets
|
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
def prepare_jobs
|
34
|
+
jobs = []
|
35
|
+
jobmasks = Hash.new { |h, k| h[k] = [] }
|
36
|
+
acc = {}
|
37
|
+
|
38
|
+
self.class.graph.each do |task_alias, dependencies|
|
39
|
+
job_hash = construct_job(task_alias)
|
40
|
+
acc[task_alias] = job_hash['jid']
|
41
|
+
job = serializer.dump(job_hash)
|
42
|
+
|
43
|
+
jobs << [dependencies.size, job]
|
44
|
+
dependencies.each { |d| jobmasks[acc[d]] << [-1, job] }
|
37
45
|
end
|
46
|
+
|
47
|
+
[jobs, jobmasks]
|
38
48
|
end
|
39
49
|
|
40
50
|
def construct_job(task_alias)
|
data/pallets.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['Andrei Horak']
|
10
10
|
spec.email = ['linkyndy@gmail.com']
|
11
11
|
|
12
|
-
spec.summary = '
|
13
|
-
spec.description = '
|
12
|
+
spec.summary = 'Simple and reliable workflow engine, written in Ruby'
|
13
|
+
spec.description = 'Simple and reliable workflow engine, written in Ruby'
|
14
14
|
spec.homepage = 'https://github.com/linkyndy/pallets'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
|
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.required_ruby_version = '>= 2.4'
|
22
22
|
|
23
|
-
spec.add_dependency 'redis'
|
23
|
+
spec.add_dependency 'redis', '~> 4.2'
|
24
24
|
spec.add_dependency 'msgpack'
|
25
25
|
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pallets
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrei Horak
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: msgpack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
description:
|
41
|
+
description: Simple and reliable workflow engine, written in Ruby
|
42
42
|
email:
|
43
43
|
- linkyndy@gmail.com
|
44
44
|
executables:
|
@@ -46,6 +46,7 @@ executables:
|
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
|
+
- ".github/FUNDING.yml"
|
49
50
|
- ".gitignore"
|
50
51
|
- ".rspec"
|
51
52
|
- ".travis.yml"
|
@@ -66,6 +67,7 @@ files:
|
|
66
67
|
- lib/pallets.rb
|
67
68
|
- lib/pallets/backends/base.rb
|
68
69
|
- lib/pallets/backends/redis.rb
|
70
|
+
- lib/pallets/backends/scripts/discard.lua
|
69
71
|
- lib/pallets/backends/scripts/give_up.lua
|
70
72
|
- lib/pallets/backends/scripts/reschedule_all.lua
|
71
73
|
- lib/pallets/backends/scripts/retry.lua
|
@@ -97,7 +99,7 @@ homepage: https://github.com/linkyndy/pallets
|
|
97
99
|
licenses:
|
98
100
|
- MIT
|
99
101
|
metadata: {}
|
100
|
-
post_install_message:
|
102
|
+
post_install_message:
|
101
103
|
rdoc_options: []
|
102
104
|
require_paths:
|
103
105
|
- lib
|
@@ -112,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
114
|
- !ruby/object:Gem::Version
|
113
115
|
version: '0'
|
114
116
|
requirements: []
|
115
|
-
rubygems_version: 3.
|
116
|
-
signing_key:
|
117
|
+
rubygems_version: 3.1.2
|
118
|
+
signing_key:
|
117
119
|
specification_version: 4
|
118
|
-
summary:
|
120
|
+
summary: Simple and reliable workflow engine, written in Ruby
|
119
121
|
test_files: []
|