job-workflow 0.5.0 → 0.6.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/.rubocop.yml +1 -2
- data/CHANGELOG.md +20 -0
- data/README.md +1 -1
- data/app/controllers/job_workflow/monitoring/application_controller.rb +11 -0
- data/app/controllers/job_workflow/monitoring/executions_controller.rb +28 -0
- data/app/controllers/job_workflow/monitoring/workflows_controller.rb +11 -0
- data/app/views/job_workflow/monitoring/executions/index.html.erb +57 -0
- data/app/views/job_workflow/monitoring/executions/show.html.erb +200 -0
- data/app/views/job_workflow/monitoring/workflows/index.html.erb +39 -0
- data/app/views/layouts/job_workflow/monitoring/application.html.erb +117 -0
- data/config/routes.rb +8 -0
- data/guides/API_REFERENCE.md +9 -6
- data/guides/DEPENDENCY_WAIT.md +9 -5
- data/guides/MONITORING_UI.md +74 -0
- data/guides/PARALLEL_PROCESSING.md +33 -21
- data/guides/PRODUCTION_DEPLOYMENT.md +1 -1
- data/guides/README.md +6 -1
- data/guides/THROTTLING.md +24 -0
- data/guides/WORKFLOW_STATUS_QUERY.md +7 -1
- data/lib/job_workflow/context.rb +7 -5
- data/lib/job_workflow/dsl.rb +0 -4
- data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +1 -1
- data/lib/job_workflow/instrumentation.rb +14 -14
- data/lib/job_workflow/job_status.rb +16 -1
- data/lib/job_workflow/monitoring/dag_layout.rb +186 -0
- data/lib/job_workflow/monitoring/engine.rb +15 -0
- data/lib/job_workflow/monitoring/execution_page.rb +16 -0
- data/lib/job_workflow/monitoring/execution_registry.rb +50 -0
- data/lib/job_workflow/monitoring/execution_view_model.rb +262 -0
- data/lib/job_workflow/monitoring/parameter_filter.rb +37 -0
- data/lib/job_workflow/monitoring/workflow_definition.rb +24 -0
- data/lib/job_workflow/monitoring/workflow_registry.rb +24 -0
- data/lib/job_workflow/monitoring.rb +120 -0
- data/lib/job_workflow/queue_adapters/abstract.rb +7 -2
- data/lib/job_workflow/queue_adapters/null_adapter.rb +12 -1
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +42 -12
- data/lib/job_workflow/railtie.rb +2 -0
- data/lib/job_workflow/runner.rb +5 -3
- data/lib/job_workflow/sub_task_job.rb +93 -0
- data/lib/job_workflow/task_enqueue.rb +19 -12
- data/lib/job_workflow/version.rb +1 -1
- data/lib/job_workflow/workflow_status.rb +20 -1
- data/lib/job_workflow.rb +2 -0
- data/sig/generated/job_workflow/context.rbs +7 -7
- data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +0 -1
- data/sig/generated/job_workflow/instrumentation.rbs +28 -28
- data/sig/generated/job_workflow/job_status.rbs +5 -2
- data/sig/generated/job_workflow/monitoring/dag_layout.rbs +80 -0
- data/sig/generated/job_workflow/monitoring/engine.rbs +8 -0
- data/sig/generated/job_workflow/monitoring/execution_page.rbs +14 -0
- data/sig/generated/job_workflow/monitoring/execution_registry.rbs +21 -0
- data/sig/generated/job_workflow/monitoring/execution_view_model.rbs +111 -0
- data/sig/generated/job_workflow/monitoring/parameter_filter.rbs +16 -0
- data/sig/generated/job_workflow/monitoring/workflow_definition.rbs +18 -0
- data/sig/generated/job_workflow/monitoring/workflow_registry.rbs +13 -0
- data/sig/generated/job_workflow/monitoring.rbs +38 -0
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +7 -4
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +5 -2
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +18 -6
- data/sig/generated/job_workflow/runner.rbs +1 -1
- data/sig/generated/job_workflow/sub_task_job.rbs +40 -0
- data/sig/generated/job_workflow/task_enqueue.rbs +5 -8
- data/sig/generated/job_workflow/workflow_status.rbs +6 -0
- data/sig-private/job-workflow.rbs +11 -0
- data/sig-private/rails.rbs +5 -0
- metadata +32 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbf6231d334745f4814282d93fa8c7b54461680e6bb57edc6632cd39b86cb71b
|
|
4
|
+
data.tar.gz: c627082defb65e2a4cd79cb2b88d3f54ce8b082a88d9ccaff3d96029ee49ea65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8caf717541c7f549d3957a5939d636881f8ad98389e5e53759dde43e561c8bbdeda0211feab0843f2625fbc974fb217b479bc2421e0bc828b9f6d910b749cd5
|
|
7
|
+
data.tar.gz: 466d87d40597d0d3c68de4cec3f2f6d158faab74100d098420cd376108fb88667f15cf1ecccdf2b810e41df402a9db5f7d84945a71de2dd756e71ae99588fe58
|
data/.rubocop.yml
CHANGED
|
@@ -41,7 +41,7 @@ Metrics/BlockLength:
|
|
|
41
41
|
Metrics/ClassLength:
|
|
42
42
|
Enabled: true
|
|
43
43
|
CountComments: false
|
|
44
|
-
Max:
|
|
44
|
+
Max: 124
|
|
45
45
|
CountAsOne:
|
|
46
46
|
- array
|
|
47
47
|
- hash
|
|
@@ -88,4 +88,3 @@ Style/StringLiteralsInInterpolation:
|
|
|
88
88
|
|
|
89
89
|
ThreadSafety/ClassAndModuleAttributes:
|
|
90
90
|
Enabled: false
|
|
91
|
-
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-05-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `JobWorkflow::SubTaskJob` for `enqueue: true` task fan-out so each-task sub-jobs run on a dedicated job class instead of reusing the workflow DSL job class directly
|
|
8
|
+
- Add a JobWorkflow monitoring UI engine with an execution DAG overview for browsing workflow definitions, root executions, DAG state, arguments, outputs, fan-out progress, and linked Mission Control Jobs rows
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Breaking:** `WorkflowStatus.find` / `find_by` are now root-workflow-only APIs. Sub-task job IDs are excluded and should be tracked via `JobStatus`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Route enqueued each-task execution through `SubTaskJob`, restoring workflow/task context from the parent workflow job and keeping parent/sub-job identity separate during async execution
|
|
17
|
+
- Extend queue-adapter and workflow-status hydration so monitoring can paginate root workflow executions, hydrate sub-task detail on demand, and keep partially completed fan-out tasks visible as `running` until the workflow actually settles
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Remove `enqueue: { concurrency: N }`. Async map task fan-out is still controlled by `enqueue`, but execution caps are now configured exclusively with `throttle`.
|
|
22
|
+
|
|
3
23
|
## [0.5.0] - 2026-05-13
|
|
4
24
|
|
|
5
25
|
### Added
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# JobWorkflow
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Early Stage (v0.
|
|
3
|
+
> ⚠️ **Early Stage (v0.6.0):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Monitoring
|
|
5
|
+
# Resolve the host app controller at load time so engine controllers inherit
|
|
6
|
+
# the app's existing authentication and access control hooks.
|
|
7
|
+
class ApplicationController < JobWorkflow::Monitoring.resolved_base_controller_class.constantize
|
|
8
|
+
layout "job_workflow/monitoring/application"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Monitoring
|
|
5
|
+
class ExecutionsController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@workflow = WorkflowRegistry.find(params[:workflow_job_class_name])
|
|
8
|
+
return render plain: "Workflow definition not found.", status: :not_found if @workflow.nil?
|
|
9
|
+
|
|
10
|
+
@page = ExecutionRegistry.page_for(
|
|
11
|
+
job_class_name: @workflow.name,
|
|
12
|
+
cursor: params[:cursor]
|
|
13
|
+
)
|
|
14
|
+
@executions = @page.executions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@workflow = WorkflowRegistry.find(params[:workflow_job_class_name])
|
|
19
|
+
return render plain: "Workflow definition not found.", status: :not_found if @workflow.nil?
|
|
20
|
+
|
|
21
|
+
@execution = ExecutionRegistry.find(params[:id])
|
|
22
|
+
return if @execution && @execution.job_class_name == @workflow.name
|
|
23
|
+
|
|
24
|
+
render plain: "Workflow execution is no longer available.", status: :not_found
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<section class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-end gap-3 mb-4">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="text-body-secondary small mb-1">Workflow</div>
|
|
4
|
+
<h1 class="display-6 fw-semibold mb-2"><%= @workflow.name %></h1>
|
|
5
|
+
<p class="text-body-secondary mb-0">Paginated root executions. sub task jobs appear only inside execution detail.</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div>
|
|
8
|
+
<%= link_to "Back to workflows", root_path, class: "btn btn-outline-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
</section>
|
|
11
|
+
|
|
12
|
+
<div class="card jw-card">
|
|
13
|
+
<div class="card-body p-0">
|
|
14
|
+
<% if @executions.empty? %>
|
|
15
|
+
<div class="p-4 text-body-secondary">No currently available workflow executions.</div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="table-responsive">
|
|
18
|
+
<table class="table table-hover align-middle mb-0">
|
|
19
|
+
<thead class="table-light">
|
|
20
|
+
<tr>
|
|
21
|
+
<th class="ps-4">Status</th>
|
|
22
|
+
<th>Current task</th>
|
|
23
|
+
<th>Queue</th>
|
|
24
|
+
<th>Job</th>
|
|
25
|
+
<th class="text-end pe-4">Detail</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<% @executions.each do |execution| %>
|
|
30
|
+
<% status_class = case execution.workflow_status
|
|
31
|
+
when :succeeded then "text-bg-success"
|
|
32
|
+
when :failed then "text-bg-danger"
|
|
33
|
+
when :running then "text-bg-primary"
|
|
34
|
+
else "text-bg-secondary"
|
|
35
|
+
end %>
|
|
36
|
+
<tr>
|
|
37
|
+
<td class="ps-4"><span class="badge <%= status_class %>"><%= execution.workflow_status %></span></td>
|
|
38
|
+
<td><%= execution.current_task_name || "-" %></td>
|
|
39
|
+
<td><span class="jw-mono"><%= execution.queue_name || "-" %></span></td>
|
|
40
|
+
<td class="jw-mono"><%= execution.job_id %></td>
|
|
41
|
+
<td class="text-end pe-4">
|
|
42
|
+
<%= link_to "Open", workflow_execution_path(@workflow.name, execution.job_id), class: "btn btn-sm btn-primary" %>
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<% end %>
|
|
46
|
+
</tbody>
|
|
47
|
+
</table>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<% if @page.next_cursor %>
|
|
54
|
+
<div class="d-flex justify-content-end mt-3">
|
|
55
|
+
<%= link_to "Next page", workflow_executions_path(@workflow.name, cursor: @page.next_cursor), class: "btn btn-outline-primary" %>
|
|
56
|
+
</div>
|
|
57
|
+
<% end %>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<% status_class = case @execution.workflow_status
|
|
2
|
+
when :succeeded then "text-bg-success"
|
|
3
|
+
when :failed then "text-bg-danger"
|
|
4
|
+
when :running then "text-bg-primary"
|
|
5
|
+
else "text-bg-secondary"
|
|
6
|
+
end %>
|
|
7
|
+
<% dag_layout = @execution.dag_layout %>
|
|
8
|
+
|
|
9
|
+
<section class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-3 mb-4">
|
|
10
|
+
<div>
|
|
11
|
+
<div class="text-body-secondary small mb-1">Execution</div>
|
|
12
|
+
<h1 class="display-6 fw-semibold mb-3"><%= @execution.job_class_name %></h1>
|
|
13
|
+
<div class="d-flex flex-wrap gap-2">
|
|
14
|
+
<span class="badge <%= status_class %>"><%= @execution.workflow_status %></span>
|
|
15
|
+
<span class="badge text-bg-light border text-dark">task <%= @execution.current_task_name || "-" %></span>
|
|
16
|
+
<span class="badge text-bg-light border text-dark">failed <%= @execution.failed_task_name || "-" %></span>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="d-flex flex-wrap gap-2">
|
|
20
|
+
<%= link_to "Back to executions", workflow_executions_path(@workflow.name), class: "btn btn-outline-secondary" %>
|
|
21
|
+
<% if @execution.mission_control_job_path %>
|
|
22
|
+
<%= link_to "Mission Control Jobs", @execution.mission_control_job_path, class: "btn btn-primary" %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<div class="row g-4 mb-4">
|
|
28
|
+
<div class="col-12 col-xl-4">
|
|
29
|
+
<div class="card jw-card h-100">
|
|
30
|
+
<div class="card-header bg-white fw-semibold">Execution Summary</div>
|
|
31
|
+
<div class="card-body">
|
|
32
|
+
<dl class="row mb-0">
|
|
33
|
+
<dt class="col-sm-4">Job ID</dt>
|
|
34
|
+
<dd class="col-sm-8 jw-mono"><%= @execution.job_id %></dd>
|
|
35
|
+
<dt class="col-sm-4">Queue</dt>
|
|
36
|
+
<dd class="col-sm-8"><%= @execution.queue_name || "-" %></dd>
|
|
37
|
+
<dt class="col-sm-4">Current</dt>
|
|
38
|
+
<dd class="col-sm-8"><%= @execution.current_task_name || "-" %></dd>
|
|
39
|
+
<dt class="col-sm-4">Failed</dt>
|
|
40
|
+
<dd class="col-sm-8"><%= @execution.failed_task_name || "-" %></dd>
|
|
41
|
+
</dl>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="col-12 col-xl-8">
|
|
47
|
+
<div class="card jw-card h-100">
|
|
48
|
+
<div class="card-header bg-white fw-semibold">Arguments</div>
|
|
49
|
+
<div class="card-body">
|
|
50
|
+
<pre class="jw-pre"><%= JSON.pretty_generate(@execution.filtered_arguments.as_json) %></pre>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="card jw-card">
|
|
57
|
+
<div class="card-header bg-white fw-semibold">DAG</div>
|
|
58
|
+
<% if dag_layout[:nodes].any? %>
|
|
59
|
+
<div class="card-body border-bottom" aria-hidden="true">
|
|
60
|
+
<div class="small text-body-secondary mb-3">graph overview</div>
|
|
61
|
+
<div class="jw-dag-viewport">
|
|
62
|
+
<div
|
|
63
|
+
class="jw-dag-canvas"
|
|
64
|
+
style="width: <%= dag_layout[:width] %>px; height: <%= dag_layout[:height] %>px;"
|
|
65
|
+
>
|
|
66
|
+
<svg
|
|
67
|
+
class="jw-dag-svg"
|
|
68
|
+
viewBox="0 0 <%= dag_layout[:width] %> <%= dag_layout[:height] %>"
|
|
69
|
+
preserveAspectRatio="xMinYMin meet"
|
|
70
|
+
>
|
|
71
|
+
<% dag_layout[:edges].each do |edge| %>
|
|
72
|
+
<path class="jw-dag-edge" d="<%= edge[:path] %>"></path>
|
|
73
|
+
<% end %>
|
|
74
|
+
</svg>
|
|
75
|
+
|
|
76
|
+
<% dag_layout[:nodes].each do |task| %>
|
|
77
|
+
<% graph_status_class = case task[:status]
|
|
78
|
+
when :succeeded then "text-bg-success"
|
|
79
|
+
when :failed then "text-bg-danger"
|
|
80
|
+
when :running then "text-bg-primary"
|
|
81
|
+
else "text-bg-secondary"
|
|
82
|
+
end %>
|
|
83
|
+
<div
|
|
84
|
+
class="jw-dag-node"
|
|
85
|
+
style="left: <%= task[:x] %>px; top: <%= task[:y] %>px; width: <%= task[:width] %>px; height: <%= task[:height] %>px;"
|
|
86
|
+
>
|
|
87
|
+
<div class="d-flex flex-column align-items-start gap-1">
|
|
88
|
+
<div class="fw-semibold jw-dag-node-title" title="<%= task[:label] %>"><%= task[:truncated_label] %></div>
|
|
89
|
+
<span class="badge <%= graph_status_class %>"><%= task[:status] %></span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<% if task[:meta_label] %>
|
|
93
|
+
<div class="jw-dag-node-meta"><%= task[:meta_label] %></div>
|
|
94
|
+
<% end %>
|
|
95
|
+
</div>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
<div class="list-group list-group-flush">
|
|
102
|
+
<% @execution.tasks.each do |task| %>
|
|
103
|
+
<% task_status_class = case task[:status]
|
|
104
|
+
when :succeeded then "text-bg-success"
|
|
105
|
+
when :failed then "text-bg-danger"
|
|
106
|
+
when :running then "text-bg-primary"
|
|
107
|
+
else "text-bg-secondary"
|
|
108
|
+
end %>
|
|
109
|
+
<div class="list-group-item p-4">
|
|
110
|
+
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
|
|
111
|
+
<div>
|
|
112
|
+
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
|
113
|
+
<span class="fw-semibold"><%= task[:name] %></span>
|
|
114
|
+
<span class="badge <%= task_status_class %>"><%= task[:status] %></span>
|
|
115
|
+
<% if task[:each] %>
|
|
116
|
+
<span class="badge text-bg-light border text-dark">
|
|
117
|
+
each <%= task[:each_progress][:succeeded] %>/<%= task[:each_progress][:total] %>
|
|
118
|
+
</span>
|
|
119
|
+
<% end %>
|
|
120
|
+
</div>
|
|
121
|
+
<% if task[:depends_on].any? %>
|
|
122
|
+
<div class="text-body-secondary small">depends on <%= task[:depends_on].join(", ") %></div>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<% if task[:each] %>
|
|
127
|
+
<div class="text-lg-end">
|
|
128
|
+
<div class="small text-body-secondary mb-1">fan-out progress</div>
|
|
129
|
+
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="<%= task[:each_progress][:total] %>" aria-valuenow="<%= task[:each_progress][:succeeded] %>">
|
|
130
|
+
<div
|
|
131
|
+
class="progress-bar"
|
|
132
|
+
style="width: <%= task[:each_progress][:total].positive? ? (task[:each_progress][:succeeded] * 100 / task[:each_progress][:total]) : 0 %>%"
|
|
133
|
+
></div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="row g-3 mt-1">
|
|
140
|
+
<div class="col-12 col-xl-6">
|
|
141
|
+
<div class="small text-body-secondary mb-2">configuration</div>
|
|
142
|
+
<pre class="jw-pre"><%= JSON.pretty_generate(task[:configuration].as_json) %></pre>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="col-12 col-xl-6">
|
|
146
|
+
<div class="small text-body-secondary mb-2">progress</div>
|
|
147
|
+
<pre class="jw-pre"><%= JSON.pretty_generate(task[:each_progress].as_json) %></pre>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<% if task[:outputs].any? %>
|
|
152
|
+
<div class="mt-3">
|
|
153
|
+
<div class="small text-body-secondary mb-2">outputs</div>
|
|
154
|
+
<pre class="jw-pre"><%= JSON.pretty_generate(task[:outputs].as_json) %></pre>
|
|
155
|
+
</div>
|
|
156
|
+
<% end %>
|
|
157
|
+
|
|
158
|
+
<% if Array(task[:sub_task_jobs]).any? %>
|
|
159
|
+
<div class="mt-3">
|
|
160
|
+
<div class="small text-body-secondary mb-2">sub task jobs</div>
|
|
161
|
+
<div class="table-responsive">
|
|
162
|
+
<table class="table table-sm align-middle mb-0">
|
|
163
|
+
<thead>
|
|
164
|
+
<tr>
|
|
165
|
+
<th>Each</th>
|
|
166
|
+
<th>Status</th>
|
|
167
|
+
<th>Job ID</th>
|
|
168
|
+
<th class="text-end">Mission Control</th>
|
|
169
|
+
</tr>
|
|
170
|
+
</thead>
|
|
171
|
+
<tbody>
|
|
172
|
+
<% Array(task[:sub_task_jobs]).each do |sub_task_job| %>
|
|
173
|
+
<% sub_task_status_class = case sub_task_job[:status]
|
|
174
|
+
when :succeeded then "text-bg-success"
|
|
175
|
+
when :failed then "text-bg-danger"
|
|
176
|
+
when :running then "text-bg-primary"
|
|
177
|
+
else "text-bg-secondary"
|
|
178
|
+
end %>
|
|
179
|
+
<tr>
|
|
180
|
+
<td><%= sub_task_job[:each_index] %></td>
|
|
181
|
+
<td><span class="badge <%= sub_task_status_class %>"><%= sub_task_job[:status] %></span></td>
|
|
182
|
+
<td class="jw-mono"><%= sub_task_job[:job_id] %></td>
|
|
183
|
+
<td class="text-end">
|
|
184
|
+
<% if sub_task_job[:mission_control_job_path] %>
|
|
185
|
+
<%= link_to "Open", sub_task_job[:mission_control_job_path], class: "btn btn-sm btn-outline-primary" %>
|
|
186
|
+
<% else %>
|
|
187
|
+
<span class="text-body-secondary">-</span>
|
|
188
|
+
<% end %>
|
|
189
|
+
</td>
|
|
190
|
+
</tr>
|
|
191
|
+
<% end %>
|
|
192
|
+
</tbody>
|
|
193
|
+
</table>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
</div>
|
|
198
|
+
<% end %>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<section class="mb-4">
|
|
2
|
+
<h1 class="display-6 fw-semibold mb-2">Workflow Definitions</h1>
|
|
3
|
+
<p class="text-body-secondary mb-0">Root execution only. Select one workflow, then drill into its executions.</p>
|
|
4
|
+
</section>
|
|
5
|
+
|
|
6
|
+
<div class="card jw-card">
|
|
7
|
+
<div class="card-body p-0">
|
|
8
|
+
<% if @workflows.empty? %>
|
|
9
|
+
<div class="p-4 text-body-secondary">No workflows are registered.</div>
|
|
10
|
+
<% else %>
|
|
11
|
+
<div class="table-responsive">
|
|
12
|
+
<table class="table table-hover align-middle mb-0">
|
|
13
|
+
<thead class="table-light">
|
|
14
|
+
<tr>
|
|
15
|
+
<th class="ps-4">Workflow</th>
|
|
16
|
+
<th>Tasks</th>
|
|
17
|
+
<th class="text-end pe-4">Executions</th>
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
20
|
+
<tbody>
|
|
21
|
+
<% @workflows.each do |workflow| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="ps-4">
|
|
24
|
+
<div class="fw-semibold"><%= workflow.job_class_name %></div>
|
|
25
|
+
</td>
|
|
26
|
+
<td>
|
|
27
|
+
<span class="badge text-bg-secondary"><%= workflow.task_count %> tasks</span>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="text-end pe-4">
|
|
30
|
+
<%= link_to "View executions", workflow_executions_path(workflow.job_class_name), class: "btn btn-sm btn-primary" %>
|
|
31
|
+
</td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>JobWorkflow Monitoring</title>
|
|
7
|
+
<link
|
|
8
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
|
|
11
|
+
crossorigin="anonymous"
|
|
12
|
+
>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--jw-surface: #f8f9fa;
|
|
16
|
+
--jw-border: #dee2e6;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
background:
|
|
21
|
+
radial-gradient(circle at top right, rgba(13, 110, 253, 0.08), transparent 24rem),
|
|
22
|
+
linear-gradient(180deg, #f8fbff 0%, #f8f9fa 100%);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.jw-shell {
|
|
26
|
+
max-width: 1200px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.jw-mono {
|
|
30
|
+
font-family: ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Consolas, monospace;
|
|
31
|
+
word-break: break-all;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.jw-pre {
|
|
35
|
+
background: #212529;
|
|
36
|
+
color: #f8f9fa;
|
|
37
|
+
border-radius: 0.75rem;
|
|
38
|
+
padding: 1rem;
|
|
39
|
+
margin: 0;
|
|
40
|
+
white-space: pre-wrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.jw-card {
|
|
44
|
+
border-color: var(--jw-border);
|
|
45
|
+
box-shadow: 0 0.5rem 1.5rem rgba(33, 37, 41, 0.06);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.jw-dag-viewport {
|
|
49
|
+
overflow-x: auto;
|
|
50
|
+
padding-bottom: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.jw-dag-canvas {
|
|
54
|
+
position: relative;
|
|
55
|
+
min-height: 12rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.jw-dag-svg {
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: 0;
|
|
61
|
+
overflow: visible;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.jw-dag-edge {
|
|
65
|
+
fill: none;
|
|
66
|
+
stroke: #adb5bd;
|
|
67
|
+
stroke-width: 2;
|
|
68
|
+
stroke-linecap: round;
|
|
69
|
+
stroke-linejoin: round;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.jw-dag-node {
|
|
73
|
+
position: absolute;
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
justify-content: flex-start;
|
|
77
|
+
gap: 0.4rem;
|
|
78
|
+
background: #fff;
|
|
79
|
+
border: 1px solid var(--jw-border);
|
|
80
|
+
border-radius: 0.85rem;
|
|
81
|
+
box-shadow: 0 0.4rem 1rem rgba(33, 37, 41, 0.08);
|
|
82
|
+
padding: 0.75rem 0.875rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.jw-dag-node-title {
|
|
86
|
+
font-size: 0.875rem;
|
|
87
|
+
line-height: 1.15;
|
|
88
|
+
max-width: 10rem;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
text-overflow: ellipsis;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.jw-dag-node-meta {
|
|
95
|
+
color: #6c757d;
|
|
96
|
+
font-size: 0.72rem;
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top">
|
|
102
|
+
<div class="container-fluid jw-shell px-3 px-lg-4">
|
|
103
|
+
<a class="navbar-brand fw-semibold" href="<%= root_path %>">JobWorkflow Monitoring</a>
|
|
104
|
+
</div>
|
|
105
|
+
</nav>
|
|
106
|
+
|
|
107
|
+
<main class="container-fluid jw-shell py-4 py-lg-5 px-3 px-lg-4">
|
|
108
|
+
<%= yield %>
|
|
109
|
+
</main>
|
|
110
|
+
|
|
111
|
+
<script
|
|
112
|
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
|
113
|
+
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
|
114
|
+
crossorigin="anonymous"
|
|
115
|
+
></script>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
data/config/routes.rb
ADDED
data/guides/API_REFERENCE.md
CHANGED
|
@@ -18,10 +18,9 @@ task(name, **options, &block)
|
|
|
18
18
|
- `depends_on` (Symbol | Array[Symbol]): Dependent tasks
|
|
19
19
|
- `each` (Proc): Proc that returns an enumerable for map task execution
|
|
20
20
|
- `enqueue` (Hash | Proc | bool): Controls whether task iterations are enqueued as sub-jobs
|
|
21
|
-
- Hash format (recommended): `{ condition: Proc, queue: String
|
|
21
|
+
- Hash format (recommended): `{ condition: Proc, queue: String }`
|
|
22
22
|
- `condition` (Proc | bool): Determines if task should be enqueued (default: true if Hash is not empty)
|
|
23
23
|
- `queue` (String): Custom queue name for the task (optional)
|
|
24
|
-
- `concurrency` (Integer): Concurrency limit for parallel processing (default: unlimited)
|
|
25
24
|
- Proc format (legacy): Proc that returns boolean
|
|
26
25
|
- bool format: true/false for simple cases
|
|
27
26
|
- Default: nil (synchronous execution)
|
|
@@ -75,7 +74,8 @@ end
|
|
|
75
74
|
# Parallel processing with collection
|
|
76
75
|
task :process_items,
|
|
77
76
|
each: ->(ctx) { ctx.arguments.items },
|
|
78
|
-
enqueue:
|
|
77
|
+
enqueue: true,
|
|
78
|
+
throttle: 5,
|
|
79
79
|
output: { result: "String" } do |ctx|
|
|
80
80
|
item = ctx.each_value
|
|
81
81
|
{ result: ProcessService.handle(item) }
|
|
@@ -187,7 +187,8 @@ class ImportJob < ApplicationJob
|
|
|
187
187
|
|
|
188
188
|
task :process,
|
|
189
189
|
each: ->(ctx) { ctx.arguments.items },
|
|
190
|
-
enqueue:
|
|
190
|
+
enqueue: true,
|
|
191
|
+
throttle: 5,
|
|
191
192
|
output: { result: "String" } do |ctx|
|
|
192
193
|
{ result: handle(ctx.each_value) }
|
|
193
194
|
end
|
|
@@ -211,7 +212,8 @@ class BatchImportJob < ApplicationJob
|
|
|
211
212
|
|
|
212
213
|
task :process,
|
|
213
214
|
each: ->(ctx) { ctx.arguments.items },
|
|
214
|
-
enqueue:
|
|
215
|
+
enqueue: true,
|
|
216
|
+
throttle: 5,
|
|
215
217
|
output: { result: "String" } do |ctx|
|
|
216
218
|
{ result: handle(ctx.each_value) }
|
|
217
219
|
end
|
|
@@ -228,7 +230,8 @@ argument :items, "Array[String]"
|
|
|
228
230
|
|
|
229
231
|
task :process_items,
|
|
230
232
|
each: ->(ctx) { ctx.arguments.items },
|
|
231
|
-
enqueue:
|
|
233
|
+
enqueue: true,
|
|
234
|
+
throttle: 5,
|
|
232
235
|
output: { result: "String", status: "Symbol" } do |ctx|
|
|
233
236
|
item = ctx.each_value
|
|
234
237
|
{
|
data/guides/DEPENDENCY_WAIT.md
CHANGED
|
@@ -14,7 +14,8 @@ class ExampleJob < ApplicationJob
|
|
|
14
14
|
|
|
15
15
|
task :process_items,
|
|
16
16
|
each: ->(ctx) { ctx.arguments.items },
|
|
17
|
-
enqueue:
|
|
17
|
+
enqueue: true,
|
|
18
|
+
throttle: 5,
|
|
18
19
|
output: { result: "Integer" } do |ctx|
|
|
19
20
|
# This creates many sub-jobs
|
|
20
21
|
{ result: ctx.each_value * 2 }
|
|
@@ -172,7 +173,8 @@ class DataPipelineJob < ApplicationJob
|
|
|
172
173
|
# Extract data from multiple sources in parallel
|
|
173
174
|
task :extract_data,
|
|
174
175
|
each: ->(ctx) { %w[users orders products inventory] },
|
|
175
|
-
enqueue:
|
|
176
|
+
enqueue: true,
|
|
177
|
+
throttle: 4,
|
|
176
178
|
output: { source: "String", count: "Integer" } do |ctx|
|
|
177
179
|
source = ctx.each_value
|
|
178
180
|
data = DataSource.fetch(source, date: ctx.arguments.date)
|
|
@@ -210,10 +212,11 @@ class APIAggregatorJob < ApplicationJob
|
|
|
210
212
|
|
|
211
213
|
argument :user_ids, "Array[Integer]"
|
|
212
214
|
|
|
213
|
-
# Fetch user data with rate limiting
|
|
215
|
+
# Fetch user data with rate limiting.
|
|
216
|
+
# Async fan-out is unbounded here; the official execution cap is throttle: 5.
|
|
214
217
|
task :fetch_users,
|
|
215
218
|
each: ->(ctx) { ctx.arguments.user_ids },
|
|
216
|
-
enqueue:
|
|
219
|
+
enqueue: true,
|
|
217
220
|
throttle: { key: "external_api", limit: 5 },
|
|
218
221
|
output: { user_id: "Integer", data: "Hash" } do |ctx|
|
|
219
222
|
user_id = ctx.each_value
|
|
@@ -270,7 +273,8 @@ dependency_wait: { poll_timeout: 60, reschedule_delay: 10 }
|
|
|
270
273
|
# ✅ Good: dependency_wait with parallel sub-jobs
|
|
271
274
|
task :process,
|
|
272
275
|
each: ->(ctx) { ctx.arguments.items },
|
|
273
|
-
enqueue:
|
|
276
|
+
enqueue: true,
|
|
277
|
+
throttle: 10 do |ctx|
|
|
274
278
|
heavy_process(ctx.each_value)
|
|
275
279
|
end
|
|
276
280
|
|