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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 388f932d85cba341084169563b80c6d18a0959f5
4
- data.tar.gz: 04bbaa05f08caf6e53eaf632c13ee481abec2399
2
+ SHA256:
3
+ metadata.gz: 7e0544b5ac0bf2eb5cbf4aa0b1795e4588df46485ed36e7bc5560d26cf47684a
4
+ data.tar.gz: 95a3e7f6ca57048773ba2f2ac72304090c5899b562766e2e99e6ad1911fcc103
5
5
  SHA512:
6
- metadata.gz: cde6012d314a8c543cb63a156f2a667cb8b926c9a18c18c89b5c52bed6c680c07e9f8235a9f4b37a36dba17031f8923f6d445a9f019bce4e7c97757954242409
7
- data.tar.gz: 9493181596052789e359bdf5e079e23707f740bd9e97b6ebc56ab0974f85f528c4fbc905bb8695c61cfb6b6e1f77ce2b675c0213a9f6f39251e4b95dad7f71fe
6
+ metadata.gz: 296c3ce03c89e6081e707014d28d8c48d65d22d6adac4ee5534551f39853eba171d12d143b52fa271857266342f12a965639749fd436126a66118fc911f47309
7
+ data.tar.gz: fccac5f172c6f8b03bf4409cd37592efc81f93d951ac1e5ac0163a6e74deabdecb607f3b5d9b2a5e7e11ab8c2e7fbecb7aa2b48c8544c18d0041e4ebd2b59467
@@ -4,9 +4,12 @@ services:
4
4
  - redis-server
5
5
  cache: bundler
6
6
  rvm:
7
- - 2.1.10
8
- - 2.2.10
9
- - 2.3.7
10
- - 2.4.4
11
- - 2.5.1
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 Andrei Horak
3
+ Copyright (c) 2019 Andrei Horak
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 }
@@ -0,0 +1,13 @@
1
+ require 'pallets'
2
+
3
+ class HelloWorld < Pallets::Workflow
4
+ task :echo
5
+ end
6
+
7
+ class Echo < Pallets::Task
8
+ def run
9
+ puts 'Hello World!'
10
+ end
11
+ end
12
+
13
+ HelloWorld.new.run
@@ -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 ||= Logger.new(STDOUT)
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
- # Saves a job after successfully processing it
10
- def save(workflow_id, job)
9
+ def get_context(workflow_id)
11
10
  raise NotImplementedError
12
11
  end
13
12
 
14
- # Discard a malformed job
15
- def discard(job)
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
- # Schedule a failed job for retry
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
- # Give up job after repeteadly failing to process it
25
- def give_up(job, old_job, at)
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
- @fail_set_key = "#{namespace}:fail-set"
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
- job = @pool.execute do |client|
24
- client.brpoplpush(@queue_key, @reliability_queue_key, timeout: @blocking_timeout)
25
- end
26
- if job
27
- # We store the job's timeout so we know when to retry jobs that are
28
- # still on the reliability queue. We do this separately since there is
29
- # no other way to atomically BRPOPLPUSH from the main queue to a
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 save(workflow_id, job)
39
+ def get_context(workflow_id)
39
40
  @pool.execute do |client|
40
- client.eval(
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 discard(job)
45
+ def save(workflow_id, job, context_buffer)
49
46
  @pool.execute do |client|
50
47
  client.eval(
51
- @scripts['discard'],
52
- [@reliability_queue_key, @reliability_set_key],
53
- [job]
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, at)
65
+ def give_up(job, old_job)
69
66
  @pool.execute do |client|
70
67
  client.eval(
71
68
  @scripts['give_up'],
72
- [@fail_set_key, @reliability_queue_key, @reliability_set_key],
73
- [at, job, old_job]
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.eval(
91
- @scripts['run_workflow'],
92
- [@workflow_key % workflow_id, @queue_key],
93
- jobs_with_order
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, ARGV[1])
3
- redis.call("ZREM", KEYS[4], ARGV[1])
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
@@ -11,7 +11,8 @@ module Pallets
11
11
  end
12
12
 
13
13
  def run
14
- Pallets.logger.info 'Starting the awesomeness of Pallets <3'
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
- @job_timeout = 1800 # 30 minutes
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
@@ -0,0 +1,13 @@
1
+ module Pallets
2
+ # Hash-like class that additionally holds a buffer for all write operations
3
+ class Context < Hash
4
+ def []=(key, value)
5
+ buffer[key] = value
6
+ super
7
+ end
8
+
9
+ def buffer
10
+ @buffer ||= {}
11
+ end
12
+ end
13
+ end
@@ -7,7 +7,11 @@ module Pallets
7
7
  else
8
8
  options.first
9
9
  end
10
- raise ArgumentError, "A task must have a name" unless name
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
@@ -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
@@ -16,6 +16,10 @@ module Pallets
16
16
  @nodes[node]
17
17
  end
18
18
 
19
+ def empty?
20
+ @nodes.empty?
21
+ end
22
+
19
23
  # Returns nodes topologically sorted, together with their order (number of
20
24
  # nodes that have to be executed prior)
21
25
  def sorted_with_order
@@ -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
@@ -1,6 +1,6 @@
1
1
  module Pallets
2
2
  class Manager
3
- attr_reader :workers, :timeout
3
+ attr_reader :workers, :scheduler
4
4
 
5
5
  def initialize(concurrency: Pallets.configuration.concurrency)
6
6
  @workers = concurrency.times.map { Worker.new(self) }
@@ -21,6 +21,10 @@ module Pallets
21
21
  @needs_to_stop
22
22
  end
23
23
 
24
+ def debug
25
+ @thread.backtrace
26
+ end
27
+
24
28
  def id
25
29
  "S#{@thread.object_id.to_s(36)}".upcase if @thread
26
30
  end
@@ -8,7 +8,9 @@ module Pallets
8
8
  end
9
9
 
10
10
  def load(data)
11
- MessagePack.unpack(data)
11
+ # Strings coming from the backend are UTF-8 (Encoding.default_external)
12
+ # while msgpack dumps ASCII-8BIT
13
+ MessagePack.unpack(data.force_encoding('ASCII-8BIT'))
12
14
  end
13
15
  end
14
16
  end
@@ -1,3 +1,3 @@
1
1
  module Pallets
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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 discard it
62
- backend.discard(job)
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(job_hash["context"])
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
- backend.save(job_hash["workflow_id"], job)
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.error "[#{id}] Error while processing: #{ex}"
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, Time.now.to_f)
93
- Pallets.logger.info "[#{id}] Given up on job"
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
@@ -10,7 +10,10 @@ module Pallets
10
10
  end
11
11
 
12
12
  def run
13
- backend.run_workflow(id, jobs_with_order)
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' => id,
37
- 'context' => context,
38
- 'created_at' => Time.now.to_f
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
 
@@ -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 = "pallets"
7
+ spec.name = 'pallets'
8
8
  spec.version = Pallets::VERSION
9
- spec.authors = ["Andrei Horak"]
10
- spec.email = ["linkyndy@gmail.com"]
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{^(test|spec|features)/}) }
17
- spec.bindir = "bin"
18
- spec.executables = ["pallets"]
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.add_dependency "redis"
22
- spec.add_dependency "msgpack"
23
- spec.add_development_dependency "bundler", "~> 1.11"
24
- spec.add_development_dependency "rake", "~> 10.0"
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.2.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: 2018-10-02 00:00:00.000000000 Z
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
- - bin/setup
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: '0'
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.14.1
109
+ rubygems_version: 2.7.6
176
110
  signing_key:
177
111
  specification_version: 4
178
112
  summary: Toy workflow engine, written in Ruby
@@ -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
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,3 +0,0 @@
1
- -- Remove job from reliability queue
2
- redis.call("LREM", KEYS[1], 0, ARGV[1])
3
- redis.call("ZREM", KEYS[2], ARGV[1])