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 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])