ductwork 0.14.1 → 0.15.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +5 -3
- data/lib/ductwork/context.rb +53 -0
- data/lib/ductwork/engine.rb +0 -18
- data/lib/ductwork/models/job.rb +13 -17
- data/lib/ductwork/models/pipeline.rb +4 -0
- data/lib/ductwork/models/step.rb +15 -0
- data/lib/ductwork/models/tuple.rb +30 -0
- data/lib/ductwork/version.rb +1 -1
- data/lib/ductwork.rb +20 -0
- data/lib/generators/ductwork/install/install_generator.rb +2 -0
- data/lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb +16 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd378a2cedabe241b11559ecc7611a6ca8aa631ca2739a97199fdf4a34387f4e
|
|
4
|
+
data.tar.gz: 3faddccd331cd7bd94cc7e1487a4f4f823fc0037ca7109bf2c093ac357488263
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb6051a653d7aa5f9dbf4cc0784ed69d364784fb14d3d24120683616eb2601a4d4f97c028c2cc8df137c2a992429af450f63589f6381977dcbabc68eb926314f
|
|
7
|
+
data.tar.gz: 8e484847a038021d399272e3565a58b4204f2752d346b515ee59503204fae7f6ee3756c780376b4df03dd73d960ef191c484dd0bd2ec33e384b268272a63aae8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Ductwork Changelog
|
|
2
2
|
|
|
3
|
+
## [0.15.0]
|
|
4
|
+
|
|
5
|
+
- chore: remove unnecessary transaction in job enqueueing method
|
|
6
|
+
- feat: expose pipeline and step classes validation method instead of running in a rails initializer
|
|
7
|
+
- feat!: introduce pipeline Context - this is a BREAKING CHANGE because step classes now need to inherit from `Ductwork::Step`; they are no longer POROs
|
|
8
|
+
|
|
3
9
|
## [0.14.1]
|
|
4
10
|
|
|
5
11
|
- fix: check if steps and pipelines directories exist before eager loading
|
data/README.md
CHANGED
|
@@ -7,6 +7,8 @@ A Ruby pipeline framework.
|
|
|
7
7
|
|
|
8
8
|
Ductwork lets you build complex pipelines quickly and easily using intuitive Ruby tooling and a natural DSL. No need to learn complicated unified object models or stand up separate runner instances—just write Ruby code and let Ductwork handle the orchestration.
|
|
9
9
|
|
|
10
|
+
There is also a paid [Ductwork Pro](https://www.getductwork.io/) version with more features and support. See the [Pricing](https://www.getductwork.io/#pricing) page to buy a license.
|
|
11
|
+
|
|
10
12
|
**[Full Documentation](https://docs.getductwork.io/)**
|
|
11
13
|
|
|
12
14
|
## Installation
|
|
@@ -60,7 +62,7 @@ end
|
|
|
60
62
|
|
|
61
63
|
### 2. Define Steps
|
|
62
64
|
|
|
63
|
-
Steps are
|
|
65
|
+
Steps are Ruby objects that inherit from `Ductwork::Step` and implement two methods:
|
|
64
66
|
- `initialize` - accepts parameters from the trigger call or previous step's return value
|
|
65
67
|
- `execute` - performs the work and returns data for the next step
|
|
66
68
|
|
|
@@ -68,7 +70,7 @@ Steps live in `app/steps`:
|
|
|
68
70
|
|
|
69
71
|
```ruby
|
|
70
72
|
# app/steps/users_requiring_enrichment.rb
|
|
71
|
-
class
|
|
73
|
+
class QueryUsersRequiringEnrichment < Ductwork::Step
|
|
72
74
|
def initialize(days_outdated)
|
|
73
75
|
@days_outdated = days_outdated
|
|
74
76
|
end
|
|
@@ -90,7 +92,7 @@ Connect steps together using Ductwork's fluent interface DSL. The key principle:
|
|
|
90
92
|
```ruby
|
|
91
93
|
class EnrichUserDataPipeline < Ductwork::Pipeline
|
|
92
94
|
define do |pipeline|
|
|
93
|
-
pipeline.start(
|
|
95
|
+
pipeline.start(QueryUsersRequiringEnrichment) # Start with a single step
|
|
94
96
|
.expand(to: LoadUserData) # Fan out to multiple steps
|
|
95
97
|
.divide(to: [FetchDataFromSourceA, # Split into parallel branches
|
|
96
98
|
FetchDataFromSourceB])
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Context
|
|
5
|
+
class OverwriteError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def initialize(pipeline_id)
|
|
8
|
+
@pipeline_id = pipeline_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get(key)
|
|
12
|
+
raise ArgumentError, "Key must be a string" if !key.is_a?(String)
|
|
13
|
+
|
|
14
|
+
Ductwork.wrap_with_app_executor do
|
|
15
|
+
Ductwork::Tuple
|
|
16
|
+
.select(:serialized_value)
|
|
17
|
+
.find_by(pipeline_id:, key:)
|
|
18
|
+
&.value
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(key, value, overwrite: false)
|
|
23
|
+
attributes = {
|
|
24
|
+
pipeline_id: pipeline_id,
|
|
25
|
+
key: key,
|
|
26
|
+
serialized_value: Ductwork::Tuple.serialize(value),
|
|
27
|
+
first_set_at: Time.current,
|
|
28
|
+
last_set_at: Time.current,
|
|
29
|
+
}
|
|
30
|
+
unique_by = %i[pipeline_id key]
|
|
31
|
+
|
|
32
|
+
if overwrite
|
|
33
|
+
Ductwork.wrap_with_app_executor do
|
|
34
|
+
Ductwork::Tuple.upsert(attributes, unique_by:)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
result = Ductwork.wrap_with_app_executor do
|
|
38
|
+
Ductwork::Tuple.insert(attributes, unique_by:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if result.rows.none?
|
|
42
|
+
raise Ductwork::Context::OverwriteError, "Can only set value once"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :pipeline_id
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/ductwork/engine.rb
CHANGED
|
@@ -23,23 +23,5 @@ module Ductwork
|
|
|
23
23
|
Ductwork.configuration ||= Ductwork::Configuration.new
|
|
24
24
|
Ductwork.logger ||= Ductwork::Configuration::DEFAULT_LOGGER
|
|
25
25
|
end
|
|
26
|
-
|
|
27
|
-
initializer "ductwork.validate_definitions", after: :load_config_initializers do
|
|
28
|
-
config.after_initialize do
|
|
29
|
-
# Load steps and pipelines so definition validation runs and bugs
|
|
30
|
-
# can be caught simply by booting the app or running tests
|
|
31
|
-
loader = Rails.autoloaders.main
|
|
32
|
-
step_directory = Rails.root.join("app/steps")
|
|
33
|
-
pipeline_directory = Rails.root.join("app/pipelines")
|
|
34
|
-
|
|
35
|
-
if step_directory.exist?
|
|
36
|
-
loader.eager_load_dir(step_directory)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
if pipeline_directory.exist?
|
|
40
|
-
loader.eager_load_dir(pipeline_directory)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
26
|
end
|
|
45
27
|
end
|
data/lib/ductwork/models/job.rb
CHANGED
|
@@ -68,22 +68,18 @@ module Ductwork
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def self.enqueue(step, args)
|
|
71
|
-
job =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
j
|
|
86
|
-
end
|
|
71
|
+
job = step.create_job!(
|
|
72
|
+
klass: step.klass,
|
|
73
|
+
started_at: Time.current,
|
|
74
|
+
input_args: JSON.dump({ args: })
|
|
75
|
+
)
|
|
76
|
+
execution = job.executions.create!(
|
|
77
|
+
started_at: Time.current,
|
|
78
|
+
retry_count: 0
|
|
79
|
+
)
|
|
80
|
+
execution.create_availability!(
|
|
81
|
+
started_at: Time.current
|
|
82
|
+
)
|
|
87
83
|
|
|
88
84
|
Ductwork.logger.info(
|
|
89
85
|
msg: "Job enqueued",
|
|
@@ -104,7 +100,7 @@ module Ductwork
|
|
|
104
100
|
job_klass: klass
|
|
105
101
|
)
|
|
106
102
|
args = JSON.parse(input_args)["args"]
|
|
107
|
-
instance = Object.const_get(klass).
|
|
103
|
+
instance = Object.const_get(klass).build_for_execution(step, args)
|
|
108
104
|
run = execution.create_run!(
|
|
109
105
|
started_at: Time.current
|
|
110
106
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
class Pipeline < Ductwork::Record # rubocop:todo Metrics/ClassLength
|
|
5
5
|
has_many :steps, class_name: "Ductwork::Step", foreign_key: "pipeline_id", dependent: :destroy
|
|
6
|
+
has_many :tuples, class_name: "Ductwork::Tuple", foreign_key: "pipeline_id", dependent: :destroy
|
|
6
7
|
|
|
7
8
|
validates :klass, presence: true
|
|
8
9
|
validates :definition, presence: true
|
|
@@ -267,6 +268,9 @@ module Ductwork
|
|
|
267
268
|
if max_depth != -1 && return_value.count > max_depth
|
|
268
269
|
halt!
|
|
269
270
|
else
|
|
271
|
+
# TODO: Brainstorm on using `insert_all` instead of iterating.
|
|
272
|
+
# Performance is bad when the return value has a lot of elements
|
|
273
|
+
# and we create a step and job individually
|
|
270
274
|
Array(return_value).each do |input_arg|
|
|
271
275
|
create_step_and_enqueue_job(edge:, input_arg:)
|
|
272
276
|
end
|
data/lib/ductwork/models/step.rb
CHANGED
|
@@ -25,5 +25,20 @@ module Ductwork
|
|
|
25
25
|
combine: "combine",
|
|
26
26
|
expand: "expand",
|
|
27
27
|
collapse: "collapse"
|
|
28
|
+
|
|
29
|
+
def self.build_for_execution(record, *args, **kwargs)
|
|
30
|
+
instance = allocate
|
|
31
|
+
instance.instance_variable_set(:@pipeline_id, record.pipeline_id)
|
|
32
|
+
instance.send(:initialize, *args, **kwargs)
|
|
33
|
+
instance
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pipeline_id
|
|
37
|
+
@pipeline_id || (@attributes && super)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def context
|
|
41
|
+
@_context ||= Ductwork::Context.new(pipeline_id)
|
|
42
|
+
end
|
|
28
43
|
end
|
|
29
44
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ductwork
|
|
4
|
+
class Tuple < Ductwork::Record
|
|
5
|
+
belongs_to :pipeline, class_name: "Ductwork::Pipeline"
|
|
6
|
+
|
|
7
|
+
# NOTE: These model-level validations are here for symmetry. The current
|
|
8
|
+
# methods used to create `Tuple` records skip validations lol
|
|
9
|
+
validates :key, presence: true
|
|
10
|
+
validates :key, uniqueness: { scope: :pipeline_id }
|
|
11
|
+
validates :first_set_at, presence: true
|
|
12
|
+
validates :last_set_at, presence: true
|
|
13
|
+
|
|
14
|
+
def self.serialize(raw_value)
|
|
15
|
+
{ raw_value: }.to_json
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def value=(raw_value)
|
|
19
|
+
self.serialized_value = self.class.serialize(raw_value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def value
|
|
23
|
+
if serialized_value.present?
|
|
24
|
+
JSON
|
|
25
|
+
.parse(serialized_value, symbolize_names: true)
|
|
26
|
+
.fetch(:raw_value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/ductwork/version.rb
CHANGED
data/lib/ductwork.rb
CHANGED
|
@@ -62,6 +62,26 @@ module Ductwork
|
|
|
62
62
|
@defined_pipelines ||= []
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def validate!
|
|
66
|
+
step_directory = Rails.root.join("app/steps")
|
|
67
|
+
pipeline_directory = Rails.root.join("app/pipelines")
|
|
68
|
+
loader = if defined?(Rails.autoloaders)
|
|
69
|
+
Rails.autoloaders.main
|
|
70
|
+
else
|
|
71
|
+
Ductwork.loader
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if step_directory.exist?
|
|
75
|
+
loader.eager_load_dir(step_directory)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if pipeline_directory.exist?
|
|
79
|
+
loader.eager_load_dir(pipeline_directory)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
65
85
|
private
|
|
66
86
|
|
|
67
87
|
def add_lifecycle_hook(target, event, block)
|
|
@@ -31,6 +31,8 @@ module Ductwork
|
|
|
31
31
|
"db/migrate/create_ductwork_results.rb"
|
|
32
32
|
migration_template "db/create_ductwork_processes.rb",
|
|
33
33
|
"db/migrate/create_ductwork_processes.rb"
|
|
34
|
+
migration_template "db/create_ductwork_tuples.rb",
|
|
35
|
+
"db/migrate/create_ductwork_tuples.rb"
|
|
34
36
|
|
|
35
37
|
route <<~ROUTE
|
|
36
38
|
# This mounts the web dashboard. It is recommended to add authentication around it.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateDuctworkTuples < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :ductwork_tuples do |table|
|
|
6
|
+
table.belongs_to :pipeline, index: true, null: false, foreign_key: { to_table: :ductwork_pipelines }
|
|
7
|
+
table.string :key, null: false
|
|
8
|
+
table.string :serialized_value
|
|
9
|
+
table.datetime :first_set_at, null: false
|
|
10
|
+
table.datetime :last_set_at, null: false
|
|
11
|
+
table.timestamps null: false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :ductwork_tuples, %i[key pipeline_id], unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ductwork
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tyler Ewing
|
|
@@ -135,6 +135,7 @@ files:
|
|
|
135
135
|
- lib/ductwork.rb
|
|
136
136
|
- lib/ductwork/cli.rb
|
|
137
137
|
- lib/ductwork/configuration.rb
|
|
138
|
+
- lib/ductwork/context.rb
|
|
138
139
|
- lib/ductwork/dsl/branch_builder.rb
|
|
139
140
|
- lib/ductwork/dsl/definition_builder.rb
|
|
140
141
|
- lib/ductwork/engine.rb
|
|
@@ -148,6 +149,7 @@ files:
|
|
|
148
149
|
- lib/ductwork/models/result.rb
|
|
149
150
|
- lib/ductwork/models/run.rb
|
|
150
151
|
- lib/ductwork/models/step.rb
|
|
152
|
+
- lib/ductwork/models/tuple.rb
|
|
151
153
|
- lib/ductwork/processes/job_worker.rb
|
|
152
154
|
- lib/ductwork/processes/job_worker_runner.rb
|
|
153
155
|
- lib/ductwork/processes/pipeline_advancer.rb
|
|
@@ -172,6 +174,7 @@ files:
|
|
|
172
174
|
- lib/generators/ductwork/install/templates/db/create_ductwork_results.rb
|
|
173
175
|
- lib/generators/ductwork/install/templates/db/create_ductwork_runs.rb
|
|
174
176
|
- lib/generators/ductwork/install/templates/db/create_ductwork_steps.rb
|
|
177
|
+
- lib/generators/ductwork/install/templates/db/create_ductwork_tuples.rb
|
|
175
178
|
homepage: https://github.com/ductwork/ductwork
|
|
176
179
|
licenses:
|
|
177
180
|
- LGPL-3.0-or-later
|