pallets 0.4.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/FUNDING.yml +1 -0
- data/.travis.yml +4 -8
- data/CHANGELOG.md +34 -1
- data/Gemfile +1 -0
- data/examples/aliases.rb +28 -0
- data/examples/anonymous.rb +13 -0
- data/examples/appsignal.rb +32 -0
- data/examples/config/appsignal.yml +12 -0
- data/examples/config_savvy.rb +14 -0
- data/lib/pallets.rb +6 -11
- data/lib/pallets/backends/base.rb +3 -3
- data/lib/pallets/backends/redis.rb +6 -4
- data/lib/pallets/backends/scripts/run_workflow.lua +2 -3
- data/lib/pallets/backends/scripts/save.lua +7 -10
- data/lib/pallets/cli.rb +2 -0
- data/lib/pallets/configuration.rb +26 -0
- data/lib/pallets/dsl/workflow.rb +12 -3
- data/lib/pallets/graph.rb +16 -17
- data/lib/pallets/middleware/appsignal_instrumenter.rb +47 -0
- data/lib/pallets/middleware/job_logger.rb +26 -0
- data/lib/pallets/middleware/stack.rb +13 -0
- data/lib/pallets/serializers/json.rb +2 -3
- data/lib/pallets/version.rb +1 -1
- data/lib/pallets/worker.rb +8 -19
- data/lib/pallets/workflow.rb +29 -10
- data/pallets.gemspec +1 -1
- metadata +12 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 46a631b6e48f2f1c4efe5bce09c5d74fa2cb064a78d5e546492c6c8ce1dec843
|
4
|
+
data.tar.gz: 61f6414e16941de41defb30ac8dc9121703f75251e47422239c311a8b7749787
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b6f77bd93576dc7dc2c5cf945d52c425a63178740c12fa958f0c18c4ca870da7b02d303c7b8e871504d84a13461f79edc52da2869069322f5a81943bdbd79df
|
7
|
+
data.tar.gz: a2a71f4a2343927a94871523b7475f587bb64181e9c6a4654a40111453e2fc2f20e82a02da44adbc32865f68c53a95dd3a9c6f0b641cfd85c7939f1e06d3521f
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
github: linkyndy
|
data/.travis.yml
CHANGED
@@ -4,12 +4,8 @@ services:
|
|
4
4
|
- redis-server
|
5
5
|
cache: bundler
|
6
6
|
rvm:
|
7
|
-
- 2.
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.
|
11
|
-
before_install:
|
12
|
-
# Bundler 2.0 needs a newer RubyGems
|
13
|
-
- gem update --system
|
14
|
-
- gem install bundler
|
7
|
+
- 2.4.10
|
8
|
+
- 2.5.8
|
9
|
+
- 2.6.6
|
10
|
+
- 2.7.1
|
15
11
|
script: bundle exec rspec
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.8.0] - 2020-06-09
|
10
|
+
### Added
|
11
|
+
- sync output in CLI (#49)
|
12
|
+
- support for configuring custom loggers (#50)
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
- improve job scheduling using jobmasks (#52)
|
16
|
+
|
17
|
+
## [0.7.0] - 2020-01-19
|
18
|
+
### Added
|
19
|
+
- support for Ruby 2.7 (#46)
|
20
|
+
|
21
|
+
## [0.6.0] - 2019-09-02
|
22
|
+
### Added
|
23
|
+
- define task aliases in order to reuse tasks within a workflow definition (#44)
|
24
|
+
- define anonymous workflows (#45)
|
25
|
+
|
26
|
+
## [0.5.1] - 2019-06-01
|
27
|
+
### Changed
|
28
|
+
- fix transaction completeness in Appsignal instrumenter (#43)
|
29
|
+
|
30
|
+
## [0.5.0] - 2019-05-12
|
31
|
+
### Added
|
32
|
+
- wrap job execution with middleware (#38)
|
33
|
+
- use `Middleware::JobLogger` for job logging (#39)
|
34
|
+
- allow Appsignal instrumentation using `Middleware::AppsignalInstrumenter` (#40)
|
35
|
+
|
36
|
+
### Removed
|
37
|
+
- support for Ruby 2.3 (#41)
|
38
|
+
|
9
39
|
## [0.4.0] - 2019-04-07
|
10
40
|
### Added
|
11
41
|
- give up workflow before it finishes by returning `false` in any of its tasks (#25)
|
@@ -47,6 +77,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
47
77
|
## 0.1.0 - 2018-09-29
|
48
78
|
- Pallets' inception <3
|
49
79
|
|
50
|
-
[Unreleased]: https://github.com/linkyndy/pallets/compare/compare/v0.
|
80
|
+
[Unreleased]: https://github.com/linkyndy/pallets/compare/compare/v0.5.1...HEAD
|
81
|
+
[0.5.1]: https://github.com/linkyndy/pallets/compare/v0.5.0...v0.5.1
|
82
|
+
[0.5.0]: https://github.com/linkyndy/pallets/compare/v0.4.0...v0.5.0
|
83
|
+
[0.4.0]: https://github.com/linkyndy/pallets/compare/v0.3.0...v0.5.0
|
51
84
|
[0.3.0]: https://github.com/linkyndy/pallets/compare/v0.2.0...v0.3.0
|
52
85
|
[0.2.0]: https://github.com/linkyndy/pallets/compare/v0.1.0...v0.2.0
|
data/Gemfile
CHANGED
data/examples/aliases.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pallets'
|
2
|
+
|
3
|
+
class Aliases < Pallets::Workflow
|
4
|
+
task 'StartSmtpServer'
|
5
|
+
task 'SendEmail', as: 'SayHello', depends_on: 'StartSmtpServer'
|
6
|
+
task 'SendEmail', as: 'SayGoodbye', depends_on: 'StartSmtpServer'
|
7
|
+
task 'StopSmtpServer' => ['SayHello', 'SayGoodbye']
|
8
|
+
end
|
9
|
+
|
10
|
+
class StartSmtpServer < Pallets::Task
|
11
|
+
def run
|
12
|
+
puts "Starting SMTP server..."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class SendEmail < Pallets::Task
|
17
|
+
def run
|
18
|
+
puts "* sending e-mail"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class StopSmtpServer < Pallets::Task
|
23
|
+
def run
|
24
|
+
puts "Stopped SMTP server"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Aliases.new.run
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'pallets'
|
2
|
+
require 'pallets/middleware/appsignal_instrumenter'
|
3
|
+
|
4
|
+
Appsignal.config = Appsignal::Config.new(
|
5
|
+
File.expand_path(File.dirname(__FILE__)),
|
6
|
+
"development"
|
7
|
+
)
|
8
|
+
Appsignal.start
|
9
|
+
Appsignal.start_logger
|
10
|
+
|
11
|
+
Pallets.configure do |c|
|
12
|
+
c.middleware << Pallets::Middleware::AppsignalInstrumenter
|
13
|
+
end
|
14
|
+
|
15
|
+
class Appsignaling < Pallets::Workflow
|
16
|
+
task 'Signaling'
|
17
|
+
task 'ReturningSignal' => 'Signaling'
|
18
|
+
end
|
19
|
+
|
20
|
+
class Signaling < Pallets::Task
|
21
|
+
def run
|
22
|
+
puts context['signal']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ReturningSignal < Pallets::Task
|
27
|
+
def run
|
28
|
+
puts 'Ho!'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Appsignaling.new(signal: 'Hey').run
|
data/examples/config_savvy.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'pallets'
|
2
3
|
|
4
|
+
class AnnounceProcessing
|
5
|
+
def self.call(worker, job, context)
|
6
|
+
puts "Starting to process job..."
|
7
|
+
yield
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
3
11
|
Pallets.configure do |c|
|
4
12
|
# Harness 4 Pallets workers per process
|
5
13
|
c.concurrency = 4
|
@@ -23,6 +31,12 @@ Pallets.configure do |c|
|
|
23
31
|
# Jobs will be retried up to 5 times upon failure. After that, they will be
|
24
32
|
# given up. Retry times are exponential and happen after: 7, 22, 87, 262, ...
|
25
33
|
c.max_failures = 5
|
34
|
+
|
35
|
+
# Custom loggers can be used too
|
36
|
+
c.logger = Logger.new(STDOUT)
|
37
|
+
# Job execution can be wrapped with middleware to provide custom logic.
|
38
|
+
# Anything that responds to `call` would do
|
39
|
+
c.middleware << AnnounceProcessing
|
26
40
|
end
|
27
41
|
|
28
42
|
class ConfigSavvy < Pallets::Workflow
|
data/lib/pallets.rb
CHANGED
@@ -9,6 +9,8 @@ require 'pallets/errors'
|
|
9
9
|
require 'pallets/graph'
|
10
10
|
require 'pallets/logger'
|
11
11
|
require 'pallets/manager'
|
12
|
+
require 'pallets/middleware/job_logger'
|
13
|
+
require 'pallets/middleware/stack'
|
12
14
|
require 'pallets/pool'
|
13
15
|
require 'pallets/scheduler'
|
14
16
|
require 'pallets/serializers/base'
|
@@ -50,18 +52,11 @@ module Pallets
|
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
53
|
-
def self.
|
54
|
-
@
|
55
|
-
logger = Pallets::Logger.new(STDOUT)
|
56
|
-
# TODO: Ruby 2.4 supports Logger initialization with the arguments below, so
|
57
|
-
# we can drop this after we drop support for Ruby 2.3
|
58
|
-
logger.level = Pallets::Logger::INFO
|
59
|
-
logger.formatter = Pallets::Logger::Formatters::Pretty.new
|
60
|
-
logger
|
61
|
-
end
|
55
|
+
def self.middleware
|
56
|
+
@middleware ||= configuration.middleware
|
62
57
|
end
|
63
58
|
|
64
|
-
def self.logger
|
65
|
-
@logger
|
59
|
+
def self.logger
|
60
|
+
@logger ||= configuration.logger
|
66
61
|
end
|
67
62
|
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
|
|
@@ -29,7 +29,7 @@ module Pallets
|
|
29
29
|
raise NotImplementedError
|
30
30
|
end
|
31
31
|
|
32
|
-
def run_workflow(
|
32
|
+
def run_workflow(wfid, jobs, jobmasks, context)
|
33
33
|
raise NotImplementedError
|
34
34
|
end
|
35
35
|
end
|
@@ -9,6 +9,7 @@ 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
|
+
JOBMASK_KEY = 'jobmask:%s'
|
12
13
|
CONTEXT_KEY = 'context:%s'
|
13
14
|
REMAINING_KEY = 'remaining:%s'
|
14
15
|
|
@@ -41,11 +42,11 @@ module Pallets
|
|
41
42
|
end
|
42
43
|
end
|
43
44
|
|
44
|
-
def save(wfid, job, context_buffer)
|
45
|
+
def save(wfid, jid, job, context_buffer)
|
45
46
|
@pool.execute do |client|
|
46
47
|
client.evalsha(
|
47
48
|
@scripts['save'],
|
48
|
-
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid],
|
49
|
+
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid, JOBMASK_KEY % jid],
|
49
50
|
context_buffer.to_a << job
|
50
51
|
)
|
51
52
|
end
|
@@ -81,13 +82,14 @@ module Pallets
|
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|
84
|
-
def run_workflow(wfid,
|
85
|
+
def run_workflow(wfid, jobs, jobmasks, context_buffer)
|
85
86
|
@pool.execute do |client|
|
86
87
|
client.multi do
|
88
|
+
jobmasks.each { |jid, jobmask| client.zadd(JOBMASK_KEY % jid, jobmask) }
|
87
89
|
client.evalsha(
|
88
90
|
@scripts['run_workflow'],
|
89
91
|
[WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, REMAINING_KEY % wfid],
|
90
|
-
|
92
|
+
jobs
|
91
93
|
)
|
92
94
|
client.hmset(CONTEXT_KEY % wfid, *context_buffer.to_a) unless context_buffer.empty?
|
93
95
|
end
|
@@ -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
25
|
-- Decrement ETA and remove it together with the context if all tasks have
|
29
26
|
-- been processed (ETA is 0)
|
30
|
-
redis.call("DECR", KEYS[6])
|
31
|
-
if
|
27
|
+
local remaining = redis.call("DECR", KEYS[6])
|
28
|
+
if remaining == 0 then
|
32
29
|
redis.call("DEL", KEYS[5], KEYS[6])
|
33
30
|
end
|
data/lib/pallets/cli.rb
CHANGED
@@ -20,6 +20,9 @@ module Pallets
|
|
20
20
|
# period, it is considered failed, and scheduled to be processed again
|
21
21
|
attr_accessor :job_timeout
|
22
22
|
|
23
|
+
# Custom logger used throughout Pallets
|
24
|
+
attr_writer :logger
|
25
|
+
|
23
26
|
# Maximum number of failures allowed per job. Can also be configured on a
|
24
27
|
# per task basis
|
25
28
|
attr_accessor :max_failures
|
@@ -30,6 +33,15 @@ module Pallets
|
|
30
33
|
# Serializer used for jobs
|
31
34
|
attr_accessor :serializer
|
32
35
|
|
36
|
+
# Middleware used to wrap job execution with custom logic. Acts like a stack
|
37
|
+
# and accepts callable objects (lambdas, procs, objects that respond to call)
|
38
|
+
# that take three arguments: the worker handling the job, the job hash and
|
39
|
+
# the context
|
40
|
+
#
|
41
|
+
# A minimal example of a middleware is:
|
42
|
+
# ->(worker, job, context, &b) { puts 'Hello World!'; b.call }
|
43
|
+
attr_reader :middleware
|
44
|
+
|
33
45
|
def initialize
|
34
46
|
@backend = :redis
|
35
47
|
@backend_args = {}
|
@@ -39,10 +51,24 @@ module Pallets
|
|
39
51
|
@job_timeout = 1_800 # 30 minutes
|
40
52
|
@max_failures = 3
|
41
53
|
@serializer = :json
|
54
|
+
@middleware = default_middleware
|
55
|
+
end
|
56
|
+
|
57
|
+
def logger
|
58
|
+
@logger || Pallets::Logger.new(STDOUT,
|
59
|
+
level: Pallets::Logger::INFO,
|
60
|
+
formatter: Pallets::Logger::Formatters::Pretty.new
|
61
|
+
)
|
42
62
|
end
|
43
63
|
|
44
64
|
def pool_size
|
45
65
|
@pool_size || @concurrency + 1
|
46
66
|
end
|
67
|
+
|
68
|
+
def default_middleware
|
69
|
+
Middleware::Stack[
|
70
|
+
Middleware::JobLogger
|
71
|
+
]
|
72
|
+
end
|
47
73
|
end
|
48
74
|
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, 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
|
@@ -12,10 +18,13 @@ module Pallets
|
|
12
18
|
end
|
13
19
|
|
14
20
|
task_class = klass.to_s
|
21
|
+
as ||= task_class
|
22
|
+
|
15
23
|
dependencies = Array(dependencies).compact.uniq.map(&:to_s)
|
16
|
-
graph.add(
|
24
|
+
graph.add(as, dependencies)
|
17
25
|
|
18
|
-
task_config[
|
26
|
+
task_config[as] = {
|
27
|
+
'workflow_class' => self.name,
|
19
28
|
'task_class' => task_class,
|
20
29
|
'max_failures' => max_failures || Pallets.configuration.max_failures
|
21
30
|
}
|
data/lib/pallets/graph.rb
CHANGED
@@ -9,40 +9,39 @@ module Pallets
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def add(node, dependencies)
|
12
|
-
|
12
|
+
raise WorkflowError, "Task #{node} is already defined in this workflow. "\
|
13
|
+
"Use `task '#{node}', as: 'FooBar'` to define an "\
|
14
|
+
"alias and reuse task" if nodes.key?(node)
|
15
|
+
|
16
|
+
nodes[node] = dependencies
|
13
17
|
end
|
14
18
|
|
15
19
|
def parents(node)
|
16
|
-
|
20
|
+
nodes[node]
|
17
21
|
end
|
18
22
|
|
19
23
|
def empty?
|
20
|
-
|
24
|
+
nodes.empty?
|
21
25
|
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
# Assign order to each node
|
30
|
-
i = 0
|
31
|
-
groups.flat_map do |group|
|
32
|
-
group_with_order = group.product([i])
|
33
|
-
i += group.size
|
34
|
-
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))
|
35
32
|
end
|
36
33
|
end
|
37
34
|
|
38
35
|
private
|
39
36
|
|
37
|
+
attr_reader :nodes
|
38
|
+
|
40
39
|
def tsort_each_node(&block)
|
41
|
-
|
40
|
+
nodes.each_key(&block)
|
42
41
|
end
|
43
42
|
|
44
43
|
def tsort_each_child(node, &block)
|
45
|
-
|
44
|
+
nodes.fetch(node).each(&block)
|
46
45
|
rescue KeyError
|
47
46
|
raise WorkflowError, "Task #{node} is marked as a dependency but not defined"
|
48
47
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'appsignal'
|
2
|
+
|
3
|
+
module Pallets
|
4
|
+
module Middleware
|
5
|
+
class AppsignalInstrumenter
|
6
|
+
extend Appsignal::Hooks::Helpers
|
7
|
+
|
8
|
+
def self.call(worker, job, context)
|
9
|
+
job_status = nil
|
10
|
+
transaction = Appsignal::Transaction.create(
|
11
|
+
SecureRandom.uuid,
|
12
|
+
Appsignal::Transaction::BACKGROUND_JOB,
|
13
|
+
Appsignal::Transaction::GenericRequest.new(queue_start: job['created_at'])
|
14
|
+
)
|
15
|
+
|
16
|
+
Appsignal.instrument('perform_job.pallets') do
|
17
|
+
begin
|
18
|
+
yield
|
19
|
+
rescue Exception => ex
|
20
|
+
job_status = :failed
|
21
|
+
transaction.set_error(ex)
|
22
|
+
raise
|
23
|
+
ensure
|
24
|
+
transaction.set_action_if_nil("#{job['task_class']}#run (#{job['workflow_class']})")
|
25
|
+
transaction.params = filtered_context(context)
|
26
|
+
formatted_metadata(job).each { |kv| transaction.set_metadata(*kv) }
|
27
|
+
transaction.set_http_or_background_queue_start
|
28
|
+
Appsignal.increment_counter('pallets_job_count', 1, status: job_status || :successful)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
ensure
|
32
|
+
Appsignal::Transaction.complete_current!
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.filtered_context(context)
|
36
|
+
Appsignal::Utils::HashSanitizer.sanitize(
|
37
|
+
context,
|
38
|
+
Appsignal.config[:filter_parameters]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.formatted_metadata(job)
|
43
|
+
job.map { |k, v| [k, truncate(string_or_inspect(v))] }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Pallets
|
2
|
+
module Middleware
|
3
|
+
class JobLogger
|
4
|
+
def self.call(worker, job, context)
|
5
|
+
Pallets.logger.info 'Started', extract_metadata(worker.id, job)
|
6
|
+
result = yield
|
7
|
+
Pallets.logger.info 'Done', extract_metadata(worker.id, job)
|
8
|
+
result
|
9
|
+
rescue => ex
|
10
|
+
Pallets.logger.warn "#{ex.class.name}: #{ex.message}", extract_metadata(worker.id, job)
|
11
|
+
Pallets.logger.warn ex.backtrace.join("\n"), extract_metadata(worker.id, job) unless ex.backtrace.nil?
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.extract_metadata(wid, job)
|
16
|
+
{
|
17
|
+
wid: wid,
|
18
|
+
wfid: job['wfid'],
|
19
|
+
jid: job['jid'],
|
20
|
+
wf: job['workflow_class'],
|
21
|
+
tsk: job['task_class'],
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Pallets
|
2
|
+
module Middleware
|
3
|
+
# Array-like class that acts like a stack and additionally provides the
|
4
|
+
# means to wrap an operation with callable objects
|
5
|
+
class Stack < Array
|
6
|
+
def invoke(*args, &block)
|
7
|
+
reverse.inject(block) do |memo, middleware|
|
8
|
+
lambda { middleware.call(*args, &memo) }
|
9
|
+
end.call
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -4,12 +4,11 @@ module Pallets
|
|
4
4
|
module Serializers
|
5
5
|
class Json < Base
|
6
6
|
def dump(data)
|
7
|
-
|
8
|
-
JSON.generate(data, quirks_mode: true)
|
7
|
+
JSON.generate(data)
|
9
8
|
end
|
10
9
|
|
11
10
|
def load(data)
|
12
|
-
JSON.parse(data
|
11
|
+
JSON.parse(data)
|
13
12
|
end
|
14
13
|
end
|
15
14
|
end
|
data/lib/pallets/version.rb
CHANGED
data/lib/pallets/worker.rb
CHANGED
@@ -69,8 +69,6 @@ module Pallets
|
|
69
69
|
return
|
70
70
|
end
|
71
71
|
|
72
|
-
Pallets.logger.info "Started", extract_metadata(job_hash)
|
73
|
-
|
74
72
|
context = Context[
|
75
73
|
serializer.load_context(backend.get_context(job_hash['wfid']))
|
76
74
|
]
|
@@ -78,7 +76,9 @@ module Pallets
|
|
78
76
|
task_class = Pallets::Util.constantize(job_hash["task_class"])
|
79
77
|
task = task_class.new(context)
|
80
78
|
begin
|
81
|
-
task_result =
|
79
|
+
task_result = middleware.invoke(self, job_hash, context) do
|
80
|
+
task.run
|
81
|
+
end
|
82
82
|
rescue => ex
|
83
83
|
handle_job_error(ex, job, job_hash)
|
84
84
|
else
|
@@ -91,8 +91,6 @@ module Pallets
|
|
91
91
|
end
|
92
92
|
|
93
93
|
def handle_job_error(ex, job, job_hash)
|
94
|
-
Pallets.logger.warn "#{ex.class.name}: #{ex.message}", extract_metadata(job_hash)
|
95
|
-
Pallets.logger.warn ex.backtrace.join("\n"), extract_metadata(job_hash) unless ex.backtrace.nil?
|
96
94
|
failures = job_hash.fetch('failures', 0) + 1
|
97
95
|
new_job = serializer.dump(job_hash.merge(
|
98
96
|
'failures' => failures,
|
@@ -106,7 +104,6 @@ module Pallets
|
|
106
104
|
backend.retry(new_job, job, retry_at)
|
107
105
|
else
|
108
106
|
backend.give_up(new_job, job)
|
109
|
-
Pallets.logger.info "Gave up after #{failures} failed attempts", extract_metadata(job_hash)
|
110
107
|
end
|
111
108
|
end
|
112
109
|
|
@@ -116,22 +113,10 @@ module Pallets
|
|
116
113
|
'reason' => 'returned_false'
|
117
114
|
))
|
118
115
|
backend.give_up(new_job, job)
|
119
|
-
Pallets.logger.info "Gave up after returning false", extract_metadata(job_hash)
|
120
116
|
end
|
121
117
|
|
122
118
|
def handle_job_success(context, job, job_hash)
|
123
|
-
backend.save(job_hash['wfid'], job, serializer.dump_context(context.buffer))
|
124
|
-
Pallets.logger.info "Done", extract_metadata(job_hash)
|
125
|
-
end
|
126
|
-
|
127
|
-
def extract_metadata(job_hash)
|
128
|
-
{
|
129
|
-
wid: id,
|
130
|
-
wfid: job_hash['wfid'],
|
131
|
-
jid: job_hash['jid'],
|
132
|
-
wf: job_hash['workflow_class'],
|
133
|
-
tsk: job_hash['task_class']
|
134
|
-
}
|
119
|
+
backend.save(job_hash['wfid'], job_hash['jid'], job, serializer.dump_context(context.buffer))
|
135
120
|
end
|
136
121
|
|
137
122
|
def backoff_in_seconds(count)
|
@@ -145,5 +130,9 @@ module Pallets
|
|
145
130
|
def serializer
|
146
131
|
@serializer ||= Pallets.serializer
|
147
132
|
end
|
133
|
+
|
134
|
+
def middleware
|
135
|
+
@middleware ||= Pallets.middleware
|
136
|
+
end
|
148
137
|
end
|
149
138
|
end
|
data/lib/pallets/workflow.rb
CHANGED
@@ -4,6 +4,12 @@ module Pallets
|
|
4
4
|
|
5
5
|
attr_reader :context
|
6
6
|
|
7
|
+
def self.build(&block)
|
8
|
+
Class.new(self).tap do |workflow_class|
|
9
|
+
workflow_class.instance_eval(&block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
def initialize(context_hash = {})
|
8
14
|
@id = nil
|
9
15
|
# Passed in context hash needs to be buffered
|
@@ -14,7 +20,7 @@ module Pallets
|
|
14
20
|
raise WorkflowError, "#{self.class.name} has no tasks. Workflows "\
|
15
21
|
"must contain at least one task" if self.class.graph.empty?
|
16
22
|
|
17
|
-
backend.run_workflow(id,
|
23
|
+
backend.run_workflow(id, *prepare_jobs, serializer.dump_context(context.buffer))
|
18
24
|
id
|
19
25
|
end
|
20
26
|
|
@@ -24,20 +30,29 @@ module Pallets
|
|
24
30
|
|
25
31
|
private
|
26
32
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
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] }
|
31
45
|
end
|
46
|
+
|
47
|
+
[jobs, jobmasks]
|
32
48
|
end
|
33
49
|
|
34
|
-
def construct_job(
|
35
|
-
|
50
|
+
def construct_job(task_alias)
|
51
|
+
Hash[self.class.task_config[task_alias]].tap do |job|
|
36
52
|
job['wfid'] = id
|
37
|
-
job['jid'] = "J#{Pallets::Util.generate_id(task_class)}".upcase
|
38
|
-
job['workflow_class'] = self.class.name
|
53
|
+
job['jid'] = "J#{Pallets::Util.generate_id(job['task_class'])}".upcase
|
39
54
|
job['created_at'] = Time.now.to_f
|
40
|
-
end
|
55
|
+
end
|
41
56
|
end
|
42
57
|
|
43
58
|
def backend
|
@@ -48,6 +63,10 @@ module Pallets
|
|
48
63
|
Pallets.serializer
|
49
64
|
end
|
50
65
|
|
66
|
+
def self.name
|
67
|
+
@name ||= super || '<Anonymous>'
|
68
|
+
end
|
69
|
+
|
51
70
|
def self.task_config
|
52
71
|
@task_config ||= {}
|
53
72
|
end
|
data/pallets.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pallets
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrei Horak
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -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"
|
@@ -56,6 +57,10 @@ files:
|
|
56
57
|
- README.md
|
57
58
|
- Rakefile
|
58
59
|
- bin/pallets
|
60
|
+
- examples/aliases.rb
|
61
|
+
- examples/anonymous.rb
|
62
|
+
- examples/appsignal.rb
|
63
|
+
- examples/config/appsignal.yml
|
59
64
|
- examples/config_savvy.rb
|
60
65
|
- examples/do_groceries.rb
|
61
66
|
- examples/hello_world.rb
|
@@ -75,6 +80,9 @@ files:
|
|
75
80
|
- lib/pallets/graph.rb
|
76
81
|
- lib/pallets/logger.rb
|
77
82
|
- lib/pallets/manager.rb
|
83
|
+
- lib/pallets/middleware/appsignal_instrumenter.rb
|
84
|
+
- lib/pallets/middleware/job_logger.rb
|
85
|
+
- lib/pallets/middleware/stack.rb
|
78
86
|
- lib/pallets/pool.rb
|
79
87
|
- lib/pallets/scheduler.rb
|
80
88
|
- lib/pallets/serializers/base.rb
|
@@ -98,15 +106,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
106
|
requirements:
|
99
107
|
- - ">="
|
100
108
|
- !ruby/object:Gem::Version
|
101
|
-
version: '2.
|
109
|
+
version: '2.4'
|
102
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
111
|
requirements:
|
104
112
|
- - ">="
|
105
113
|
- !ruby/object:Gem::Version
|
106
114
|
version: '0'
|
107
115
|
requirements: []
|
108
|
-
|
109
|
-
rubygems_version: 2.5.2.3
|
116
|
+
rubygems_version: 3.1.2
|
110
117
|
signing_key:
|
111
118
|
specification_version: 4
|
112
119
|
summary: Toy workflow engine, written in Ruby
|