geneva_drive 0.4.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/CHANGELOG.md +38 -0
- data/MANUAL.md +1037 -355
- data/README.md +3 -1
- data/Rakefile +13 -2
- data/bin/md2html +96 -19
- data/lib/generators/geneva_drive/install/install_generator.rb +25 -0
- data/lib/generators/geneva_drive/install/templates/add_metadata_to_step_executions.rb +17 -0
- data/lib/generators/geneva_drive/install/templates/add_metadata_to_workflows.rb +17 -0
- data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +29 -0
- data/lib/generators/geneva_drive/install/templates/add_started_at_index_to_step_executions.rb +48 -0
- data/lib/generators/geneva_drive/install/templates/allow_null_hero_on_workflows.rb +66 -0
- data/lib/generators/geneva_drive/install/templates/create_step_executions_migration.rb +25 -5
- data/lib/generators/geneva_drive/install/templates/create_workflows_migration.rb +14 -3
- data/lib/generators/geneva_drive/install/templates/initializer.rb.tt +22 -2
- data/lib/geneva_drive/combined_exception_policy.rb +106 -0
- data/lib/geneva_drive/exception_policy.rb +354 -0
- data/lib/geneva_drive/executor.rb +430 -148
- data/lib/geneva_drive/flow_control.rb +82 -11
- data/lib/geneva_drive/iterable_step.rb +199 -0
- data/lib/geneva_drive/job_options.rb +70 -0
- data/lib/geneva_drive/jobs/housekeeping_job.rb +174 -52
- data/lib/geneva_drive/jobs/perform_step_job.rb +91 -3
- data/lib/geneva_drive/migration_helpers.rb +55 -20
- data/lib/geneva_drive/resumable_step_definition.rb +97 -0
- data/lib/geneva_drive/step_definition.rb +160 -40
- data/lib/geneva_drive/step_execution/metadata_accessor.rb +121 -0
- data/lib/geneva_drive/step_execution.rb +124 -2
- data/lib/geneva_drive/test_helpers.rb +123 -4
- data/lib/geneva_drive/version.rb +1 -1
- data/lib/geneva_drive/workflow/metadata_accessor.rb +85 -0
- data/lib/geneva_drive/workflow.rb +485 -73
- data/lib/geneva_drive.rb +100 -2
- data/test/dsl/class_level_on_exception_test.rb +200 -0
- data/test/dsl/exception_policy_test.rb +331 -0
- data/test/dsl/step_definition_test.rb +126 -2
- data/test/dsl/step_level_exception_policy_test.rb +143 -0
- data/test/dummy/config/storage.yml +7 -0
- data/test/dummy_install/config/storage.yml +7 -0
- data/test/jobs/housekeeping_job_test.rb +451 -5
- data/test/jobs/perform_step_job_double_deferral_test.rb +268 -0
- data/test/jobs/perform_step_job_test.rb +25 -2
- data/test/migration_helpers_test.rb +137 -0
- data/test/step_execution/metadata_accessor_test.rb +147 -0
- data/test/test_helper.rb +37 -0
- data/test/test_helper_test.rb +281 -0
- data/test/workflow/class_level_on_exception_integration_test.rb +313 -0
- data/test/workflow/composable_exception_policy_test.rb +231 -0
- data/test/workflow/cursor_size_limit_test.rb +75 -0
- data/test/workflow/executor_test.rb +100 -4
- data/test/workflow/external_cancel_test.rb +245 -0
- data/test/workflow/flow_control_test.rb +4 -3
- data/test/workflow/instrumentation_test.rb +28 -0
- data/test/workflow/max_reattempts_test.rb +51 -0
- data/test/workflow/pause_resume_test.rb +574 -0
- data/test/workflow/reattempt_on_exception_integration_test.rb +68 -0
- data/test/workflow/resumable_step_integration_test.rb +341 -0
- data/test/workflow/resumable_step_test.rb +615 -0
- data/test/workflow/resumable_without_migration_test.rb +103 -0
- data/test/workflow/resume_and_skip_test.rb +57 -41
- data/test/workflow/with_inline_enqueue_test.rb +137 -0
- data/test/workflow/workflow_test.rb +117 -2
- metadata +47 -61
- data/test/dummy/db/migrate/20241217000001_create_geneva_drive_workflows.rb +0 -68
- data/test/dummy/db/migrate/20241217000002_create_geneva_drive_step_executions.rb +0 -90
- data/test/dummy/db/migrate/20241217000003_add_finished_at_to_geneva_drive_step_executions.rb +0 -25
- data/test/dummy/db/migrate/20241217000004_add_error_class_name_to_geneva_drive_step_executions.rb +0 -7
- data/test/dummy/db/schema.rb +0 -72
- data/test/dummy/log/development.log +0 -112
- data/test/dummy/log/test.log +0 -4
- data/test/dummy/tmp/local_secret.txt +0 -1
- data/test/dummy_install/Rakefile +0 -6
- data/test/dummy_install/app/assets/stylesheets/application.css +0 -15
- data/test/dummy_install/app/controllers/application_controller.rb +0 -4
- data/test/dummy_install/app/helpers/application_helper.rb +0 -2
- data/test/dummy_install/app/jobs/application_job.rb +0 -7
- data/test/dummy_install/app/models/application_record.rb +0 -3
- data/test/dummy_install/app/models/user.rb +0 -9
- data/test/dummy_install/app/views/layouts/application.html.erb +0 -27
- data/test/dummy_install/app/views/layouts/mailer.html.erb +0 -13
- data/test/dummy_install/app/views/layouts/mailer.text.erb +0 -1
- data/test/dummy_install/app/views/pwa/manifest.json.erb +0 -22
- data/test/dummy_install/app/views/pwa/service-worker.js +0 -26
- data/test/dummy_install/bin/dev +0 -2
- data/test/dummy_install/bin/rails +0 -4
- data/test/dummy_install/bin/rake +0 -4
- data/test/dummy_install/bin/setup +0 -34
- data/test/dummy_install/config/application.rb +0 -29
- data/test/dummy_install/config/boot.rb +0 -5
- data/test/dummy_install/config/cable.yml +0 -10
- data/test/dummy_install/config/database.yml +0 -13
- data/test/dummy_install/config/environment.rb +0 -5
- data/test/dummy_install/config/environments/development.rb +0 -51
- data/test/dummy_install/config/environments/production.rb +0 -70
- data/test/dummy_install/config/environments/test.rb +0 -39
- data/test/dummy_install/config/initializers/assets.rb +0 -7
- data/test/dummy_install/config/initializers/content_security_policy.rb +0 -25
- data/test/dummy_install/config/initializers/filter_parameter_logging.rb +0 -8
- data/test/dummy_install/config/initializers/geneva_drive.rb +0 -76
- data/test/dummy_install/config/initializers/inflections.rb +0 -16
- data/test/dummy_install/config/locales/en.yml +0 -31
- data/test/dummy_install/config/puma.rb +0 -38
- data/test/dummy_install/config/routes.rb +0 -3
- data/test/dummy_install/config.ru +0 -6
- data/test/dummy_install/db/migrate/20241217000000_create_users.rb +0 -12
- data/test/dummy_install/db/migrate/20260128104738_create_geneva_drive_workflows.rb +0 -74
- data/test/dummy_install/db/migrate/20260128104739_create_geneva_drive_step_executions.rb +0 -97
- data/test/dummy_install/db/migrate/20260128104740_add_finished_at_to_geneva_drive_step_executions.rb +0 -25
- data/test/dummy_install/db/migrate/20260128104741_add_error_class_name_to_geneva_drive_step_executions.rb +0 -7
- data/test/dummy_install/db/migrate/20260128104742_add_resumable_step_support_to_geneva_drive_step_executions.rb +0 -25
- data/test/dummy_install/db/schema.rb +0 -76
- data/test/dummy_install/log/test.log +0 -4031
- data/test/dummy_install/public/400.html +0 -114
- data/test/dummy_install/public/404.html +0 -114
- data/test/dummy_install/public/406-unsupported-browser.html +0 -114
- data/test/dummy_install/public/422.html +0 -114
- data/test/dummy_install/public/500.html +0 -114
- data/test/dummy_install/public/icon.png +0 -0
- data/test/dummy_install/public/icon.svg +0 -3
- data/test/dummy_install/tmp/local_secret.txt +0 -1
- data/test/generators/install_generator_test.rb +0 -94
data/README.md
CHANGED
|
@@ -164,12 +164,14 @@ class PremiumWorkflow < BaseWorkflow
|
|
|
164
164
|
cancel_if { hero.subscription_expired? }
|
|
165
165
|
set_step_job_options queue: :premium, priority: 10
|
|
166
166
|
|
|
167
|
-
step :premium_feature do
|
|
167
|
+
step :premium_feature, job_options: {priority: -1} do
|
|
168
168
|
# Inherits cancel_if from parent
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
Step job options override workflow defaults when the step is initially enqueued, reattempted, or resumed. Options are passed directly to Active Job's `set` method.
|
|
174
|
+
|
|
173
175
|
### Hero Deletion Handling
|
|
174
176
|
|
|
175
177
|
By default, workflows cancel if their hero is deleted:
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
require "bundler/setup"
|
|
2
|
+
require "bundler/gem_tasks"
|
|
3
|
+
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
require "standard/rake"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
YARD::Rake::YardocTask.new("yard")
|
|
9
|
+
|
|
10
|
+
task doc: [:yard] do
|
|
11
|
+
outfile = "doc/MANUAL.html"
|
|
12
|
+
`bin/md2html MANUAL.md #{outfile}`
|
|
13
|
+
warn "#{outfile} written"
|
|
14
|
+
end
|
|
2
15
|
|
|
3
16
|
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
4
17
|
load "rails/tasks/engine.rake"
|
|
5
|
-
|
|
6
|
-
require "bundler/gem_tasks"
|
data/bin/md2html
CHANGED
|
@@ -7,9 +7,11 @@ require "bundler/inline"
|
|
|
7
7
|
gemfile do
|
|
8
8
|
source "https://rubygems.org"
|
|
9
9
|
gem "redcarpet"
|
|
10
|
+
gem "nokogiri"
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
require "redcarpet"
|
|
14
|
+
require "nokogiri"
|
|
13
15
|
require "cgi"
|
|
14
16
|
require "erb"
|
|
15
17
|
|
|
@@ -40,7 +42,24 @@ class SyntaxHighlightingHTML < Redcarpet::Render::HTML
|
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
def
|
|
45
|
+
def build_toc(html_body)
|
|
46
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html_body)
|
|
47
|
+
headings = doc.css("h1, h2")
|
|
48
|
+
return "" if headings.empty?
|
|
49
|
+
|
|
50
|
+
toc = +%(<nav class="toc-pane"><ul>\n)
|
|
51
|
+
headings.each do |h|
|
|
52
|
+
id = h["id"]
|
|
53
|
+
next unless id
|
|
54
|
+
level = h.name # "h1" or "h2"
|
|
55
|
+
css_class = (level == "h2") ? ' class="toc-h2"' : ""
|
|
56
|
+
toc << %(<li#{css_class}><a href="##{CGI.escapeHTML(id)}">#{CGI.escapeHTML(h.text)}</a></li>\n)
|
|
57
|
+
end
|
|
58
|
+
toc << %(</ul></nav>\n)
|
|
59
|
+
toc
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def html_template(title, content, toc: "")
|
|
44
63
|
<<~HTML
|
|
45
64
|
<!DOCTYPE html>
|
|
46
65
|
<html lang="en">
|
|
@@ -63,21 +82,73 @@ def html_template(title, content)
|
|
|
63
82
|
--table-border: #dfe2e5;
|
|
64
83
|
--blockquote-color: #6a737d;
|
|
65
84
|
--blockquote-border: #dfe2e5;
|
|
85
|
+
--toc-width: 280px;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
* {
|
|
69
89
|
box-sizing: border-box;
|
|
70
90
|
}
|
|
71
91
|
|
|
72
|
-
body {
|
|
92
|
+
html, body {
|
|
93
|
+
margin: 0;
|
|
94
|
+
padding: 0;
|
|
73
95
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
74
96
|
font-size: 16px;
|
|
75
97
|
line-height: 1.6;
|
|
98
|
+
color: var(--text-color);
|
|
99
|
+
background-color: var(--bg-color);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* TOC sidebar — fixed, scrolls independently */
|
|
103
|
+
.toc-pane {
|
|
104
|
+
position: fixed;
|
|
105
|
+
top: 0;
|
|
106
|
+
left: 0;
|
|
107
|
+
width: var(--toc-width);
|
|
108
|
+
height: 100vh;
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
border-right: 1px solid var(--border-color);
|
|
111
|
+
padding: 2rem 1.25rem;
|
|
112
|
+
font-size: 13px;
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
background-color: var(--bg-color);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.toc-pane ul {
|
|
118
|
+
list-style: none;
|
|
119
|
+
padding: 0;
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.toc-pane li {
|
|
124
|
+
margin: 0.25em 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.toc-pane li.toc-h2 {
|
|
128
|
+
padding-left: 1em;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.toc-pane a {
|
|
132
|
+
color: var(--link-color);
|
|
133
|
+
text-decoration: underline;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.toc-pane a:hover {
|
|
137
|
+
color: #0550ae;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Body scrolls normally — browser scrollbar at window edge */
|
|
141
|
+
body.has-toc {
|
|
142
|
+
margin-left: var(--toc-width);
|
|
143
|
+
padding: 2rem 3rem;
|
|
144
|
+
max-width: calc(960px + var(--toc-width));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Without TOC, body is a simple centered column */
|
|
148
|
+
body:not(.has-toc) {
|
|
76
149
|
max-width: 900px;
|
|
77
150
|
margin: 0 auto;
|
|
78
151
|
padding: 2rem;
|
|
79
|
-
color: var(--text-color);
|
|
80
|
-
background-color: var(--bg-color);
|
|
81
152
|
}
|
|
82
153
|
|
|
83
154
|
/* Headings */
|
|
@@ -241,12 +312,7 @@ def html_template(title, content)
|
|
|
241
312
|
height: auto;
|
|
242
313
|
}
|
|
243
314
|
|
|
244
|
-
/* Print
|
|
245
|
-
.print-footer {
|
|
246
|
-
display: none;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/* Print styles - using Chrome 131+ @page margin boxes */
|
|
315
|
+
/* Print styles */
|
|
250
316
|
@media print {
|
|
251
317
|
@page {
|
|
252
318
|
margin: 1.5cm;
|
|
@@ -265,7 +331,12 @@ def html_template(title, content)
|
|
|
265
331
|
}
|
|
266
332
|
}
|
|
267
333
|
|
|
268
|
-
|
|
334
|
+
.toc-pane {
|
|
335
|
+
display: none;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
body, body.has-toc {
|
|
339
|
+
margin-left: 0;
|
|
269
340
|
max-width: none;
|
|
270
341
|
padding: 0;
|
|
271
342
|
font-size: 11pt;
|
|
@@ -296,8 +367,8 @@ def html_template(title, content)
|
|
|
296
367
|
}
|
|
297
368
|
</style>
|
|
298
369
|
</head>
|
|
299
|
-
<body>
|
|
300
|
-
#{content}
|
|
370
|
+
<body#{' class="has-toc"' unless toc.empty?}>
|
|
371
|
+
#{toc}#{content}
|
|
301
372
|
|
|
302
373
|
<!-- Prism.js for syntax highlighting -->
|
|
303
374
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
|
@@ -338,7 +409,7 @@ def html_template(title, content)
|
|
|
338
409
|
HTML
|
|
339
410
|
end
|
|
340
411
|
|
|
341
|
-
def convert_markdown_to_html(input_path, output_path = nil)
|
|
412
|
+
def convert_markdown_to_html(input_path, output_path = nil, toc: true)
|
|
342
413
|
unless File.exist?(input_path)
|
|
343
414
|
abort "Error: File not found: #{input_path}"
|
|
344
415
|
end
|
|
@@ -373,7 +444,8 @@ def convert_markdown_to_html(input_path, output_path = nil)
|
|
|
373
444
|
})
|
|
374
445
|
|
|
375
446
|
html_body = markdown.render(markdown_content)
|
|
376
|
-
|
|
447
|
+
toc_html = toc ? build_toc(html_body) : ""
|
|
448
|
+
html_document = html_template(title, html_body, toc: toc_html)
|
|
377
449
|
|
|
378
450
|
File.write(output_path, html_document)
|
|
379
451
|
puts "Created: #{output_path}"
|
|
@@ -382,7 +454,7 @@ def convert_markdown_to_html(input_path, output_path = nil)
|
|
|
382
454
|
end
|
|
383
455
|
|
|
384
456
|
if __FILE__ == $0
|
|
385
|
-
options = {open: true}
|
|
457
|
+
options = {open: false, toc: true}
|
|
386
458
|
|
|
387
459
|
parser = OptionParser.new do |opts|
|
|
388
460
|
opts.banner = "Usage: #{$0} [options] INPUT.md [OUTPUT.html]"
|
|
@@ -390,14 +462,19 @@ if __FILE__ == $0
|
|
|
390
462
|
opts.separator "Converts a GitHub-Flavored Markdown file to styled HTML with:"
|
|
391
463
|
opts.separator " - Prism.js syntax highlighting"
|
|
392
464
|
opts.separator " - Mermaid.js diagram rendering"
|
|
465
|
+
opts.separator " - Table of contents sidebar"
|
|
393
466
|
opts.separator " - Print-optimized CSS with page numbers"
|
|
394
467
|
opts.separator ""
|
|
395
468
|
opts.separator "If OUTPUT is not specified, uses INPUT filename with .html extension."
|
|
396
469
|
opts.separator ""
|
|
397
470
|
opts.separator "Options:"
|
|
398
471
|
|
|
399
|
-
opts.on("--no-
|
|
400
|
-
options[:
|
|
472
|
+
opts.on("--no-toc", "Don't generate table of contents sidebar") do
|
|
473
|
+
options[:toc] = false
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
opts.on("--open", "Open the result in the browser") do
|
|
477
|
+
options[:open] = true
|
|
401
478
|
end
|
|
402
479
|
|
|
403
480
|
opts.on("-h", "--help", "Show this help message") do
|
|
@@ -416,7 +493,7 @@ if __FILE__ == $0
|
|
|
416
493
|
input_path = ARGV[0]
|
|
417
494
|
output_path = ARGV[1]
|
|
418
495
|
|
|
419
|
-
result_path = convert_markdown_to_html(input_path, output_path)
|
|
496
|
+
result_path = convert_markdown_to_html(input_path, output_path, toc: options[:toc])
|
|
420
497
|
|
|
421
498
|
# Open in browser if on macOS and not disabled
|
|
422
499
|
if options[:open] && RUBY_PLATFORM =~ /darwin/
|
|
@@ -40,6 +40,31 @@ module GenevaDrive
|
|
|
40
40
|
"add_error_class_name_to_step_executions.rb",
|
|
41
41
|
"db/migrate/add_error_class_name_to_geneva_drive_step_executions.rb"
|
|
42
42
|
)
|
|
43
|
+
|
|
44
|
+
migration_template(
|
|
45
|
+
"add_metadata_to_step_executions.rb",
|
|
46
|
+
"db/migrate/add_metadata_to_geneva_drive_step_executions.rb"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
migration_template(
|
|
50
|
+
"add_metadata_to_workflows.rb",
|
|
51
|
+
"db/migrate/add_metadata_to_geneva_drive_workflows.rb"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
migration_template(
|
|
55
|
+
"allow_null_hero_on_workflows.rb",
|
|
56
|
+
"db/migrate/allow_null_hero_on_geneva_drive_workflows.rb"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
migration_template(
|
|
60
|
+
"add_started_at_index_to_step_executions.rb",
|
|
61
|
+
"db/migrate/add_started_at_index_to_geneva_drive_step_executions.rb"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
migration_template(
|
|
65
|
+
"add_resumable_step_support.rb",
|
|
66
|
+
"db/migrate/add_resumable_step_support_to_geneva_drive_step_executions.rb"
|
|
67
|
+
)
|
|
43
68
|
end
|
|
44
69
|
|
|
45
70
|
# Creates the initializer file.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMetadataToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
return if column_exists?(:geneva_drive_step_executions, :metadata)
|
|
6
|
+
|
|
7
|
+
adapter = connection.adapter_name.downcase
|
|
8
|
+
|
|
9
|
+
if adapter.include?("postgresql")
|
|
10
|
+
add_column :geneva_drive_step_executions, :metadata, :jsonb
|
|
11
|
+
elsif adapter.include?("mysql")
|
|
12
|
+
add_column :geneva_drive_step_executions, :metadata, :text, limit: 4_294_967_295
|
|
13
|
+
else
|
|
14
|
+
add_column :geneva_drive_step_executions, :metadata, :text
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMetadataToGenevaDriveWorkflows < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
return if column_exists?(:geneva_drive_workflows, :metadata)
|
|
6
|
+
|
|
7
|
+
adapter = connection.adapter_name.downcase
|
|
8
|
+
|
|
9
|
+
if adapter.include?("postgresql")
|
|
10
|
+
add_column :geneva_drive_workflows, :metadata, :jsonb
|
|
11
|
+
elsif adapter.include?("mysql")
|
|
12
|
+
add_column :geneva_drive_workflows, :metadata, :text, limit: 4_294_967_295
|
|
13
|
+
else
|
|
14
|
+
add_column :geneva_drive_workflows, :metadata, :text
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddResumableStepSupportToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
4
|
+
include GenevaDrive::MigrationHelpers
|
|
5
|
+
|
|
6
|
+
def change
|
|
7
|
+
unless column_exists?(:geneva_drive_step_executions, :cursor)
|
|
8
|
+
# Cursor for resumable steps. Use database-native JSON type:
|
|
9
|
+
# - PostgreSQL: jsonb (indexed, efficient, supports containment queries)
|
|
10
|
+
# - MySQL 5.7+: json (native validation and storage)
|
|
11
|
+
# - SQLite: json (Rails handles as TEXT with serialization)
|
|
12
|
+
if connection.adapter_name.downcase.include?("postgresql")
|
|
13
|
+
add_column :geneva_drive_step_executions, :cursor, :jsonb
|
|
14
|
+
else
|
|
15
|
+
add_column :geneva_drive_step_executions, :cursor, :json
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless column_exists?(:geneva_drive_step_executions, :continues_from_id)
|
|
20
|
+
# Link successor executions to their predecessor, chaining the execution
|
|
21
|
+
# records of a resumable step. Match the primary key type (bigint or uuid)
|
|
22
|
+
# of the step_executions table.
|
|
23
|
+
# No foreign key constraint - SQLite rewrites the table on add_foreign_key,
|
|
24
|
+
# which can destroy data.
|
|
25
|
+
add_column :geneva_drive_step_executions, :continues_from_id, geneva_drive_key_type
|
|
26
|
+
add_index :geneva_drive_step_executions, :continues_from_id
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddStartedAtIndexToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
4
|
+
# Partial index scoped to state = 'in_progress'. HousekeepingJob's
|
|
5
|
+
# recover_stuck_in_progress! filters `state = 'in_progress' AND
|
|
6
|
+
# started_at < cutoff LIMIT N`; without this the planner walks the whole
|
|
7
|
+
# in-progress set looking for old rows and trips the caller's
|
|
8
|
+
# statement_timeout on large / mostly-fresh workloads.
|
|
9
|
+
INDEX_NAME = :index_geneva_drive_step_executions_in_progress_started_at
|
|
10
|
+
|
|
11
|
+
disable_ddl_transaction!
|
|
12
|
+
|
|
13
|
+
def up
|
|
14
|
+
return if index_name_exists?(:geneva_drive_step_executions, INDEX_NAME)
|
|
15
|
+
|
|
16
|
+
adapter = connection.adapter_name.downcase
|
|
17
|
+
|
|
18
|
+
if adapter.include?("postgresql")
|
|
19
|
+
add_index :geneva_drive_step_executions, :started_at,
|
|
20
|
+
name: INDEX_NAME,
|
|
21
|
+
where: "state = 'in_progress'",
|
|
22
|
+
algorithm: :concurrently
|
|
23
|
+
elsif adapter.include?("sqlite")
|
|
24
|
+
# SQLite supports partial indexes but has no CONCURRENTLY.
|
|
25
|
+
add_index :geneva_drive_step_executions, :started_at,
|
|
26
|
+
name: INDEX_NAME,
|
|
27
|
+
where: "state = 'in_progress'"
|
|
28
|
+
else
|
|
29
|
+
# MySQL does not support partial indexes; fall back to a plain index.
|
|
30
|
+
# Composite (state, started_at) so the planner can seek by state first.
|
|
31
|
+
add_index :geneva_drive_step_executions, [:state, :started_at],
|
|
32
|
+
name: INDEX_NAME
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def down
|
|
37
|
+
return unless index_name_exists?(:geneva_drive_step_executions, INDEX_NAME)
|
|
38
|
+
|
|
39
|
+
adapter = connection.adapter_name.downcase
|
|
40
|
+
if adapter.include?("postgresql")
|
|
41
|
+
remove_index :geneva_drive_step_executions,
|
|
42
|
+
name: INDEX_NAME,
|
|
43
|
+
algorithm: :concurrently
|
|
44
|
+
else
|
|
45
|
+
remove_index :geneva_drive_step_executions, name: INDEX_NAME
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AllowNullHeroOnGenevaDriveWorkflows < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
return unless column_exists?(:geneva_drive_workflows, :hero_type)
|
|
6
|
+
|
|
7
|
+
adapter = connection.adapter_name.downcase
|
|
8
|
+
if adapter.include?("sqlite")
|
|
9
|
+
allow_null_hero_sqlite
|
|
10
|
+
else
|
|
11
|
+
change_column_null :geneva_drive_workflows, :hero_type, true
|
|
12
|
+
change_column_null :geneva_drive_workflows, :hero_id, true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def allow_null_hero_sqlite
|
|
19
|
+
reversible do |direction|
|
|
20
|
+
direction.up do
|
|
21
|
+
# SQLite cannot ALTER COLUMN to remove NOT NULL without a full table
|
|
22
|
+
# recreation. We perform an atomic table swap to avoid foreign-key
|
|
23
|
+
# corruption issues. See CLAUDE.md / AGENTS.md for context.
|
|
24
|
+
|
|
25
|
+
# 1. Introspect the existing table via ActiveRecord
|
|
26
|
+
old_columns = connection.columns(:geneva_drive_workflows)
|
|
27
|
+
old_pk = connection.primary_key(:geneva_drive_workflows)
|
|
28
|
+
pk_col = old_columns.find { |c| c.name == old_pk }
|
|
29
|
+
data_cols = old_columns.reject { |c| c.name == old_pk }
|
|
30
|
+
|
|
31
|
+
# 2. Create the replacement table using the schema DSL
|
|
32
|
+
create_table :geneva_drive_workflows_new, id: pk_col.type do |t|
|
|
33
|
+
data_cols.each do |col|
|
|
34
|
+
nullable = %w[hero_type hero_id].include?(col.name) || col.null
|
|
35
|
+
opts = {null: nullable}
|
|
36
|
+
opts[:default] = col.default_function || col.default unless col.default.nil? && col.default_function.nil?
|
|
37
|
+
t.column col.name, col.type, **opts
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 3. Copy all data
|
|
42
|
+
col_names = old_columns.map(&:name).join(", ")
|
|
43
|
+
execute "INSERT INTO geneva_drive_workflows_new (#{col_names}) SELECT #{col_names} FROM geneva_drive_workflows"
|
|
44
|
+
|
|
45
|
+
# 4. Atomic swap
|
|
46
|
+
drop_table :geneva_drive_workflows
|
|
47
|
+
rename_table :geneva_drive_workflows_new, :geneva_drive_workflows
|
|
48
|
+
|
|
49
|
+
# 5. Recreate indexes (they are lost during the swap)
|
|
50
|
+
add_index :geneva_drive_workflows, :type
|
|
51
|
+
add_index :geneva_drive_workflows, :state
|
|
52
|
+
add_index :geneva_drive_workflows, [:hero_type, :hero_id]
|
|
53
|
+
execute <<-SQL
|
|
54
|
+
CREATE UNIQUE INDEX index_geneva_drive_workflows_unique_ongoing
|
|
55
|
+
ON geneva_drive_workflows (type, hero_type, hero_id)
|
|
56
|
+
WHERE state NOT IN ('finished', 'canceled') AND allow_multiple = 0;
|
|
57
|
+
SQL
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
direction.down do
|
|
61
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
62
|
+
"Cannot re-add NOT NULL on hero columns in SQLite without risking data loss"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -5,14 +5,18 @@ class CreateGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
|
5
5
|
|
|
6
6
|
def change
|
|
7
7
|
key_type = geneva_drive_key_type
|
|
8
|
+
adapter = connection.adapter_name.downcase
|
|
9
|
+
|
|
10
|
+
# Build reference options - we add foreign key separately to avoid MySQL type mismatch
|
|
11
|
+
reference_options = {
|
|
12
|
+
null: false,
|
|
13
|
+
index: true
|
|
14
|
+
}
|
|
15
|
+
reference_options[:type] = key_type if key_type == :uuid
|
|
8
16
|
|
|
9
17
|
create_table :geneva_drive_step_executions, **geneva_drive_table_options do |t|
|
|
10
18
|
# Link to workflow (cascade delete when workflow is deleted)
|
|
11
|
-
t.references :workflow,
|
|
12
|
-
null: false,
|
|
13
|
-
foreign_key: {to_table: :geneva_drive_workflows, on_delete: :cascade},
|
|
14
|
-
index: true,
|
|
15
|
-
type: key_type
|
|
19
|
+
t.references :workflow, **reference_options
|
|
16
20
|
|
|
17
21
|
# Which step this execution represents
|
|
18
22
|
t.string :step_name, null: false
|
|
@@ -41,6 +45,15 @@ class CreateGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
|
41
45
|
# Job tracking (for debugging)
|
|
42
46
|
t.string :job_id
|
|
43
47
|
|
|
48
|
+
# Freeform JSON metadata for executor bookkeeping
|
|
49
|
+
if adapter.include?("postgresql")
|
|
50
|
+
t.jsonb :metadata
|
|
51
|
+
elsif adapter.include?("mysql")
|
|
52
|
+
t.text :metadata, limit: 4_294_967_295
|
|
53
|
+
else
|
|
54
|
+
t.text :metadata
|
|
55
|
+
end
|
|
56
|
+
|
|
44
57
|
t.timestamps
|
|
45
58
|
end
|
|
46
59
|
|
|
@@ -55,6 +68,13 @@ class CreateGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
|
|
|
55
68
|
# Index for common query patterns
|
|
56
69
|
add_index :geneva_drive_step_executions, [:workflow_id, :state]
|
|
57
70
|
|
|
71
|
+
# Add foreign key separately to avoid MySQL type mismatch (UNSIGNED vs SIGNED bigint)
|
|
72
|
+
# MySQL creates primary keys as UNSIGNED but references as SIGNED, causing FK constraint failure
|
|
73
|
+
unless adapter.include?("mysql")
|
|
74
|
+
add_foreign_key :geneva_drive_step_executions, :geneva_drive_workflows,
|
|
75
|
+
column: :workflow_id, on_delete: :cascade
|
|
76
|
+
end
|
|
77
|
+
|
|
58
78
|
reversible do |direction|
|
|
59
79
|
direction.up { create_db_specific_indices }
|
|
60
80
|
end
|
|
@@ -4,13 +4,15 @@ class CreateGenevaDriveWorkflows < ActiveRecord::Migration[7.2]
|
|
|
4
4
|
include GenevaDrive::MigrationHelpers
|
|
5
5
|
|
|
6
6
|
def change
|
|
7
|
+
adapter = connection.adapter_name.downcase
|
|
8
|
+
|
|
7
9
|
create_table :geneva_drive_workflows, **geneva_drive_table_options do |t|
|
|
8
10
|
# Core identification (STI)
|
|
9
11
|
t.string :type, null: false, index: true
|
|
10
12
|
|
|
11
|
-
# Polymorphic association to the hero of the workflow
|
|
12
|
-
t.string :hero_type
|
|
13
|
-
t.column :hero_id, geneva_drive_key_type
|
|
13
|
+
# Polymorphic association to the hero of the workflow (optional)
|
|
14
|
+
t.string :hero_type
|
|
15
|
+
t.column :hero_id, geneva_drive_key_type
|
|
14
16
|
|
|
15
17
|
# State machine
|
|
16
18
|
t.string :state, null: false, default: "ready", index: true
|
|
@@ -22,6 +24,15 @@ class CreateGenevaDriveWorkflows < ActiveRecord::Migration[7.2]
|
|
|
22
24
|
# Multiple workflows of same type for same hero
|
|
23
25
|
t.boolean :allow_multiple, default: false, null: false
|
|
24
26
|
|
|
27
|
+
# Freeform JSON metadata
|
|
28
|
+
if adapter.include?("postgresql")
|
|
29
|
+
t.jsonb :metadata
|
|
30
|
+
elsif adapter.include?("mysql")
|
|
31
|
+
t.text :metadata, limit: 4_294_967_295
|
|
32
|
+
else
|
|
33
|
+
t.text :metadata
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
# Timestamps
|
|
26
37
|
t.datetime :started_at
|
|
27
38
|
t.datetime :transitioned_at
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# How long to keep completed/canceled workflows before automatic deletion.
|
|
4
|
-
# Set to nil to disable automatic cleanup
|
|
4
|
+
# Set to nil to disable automatic cleanup.
|
|
5
5
|
# When enabled, finished and canceled workflows older than this threshold
|
|
6
6
|
# will be deleted along with their step execution records.
|
|
7
7
|
#
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# still in "scheduled" state before being considered stuck. This can
|
|
19
19
|
# happen if jobs fail to enqueue or are lost by the queue backend.
|
|
20
20
|
#
|
|
21
|
-
# GenevaDrive.stuck_scheduled_threshold =
|
|
21
|
+
# GenevaDrive.stuck_scheduled_threshold = 15.minutes
|
|
22
22
|
|
|
23
23
|
# Action to take when recovering stuck step executions.
|
|
24
24
|
# - :reattempt - Mark as recovered and schedule a retry (default)
|
|
@@ -30,3 +30,23 @@
|
|
|
30
30
|
# housekeeping run. Prevents runaway processing if there's a large backlog.
|
|
31
31
|
#
|
|
32
32
|
# GenevaDrive.housekeeping_batch_size = 1000
|
|
33
|
+
|
|
34
|
+
# Maximum size in bytes of a resumable step cursor once serialized to JSON.
|
|
35
|
+
# The cursor is a position marker (an id, page number, or pagination token),
|
|
36
|
+
# not a place to store the data being processed - it is rewritten on every
|
|
37
|
+
# checkpoint and copied to every successor execution. Exceeding the limit
|
|
38
|
+
# raises GenevaDrive::CursorTooLargeError. Set to nil to disable the check.
|
|
39
|
+
#
|
|
40
|
+
# GenevaDrive.max_cursor_size = 128.kilobytes
|
|
41
|
+
|
|
42
|
+
# Whether to defer job enqueueing until after the database transaction commits.
|
|
43
|
+
# In production this ensures step execution records are visible to job workers
|
|
44
|
+
# before the job runs. In test environments (the default when Rails.env.test?)
|
|
45
|
+
# this is set to false so that jobs are enqueued immediately — transactional
|
|
46
|
+
# tests (especially with SQLite) never commit the outermost transaction, which
|
|
47
|
+
# can cause after_commit callbacks to misbehave or not fire at all.
|
|
48
|
+
#
|
|
49
|
+
# Set to true in tests if you need strict transaction semantics for a specific
|
|
50
|
+
# test suite, or if you are not using transactional tests.
|
|
51
|
+
#
|
|
52
|
+
# GenevaDrive.enqueue_after_commit = !Rails.env.test?
|