pallets 0.2.0 → 0.3.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 +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