pallets 0.5.1 → 0.10.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
2
  SHA256:
3
- metadata.gz: 4ffde33a5c57e9528172a4ba0b75c1a9cbaca30e62fd5ebe0028cad69e5590b9
4
- data.tar.gz: ab123ae6de59a28fe8c99a886ec7299adde7ded2121e55719465aef7ff2c7af2
3
+ metadata.gz: 715e8a2c758fa925afce7091b931d4ea71b02f548290d4daf73604cdd463de3d
4
+ data.tar.gz: 750fd79e4d8aea7920ac4a019ac410d2ec07b910a1cf70e673a628d1604a90de
5
5
  SHA512:
6
- metadata.gz: e96b0c486602044964eb01fafb82ac329d8ca779f70e85d01781a8605e13e141e5e14bdc443e0ce64bbdfed5bf6502f1ca7e77d50bb82afa68d5523e135e1558
7
- data.tar.gz: e02b283810944d41e23ba4c96d8380f7f0a1a5799615d7c73c264af251b6447f9b1ea925bd6cfb790f53c55269b3728480efc0231ee89ce19121800c72955648
6
+ metadata.gz: f3c7d08ad9b0de820f14ec4e3a3bbce77d5e10e9d6bfd99b58273360ce8dc6eeedec8d139d0f9445082848f5b9587dc67b706e49794746cc3208d6cde3aa9516
7
+ data.tar.gz: '0584d9d3faaebea5c615ee6d9c6dc0397d4e65c04cd80b1722a4cea2794bcf5c61a4762a539a2abf2d21e6a9f52de9a7b8f8e5241615168461bdb674a1465772'
@@ -0,0 +1 @@
1
+ github: linkyndy
@@ -4,11 +4,8 @@ services:
4
4
  - redis-server
5
5
  cache: bundler
6
6
  rvm:
7
- - 2.4.6
8
- - 2.5.5
9
- - 2.6.3
10
- before_install:
11
- # Bundler 2.0 needs a newer RubyGems
12
- - gem update --system
13
- - gem install bundler
7
+ - 2.4.10
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
14
11
  script: bundle exec rspec
@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.0] - 2020-08-30
10
+ ### Added
11
+ - configure polling interval for scheduler (#60)
12
+
13
+ ### Changed
14
+ - handle persisting unforseen worker errors more gracefully (#59)
15
+ - add initial wait to scheduler startup (#60)
16
+
17
+ ## [0.9.0] - 2020-07-05
18
+ ### Added
19
+ - limit number of jobs in given up set by number (#56)
20
+ - job duration and metadata to all task logs (#57)
21
+
22
+ ### Changed
23
+ - remove all related workflow keys when giving up on a job (#55)
24
+ - support redis-rb ~> 4.2 (#58)
25
+
26
+ ### Removed
27
+ - support for configuring custom loggers (#57)
28
+
29
+ ## [0.8.0] - 2020-06-09
30
+ ### Added
31
+ - sync output in CLI (#49)
32
+ - support for configuring custom loggers (#50)
33
+
34
+ ### Changed
35
+ - improve job scheduling using jobmasks (#52)
36
+
37
+ ## [0.7.0] - 2020-01-19
38
+ ### Added
39
+ - support for Ruby 2.7 (#46)
40
+
41
+ ## [0.6.0] - 2019-09-02
42
+ ### Added
43
+ - define task aliases in order to reuse tasks within a workflow definition (#44)
44
+ - define anonymous workflows (#45)
45
+
9
46
  ## [0.5.1] - 2019-06-01
10
47
  ### Changed
11
48
  - fix transaction completeness in Appsignal instrumenter (#43)
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Andrei Horak
3
+ Copyright (c) 2020 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.com/linkyndy/pallets.svg?branch=master)](https://travis-ci.com/linkyndy/pallets)
4
4
 
5
- Toy workflow engine, written in Ruby
5
+ Simple and reliable workflow engine, written in Ruby
6
6
 
7
7
  ## It is plain simple!
8
8
 
@@ -11,10 +11,10 @@ Toy workflow engine, written in Ruby
11
11
  require 'pallets'
12
12
 
13
13
  class MyWorkflow < Pallets::Workflow
14
- task Foo
15
- task Bar => Foo
16
- task Baz => Foo
17
- task Qux => [Bar, Baz]
14
+ task 'Foo'
15
+ task 'Bar' => 'Foo'
16
+ task 'Baz' => 'Foo'
17
+ task 'Qux' => ['Bar', 'Baz']
18
18
  end
19
19
 
20
20
  class Foo < Pallets::Task
@@ -120,7 +120,7 @@ end
120
120
 
121
121
  ## Motivation
122
122
 
123
- The main reason for Pallet's existence was the need of a fast, simple and reliable workflow engine, one that is easily extensible with various backends and serializer, one that does not lose your data and one that is intelligent enough to concurrently schedule a workflow's tasks.
123
+ The main reason for Pallets' existence was the need of a fast, simple and reliable workflow engine, one that is easily extensible with various backends and serializer, one that does not lose your data and one that is intelligent enough to concurrently schedule a workflow's tasks.
124
124
 
125
125
  ## Status
126
126
 
@@ -0,0 +1,28 @@
1
+ require 'pallets'
2
+
3
+ class Aliases < Pallets::Workflow
4
+ task 'StartSmtpServer'
5
+ task 'SendEmail', as: 'SayHello', depends_on: 'StartSmtpServer'
6
+ task 'SendEmail', as: 'SayGoodbye', depends_on: 'StartSmtpServer'
7
+ task 'StopSmtpServer' => ['SayHello', 'SayGoodbye']
8
+ end
9
+
10
+ class StartSmtpServer < Pallets::Task
11
+ def run
12
+ puts "Starting SMTP server..."
13
+ end
14
+ end
15
+
16
+ class SendEmail < Pallets::Task
17
+ def run
18
+ puts "* sending e-mail"
19
+ end
20
+ end
21
+
22
+ class StopSmtpServer < Pallets::Task
23
+ def run
24
+ puts "Stopped SMTP server"
25
+ end
26
+ end
27
+
28
+ Aliases.new.run
@@ -0,0 +1,13 @@
1
+ require 'pallets'
2
+
3
+ class Anonymous < Pallets::Task
4
+ def run
5
+ puts 'This is anonymous!'
6
+ end
7
+ end
8
+
9
+ workflow = Pallets::Workflow.build do
10
+ task 'Anonymous'
11
+ end
12
+
13
+ workflow.new.run
@@ -43,7 +43,7 @@ end
43
43
 
44
44
  class Volatile < Pallets::Task
45
45
  def run
46
- raise 'I am ràndomly failing' if [true, false].sample
46
+ raise 'I am randomly failing' if [true, false].sample
47
47
  end
48
48
  end
49
49
 
@@ -38,6 +38,7 @@ module Pallets
38
38
  cls.new(
39
39
  blocking_timeout: configuration.blocking_timeout,
40
40
  failed_job_lifespan: configuration.failed_job_lifespan,
41
+ failed_job_max_count: configuration.failed_job_max_count,
41
42
  job_timeout: configuration.job_timeout,
42
43
  pool_size: configuration.pool_size,
43
44
  **configuration.backend_args
@@ -62,8 +63,4 @@ module Pallets
62
63
  formatter: Pallets::Logger::Formatters::Pretty.new
63
64
  )
64
65
  end
65
-
66
- def self.logger=(logger)
67
- @logger = logger
68
- end
69
66
  end
@@ -6,12 +6,12 @@ module Pallets
6
6
  raise NotImplementedError
7
7
  end
8
8
 
9
- def get_context(workflow_id)
9
+ def get_context(wfid)
10
10
  raise NotImplementedError
11
11
  end
12
12
 
13
13
  # Saves a job after successfully processing it
14
- def save(workflow_id, job, context_buffer)
14
+ def save(wfid, jid, job, context_buffer)
15
15
  raise NotImplementedError
16
16
  end
17
17
 
@@ -20,8 +20,13 @@ module Pallets
20
20
  raise NotImplementedError
21
21
  end
22
22
 
23
+ # Discards malformed job
24
+ def discard(job)
25
+ raise NotImplementedError
26
+ end
27
+
23
28
  # Gives up job after repeteadly failing to process it
24
- def give_up(job, old_job)
29
+ def give_up(wfid, job, old_job)
25
30
  raise NotImplementedError
26
31
  end
27
32
 
@@ -29,7 +34,7 @@ module Pallets
29
34
  raise NotImplementedError
30
35
  end
31
36
 
32
- def run_workflow(workflow_id, jobs_with_dependencies, context)
37
+ def run_workflow(wfid, jobs, jobmasks, context)
33
38
  raise NotImplementedError
34
39
  end
35
40
  end
@@ -9,12 +9,15 @@ module Pallets
9
9
  RETRY_SET_KEY = 'retry-set'
10
10
  GIVEN_UP_SET_KEY = 'given-up-set'
11
11
  WORKFLOW_QUEUE_KEY = 'workflow-queue:%s'
12
+ JOBMASKS_KEY = 'jobmasks:%s'
13
+ JOBMASK_KEY = 'jobmask:%s'
12
14
  CONTEXT_KEY = 'context:%s'
13
15
  REMAINING_KEY = 'remaining:%s'
14
16
 
15
- def initialize(blocking_timeout:, failed_job_lifespan:, job_timeout:, pool_size:, **options)
17
+ def initialize(blocking_timeout:, failed_job_lifespan:, failed_job_max_count:, job_timeout:, pool_size:, **options)
16
18
  @blocking_timeout = blocking_timeout
17
19
  @failed_job_lifespan = failed_job_lifespan
20
+ @failed_job_max_count = failed_job_max_count
18
21
  @job_timeout = job_timeout
19
22
  @pool = Pallets::Pool.new(pool_size) { ::Redis.new(options) }
20
23
 
@@ -41,11 +44,11 @@ module Pallets
41
44
  end
42
45
  end
43
46
 
44
- def save(wfid, job, context_buffer)
47
+ def save(wfid, jid, job, context_buffer)
45
48
  @pool.execute do |client|
46
49
  client.evalsha(
47
50
  @scripts['save'],
48
- [WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid],
51
+ [WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, CONTEXT_KEY % wfid, REMAINING_KEY % wfid, JOBMASK_KEY % jid, JOBMASKS_KEY % wfid],
49
52
  context_buffer.to_a << job
50
53
  )
51
54
  end
@@ -61,12 +64,22 @@ module Pallets
61
64
  end
62
65
  end
63
66
 
64
- def give_up(job, old_job)
67
+ def discard(job)
65
68
  @pool.execute do |client|
66
69
  client.evalsha(
67
- @scripts['give_up'],
70
+ @scripts['discard'],
68
71
  [GIVEN_UP_SET_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY],
69
- [Time.now.to_f, job, old_job, Time.now.to_f - @failed_job_lifespan]
72
+ [Time.now.to_f, job, Time.now.to_f - @failed_job_lifespan, @failed_job_max_count]
73
+ )
74
+ end
75
+ end
76
+
77
+ def give_up(wfid, job, old_job)
78
+ @pool.execute do |client|
79
+ client.evalsha(
80
+ @scripts['give_up'],
81
+ [GIVEN_UP_SET_KEY, RELIABILITY_QUEUE_KEY, RELIABILITY_SET_KEY, JOBMASKS_KEY % wfid, WORKFLOW_QUEUE_KEY % wfid, REMAINING_KEY % wfid, CONTEXT_KEY % wfid],
82
+ [Time.now.to_f, job, old_job, Time.now.to_f - @failed_job_lifespan, @failed_job_max_count]
70
83
  )
71
84
  end
72
85
  end
@@ -81,13 +94,15 @@ module Pallets
81
94
  end
82
95
  end
83
96
 
84
- def run_workflow(wfid, jobs_with_order, context_buffer)
97
+ def run_workflow(wfid, jobs, jobmasks, context_buffer)
85
98
  @pool.execute do |client|
86
99
  client.multi do
100
+ jobmasks.each { |jid, jobmask| client.zadd(JOBMASK_KEY % jid, jobmask) }
101
+ client.sadd(JOBMASKS_KEY % wfid, jobmasks.map { |jid, _| JOBMASK_KEY % jid }) unless jobmasks.empty?
87
102
  client.evalsha(
88
103
  @scripts['run_workflow'],
89
104
  [WORKFLOW_QUEUE_KEY % wfid, QUEUE_KEY, REMAINING_KEY % wfid],
90
- jobs_with_order
105
+ jobs
91
106
  )
92
107
  client.hmset(CONTEXT_KEY % wfid, *context_buffer.to_a) unless context_buffer.empty?
93
108
  end
@@ -0,0 +1,11 @@
1
+ -- Remove job from reliability queue
2
+ redis.call("LREM", KEYS[2], 0, ARGV[2])
3
+ redis.call("ZREM", KEYS[3], ARGV[2])
4
+
5
+ -- Add job and its fail time (score) to failed sorted set
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) and make sure the number of jobs is capped
10
+ redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[3])
11
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, -ARGV[4] - 1)
@@ -5,6 +5,11 @@ redis.call("ZREM", KEYS[3], ARGV[3])
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
7
 
8
+ -- Remove all related workflow keys
9
+ local keys = redis.call("SMEMBERS", KEYS[4])
10
+ redis.call("DEL", KEYS[4], KEYS[5], KEYS[6], KEYS[7], unpack(keys))
11
+
8
12
  -- Remove any jobs that have been given up long enough ago (their score is
9
- -- below given value)
13
+ -- below given value) and make sure the number of jobs is capped
10
14
  redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[4])
15
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, -ARGV[5] - 1)
@@ -6,9 +6,8 @@ redis.call("SET", KEYS[3], eta)
6
6
 
7
7
  -- Queue jobs that are ready to be processed (their score is 0) and
8
8
  -- remove queued jobs from the sorted set
9
- local count = redis.call("ZCOUNT", KEYS[1], 0, 0)
10
- if count > 0 then
11
- local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
9
+ local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
10
+ if #work > 0 then
12
11
  redis.call("LPUSH", KEYS[2], unpack(work))
13
12
  redis.call("ZREM", KEYS[1], unpack(work))
14
13
  end
@@ -10,24 +10,21 @@ if #ARGV > 0 then
10
10
  redis.call("HMSET", KEYS[5], unpack(ARGV))
11
11
  end
12
12
 
13
- -- Decrement all jobs from the sorted set
14
- local all_pending = redis.call("ZRANGE", KEYS[1], 0, -1)
15
- for score, task in pairs(all_pending) do
16
- redis.call("ZINCRBY", KEYS[1], -1, task)
17
- end
13
+ -- Decrement jobs from the sorted set by applying a jobmask
14
+ redis.call("ZUNIONSTORE", KEYS[1], 2, KEYS[1], KEYS[7])
15
+ redis.call("DEL", KEYS[7])
18
16
 
19
17
  -- Queue jobs that are ready to be processed (their score is 0) and
20
18
  -- remove queued jobs from sorted set
21
- local count = redis.call("ZCOUNT", KEYS[1], 0, 0)
22
- if count > 0 then
23
- local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
19
+ local work = redis.call("ZRANGEBYSCORE", KEYS[1], 0, 0)
20
+ if #work > 0 then
24
21
  redis.call("LPUSH", KEYS[2], unpack(work))
25
22
  redis.call("ZREM", KEYS[1], unpack(work))
26
23
  end
27
24
 
28
- -- 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])
25
+ -- Decrement ETA and remove it together with the context and jobmasks if all
26
+ -- tasks have been processed (ETA is 0) or if workflow has been given up (ETA is -1)
27
+ local remaining = redis.call("DECR", KEYS[6])
28
+ if remaining <= 0 then
29
+ redis.call("DEL", KEYS[5], KEYS[6], KEYS[8])
33
30
  end
@@ -1,5 +1,7 @@
1
1
  require 'optparse'
2
2
 
3
+ $stdout.sync = true
4
+
3
5
  module Pallets
4
6
  class CLI
5
7
  def initialize
@@ -61,10 +63,18 @@ module Pallets
61
63
  Pallets.configuration.max_failures = max_failures
62
64
  end
63
65
 
66
+ opts.on('-i', '--scheduler-polling-interval NUM', Integer, 'Seconds between scheduler backend polls') do |scheduler_polling_interval|
67
+ Pallets.configuration.scheduler_polling_interval = scheduler_polling_interval
68
+ end
69
+
64
70
  opts.on('-l', '--failed-job-lifespan NUM', Integer, 'Seconds a job stays in the given up set') do |failed_job_lifespan|
65
71
  Pallets.configuration.failed_job_lifespan = failed_job_lifespan
66
72
  end
67
73
 
74
+ opts.on('-m', '--failed-job-max-count NUM', Integer, 'Maximum number of jobs in the given up set') do |failed_job_max_count|
75
+ Pallets.configuration.failed_job_max_count = failed_job_max_count
76
+ end
77
+
68
78
  opts.on('-p', '--pool-size NUM', Integer, 'Size of backend pool') do |pool_size|
69
79
  Pallets.configuration.pool_size = pool_size
70
80
  end
@@ -16,6 +16,10 @@ module Pallets
16
16
  # this period, jobs will be permanently deleted
17
17
  attr_accessor :failed_job_lifespan
18
18
 
19
+ # Maximum number of failed jobs that can be in the given up set. When this
20
+ # number is reached, the oldest jobs will be permanently deleted
21
+ attr_accessor :failed_job_max_count
22
+
19
23
  # Number of seconds allowed for a job to be processed. If a job exceeds this
20
24
  # period, it is considered failed, and scheduled to be processed again
21
25
  attr_accessor :job_timeout
@@ -27,6 +31,12 @@ module Pallets
27
31
  # Number of connections to the backend
28
32
  attr_writer :pool_size
29
33
 
34
+ # Number of seconds at which the scheduler checks whether there are jobs
35
+ # due to be (re)processed. Note that this interval is per process; it might
36
+ # require tweaking in case of running multiple Pallets instances, so that
37
+ # the backend is not polled too often
38
+ attr_accessor :scheduler_polling_interval
39
+
30
40
  # Serializer used for jobs
31
41
  attr_accessor :serializer
32
42
 
@@ -45,8 +55,10 @@ module Pallets
45
55
  @blocking_timeout = 5
46
56
  @concurrency = 2
47
57
  @failed_job_lifespan = 7_776_000 # 3 months
58
+ @failed_job_max_count = 1_000
48
59
  @job_timeout = 1_800 # 30 minutes
49
60
  @max_failures = 3
61
+ @scheduler_polling_interval = 10
50
62
  @serializer = :json
51
63
  @middleware = default_middleware
52
64
  end
@@ -1,7 +1,13 @@
1
1
  module Pallets
2
2
  module DSL
3
3
  module Workflow
4
- def task(arg, depends_on: nil, max_failures: nil, &block)
4
+ def task(arg=nil, as: nil, depends_on: nil, max_failures: nil, **kwargs)
5
+ # Have to work more to keep Pallets' nice DSL valid in Ruby 2.7
6
+ arg = !kwargs.empty? ? kwargs : arg
7
+ raise ArgumentError, 'Task is incorrectly defined. It must receive '\
8
+ 'either a name, or a name => dependencies pair as '\
9
+ 'the first argument' unless arg
10
+
5
11
  klass, dependencies = case arg
6
12
  when Hash
7
13
  # The `task Foo => Bar` notation
@@ -12,10 +18,13 @@ module Pallets
12
18
  end
13
19
 
14
20
  task_class = klass.to_s
21
+ as ||= task_class
22
+
15
23
  dependencies = Array(dependencies).compact.uniq.map(&:to_s)
16
- graph.add(task_class, dependencies)
24
+ graph.add(as, dependencies)
17
25
 
18
- task_config[task_class] = {
26
+ task_config[as] = {
27
+ 'workflow_class' => self.name,
19
28
  'task_class' => task_class,
20
29
  'max_failures' => max_failures || Pallets.configuration.max_failures
21
30
  }
@@ -9,40 +9,39 @@ module Pallets
9
9
  end
10
10
 
11
11
  def add(node, dependencies)
12
- @nodes[node] = dependencies
12
+ raise WorkflowError, "Task #{node} is already defined in this workflow. "\
13
+ "Use `task '#{node}', as: 'FooBar'` to define an "\
14
+ "alias and reuse task" if nodes.key?(node)
15
+
16
+ nodes[node] = dependencies
13
17
  end
14
18
 
15
19
  def parents(node)
16
- @nodes[node]
20
+ nodes[node]
17
21
  end
18
22
 
19
23
  def empty?
20
- @nodes.empty?
24
+ nodes.empty?
21
25
  end
22
26
 
23
- # Returns nodes topologically sorted, together with their order (number of
24
- # nodes that have to be executed prior)
25
- def sorted_with_order
26
- # Identify groups of nodes that can be executed concurrently
27
- groups = tsort_each.slice_when { |a, b| parents(a) != parents(b) }
28
-
29
- # Assign order to each node
30
- i = 0
31
- groups.flat_map do |group|
32
- group_with_order = group.product([i])
33
- i += group.size
34
- group_with_order
27
+ def each
28
+ return enum_for(__method__) unless block_given?
29
+
30
+ tsort_each do |node|
31
+ yield(node, parents(node))
35
32
  end
36
33
  end
37
34
 
38
35
  private
39
36
 
37
+ attr_reader :nodes
38
+
40
39
  def tsort_each_node(&block)
41
- @nodes.each_key(&block)
40
+ nodes.each_key(&block)
42
41
  end
43
42
 
44
43
  def tsort_each_child(node, &block)
45
- @nodes.fetch(node).each(&block)
44
+ nodes.fetch(node).each(&block)
46
45
  rescue KeyError
47
46
  raise WorkflowError, "Task #{node} is marked as a dependency but not defined"
48
47
  end
@@ -5,14 +5,22 @@ module Pallets
5
5
  class Logger < ::Logger
6
6
  # Overwrite severity methods to add metadata capabilities
7
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?
8
+ define_method severity do |message|
9
+ metadata = Thread.current[:pallets_log_metadata]
10
+ return super(message) if metadata.nil?
10
11
 
11
12
  formatted_metadata = ' ' + metadata.map { |k, v| "#{k}=#{v}" }.join(' ')
12
13
  super(formatted_metadata) { message }
13
14
  end
14
15
  end
15
16
 
17
+ def with_metadata(hash)
18
+ Thread.current[:pallets_log_metadata] = hash
19
+ yield
20
+ ensure
21
+ Thread.current[:pallets_log_metadata] = nil
22
+ end
23
+
16
24
  module Formatters
17
25
  class Pretty < ::Logger::Formatter
18
26
  def call(severity, time, metadata, message)
@@ -2,14 +2,21 @@ module Pallets
2
2
  module Middleware
3
3
  class JobLogger
4
4
  def self.call(worker, job, context)
5
- Pallets.logger.info 'Started', extract_metadata(worker.id, job)
6
- result = yield
7
- Pallets.logger.info 'Done', extract_metadata(worker.id, job)
8
- result
9
- rescue => ex
10
- Pallets.logger.warn "#{ex.class.name}: #{ex.message}", extract_metadata(worker.id, job)
11
- Pallets.logger.warn ex.backtrace.join("\n"), extract_metadata(worker.id, job) unless ex.backtrace.nil?
12
- raise
5
+ start_time = current_time
6
+
7
+ Pallets.logger.with_metadata(extract_metadata(worker.id, job)) do
8
+ begin
9
+ Pallets.logger.info 'Started'
10
+ result = yield
11
+ Pallets.logger.info "Done in #{(current_time - start_time).round(3)}s"
12
+ result
13
+ rescue => ex
14
+ Pallets.logger.warn "Failed after #{(current_time - start_time).round(3)}s"
15
+ Pallets.logger.warn "#{ex.class.name}: #{ex.message}"
16
+ Pallets.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
17
+ raise
18
+ end
19
+ end
13
20
  end
14
21
 
15
22
  def self.extract_metadata(wid, job)
@@ -21,6 +28,10 @@ module Pallets
21
28
  tsk: job['task_class'],
22
29
  }
23
30
  end
31
+
32
+ def self.current_time
33
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
+ end
24
35
  end
25
36
  end
26
37
  end
@@ -7,7 +7,7 @@ module Pallets
7
7
  end
8
8
 
9
9
  def start
10
- @thread ||= Thread.new { work }
10
+ @thread ||= Thread.new { wait_initial_bit; work }
11
11
  end
12
12
 
13
13
  def shutdown
@@ -40,11 +40,17 @@ module Pallets
40
40
  end
41
41
  end
42
42
 
43
- def wait_a_bit
44
- # Wait for roughly 10 seconds
43
+ def wait_initial_bit
44
+ # Randomly wait a bit before starting working, so that multiple processes
45
+ # will not hit the backend all at once
46
+ wait_a_bit(rand(Pallets.configuration.scheduler_polling_interval))
47
+ end
48
+
49
+ def wait_a_bit(seconds = Pallets.configuration.scheduler_polling_interval)
50
+ # Wait for roughly the configured number of seconds
45
51
  # We don't want to block the entire polling interval, since we want to
46
52
  # deal with shutdowns synchronously and as fast as possible
47
- 10.times do
53
+ seconds.times do
48
54
  break if needs_to_stop?
49
55
  sleep 1
50
56
  end
@@ -1,3 +1,3 @@
1
1
  module Pallets
2
- VERSION = "0.5.1"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -53,8 +53,10 @@ module Pallets
53
53
  rescue Pallets::Shutdown
54
54
  @manager.remove_worker(self)
55
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?
56
+ Pallets.logger.error "#{ex.class.name}: #{ex.message}"
57
+ Pallets.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
58
+ # Do not flood the process in case of persisting unforeseen errors
59
+ sleep 1
58
60
  @manager.replace_worker(self)
59
61
  end
60
62
 
@@ -64,8 +66,8 @@ module Pallets
64
66
  rescue
65
67
  # We ensure only valid jobs are created. If something fishy reaches this
66
68
  # 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
69
+ backend.discard(job)
70
+ Pallets.logger.error "Could not deserialize #{job}. Gave up job"
69
71
  return
70
72
  end
71
73
 
@@ -103,7 +105,7 @@ module Pallets
103
105
  retry_at = Time.now.to_f + backoff_in_seconds(failures)
104
106
  backend.retry(new_job, job, retry_at)
105
107
  else
106
- backend.give_up(new_job, job)
108
+ backend.give_up(job_hash['wfid'], new_job, job)
107
109
  end
108
110
  end
109
111
 
@@ -112,11 +114,11 @@ module Pallets
112
114
  'given_up_at' => Time.now.to_f,
113
115
  'reason' => 'returned_false'
114
116
  ))
115
- backend.give_up(new_job, job)
117
+ backend.give_up(job_hash['wfid'], new_job, job)
116
118
  end
117
119
 
118
120
  def handle_job_success(context, job, job_hash)
119
- backend.save(job_hash['wfid'], job, serializer.dump_context(context.buffer))
121
+ backend.save(job_hash['wfid'], job_hash['jid'], job, serializer.dump_context(context.buffer))
120
122
  end
121
123
 
122
124
  def backoff_in_seconds(count)
@@ -4,6 +4,12 @@ module Pallets
4
4
 
5
5
  attr_reader :context
6
6
 
7
+ def self.build(&block)
8
+ Class.new(self).tap do |workflow_class|
9
+ workflow_class.instance_eval(&block)
10
+ end
11
+ end
12
+
7
13
  def initialize(context_hash = {})
8
14
  @id = nil
9
15
  # Passed in context hash needs to be buffered
@@ -14,7 +20,7 @@ module Pallets
14
20
  raise WorkflowError, "#{self.class.name} has no tasks. Workflows "\
15
21
  "must contain at least one task" if self.class.graph.empty?
16
22
 
17
- backend.run_workflow(id, jobs_with_order, serializer.dump_context(context.buffer))
23
+ backend.run_workflow(id, *prepare_jobs, serializer.dump_context(context.buffer))
18
24
  id
19
25
  end
20
26
 
@@ -24,20 +30,29 @@ module Pallets
24
30
 
25
31
  private
26
32
 
27
- def jobs_with_order
28
- self.class.graph.sorted_with_order.map do |task_class, order|
29
- job = serializer.dump(construct_job(task_class))
30
- [order, job]
33
+ def prepare_jobs
34
+ jobs = []
35
+ jobmasks = Hash.new { |h, k| h[k] = [] }
36
+ acc = {}
37
+
38
+ self.class.graph.each do |task_alias, dependencies|
39
+ job_hash = construct_job(task_alias)
40
+ acc[task_alias] = job_hash['jid']
41
+ job = serializer.dump(job_hash)
42
+
43
+ jobs << [dependencies.size, job]
44
+ dependencies.each { |d| jobmasks[acc[d]] << [-1, job] }
31
45
  end
46
+
47
+ [jobs, jobmasks]
32
48
  end
33
49
 
34
- def construct_job(task_class)
35
- {}.tap do |job|
50
+ def construct_job(task_alias)
51
+ Hash[self.class.task_config[task_alias]].tap do |job|
36
52
  job['wfid'] = id
37
- job['jid'] = "J#{Pallets::Util.generate_id(task_class)}".upcase
38
- job['workflow_class'] = self.class.name
53
+ job['jid'] = "J#{Pallets::Util.generate_id(job['task_class'])}".upcase
39
54
  job['created_at'] = Time.now.to_f
40
- end.merge(self.class.task_config[task_class])
55
+ end
41
56
  end
42
57
 
43
58
  def backend
@@ -48,6 +63,10 @@ module Pallets
48
63
  Pallets.serializer
49
64
  end
50
65
 
66
+ def self.name
67
+ @name ||= super || '<Anonymous>'
68
+ end
69
+
51
70
  def self.task_config
52
71
  @task_config ||= {}
53
72
  end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Andrei Horak']
10
10
  spec.email = ['linkyndy@gmail.com']
11
11
 
12
- spec.summary = 'Toy workflow engine, written in Ruby'
13
- spec.description = 'Toy workflow engine, written in Ruby'
12
+ spec.summary = 'Simple and reliable workflow engine, written in Ruby'
13
+ spec.description = 'Simple and reliable workflow engine, written in Ruby'
14
14
  spec.homepage = 'https://github.com/linkyndy/pallets'
15
15
  spec.license = 'MIT'
16
16
 
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.required_ruby_version = '>= 2.4'
22
22
 
23
- spec.add_dependency 'redis'
23
+ spec.add_dependency 'redis', '~> 4.2'
24
24
  spec.add_dependency 'msgpack'
25
25
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pallets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Horak
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-01 00:00:00.000000000 Z
11
+ date: 2020-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '4.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '4.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: msgpack
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- description: Toy workflow engine, written in Ruby
41
+ description: Simple and reliable workflow engine, written in Ruby
42
42
  email:
43
43
  - linkyndy@gmail.com
44
44
  executables:
@@ -46,6 +46,7 @@ executables:
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
+ - ".github/FUNDING.yml"
49
50
  - ".gitignore"
50
51
  - ".rspec"
51
52
  - ".travis.yml"
@@ -56,6 +57,8 @@ files:
56
57
  - README.md
57
58
  - Rakefile
58
59
  - bin/pallets
60
+ - examples/aliases.rb
61
+ - examples/anonymous.rb
59
62
  - examples/appsignal.rb
60
63
  - examples/config/appsignal.yml
61
64
  - examples/config_savvy.rb
@@ -64,6 +67,7 @@ files:
64
67
  - lib/pallets.rb
65
68
  - lib/pallets/backends/base.rb
66
69
  - lib/pallets/backends/redis.rb
70
+ - lib/pallets/backends/scripts/discard.lua
67
71
  - lib/pallets/backends/scripts/give_up.lua
68
72
  - lib/pallets/backends/scripts/reschedule_all.lua
69
73
  - lib/pallets/backends/scripts/retry.lua
@@ -95,7 +99,7 @@ homepage: https://github.com/linkyndy/pallets
95
99
  licenses:
96
100
  - MIT
97
101
  metadata: {}
98
- post_install_message:
102
+ post_install_message:
99
103
  rdoc_options: []
100
104
  require_paths:
101
105
  - lib
@@ -110,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
114
  - !ruby/object:Gem::Version
111
115
  version: '0'
112
116
  requirements: []
113
- rubygems_version: 3.0.3
114
- signing_key:
117
+ rubygems_version: 3.1.2
118
+ signing_key:
115
119
  specification_version: 4
116
- summary: Toy workflow engine, written in Ruby
120
+ summary: Simple and reliable workflow engine, written in Ruby
117
121
  test_files: []