pallets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ --format Fuubar
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ language: ruby
3
+ services:
4
+ - redis-server
5
+ cache: bundler
6
+ rvm:
7
+ - 2.1.10
8
+ - 2.2.10
9
+ - 2.3.7
10
+ - 2.4.4
11
+ - 2.5.1
12
+ script: bundle exec rspec
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pallets.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pallets'
4
+ require 'pallets/cli'
5
+
6
+ cli = Pallets::CLI.new
7
+ cli.run
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
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