pallets 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTING.md +19 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +91 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/pallets +7 -0
- data/bin/setup +8 -0
- data/lib/pallets.rb +53 -0
- data/lib/pallets/backends/base.rb +38 -0
- data/lib/pallets/backends/redis.rb +109 -0
- data/lib/pallets/backends/scripts/discard.lua +3 -0
- data/lib/pallets/backends/scripts/give_up.lua +6 -0
- data/lib/pallets/backends/scripts/reschedule_all.lua +22 -0
- data/lib/pallets/backends/scripts/retry.lua +6 -0
- data/lib/pallets/backends/scripts/run_workflow.lua +11 -0
- data/lib/pallets/backends/scripts/save.lua +18 -0
- data/lib/pallets/cli.rb +102 -0
- data/lib/pallets/configuration.rb +44 -0
- data/lib/pallets/dsl/workflow.rb +32 -0
- data/lib/pallets/errors.rb +4 -0
- data/lib/pallets/graph.rb +44 -0
- data/lib/pallets/manager.rb +57 -0
- data/lib/pallets/pool.rb +26 -0
- data/lib/pallets/scheduler.rb +53 -0
- data/lib/pallets/serializers/base.rb +13 -0
- data/lib/pallets/serializers/json.rb +15 -0
- data/lib/pallets/task.rb +12 -0
- data/lib/pallets/version.rb +3 -0
- data/lib/pallets/worker.rb +109 -0
- data/lib/pallets/workflow.rb +58 -0
- data/pallets.gemspec +28 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fd28e870c825c99c372243b13d14574d3d6ec36b83bf031230938fe0121caeae
|
4
|
+
data.tar.gz: f9ad65469c6d58510c01ed22591e875c941a3609eaff4191b1156ee6215a1748
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 33aaa7382095bda19a78c7baa5bfab9cc2246aa3b8b309706b54ca381f34f06df2e0196a04b110c6daee6b5720894ba12ddadd89e952b947014dbb0df85ed8df
|
7
|
+
data.tar.gz: 69a052b8663b578757bcda235a2d46159cc74871c84efe319cbb5da1aaba3992d5bbc84261e65e8b0cd51e95f3022a67dc35dc27e696f6b648febc2054fe0e3a
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
## Specific to RubyMotion:
|
14
|
+
.dat*
|
15
|
+
.repl_history
|
16
|
+
build/
|
17
|
+
|
18
|
+
## Documentation cache and generated files:
|
19
|
+
/.yardoc/
|
20
|
+
/_yardoc/
|
21
|
+
/doc/
|
22
|
+
/rdoc/
|
23
|
+
|
24
|
+
## Environment normalization:
|
25
|
+
/.bundle/
|
26
|
+
/vendor/bundle
|
27
|
+
/lib/bundler/man/
|
28
|
+
|
29
|
+
# for a library or gem, you might want to ignore these files since the code is
|
30
|
+
# intended to run in multiple environments; otherwise, check them in:
|
31
|
+
Gemfile.lock
|
32
|
+
.ruby-version
|
33
|
+
.ruby-gemset
|
34
|
+
|
35
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
36
|
+
.rvmrc
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
Any contribution is **highly** appreciated, no matter whether it is a small bug fix, feature implementation or typo in the docs.
|
4
|
+
|
5
|
+
You can contribute in many ways:
|
6
|
+
|
7
|
+
* report bugs;
|
8
|
+
* fix bugs;
|
9
|
+
* implement features;
|
10
|
+
* write documentation;
|
11
|
+
* simply tell us what you love about pallets or what you'd like to see in it!
|
12
|
+
|
13
|
+
## Getting Started!
|
14
|
+
|
15
|
+
**Fork** this repo, do your magic, `rspec` and submit a pull request on the **master** branch!
|
16
|
+
|
17
|
+
> Issues, features, bugs and questions are tracked at https://github.com/linkyndy/pallets/issues
|
18
|
+
|
19
|
+
> Documentation is found at https://github.com/linkyndy/pallets/wiki
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 Andrei Horak
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Pallets
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.com/linkyndy/pallets.svg?branch=master)](https://travis-ci.com/linkyndy/pallets)
|
4
|
+
|
5
|
+
Toy workflow engine, written in Ruby
|
6
|
+
|
7
|
+
## It is plain simple!
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# my_workflow.rb
|
11
|
+
require 'pallets'
|
12
|
+
|
13
|
+
class MyWorkflow < Pallets::Workflow
|
14
|
+
task :foo
|
15
|
+
task :bar => :foo
|
16
|
+
task :baz => :foo
|
17
|
+
task :qux => [:bar, :baz]
|
18
|
+
end
|
19
|
+
|
20
|
+
class Foo < Pallets::Task
|
21
|
+
def run
|
22
|
+
puts 'I love Pallets! <3'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# [other task definitions are ommited, for now]
|
26
|
+
|
27
|
+
MyWorkflow.new.run
|
28
|
+
```
|
29
|
+
|
30
|
+
That's basically it! Curious for more? Read on!
|
31
|
+
|
32
|
+
> Don't forget to run pallets, so it can process your tasks: `bundle exec pallets -r ./my_workflow`
|
33
|
+
|
34
|
+
## Features
|
35
|
+
|
36
|
+
* faaast!
|
37
|
+
* reliable
|
38
|
+
* retries failed tasks
|
39
|
+
* Redis backend out of the box
|
40
|
+
* JSON serializer out of the box
|
41
|
+
* beautiful DSL
|
42
|
+
* convention over configuration
|
43
|
+
* thoroughly tested
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
```
|
48
|
+
# Gemfile
|
49
|
+
gem 'pallets'
|
50
|
+
|
51
|
+
# or
|
52
|
+
|
53
|
+
gem install pallets
|
54
|
+
```
|
55
|
+
|
56
|
+
## Configuration
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Pallets.configure do |c|
|
60
|
+
# How many workers to process incoming jobs?
|
61
|
+
c.concurrency = 2
|
62
|
+
|
63
|
+
c.backend = :redis
|
64
|
+
c.serializer = :json
|
65
|
+
|
66
|
+
c.backend_args = { db: 1 }
|
67
|
+
|
68
|
+
# What's the maximum allowed time to process a job?
|
69
|
+
c.job_timeout = 1800
|
70
|
+
# How many times should a job be retried?
|
71
|
+
c.max_failures = 3
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
For the complete set of options, see [pallets/configuration.rb](lib/pallets/configuration.rb)
|
76
|
+
|
77
|
+
## Motivation
|
78
|
+
|
79
|
+
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.
|
80
|
+
|
81
|
+
## Status
|
82
|
+
|
83
|
+
Pallets is under active development and it is not _yet_ production-ready.
|
84
|
+
|
85
|
+
## How to contribute?
|
86
|
+
|
87
|
+
Any contribution is **highly** appreciated! See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
|
88
|
+
|
89
|
+
## License
|
90
|
+
|
91
|
+
See [LICENSE](LICENSE)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
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/pallets
ADDED
data/bin/setup
ADDED
data/lib/pallets.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "pallets/version"
|
2
|
+
|
3
|
+
require 'pallets/backends/base'
|
4
|
+
require 'pallets/backends/redis'
|
5
|
+
require 'pallets/configuration'
|
6
|
+
require 'pallets/dsl/workflow'
|
7
|
+
require 'pallets/errors'
|
8
|
+
require 'pallets/graph'
|
9
|
+
require 'pallets/manager'
|
10
|
+
require 'pallets/pool'
|
11
|
+
require 'pallets/scheduler'
|
12
|
+
require 'pallets/serializers/base'
|
13
|
+
require 'pallets/serializers/json'
|
14
|
+
require 'pallets/task'
|
15
|
+
require 'pallets/worker'
|
16
|
+
require 'pallets/workflow'
|
17
|
+
|
18
|
+
require 'active_support/inflector'
|
19
|
+
require 'logger'
|
20
|
+
|
21
|
+
module Pallets
|
22
|
+
def self.configuration
|
23
|
+
@configuration ||= Configuration.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.configure
|
27
|
+
yield configuration
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.backend
|
31
|
+
@backend ||= begin
|
32
|
+
cls = "Pallets::Backends::#{configuration.backend.capitalize}".constantize
|
33
|
+
cls.new(
|
34
|
+
namespace: configuration.namespace,
|
35
|
+
blocking_timeout: configuration.blocking_timeout,
|
36
|
+
job_timeout: configuration.job_timeout,
|
37
|
+
pool_size: configuration.pool_size,
|
38
|
+
**configuration.backend_args
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.serializer
|
44
|
+
@serializer ||= begin
|
45
|
+
cls = "Pallets::Serializers::#{configuration.serializer.capitalize}".constantize
|
46
|
+
cls.new
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.logger
|
51
|
+
@logger ||= Logger.new(STDOUT)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Pallets
|
2
|
+
module Backends
|
3
|
+
class Base
|
4
|
+
# Picks a job that is ready for processing
|
5
|
+
def pick
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Saves a job after successfully processing it
|
10
|
+
def save(workflow_id, job)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
# Discard a malformed job
|
15
|
+
def discard(job)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Schedule a failed job for retry
|
20
|
+
def retry(job, old_job, at)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
# Give up job after repeteadly failing to process it
|
25
|
+
def give_up(job, old_job, at)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def reschedule_all(earlier_than)
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_workflow(workflow_id, jobs_with_dependencies)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Pallets
|
4
|
+
module Backends
|
5
|
+
class Redis < Base
|
6
|
+
def initialize(namespace:, blocking_timeout:, job_timeout:, pool_size:, **options)
|
7
|
+
@namespace = namespace
|
8
|
+
@blocking_timeout = blocking_timeout
|
9
|
+
@job_timeout = job_timeout
|
10
|
+
@pool = Pallets::Pool.new(pool_size) { ::Redis.new(options) }
|
11
|
+
|
12
|
+
@queue_key = "#{namespace}:queue"
|
13
|
+
@reliability_queue_key = "#{namespace}:reliability-queue"
|
14
|
+
@reliability_set_key = "#{namespace}:reliability-set"
|
15
|
+
@retry_set_key = "#{namespace}:retry-set"
|
16
|
+
@fail_set_key = "#{namespace}:fail-set"
|
17
|
+
@workflow_key = "#{namespace}:workflows:%s"
|
18
|
+
|
19
|
+
register_scripts
|
20
|
+
end
|
21
|
+
|
22
|
+
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|
|
32
|
+
client.zadd(@reliability_set_key, Time.now.to_f + @job_timeout, job)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
job
|
36
|
+
end
|
37
|
+
|
38
|
+
def save(workflow_id, job)
|
39
|
+
@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
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def discard(job)
|
49
|
+
@pool.execute do |client|
|
50
|
+
client.eval(
|
51
|
+
@scripts['discard'],
|
52
|
+
[@reliability_queue_key, @reliability_set_key],
|
53
|
+
[job]
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def retry(job, old_job, at)
|
59
|
+
@pool.execute do |client|
|
60
|
+
client.eval(
|
61
|
+
@scripts['retry'],
|
62
|
+
[@retry_set_key, @reliability_queue_key, @reliability_set_key],
|
63
|
+
[at, job, old_job]
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def give_up(job, old_job, at)
|
69
|
+
@pool.execute do |client|
|
70
|
+
client.eval(
|
71
|
+
@scripts['give_up'],
|
72
|
+
[@fail_set_key, @reliability_queue_key, @reliability_set_key],
|
73
|
+
[at, job, old_job]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def reschedule_all(earlier_than)
|
79
|
+
@pool.execute do |client|
|
80
|
+
client.eval(
|
81
|
+
@scripts['reschedule_all'],
|
82
|
+
[@reliability_set_key, @reliability_queue_key, @retry_set_key, @queue_key],
|
83
|
+
[earlier_than]
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def run_workflow(workflow_id, jobs_with_order)
|
89
|
+
@pool.execute do |client|
|
90
|
+
client.eval(
|
91
|
+
@scripts['run_workflow'],
|
92
|
+
[@workflow_key % workflow_id, @queue_key],
|
93
|
+
jobs_with_order
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def register_scripts
|
101
|
+
@scripts ||= Dir["#{__dir__}/scripts/*.lua"].map do |file|
|
102
|
+
name = File.basename(file, '.lua')
|
103
|
+
script = File.read(file)
|
104
|
+
[name, script]
|
105
|
+
end.to_h
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|