pallets 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +8 -5
- data/CHANGELOG.md +34 -0
- data/Gemfile +14 -1
- data/LICENSE +1 -1
- data/README.md +1 -1
- data/examples/config_savvy.rb +45 -0
- data/examples/do_groceries.rb +62 -0
- data/examples/hello_world.rb +13 -0
- data/lib/pallets.rb +15 -2
- data/lib/pallets/backends/base.rb +7 -8
- data/lib/pallets/backends/redis.rb +31 -31
- data/lib/pallets/backends/scripts/give_up.lua +4 -0
- data/lib/pallets/backends/scripts/run_workflow.lua +4 -1
- data/lib/pallets/backends/scripts/save.lua +17 -2
- data/lib/pallets/cli.rb +21 -3
- data/lib/pallets/configuration.rb +6 -1
- data/lib/pallets/context.rb +13 -0
- data/lib/pallets/dsl/workflow.rb +5 -1
- data/lib/pallets/errors.rb +10 -0
- data/lib/pallets/graph.rb +4 -0
- data/lib/pallets/logger.rb +24 -0
- data/lib/pallets/manager.rb +1 -1
- data/lib/pallets/scheduler.rb +4 -0
- data/lib/pallets/serializers/msgpack.rb +3 -1
- data/lib/pallets/version.rb +1 -1
- data/lib/pallets/worker.rb +33 -10
- data/lib/pallets/workflow.rb +7 -4
- data/pallets.gemspec +11 -14
- metadata +12 -78
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/pallets/backends/scripts/discard.lua +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7e0544b5ac0bf2eb5cbf4aa0b1795e4588df46485ed36e7bc5560d26cf47684a
|
4
|
+
data.tar.gz: 95a3e7f6ca57048773ba2f2ac72304090c5899b562766e2e99e6ad1911fcc103
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 296c3ce03c89e6081e707014d28d8c48d65d22d6adac4ee5534551f39853eba171d12d143b52fa271857266342f12a965639749fd436126a66118fc911f47309
|
7
|
+
data.tar.gz: fccac5f172c6f8b03bf4409cd37592efc81f93d951ac1e5ac0163a6e74deabdecb607f3b5d9b2a5e7e11ab8c2e7fbecb7aa2b48c8544c18d0041e4ebd2b59467
|
data/.travis.yml
CHANGED
@@ -4,9 +4,12 @@ services:
|
|
4
4
|
- redis-server
|
5
5
|
cache: bundler
|
6
6
|
rvm:
|
7
|
-
- 2.
|
8
|
-
- 2.
|
9
|
-
- 2.3
|
10
|
-
- 2.
|
11
|
-
|
7
|
+
- 2.3.8
|
8
|
+
- 2.4.5
|
9
|
+
- 2.5.3
|
10
|
+
- 2.6.0
|
11
|
+
before_install:
|
12
|
+
# Bundler 2.0 needs a newer RubyGems
|
13
|
+
- gem update --system
|
14
|
+
- gem install bundler
|
12
15
|
script: bundle exec rspec
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.3.0] - 2019-02-08
|
10
|
+
### Added
|
11
|
+
- shared contexts (#9)
|
12
|
+
- handle TERM and TTIN signals (#15, #17)
|
13
|
+
- configure how long failed jobs are kept (#21)
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- use a single Redis connection when picking up work (#11)
|
17
|
+
- improve logging (#14)
|
18
|
+
- fix handling empty workflows and contexts (#18)
|
19
|
+
- fix encoding for msgpack serializer (#19)
|
20
|
+
- malformed jobs are given up rather than discarded (#22)
|
21
|
+
|
22
|
+
### Removed
|
23
|
+
- support for Ruby 2.1 & 2.2 (#13)
|
24
|
+
|
25
|
+
## [0.2.0] - 2018-10-02
|
26
|
+
### Added
|
27
|
+
- msgpack serializer (#5)
|
28
|
+
|
29
|
+
## 0.1.0 - 2018-09-29
|
30
|
+
- Pallets' inception <3
|
31
|
+
|
32
|
+
[Unreleased]: https://github.com/linkyndy/pallets/compare/compare/v0.3.0...HEAD
|
33
|
+
[0.3.0]: https://github.com/linkyndy/pallets/compare/v0.2.0...v0.3.0
|
34
|
+
[0.2.0]: https://github.com/linkyndy/pallets/compare/v0.1.0...v0.2.0
|
data/Gemfile
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in pallets.gemspec
|
4
3
|
gemspec
|
4
|
+
|
5
|
+
gem 'rake'
|
6
|
+
|
7
|
+
group :test do
|
8
|
+
gem 'rspec'
|
9
|
+
# Better RSpec formatting
|
10
|
+
gem 'fuubar'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :development, :test do
|
14
|
+
gem 'pry-byebug'
|
15
|
+
# Time travel in style
|
16
|
+
gem 'timecop'
|
17
|
+
end
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -27,7 +27,7 @@ end
|
|
27
27
|
MyWorkflow.new.run
|
28
28
|
```
|
29
29
|
|
30
|
-
That's basically it! Curious for more? Read on!
|
30
|
+
That's basically it! Curious for more? Read on or [check the examples](examples/)!
|
31
31
|
|
32
32
|
> Don't forget to run pallets, so it can process your tasks: `bundle exec pallets -r ./my_workflow`
|
33
33
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'pallets'
|
2
|
+
|
3
|
+
Pallets.configure do |c|
|
4
|
+
# Harness 4 Pallets workers per process
|
5
|
+
c.concurrency = 4
|
6
|
+
|
7
|
+
# The default one, though
|
8
|
+
c.backend = :redis
|
9
|
+
# Useful to connect to a hosted Redis instance. Takes all options `Redis.new`
|
10
|
+
# accepts, like `db`, `timeout`, `host`, `port`, `password`, `sentinels`.
|
11
|
+
# Check https://www.rubydoc.info/github/redis/redis-rb/Redis:initialize for
|
12
|
+
# more details
|
13
|
+
c.backend_args = { url: 'redis://127.0.0.1:6379/1' }
|
14
|
+
# Use a maximum of 10 backend connections (Redis, in this case)
|
15
|
+
c.pool_size = 10
|
16
|
+
|
17
|
+
# A tad faster than JSON
|
18
|
+
c.serializer = :msgpack
|
19
|
+
|
20
|
+
# Allow 10 minutes for a job to process. After this, we assume the job did not
|
21
|
+
# finish and we retry it
|
22
|
+
c.job_timeout = 600
|
23
|
+
# Jobs will be retried up to 5 times upon failure. After that, they will be
|
24
|
+
# given up. Retry times are exponential and happen after: 7, 22, 87, 262, ...
|
25
|
+
c.max_failures = 5
|
26
|
+
end
|
27
|
+
|
28
|
+
class ConfigSavvy < Pallets::Workflow
|
29
|
+
task :volatile
|
30
|
+
task :success => :volatile
|
31
|
+
end
|
32
|
+
|
33
|
+
class Volatile < Pallets::Task
|
34
|
+
def run
|
35
|
+
raise 'I am ràndomly failing' if [true, false].sample
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Success < Pallets::Task
|
40
|
+
def run
|
41
|
+
puts 'I am executed after Volatile manages to successfully execute'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
ConfigSavvy.new.run
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'pallets'
|
2
|
+
|
3
|
+
Pallets.configure do |c|
|
4
|
+
c.backend_args = { url: 'redis://127.0.0.1:6379/13' }
|
5
|
+
end
|
6
|
+
|
7
|
+
class DoGroceries < Pallets::Workflow
|
8
|
+
task :enter_shop
|
9
|
+
task :get_shopping_cart => :enter_shop
|
10
|
+
task :put_milk => :get_shopping_cart
|
11
|
+
task :put_bread => :get_shopping_cart
|
12
|
+
task :pay => [:put_milk, :put_bread]
|
13
|
+
task :go_home => :pay
|
14
|
+
end
|
15
|
+
|
16
|
+
class EnterShop < Pallets::Task
|
17
|
+
def run
|
18
|
+
puts "Entering #{context['shop_name']}"
|
19
|
+
raise 'Cannot enter shop!' if (context['i'].to_i % 10).zero?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class GetShoppingCart < Pallets::Task
|
24
|
+
def run
|
25
|
+
puts "Where's that 50 cent coin??"
|
26
|
+
context['need_to_return_coin'] = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class PutMilk < Pallets::Task
|
31
|
+
def run
|
32
|
+
puts "Whole or half? Hmm..."
|
33
|
+
sleep 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class PutBread < Pallets::Task
|
38
|
+
def run
|
39
|
+
raise 'Out of bread!' if (context['i'].to_i % 30).zero?
|
40
|
+
puts "Got the bread"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Pay < Pallets::Task
|
45
|
+
def run
|
46
|
+
puts "Paying by #{context['pay_by']}"
|
47
|
+
sleep 2
|
48
|
+
raise 'Payment failed!' if (context['i'].to_i % 100).zero?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class GoHome < Pallets::Task
|
53
|
+
def run
|
54
|
+
puts "Done!!"
|
55
|
+
|
56
|
+
if context['need_to_return_coin']
|
57
|
+
puts '...forgot to get my coin back...'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
1_000.times { |i| DoGroceries.new(shop_name: 'Pallet Shop', pay_by: :card, i: i).run }
|
data/lib/pallets.rb
CHANGED
@@ -3,9 +3,11 @@ require "pallets/version"
|
|
3
3
|
require 'pallets/backends/base'
|
4
4
|
require 'pallets/backends/redis'
|
5
5
|
require 'pallets/configuration'
|
6
|
+
require 'pallets/context'
|
6
7
|
require 'pallets/dsl/workflow'
|
7
8
|
require 'pallets/errors'
|
8
9
|
require 'pallets/graph'
|
10
|
+
require 'pallets/logger'
|
9
11
|
require 'pallets/manager'
|
10
12
|
require 'pallets/pool'
|
11
13
|
require 'pallets/scheduler'
|
@@ -17,7 +19,6 @@ require 'pallets/util'
|
|
17
19
|
require 'pallets/worker'
|
18
20
|
require 'pallets/workflow'
|
19
21
|
|
20
|
-
require 'logger'
|
21
22
|
require 'securerandom'
|
22
23
|
|
23
24
|
module Pallets
|
@@ -35,6 +36,7 @@ module Pallets
|
|
35
36
|
cls.new(
|
36
37
|
namespace: configuration.namespace,
|
37
38
|
blocking_timeout: configuration.blocking_timeout,
|
39
|
+
failed_job_lifespan: configuration.failed_job_lifespan,
|
38
40
|
job_timeout: configuration.job_timeout,
|
39
41
|
pool_size: configuration.pool_size,
|
40
42
|
**configuration.backend_args
|
@@ -50,6 +52,17 @@ module Pallets
|
|
50
52
|
end
|
51
53
|
|
52
54
|
def self.logger
|
53
|
-
@logger ||=
|
55
|
+
@logger ||= begin
|
56
|
+
logger = Pallets::Logger.new(STDOUT)
|
57
|
+
# TODO: Ruby 2.4 supports Logger initialization with the arguments below, so
|
58
|
+
# we can drop this after we drop support for Ruby 2.3
|
59
|
+
logger.level = Pallets::Logger::INFO
|
60
|
+
logger.formatter = Pallets::Logger::Formatters::Pretty.new
|
61
|
+
logger
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.logger=(logger)
|
66
|
+
@logger = logger
|
54
67
|
end
|
55
68
|
end
|
@@ -6,23 +6,22 @@ module Pallets
|
|
6
6
|
raise NotImplementedError
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
def save(workflow_id, job)
|
9
|
+
def get_context(workflow_id)
|
11
10
|
raise NotImplementedError
|
12
11
|
end
|
13
12
|
|
14
|
-
#
|
15
|
-
def
|
13
|
+
# Saves a job after successfully processing it
|
14
|
+
def save(workflow_id, job, context_buffer)
|
16
15
|
raise NotImplementedError
|
17
16
|
end
|
18
17
|
|
19
|
-
#
|
18
|
+
# Schedules a failed job for retry
|
20
19
|
def retry(job, old_job, at)
|
21
20
|
raise NotImplementedError
|
22
21
|
end
|
23
22
|
|
24
|
-
#
|
25
|
-
def give_up(job, old_job
|
23
|
+
# Gives up job after repeteadly failing to process it
|
24
|
+
def give_up(job, old_job)
|
26
25
|
raise NotImplementedError
|
27
26
|
end
|
28
27
|
|
@@ -30,7 +29,7 @@ module Pallets
|
|
30
29
|
raise NotImplementedError
|
31
30
|
end
|
32
31
|
|
33
|
-
def run_workflow(workflow_id, jobs_with_dependencies)
|
32
|
+
def run_workflow(workflow_id, jobs_with_dependencies, context)
|
34
33
|
raise NotImplementedError
|
35
34
|
end
|
36
35
|
end
|
@@ -3,9 +3,10 @@ require 'redis'
|
|
3
3
|
module Pallets
|
4
4
|
module Backends
|
5
5
|
class Redis < Base
|
6
|
-
def initialize(namespace:, blocking_timeout:, job_timeout:, pool_size:, **options)
|
6
|
+
def initialize(namespace:, blocking_timeout:, failed_job_lifespan:, job_timeout:, pool_size:, **options)
|
7
7
|
@namespace = namespace
|
8
8
|
@blocking_timeout = blocking_timeout
|
9
|
+
@failed_job_lifespan = failed_job_lifespan
|
9
10
|
@job_timeout = job_timeout
|
10
11
|
@pool = Pallets::Pool.new(pool_size) { ::Redis.new(options) }
|
11
12
|
|
@@ -13,44 +14,40 @@ module Pallets
|
|
13
14
|
@reliability_queue_key = "#{namespace}:reliability-queue"
|
14
15
|
@reliability_set_key = "#{namespace}:reliability-set"
|
15
16
|
@retry_set_key = "#{namespace}:retry-set"
|
16
|
-
@
|
17
|
+
@given_up_set_key = "#{namespace}:given-up-set"
|
17
18
|
@workflow_key = "#{namespace}:workflows:%s"
|
19
|
+
@context_key = "#{namespace}:contexts:%s"
|
20
|
+
@eta_key = "#{namespace}:etas:%s"
|
18
21
|
|
19
22
|
register_scripts
|
20
23
|
end
|
21
24
|
|
22
25
|
def pick
|
23
|
-
|
24
|
-
client.brpoplpush(@queue_key, @reliability_queue_key, timeout: @blocking_timeout)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
# sorted set
|
31
|
-
@pool.execute do |client|
|
26
|
+
@pool.execute do |client|
|
27
|
+
job = client.brpoplpush(@queue_key, @reliability_queue_key, timeout: @blocking_timeout)
|
28
|
+
if job
|
29
|
+
# We store the job's timeout so we know when to retry jobs that are
|
30
|
+
# still on the reliability queue. We do this separately since there is
|
31
|
+
# no other way to atomically BRPOPLPUSH from the main queue to a
|
32
|
+
# sorted set
|
32
33
|
client.zadd(@reliability_set_key, Time.now.to_f + @job_timeout, job)
|
33
34
|
end
|
35
|
+
job
|
34
36
|
end
|
35
|
-
job
|
36
37
|
end
|
37
38
|
|
38
|
-
def
|
39
|
+
def get_context(workflow_id)
|
39
40
|
@pool.execute do |client|
|
40
|
-
client.
|
41
|
-
@scripts['save'],
|
42
|
-
[@workflow_key % workflow_id, @queue_key, @reliability_queue_key, @reliability_set_key],
|
43
|
-
[job]
|
44
|
-
)
|
41
|
+
client.hgetall(@context_key % workflow_id)
|
45
42
|
end
|
46
43
|
end
|
47
44
|
|
48
|
-
def
|
45
|
+
def save(workflow_id, job, context_buffer)
|
49
46
|
@pool.execute do |client|
|
50
47
|
client.eval(
|
51
|
-
@scripts['
|
52
|
-
[@reliability_queue_key, @reliability_set_key],
|
53
|
-
|
48
|
+
@scripts['save'],
|
49
|
+
[@workflow_key % workflow_id, @queue_key, @reliability_queue_key, @reliability_set_key, @context_key % workflow_id, @eta_key % workflow_id],
|
50
|
+
context_buffer.to_a << job
|
54
51
|
)
|
55
52
|
end
|
56
53
|
end
|
@@ -65,12 +62,12 @@ module Pallets
|
|
65
62
|
end
|
66
63
|
end
|
67
64
|
|
68
|
-
def give_up(job, old_job
|
65
|
+
def give_up(job, old_job)
|
69
66
|
@pool.execute do |client|
|
70
67
|
client.eval(
|
71
68
|
@scripts['give_up'],
|
72
|
-
[@
|
73
|
-
[
|
69
|
+
[@given_up_set_key, @reliability_queue_key, @reliability_set_key],
|
70
|
+
[Time.now.to_f, job, old_job, Time.now.to_f - @failed_job_lifespan]
|
74
71
|
)
|
75
72
|
end
|
76
73
|
end
|
@@ -85,13 +82,16 @@ module Pallets
|
|
85
82
|
end
|
86
83
|
end
|
87
84
|
|
88
|
-
def run_workflow(workflow_id, jobs_with_order)
|
85
|
+
def run_workflow(workflow_id, jobs_with_order, context)
|
89
86
|
@pool.execute do |client|
|
90
|
-
client.
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
87
|
+
client.multi do
|
88
|
+
client.eval(
|
89
|
+
@scripts['run_workflow'],
|
90
|
+
[@workflow_key % workflow_id, @queue_key, @eta_key % workflow_id],
|
91
|
+
jobs_with_order
|
92
|
+
)
|
93
|
+
client.hmset(@context_key % workflow_id, *context.to_a) unless context.empty?
|
94
|
+
end
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
@@ -4,3 +4,7 @@ redis.call("ZREM", KEYS[3], ARGV[3])
|
|
4
4
|
|
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
|
+
|
8
|
+
-- Remove any jobs that have been given up long enough ago (their score is
|
9
|
+
-- below given value)
|
10
|
+
redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[4])
|
@@ -1,5 +1,8 @@
|
|
1
1
|
-- Add all jobs to sorted set
|
2
|
-
redis.call("ZADD", KEYS[1], unpack(ARGV))
|
2
|
+
local eta = redis.call("ZADD", KEYS[1], unpack(ARGV))
|
3
|
+
|
4
|
+
-- Set ETA key; this is merely the number of jobs that need to be processed
|
5
|
+
redis.call("SET", KEYS[3], eta)
|
3
6
|
|
4
7
|
-- Queue jobs that are ready to be processed (their score is 0) and
|
5
8
|
-- remove queued jobs from the sorted set
|
@@ -1,6 +1,14 @@
|
|
1
|
+
-- NOTE: We store the job as the last argument passed to this script because it
|
2
|
+
-- is more efficient to pop in Lua than shift
|
3
|
+
local job = table.remove(ARGV)
|
1
4
|
-- Remove job from reliability queue
|
2
|
-
redis.call("LREM", KEYS[3], 0,
|
3
|
-
redis.call("ZREM", KEYS[4],
|
5
|
+
redis.call("LREM", KEYS[3], 0, job)
|
6
|
+
redis.call("ZREM", KEYS[4], job)
|
7
|
+
|
8
|
+
-- Update context hash with buffer
|
9
|
+
if #ARGV > 0 then
|
10
|
+
redis.call("HMSET", KEYS[5], unpack(ARGV))
|
11
|
+
end
|
4
12
|
|
5
13
|
-- Decrement all jobs from the sorted set
|
6
14
|
local all_pending = redis.call("ZRANGE", KEYS[1], 0, -1)
|
@@ -16,3 +24,10 @@ if count > 0 then
|
|
16
24
|
redis.call("LPUSH", KEYS[2], unpack(work))
|
17
25
|
redis.call("ZREM", KEYS[1], unpack(work))
|
18
26
|
end
|
27
|
+
|
28
|
+
-- Decrement ETA and remove it together with the context if all tasks have
|
29
|
+
-- been processed (ETA is 0)
|
30
|
+
redis.call("DECR", KEYS[6])
|
31
|
+
if tonumber(redis.call("GET", KEYS[6])) == 0 then
|
32
|
+
redis.call("DEL", KEYS[5], KEYS[6])
|
33
|
+
end
|
data/lib/pallets/cli.rb
CHANGED
@@ -11,7 +11,8 @@ module Pallets
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def run
|
14
|
-
Pallets.logger.info 'Starting the
|
14
|
+
Pallets.logger.info 'Starting the awesome Pallets <3'
|
15
|
+
Pallets.logger.info "Running on #{RUBY_DESCRIPTION}"
|
15
16
|
|
16
17
|
@manager.start
|
17
18
|
|
@@ -30,8 +31,17 @@ module Pallets
|
|
30
31
|
|
31
32
|
def handle_signal(signal)
|
32
33
|
case signal
|
33
|
-
when 'INT'
|
34
|
+
when 'INT', 'TERM'
|
34
35
|
raise Interrupt
|
36
|
+
when 'TTIN'
|
37
|
+
print_backtraces
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def print_backtraces
|
42
|
+
(@manager.workers + [@manager.scheduler]).each do |actor|
|
43
|
+
Pallets.logger.info "Debugging #{actor.id}"
|
44
|
+
Pallets.logger.info actor.debug.join("\n") unless actor.debug.nil?
|
35
45
|
end
|
36
46
|
end
|
37
47
|
|
@@ -51,6 +61,10 @@ module Pallets
|
|
51
61
|
Pallets.configuration.max_failures = max_failures
|
52
62
|
end
|
53
63
|
|
64
|
+
opts.on('-l', '--failed-job-lifespan NUM', Integer, 'Seconds a job stays in the given up set') do |failed_job_lifespan|
|
65
|
+
Pallets.configuration.failed_job_lifespan = failed_job_lifespan
|
66
|
+
end
|
67
|
+
|
54
68
|
opts.on('-n', '--namespace NAME', 'Namespace to use for backend') do |namespace|
|
55
69
|
Pallets.configuration.namespace = namespace
|
56
70
|
end
|
@@ -71,6 +85,10 @@ module Pallets
|
|
71
85
|
Pallets.configuration.serializer = serializer
|
72
86
|
end
|
73
87
|
|
88
|
+
opts.on('-t', '--job-timeout NUM', Integer, 'Seconds allowed for a job to be processed') do |job_timeout|
|
89
|
+
Pallets.configuration.job_timeout = job_timeout
|
90
|
+
end
|
91
|
+
|
74
92
|
opts.on('-u', '--blocking-timeout NUM', Integer, 'Seconds to block while waiting for work') do |blocking_timeout|
|
75
93
|
Pallets.configuration.blocking_timeout = blocking_timeout
|
76
94
|
end
|
@@ -92,7 +110,7 @@ module Pallets
|
|
92
110
|
end
|
93
111
|
|
94
112
|
def setup_signal_handlers
|
95
|
-
%w(INT).each do |signal|
|
113
|
+
%w(INT TERM TTIN).each do |signal|
|
96
114
|
trap signal do
|
97
115
|
@signal_queue.push signal
|
98
116
|
end
|
@@ -12,6 +12,10 @@ module Pallets
|
|
12
12
|
# Number of workers to process jobs
|
13
13
|
attr_accessor :concurrency
|
14
14
|
|
15
|
+
# Minimum number of seconds a failed job stays in the given up set. After
|
16
|
+
# this period, jobs will be permanently deleted
|
17
|
+
attr_accessor :failed_job_lifespan
|
18
|
+
|
15
19
|
# Number of seconds allowed for a job to be processed. If a job exceeds this
|
16
20
|
# period, it is considered failed, and scheduled to be processed again
|
17
21
|
attr_accessor :job_timeout
|
@@ -34,7 +38,8 @@ module Pallets
|
|
34
38
|
@backend_args = {}
|
35
39
|
@blocking_timeout = 5
|
36
40
|
@concurrency = 2
|
37
|
-
@
|
41
|
+
@failed_job_lifespan = 7_776_000 # 3 months
|
42
|
+
@job_timeout = 1_800 # 30 minutes
|
38
43
|
@max_failures = 3
|
39
44
|
@namespace = 'pallets'
|
40
45
|
@pool_size = 5
|
data/lib/pallets/dsl/workflow.rb
CHANGED
@@ -7,7 +7,11 @@ module Pallets
|
|
7
7
|
else
|
8
8
|
options.first
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
|
+
unless name
|
12
|
+
raise WorkflowError, "Task has no name. Provide a name using " \
|
13
|
+
"`task :name, *args` or `task name: :arg` syntax"
|
14
|
+
end
|
11
15
|
|
12
16
|
# Handle nils, symbols or arrays consistently
|
13
17
|
name = name.to_sym
|
data/lib/pallets/errors.rb
CHANGED
@@ -1,4 +1,14 @@
|
|
1
1
|
module Pallets
|
2
|
+
# Generic class for all Pallets-related errors
|
3
|
+
class PalletsError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Raised when a workflow is not properly defined
|
7
|
+
class WorkflowError < PalletsError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Raised when Pallets needs to shutdown
|
11
|
+
# NOTE: Do not rescue it!
|
2
12
|
class Shutdown < Interrupt
|
3
13
|
end
|
4
14
|
end
|
data/lib/pallets/graph.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Pallets
|
5
|
+
class Logger < ::Logger
|
6
|
+
# Overwrite severity methods to add metadata capabilities
|
7
|
+
%i[debug info warn error fatal unknown].each do |severity|
|
8
|
+
define_method severity do |message, metadata = {}|
|
9
|
+
return super(message) if metadata.empty?
|
10
|
+
|
11
|
+
formatted_metadata = ' ' + metadata.map { |k, v| "#{k}=#{v}" }.join(' ')
|
12
|
+
super(formatted_metadata) { message }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Formatters
|
17
|
+
class Pretty < ::Logger::Formatter
|
18
|
+
def call(severity, time, metadata, message)
|
19
|
+
"#{time.utc.iso8601(4)} pid=#{Process.pid}#{metadata} #{severity}: #{message}\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/pallets/manager.rb
CHANGED
data/lib/pallets/scheduler.rb
CHANGED
data/lib/pallets/version.rb
CHANGED
data/lib/pallets/worker.rb
CHANGED
@@ -26,6 +26,10 @@ module Pallets
|
|
26
26
|
@needs_to_stop
|
27
27
|
end
|
28
28
|
|
29
|
+
def debug
|
30
|
+
@thread.backtrace
|
31
|
+
end
|
32
|
+
|
29
33
|
def id
|
30
34
|
"W#{@thread.object_id.to_s(36)}".upcase if @thread
|
31
35
|
end
|
@@ -49,34 +53,40 @@ module Pallets
|
|
49
53
|
rescue Pallets::Shutdown
|
50
54
|
@manager.remove_worker(self)
|
51
55
|
rescue => ex
|
56
|
+
Pallets.logger.error "#{ex.class.name}: #{ex.message}", wid: id
|
57
|
+
Pallets.logger.error ex.backtrace.join("\n"), wid: id unless ex.backtrace.nil?
|
52
58
|
@manager.replace_worker(self)
|
53
59
|
end
|
54
60
|
|
55
61
|
def process(job)
|
56
|
-
Pallets.logger.info "[#{id}] Picked job: #{job}"
|
57
62
|
begin
|
58
63
|
job_hash = serializer.load(job)
|
59
64
|
rescue
|
60
65
|
# We ensure only valid jobs are created. If something fishy reaches this
|
61
|
-
# point, just
|
62
|
-
backend.
|
66
|
+
# point, just give up on it
|
67
|
+
backend.give_up(job, job)
|
68
|
+
Pallets.logger.error "Could not deserialize #{job}. Gave up job", wid: id
|
63
69
|
return
|
64
70
|
end
|
65
71
|
|
72
|
+
Pallets.logger.info "Started", extract_metadata(job_hash)
|
73
|
+
|
74
|
+
context = Context[backend.get_context(job_hash['workflow_id'])]
|
75
|
+
|
66
76
|
task_class = Pallets::Util.constantize(job_hash["class_name"])
|
67
|
-
task = task_class.new(
|
77
|
+
task = task_class.new(context)
|
68
78
|
begin
|
69
79
|
task.run
|
70
80
|
rescue => ex
|
71
81
|
handle_job_error(ex, job, job_hash)
|
72
82
|
else
|
73
|
-
|
74
|
-
Pallets.logger.info "[#{id}] Successfully processed #{job}"
|
83
|
+
handle_job_success(context, job, job_hash)
|
75
84
|
end
|
76
85
|
end
|
77
86
|
|
78
87
|
def handle_job_error(ex, job, job_hash)
|
79
|
-
Pallets.logger.
|
88
|
+
Pallets.logger.warn "#{ex.class.name}: #{ex.message}", extract_metadata(job_hash)
|
89
|
+
Pallets.logger.warn ex.backtrace.join("\n"), extract_metadata(job_hash) unless ex.backtrace.nil?
|
80
90
|
failures = job_hash.fetch('failures', 0) + 1
|
81
91
|
new_job = serializer.dump(job_hash.merge(
|
82
92
|
'failures' => failures,
|
@@ -87,13 +97,26 @@ module Pallets
|
|
87
97
|
if failures < job_hash['max_failures']
|
88
98
|
retry_at = Time.now.to_f + backoff_in_seconds(failures)
|
89
99
|
backend.retry(new_job, job, retry_at)
|
90
|
-
Pallets.logger.info "[#{id}] Scheduled job for retry"
|
91
100
|
else
|
92
|
-
backend.give_up(new_job, job
|
93
|
-
Pallets.logger.info "
|
101
|
+
backend.give_up(new_job, job)
|
102
|
+
Pallets.logger.info "Gave up after #{failures} failed attempts", extract_metadata(job_hash)
|
94
103
|
end
|
95
104
|
end
|
96
105
|
|
106
|
+
def handle_job_success(context, job, job_hash)
|
107
|
+
backend.save(job_hash['workflow_id'], job, context.buffer)
|
108
|
+
Pallets.logger.info "Done", extract_metadata(job_hash)
|
109
|
+
end
|
110
|
+
|
111
|
+
def extract_metadata(job_hash)
|
112
|
+
{
|
113
|
+
wid: id,
|
114
|
+
wfid: job_hash['workflow_id'],
|
115
|
+
wf: job_hash['workflow_class_name'],
|
116
|
+
tsk: job_hash['class_name']
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
97
120
|
def backoff_in_seconds(count)
|
98
121
|
count ** 4 + 6
|
99
122
|
end
|
data/lib/pallets/workflow.rb
CHANGED
@@ -10,7 +10,10 @@ module Pallets
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def run
|
13
|
-
|
13
|
+
raise WorkflowError, "#{self.class.name} has no tasks. Workflows "\
|
14
|
+
"must contain at least one task" if self.class.graph.empty?
|
15
|
+
|
16
|
+
backend.run_workflow(id, jobs_with_order, context)
|
14
17
|
id
|
15
18
|
end
|
16
19
|
|
@@ -33,9 +36,9 @@ module Pallets
|
|
33
36
|
|
34
37
|
def job_hash
|
35
38
|
{
|
36
|
-
'workflow_id'
|
37
|
-
'
|
38
|
-
'created_at'
|
39
|
+
'workflow_id' => id,
|
40
|
+
'workflow_class_name' => self.class.name,
|
41
|
+
'created_at' => Time.now.to_f
|
39
42
|
}
|
40
43
|
end
|
41
44
|
|
data/pallets.gemspec
CHANGED
@@ -4,25 +4,22 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'pallets/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'pallets'
|
8
8
|
spec.version = Pallets::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Andrei Horak']
|
10
|
+
spec.email = ['linkyndy@gmail.com']
|
11
11
|
|
12
12
|
spec.summary = 'Toy workflow engine, written in Ruby'
|
13
13
|
spec.description = 'Toy workflow engine, written in Ruby'
|
14
14
|
spec.homepage = 'https://github.com/linkyndy/pallets'
|
15
|
+
spec.license = 'MIT'
|
15
16
|
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(
|
17
|
-
spec.
|
18
|
-
spec.
|
19
|
-
spec.require_paths = ["lib"]
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
|
18
|
+
spec.executables = ['pallets']
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.
|
22
|
-
|
23
|
-
spec.
|
24
|
-
spec.
|
25
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
-
spec.add_development_dependency "timecop"
|
27
|
-
spec.add_development_dependency "fuubar"
|
21
|
+
spec.required_ruby_version = '>= 2.3'
|
22
|
+
|
23
|
+
spec.add_dependency 'redis'
|
24
|
+
spec.add_dependency 'msgpack'
|
28
25
|
end
|
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.3.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: 2019-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -38,76 +38,6 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: bundler
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.11'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '1.11'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rake
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '10.0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '10.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rspec
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '3.0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '3.0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: timecop
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: fuubar
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
41
|
description: Toy workflow engine, written in Ruby
|
112
42
|
email:
|
113
43
|
- linkyndy@gmail.com
|
@@ -119,18 +49,19 @@ files:
|
|
119
49
|
- ".gitignore"
|
120
50
|
- ".rspec"
|
121
51
|
- ".travis.yml"
|
52
|
+
- CHANGELOG.md
|
122
53
|
- CONTRIBUTING.md
|
123
54
|
- Gemfile
|
124
55
|
- LICENSE
|
125
56
|
- README.md
|
126
57
|
- Rakefile
|
127
|
-
- bin/console
|
128
58
|
- bin/pallets
|
129
|
-
-
|
59
|
+
- examples/config_savvy.rb
|
60
|
+
- examples/do_groceries.rb
|
61
|
+
- examples/hello_world.rb
|
130
62
|
- lib/pallets.rb
|
131
63
|
- lib/pallets/backends/base.rb
|
132
64
|
- lib/pallets/backends/redis.rb
|
133
|
-
- lib/pallets/backends/scripts/discard.lua
|
134
65
|
- lib/pallets/backends/scripts/give_up.lua
|
135
66
|
- lib/pallets/backends/scripts/reschedule_all.lua
|
136
67
|
- lib/pallets/backends/scripts/retry.lua
|
@@ -138,9 +69,11 @@ files:
|
|
138
69
|
- lib/pallets/backends/scripts/save.lua
|
139
70
|
- lib/pallets/cli.rb
|
140
71
|
- lib/pallets/configuration.rb
|
72
|
+
- lib/pallets/context.rb
|
141
73
|
- lib/pallets/dsl/workflow.rb
|
142
74
|
- lib/pallets/errors.rb
|
143
75
|
- lib/pallets/graph.rb
|
76
|
+
- lib/pallets/logger.rb
|
144
77
|
- lib/pallets/manager.rb
|
145
78
|
- lib/pallets/pool.rb
|
146
79
|
- lib/pallets/scheduler.rb
|
@@ -154,7 +87,8 @@ files:
|
|
154
87
|
- lib/pallets/workflow.rb
|
155
88
|
- pallets.gemspec
|
156
89
|
homepage: https://github.com/linkyndy/pallets
|
157
|
-
licenses:
|
90
|
+
licenses:
|
91
|
+
- MIT
|
158
92
|
metadata: {}
|
159
93
|
post_install_message:
|
160
94
|
rdoc_options: []
|
@@ -164,7 +98,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
164
98
|
requirements:
|
165
99
|
- - ">="
|
166
100
|
- !ruby/object:Gem::Version
|
167
|
-
version: '
|
101
|
+
version: '2.3'
|
168
102
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
103
|
requirements:
|
170
104
|
- - ">="
|
@@ -172,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
172
106
|
version: '0'
|
173
107
|
requirements: []
|
174
108
|
rubyforge_project:
|
175
|
-
rubygems_version: 2.6
|
109
|
+
rubygems_version: 2.7.6
|
176
110
|
signing_key:
|
177
111
|
specification_version: 4
|
178
112
|
summary: Toy workflow engine, written in Ruby
|
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "pallets"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
data/bin/setup
DELETED