good_pipeline 0.1.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +217 -0
- data/Rakefile +20 -0
- data/app/controllers/good_pipeline/application_controller.rb +9 -0
- data/app/controllers/good_pipeline/frontends_controller.rb +31 -0
- data/app/controllers/good_pipeline/pipelines_controller.rb +57 -0
- data/app/frontend/good_pipeline/style.css +518 -0
- data/app/helpers/good_pipeline/pipelines_helper.rb +119 -0
- data/app/jobs/good_pipeline/pipeline_callback_job.rb +52 -0
- data/app/jobs/good_pipeline/pipeline_reconciliation_job.rb +10 -0
- data/app/jobs/good_pipeline/step_finished_job.rb +10 -0
- data/app/models/good_pipeline/chain_record.rb +18 -0
- data/app/models/good_pipeline/dependency_record.rb +23 -0
- data/app/models/good_pipeline/pipeline_record.rb +73 -0
- data/app/models/good_pipeline/step_record.rb +74 -0
- data/app/views/good_pipeline/pipelines/_chain_links.html.erb +30 -0
- data/app/views/good_pipeline/pipelines/_pagination.html.erb +24 -0
- data/app/views/good_pipeline/pipelines/_pipeline_row.html.erb +7 -0
- data/app/views/good_pipeline/pipelines/_steps_table.html.erb +33 -0
- data/app/views/good_pipeline/pipelines/definitions.html.erb +49 -0
- data/app/views/good_pipeline/pipelines/index.html.erb +43 -0
- data/app/views/good_pipeline/pipelines/show.html.erb +40 -0
- data/app/views/layouts/good_pipeline/application.html.erb +40 -0
- data/config/routes.rb +13 -0
- data/demo/Rakefile +5 -0
- data/demo/app/jobs/always_failing_job.rb +12 -0
- data/demo/app/jobs/application_job.rb +4 -0
- data/demo/app/jobs/cleanup_job.rb +5 -0
- data/demo/app/jobs/download_job.rb +5 -0
- data/demo/app/jobs/failing_job.rb +12 -0
- data/demo/app/jobs/publish_job.rb +5 -0
- data/demo/app/jobs/retryable_job.rb +19 -0
- data/demo/app/jobs/thumbnail_job.rb +5 -0
- data/demo/app/jobs/transcode_job.rb +5 -0
- data/demo/app/pipelines/analytics_pipeline.rb +7 -0
- data/demo/app/pipelines/archive_pipeline.rb +7 -0
- data/demo/app/pipelines/continue_test_pipeline.rb +11 -0
- data/demo/app/pipelines/halt_test_pipeline.rb +10 -0
- data/demo/app/pipelines/notification_pipeline.rb +7 -0
- data/demo/app/pipelines/test_pipeline.rb +5 -0
- data/demo/app/pipelines/video_processing_pipeline.rb +14 -0
- data/demo/bin/rails +6 -0
- data/demo/config/application.rb +18 -0
- data/demo/config/boot.rb +5 -0
- data/demo/config/database.yml +15 -0
- data/demo/config/environment.rb +5 -0
- data/demo/config/environments/development.rb +9 -0
- data/demo/config/environments/test.rb +10 -0
- data/demo/config/routes.rb +6 -0
- data/demo/config.ru +5 -0
- data/demo/db/migrate/20260319205325_create_good_jobs.rb +112 -0
- data/demo/db/migrate/20260319205326_create_good_pipeline_tables.rb +53 -0
- data/demo/db/seeds.rb +153 -0
- data/demo/test/good_pipeline/test_chain_record.rb +29 -0
- data/demo/test/good_pipeline/test_cleanup.rb +93 -0
- data/demo/test/good_pipeline/test_coordinator.rb +286 -0
- data/demo/test/good_pipeline/test_dependency_record.rb +46 -0
- data/demo/test/good_pipeline/test_failure_metadata.rb +77 -0
- data/demo/test/good_pipeline/test_introspection.rb +46 -0
- data/demo/test/good_pipeline/test_pipeline_callback_job.rb +132 -0
- data/demo/test/good_pipeline/test_pipeline_reconciliation_job.rb +33 -0
- data/demo/test/good_pipeline/test_pipeline_record.rb +183 -0
- data/demo/test/good_pipeline/test_runner.rb +86 -0
- data/demo/test/good_pipeline/test_step_finished_job.rb +37 -0
- data/demo/test/good_pipeline/test_step_record.rb +208 -0
- data/demo/test/integration/test_concurrent_fan_in.rb +109 -0
- data/demo/test/integration/test_end_to_end.rb +89 -0
- data/demo/test/integration/test_enqueue_atomicity.rb +59 -0
- data/demo/test/integration/test_pipeline_chaining.rb +183 -0
- data/demo/test/integration/test_retry_scenarios.rb +90 -0
- data/demo/test/integration/test_step_finished_idempotency.rb +38 -0
- data/demo/test/test_helper.rb +71 -0
- data/dev-docker-compose.yml +16 -0
- data/docs/.vitepress/config.mts +66 -0
- data/docs/.vitepress/theme/custom.css +21 -0
- data/docs/.vitepress/theme/index.ts +4 -0
- data/docs/architecture.md +184 -0
- data/docs/callbacks.md +66 -0
- data/docs/cleanup.md +45 -0
- data/docs/dag-validation.md +88 -0
- data/docs/dashboard.md +66 -0
- data/docs/defining-pipelines.md +167 -0
- data/docs/failure-strategies.md +138 -0
- data/docs/getting-started.md +77 -0
- data/docs/index.md +23 -0
- data/docs/introduction.md +42 -0
- data/docs/monitoring.md +103 -0
- data/docs/package-lock.json +2510 -0
- data/docs/package.json +11 -0
- data/docs/pipeline-chaining.md +104 -0
- data/docs/public/screenshots/definitions.png +0 -0
- data/docs/public/screenshots/index.png +0 -0
- data/docs/public/screenshots/show.png +0 -0
- data/docs/screenshots/definitions.png +0 -0
- data/docs/screenshots/index.png +0 -0
- data/docs/screenshots/show.png +0 -0
- data/lib/generators/good_pipeline/install/install_generator.rb +20 -0
- data/lib/generators/good_pipeline/install/templates/create_good_pipeline_tables.rb.erb +51 -0
- data/lib/good_pipeline/chain.rb +54 -0
- data/lib/good_pipeline/chain_coordinator.rb +53 -0
- data/lib/good_pipeline/coordinator.rb +176 -0
- data/lib/good_pipeline/cycle_detector.rb +36 -0
- data/lib/good_pipeline/engine.rb +23 -0
- data/lib/good_pipeline/errors.rb +11 -0
- data/lib/good_pipeline/failure_metadata.rb +29 -0
- data/lib/good_pipeline/graph_validator.rb +71 -0
- data/lib/good_pipeline/pipeline.rb +122 -0
- data/lib/good_pipeline/runner.rb +63 -0
- data/lib/good_pipeline/step_definition.rb +18 -0
- data/lib/good_pipeline/version.rb +5 -0
- data/lib/good_pipeline.rb +45 -0
- data/mise.toml +10 -0
- data/sig/good_pipeline.rbs +4 -0
- metadata +204 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodPipeline
|
|
4
|
+
class DependencyRecord < ActiveRecord::Base
|
|
5
|
+
self.table_name = "good_pipeline_dependencies"
|
|
6
|
+
self.record_timestamps = false
|
|
7
|
+
|
|
8
|
+
belongs_to :pipeline,
|
|
9
|
+
class_name: "GoodPipeline::PipelineRecord",
|
|
10
|
+
foreign_key: :pipeline_id,
|
|
11
|
+
inverse_of: :dependencies
|
|
12
|
+
|
|
13
|
+
belongs_to :step,
|
|
14
|
+
class_name: "GoodPipeline::StepRecord",
|
|
15
|
+
foreign_key: :step_id,
|
|
16
|
+
inverse_of: :upstream_dependencies
|
|
17
|
+
|
|
18
|
+
belongs_to :depends_on_step,
|
|
19
|
+
class_name: "GoodPipeline::StepRecord",
|
|
20
|
+
foreign_key: :depends_on_step_id,
|
|
21
|
+
inverse_of: :downstream_dependencies
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodPipeline
|
|
4
|
+
class PipelineRecord < ActiveRecord::Base
|
|
5
|
+
self.table_name = "good_pipeline_pipelines"
|
|
6
|
+
self.inheritance_column = nil
|
|
7
|
+
|
|
8
|
+
TERMINAL_STATUSES = %w[succeeded failed halted skipped].freeze
|
|
9
|
+
|
|
10
|
+
VALID_TRANSITIONS = {
|
|
11
|
+
"pending" => %w[running skipped],
|
|
12
|
+
"running" => %w[succeeded failed halted]
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
enum :status, {
|
|
16
|
+
pending: "pending",
|
|
17
|
+
running: "running",
|
|
18
|
+
succeeded: "succeeded",
|
|
19
|
+
failed: "failed",
|
|
20
|
+
halted: "halted",
|
|
21
|
+
skipped: "skipped"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
enum :on_failure_strategy, { halt: "halt", continue: "continue", ignore: "ignore" }
|
|
25
|
+
|
|
26
|
+
has_many :steps,
|
|
27
|
+
class_name: "GoodPipeline::StepRecord",
|
|
28
|
+
foreign_key: :pipeline_id,
|
|
29
|
+
inverse_of: :pipeline,
|
|
30
|
+
dependent: :delete_all
|
|
31
|
+
|
|
32
|
+
has_many :dependencies,
|
|
33
|
+
class_name: "GoodPipeline::DependencyRecord",
|
|
34
|
+
foreign_key: :pipeline_id,
|
|
35
|
+
inverse_of: :pipeline,
|
|
36
|
+
dependent: :delete_all
|
|
37
|
+
|
|
38
|
+
has_many :downstream_chains,
|
|
39
|
+
class_name: "GoodPipeline::ChainRecord",
|
|
40
|
+
foreign_key: :upstream_pipeline_id,
|
|
41
|
+
inverse_of: :upstream_pipeline,
|
|
42
|
+
dependent: :delete_all
|
|
43
|
+
|
|
44
|
+
has_many :downstream_pipelines,
|
|
45
|
+
through: :downstream_chains,
|
|
46
|
+
source: :downstream_pipeline
|
|
47
|
+
|
|
48
|
+
has_many :upstream_chains,
|
|
49
|
+
class_name: "GoodPipeline::ChainRecord",
|
|
50
|
+
foreign_key: :downstream_pipeline_id,
|
|
51
|
+
inverse_of: :downstream_pipeline,
|
|
52
|
+
dependent: :delete_all
|
|
53
|
+
|
|
54
|
+
has_many :upstream_pipelines,
|
|
55
|
+
through: :upstream_chains,
|
|
56
|
+
source: :upstream_pipeline
|
|
57
|
+
|
|
58
|
+
def terminal?
|
|
59
|
+
TERMINAL_STATUSES.include?(status)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transition_to!(new_status)
|
|
63
|
+
new_status = new_status.to_s
|
|
64
|
+
allowed = VALID_TRANSITIONS.fetch(status, [])
|
|
65
|
+
|
|
66
|
+
unless allowed.include?(new_status)
|
|
67
|
+
raise InvalidTransition, "cannot transition pipeline from '#{status}' to '#{new_status}'"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
update!(status: new_status)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodPipeline
|
|
4
|
+
class StepRecord < ActiveRecord::Base
|
|
5
|
+
self.table_name = "good_pipeline_steps"
|
|
6
|
+
|
|
7
|
+
TERMINAL_COORDINATION_STATUSES = %w[succeeded failed skipped].freeze
|
|
8
|
+
|
|
9
|
+
VALID_COORDINATION_TRANSITIONS = {
|
|
10
|
+
"pending" => %w[enqueued skipped],
|
|
11
|
+
"enqueued" => %w[succeeded failed]
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
enum :coordination_status, {
|
|
15
|
+
pending: "pending",
|
|
16
|
+
enqueued: "enqueued",
|
|
17
|
+
succeeded: "succeeded",
|
|
18
|
+
failed: "failed",
|
|
19
|
+
skipped: "skipped"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum :on_failure_strategy, { halt: "halt", continue: "continue", ignore: "ignore" }
|
|
23
|
+
|
|
24
|
+
belongs_to :pipeline,
|
|
25
|
+
class_name: "GoodPipeline::PipelineRecord",
|
|
26
|
+
foreign_key: :pipeline_id,
|
|
27
|
+
inverse_of: :steps
|
|
28
|
+
|
|
29
|
+
has_many :upstream_dependencies,
|
|
30
|
+
class_name: "GoodPipeline::DependencyRecord",
|
|
31
|
+
foreign_key: :step_id,
|
|
32
|
+
inverse_of: :step,
|
|
33
|
+
dependent: :delete_all
|
|
34
|
+
|
|
35
|
+
has_many :upstream_steps,
|
|
36
|
+
through: :upstream_dependencies,
|
|
37
|
+
source: :depends_on_step
|
|
38
|
+
|
|
39
|
+
has_many :downstream_dependencies,
|
|
40
|
+
class_name: "GoodPipeline::DependencyRecord",
|
|
41
|
+
foreign_key: :depends_on_step_id,
|
|
42
|
+
inverse_of: :depends_on_step,
|
|
43
|
+
dependent: :delete_all
|
|
44
|
+
|
|
45
|
+
has_many :downstream_steps,
|
|
46
|
+
through: :downstream_dependencies,
|
|
47
|
+
source: :step
|
|
48
|
+
|
|
49
|
+
def duration
|
|
50
|
+
return nil unless good_job_id
|
|
51
|
+
|
|
52
|
+
good_job = GoodJob::Job.find_by(id: good_job_id)
|
|
53
|
+
return nil unless good_job&.performed_at && good_job.finished_at
|
|
54
|
+
|
|
55
|
+
good_job.finished_at - good_job.performed_at
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def terminal_coordination_status?
|
|
59
|
+
TERMINAL_COORDINATION_STATUSES.include?(coordination_status)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def transition_coordination_status_to!(new_status)
|
|
63
|
+
new_status = new_status.to_s
|
|
64
|
+
allowed = VALID_COORDINATION_TRANSITIONS.fetch(coordination_status, [])
|
|
65
|
+
|
|
66
|
+
unless allowed.include?(new_status)
|
|
67
|
+
raise InvalidTransition,
|
|
68
|
+
"cannot transition step '#{key}' coordination_status from '#{coordination_status}' to '#{new_status}'"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
update!(coordination_status: new_status)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% if pipeline.upstream_pipelines.any? || pipeline.downstream_pipelines.any? %>
|
|
2
|
+
<div class="chain-cards">
|
|
3
|
+
<% if pipeline.upstream_pipelines.any? %>
|
|
4
|
+
<article>
|
|
5
|
+
<header><h4>Upstream</h4></header>
|
|
6
|
+
<div class="chain-chips">
|
|
7
|
+
<% pipeline.upstream_pipelines.each do |upstream| %>
|
|
8
|
+
<%= link_to pipeline_path(upstream), class: "chain-chip" do %>
|
|
9
|
+
<span class="chain-label"><%= humanized_type(upstream.type) %></span>
|
|
10
|
+
<%= status_badge(upstream.status) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
</article>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% if pipeline.downstream_pipelines.any? %>
|
|
17
|
+
<article>
|
|
18
|
+
<header><h4>Downstream</h4></header>
|
|
19
|
+
<div class="chain-chips">
|
|
20
|
+
<% pipeline.downstream_pipelines.each do |downstream| %>
|
|
21
|
+
<%= link_to pipeline_path(downstream), class: "chain-chip" do %>
|
|
22
|
+
<span class="chain-label"><%= humanized_type(downstream.type) %></span>
|
|
23
|
+
<%= status_badge(downstream.status) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</article>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<% if params[:after_created_at].present? || @has_next_page %>
|
|
2
|
+
<nav>
|
|
3
|
+
<ul>
|
|
4
|
+
<li>
|
|
5
|
+
<% if params[:after_created_at].present? %>
|
|
6
|
+
<%= link_to "\u2190 First page", pipelines_path(status: @status, pipeline_type: @pipeline_type) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</li>
|
|
9
|
+
</ul>
|
|
10
|
+
<ul>
|
|
11
|
+
<li>
|
|
12
|
+
<% if @has_next_page %>
|
|
13
|
+
<% last_pipeline = @pipelines.last %>
|
|
14
|
+
<%= link_to "Next \u2192", pipelines_path(
|
|
15
|
+
status: @status,
|
|
16
|
+
pipeline_type: @pipeline_type,
|
|
17
|
+
after_created_at: last_pipeline.created_at.iso8601(6),
|
|
18
|
+
after_id: last_pipeline.id
|
|
19
|
+
) %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</nav>
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<tr data-href="<%= pipeline_path(pipeline) %>" class="<%= 'row-running' if pipeline.running? %>">
|
|
2
|
+
<td><span class="execution-id"><%= pipeline.id.to_s.first(8) %></span></td>
|
|
3
|
+
<td><span class="pipeline-name"><%= humanized_type(pipeline.type) %></span></td>
|
|
4
|
+
<td><%= status_badge(pipeline.status) %></td>
|
|
5
|
+
<td><%= pipeline.created_at.strftime("%Y-%m-%d %I:%M %p") %></td>
|
|
6
|
+
<td><%= pipeline_duration(pipeline) || "\u2014" %></td>
|
|
7
|
+
</tr>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<header><h3>Steps</h3></header>
|
|
2
|
+
<table>
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
<th>Key</th>
|
|
6
|
+
<th>Job Class</th>
|
|
7
|
+
<th>Status</th>
|
|
8
|
+
<th>Duration</th>
|
|
9
|
+
<th>Error</th>
|
|
10
|
+
</tr>
|
|
11
|
+
</thead>
|
|
12
|
+
<tbody>
|
|
13
|
+
<% pipeline.steps.each do |step| %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td>
|
|
16
|
+
<% if (url = good_job_step_url(step)) %>
|
|
17
|
+
<%= link_to step.key, url, target: "_blank", rel: "noopener" %>
|
|
18
|
+
<% else %>
|
|
19
|
+
<%= step.key %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</td>
|
|
22
|
+
<td><%= step.job_class %></td>
|
|
23
|
+
<td><%= status_badge(step.coordination_status) %></td>
|
|
24
|
+
<td><%= step.duration ? "#{step.duration.round(2)}s" : "\u2014" %></td>
|
|
25
|
+
<td>
|
|
26
|
+
<% if step.error_class %>
|
|
27
|
+
<small><%= step.error_class %>: <%= step.error_message %></small>
|
|
28
|
+
<% end %>
|
|
29
|
+
</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<% end %>
|
|
32
|
+
</tbody>
|
|
33
|
+
</table>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<h1 class="page-title">Pipeline Definitions</h1>
|
|
2
|
+
|
|
3
|
+
<% if @pipelines.any? %>
|
|
4
|
+
<div class="definitions-layout">
|
|
5
|
+
<div class="definitions-sidebar">
|
|
6
|
+
<% @pipelines.each_with_index do |pipeline, index| %>
|
|
7
|
+
<button type="button" class="definition-item <%= 'active' if index.zero? %>" data-pipeline="<%= pipeline.type %>">
|
|
8
|
+
<span class="definition-item-name"><%= humanized_type(pipeline.type) %></span>
|
|
9
|
+
<span class="definition-item-meta"><%= pipeline.steps.size %> steps</span>
|
|
10
|
+
</button>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="definitions-detail">
|
|
15
|
+
<% @pipelines.each_with_index do |pipeline, index| %>
|
|
16
|
+
<div class="definition-panel <%= 'active' if index.zero? %>" data-pipeline="<%= pipeline.type %>">
|
|
17
|
+
<article>
|
|
18
|
+
<header class="definition-header">
|
|
19
|
+
<div>
|
|
20
|
+
<h2><%= humanized_type(pipeline.type) %></h2>
|
|
21
|
+
<span class="definition-subtitle">on failure: <%= pipeline.on_failure_strategy %></span>
|
|
22
|
+
</div>
|
|
23
|
+
<%= link_to "View Executions \u2192", pipelines_path(pipeline_type: pipeline.type), class: "definition-executions-link" %>
|
|
24
|
+
</header>
|
|
25
|
+
<% if pipeline.steps.any? %>
|
|
26
|
+
<pre class="mermaid"><%= raw mermaid_definition_diagram(pipeline) %></pre>
|
|
27
|
+
<% else %>
|
|
28
|
+
<p class="empty-state">No steps defined.</p>
|
|
29
|
+
<% end %>
|
|
30
|
+
</article>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
document.querySelectorAll('.definition-item').forEach(function(button) {
|
|
38
|
+
button.addEventListener('click', function() {
|
|
39
|
+
var type = this.dataset.pipeline;
|
|
40
|
+
document.querySelectorAll('.definition-item').forEach(function(b) { b.classList.remove('active'); });
|
|
41
|
+
document.querySelectorAll('.definition-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
42
|
+
this.classList.add('active');
|
|
43
|
+
document.querySelector('.definition-panel[data-pipeline="' + type + '"]').classList.add('active');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
<% else %>
|
|
48
|
+
<p class="empty-state">No pipeline executions found yet. Run a pipeline to see its definition here.</p>
|
|
49
|
+
<% end %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<h1 class="page-title">Pipeline Executions</h1>
|
|
2
|
+
|
|
3
|
+
<div class="filters-row">
|
|
4
|
+
<ul class="status-tabs">
|
|
5
|
+
<li><%= link_to "All (#{@total_count})", pipelines_path(pipeline_type: @pipeline_type), class: ("active" if @status.nil?) %></li>
|
|
6
|
+
<% %w[running succeeded failed halted skipped].each do |status| %>
|
|
7
|
+
<li><%= link_to "#{status.titleize} (#{@status_counts[status] || 0})", pipelines_path(status: status, pipeline_type: @pipeline_type), class: ("active" if @status == status), data: { status: status } %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
<select class="pipeline-type-select" onchange="window.location = this.value;">
|
|
11
|
+
<option value="<%= pipelines_path(status: @status) %>" <%= "selected" if @pipeline_type.nil? %>>All Pipelines</option>
|
|
12
|
+
<% @pipeline_types.each do |pipeline_type| %>
|
|
13
|
+
<option value="<%= pipelines_path(status: @status, pipeline_type: pipeline_type) %>" <%= "selected" if @pipeline_type == pipeline_type %>>
|
|
14
|
+
<%= humanized_type(pipeline_type) %>
|
|
15
|
+
</option>
|
|
16
|
+
<% end %>
|
|
17
|
+
</select>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<% if @pipelines.any? %>
|
|
21
|
+
<div class="table-card">
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
<tr>
|
|
25
|
+
<th>Execution ID</th>
|
|
26
|
+
<th>Pipeline Name</th>
|
|
27
|
+
<th>Status</th>
|
|
28
|
+
<th>Start Time</th>
|
|
29
|
+
<th>Duration</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<% @pipelines.each do |pipeline| %>
|
|
34
|
+
<%= render "pipeline_row", pipeline: pipeline %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<%= render "pagination" %>
|
|
41
|
+
<% else %>
|
|
42
|
+
<p class="empty-state">No pipelines found.</p>
|
|
43
|
+
<% end %>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<p><%= link_to "\u2190 All Pipelines", pipelines_path %></p>
|
|
2
|
+
|
|
3
|
+
<div class="pipeline-detail-grid">
|
|
4
|
+
<div class="pipeline-detail-left">
|
|
5
|
+
<article class="pipeline-details">
|
|
6
|
+
<header><h2><%= humanized_type(@pipeline.type) %></h2></header>
|
|
7
|
+
<dl>
|
|
8
|
+
<dt>Status</dt>
|
|
9
|
+
<dd><%= status_badge(@pipeline.status) %></dd>
|
|
10
|
+
<dt>Failure Strategy</dt>
|
|
11
|
+
<dd><span class="strategy-label"><%= @pipeline.on_failure_strategy %></span></dd>
|
|
12
|
+
<dt>Params</dt>
|
|
13
|
+
<dd><code><%= @pipeline.params&.to_json %></code></dd>
|
|
14
|
+
<dt>Created</dt>
|
|
15
|
+
<dd><%= relative_time_tag(@pipeline.created_at) %></dd>
|
|
16
|
+
<% if pipeline_duration(@pipeline) %>
|
|
17
|
+
<dt>Duration</dt>
|
|
18
|
+
<dd><%= pipeline_duration(@pipeline) %></dd>
|
|
19
|
+
<% end %>
|
|
20
|
+
</dl>
|
|
21
|
+
</article>
|
|
22
|
+
|
|
23
|
+
<%= render "chain_links", pipeline: @pipeline %>
|
|
24
|
+
|
|
25
|
+
<article class="table-card">
|
|
26
|
+
<%= render "steps_table", pipeline: @pipeline %>
|
|
27
|
+
</article>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="pipeline-detail-right">
|
|
31
|
+
<article>
|
|
32
|
+
<header><h3>Pipeline Graph</h3></header>
|
|
33
|
+
<% if @pipeline.steps.any? %>
|
|
34
|
+
<pre class="mermaid"><%= raw mermaid_diagram(@pipeline) %></pre>
|
|
35
|
+
<% else %>
|
|
36
|
+
<p class="empty-state">No steps defined.</p>
|
|
37
|
+
<% end %>
|
|
38
|
+
</article>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>GoodPipeline</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
|
8
|
+
<link rel="stylesheet" href="<%= frontend_static_path(id: 'style', format: :css) %>">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="dashboard-header">
|
|
12
|
+
<span class="header-left">
|
|
13
|
+
<%= link_to "GoodPipeline", root_path %>
|
|
14
|
+
<%= link_to "Definitions", definitions_pipelines_path, class: "header-nav-link" %>
|
|
15
|
+
</span>
|
|
16
|
+
<span class="header-right">
|
|
17
|
+
<span class="header-version">v<%= GoodPipeline::VERSION %></span>
|
|
18
|
+
<a href="https://github.com/milkstrawai/good_pipeline" target="_blank" rel="noopener" class="header-github" title="GitHub">
|
|
19
|
+
<svg height="18" width="18" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
20
|
+
</a>
|
|
21
|
+
</span>
|
|
22
|
+
</header>
|
|
23
|
+
<main class="dashboard-content">
|
|
24
|
+
<%= yield %>
|
|
25
|
+
</main>
|
|
26
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
27
|
+
<script>
|
|
28
|
+
mermaid.initialize({
|
|
29
|
+
startOnLoad: true,
|
|
30
|
+
theme: 'neutral',
|
|
31
|
+
flowchart: { curve: 'basis', nodeSpacing: 30, rankSpacing: 50 }
|
|
32
|
+
});
|
|
33
|
+
document.querySelectorAll('tr[data-href]').forEach(function(row) {
|
|
34
|
+
row.addEventListener('click', function() {
|
|
35
|
+
window.location = this.dataset.href;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
GoodPipeline::Engine.routes.draw do
|
|
4
|
+
root "pipelines#index"
|
|
5
|
+
|
|
6
|
+
resources :pipelines, only: %i[index show] do
|
|
7
|
+
get :definitions, on: :collection
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
scope :frontend, controller: :frontends, defaults: { version: GoodPipeline::VERSION.tr(".", "-") } do
|
|
11
|
+
get "static/:version/:id", action: :static, as: :frontend_static
|
|
12
|
+
end
|
|
13
|
+
end
|
data/demo/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AlwaysFailingJob < ApplicationJob
|
|
4
|
+
class AlwaysFailingError < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
retry_on AlwaysFailingError, wait: 0, attempts: 3
|
|
8
|
+
|
|
9
|
+
def perform(**_kwargs)
|
|
10
|
+
raise AlwaysFailingError, "always fails"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class RetryableJob < ApplicationJob
|
|
4
|
+
class RetryableError < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
retry_on RetryableError, wait: 0, attempts: 5
|
|
8
|
+
|
|
9
|
+
def perform(tracker_key:)
|
|
10
|
+
result = ActiveRecord::Base.connection.execute(
|
|
11
|
+
ActiveRecord::Base.sanitize_sql(
|
|
12
|
+
["UPDATE attempt_trackers SET count = count + 1 WHERE key = ? RETURNING count", tracker_key]
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
count = result.first["count"]
|
|
16
|
+
|
|
17
|
+
raise RetryableError, "attempt #{count}" if count < 3
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ContinueTestPipeline < GoodPipeline::Pipeline
|
|
4
|
+
failure_strategy :continue
|
|
5
|
+
|
|
6
|
+
def configure(**_kwargs)
|
|
7
|
+
run :step_a, FailingJob
|
|
8
|
+
run :step_b, DownloadJob
|
|
9
|
+
run :step_c, DownloadJob, after: %i[step_a step_b]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class VideoProcessingPipeline < GoodPipeline::Pipeline
|
|
4
|
+
description "Downloads, transcodes and publishes a video"
|
|
5
|
+
failure_strategy :halt
|
|
6
|
+
|
|
7
|
+
def configure(video_id:, **)
|
|
8
|
+
run :download, DownloadJob, with: { video_id: video_id }
|
|
9
|
+
run :transcode, TranscodeJob, after: :download
|
|
10
|
+
run :thumbnail, ThumbnailJob, after: :download
|
|
11
|
+
run :publish, PublishJob, after: %i[transcode thumbnail]
|
|
12
|
+
run :cleanup, CleanupJob, after: :publish
|
|
13
|
+
end
|
|
14
|
+
end
|
data/demo/bin/rails
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "boot"
|
|
4
|
+
require "rails/all"
|
|
5
|
+
require "good_job"
|
|
6
|
+
require "good_job/engine"
|
|
7
|
+
require "good_pipeline"
|
|
8
|
+
require "good_pipeline/engine"
|
|
9
|
+
|
|
10
|
+
Bundler.require(*Rails.groups)
|
|
11
|
+
|
|
12
|
+
module TestApp
|
|
13
|
+
class Application < Rails::Application
|
|
14
|
+
config.load_defaults Rails::VERSION::STRING.to_f
|
|
15
|
+
|
|
16
|
+
config.active_job.queue_adapter = :good_job
|
|
17
|
+
end
|
|
18
|
+
end
|