geneva_drive 0.7.0 → 0.9.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 +21 -0
- data/MANUAL.md +299 -5
- data/README.md +14 -1
- data/Rakefile +28 -1
- data/bin/matrix +76 -0
- data/bin/md2html +114 -0
- data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
- data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +5 -10
- data/lib/generators/geneva_drive/install/templates/create_workflows_migration.rb +1 -1
- data/lib/generators/geneva_drive/install/templates/initializer.rb.tt +33 -2
- data/lib/geneva_drive/dispatchers/active_job_dispatcher.rb +33 -0
- data/lib/geneva_drive/dispatchers/sidekiq_dispatcher.rb +112 -0
- data/lib/geneva_drive/engine.rb +13 -0
- data/lib/geneva_drive/executor.rb +23 -3
- data/lib/geneva_drive/job_options.rb +7 -3
- data/lib/geneva_drive/jobs/perform_step_job.rb +2 -35
- data/lib/geneva_drive/jobs/sidekiq_housekeeping_job.rb +32 -0
- data/lib/geneva_drive/jobs/sidekiq_perform_step_job.rb +35 -0
- data/lib/geneva_drive/jobs/step_execution_lookup.rb +42 -0
- data/lib/geneva_drive/json_column.rb +72 -0
- data/lib/geneva_drive/migration_helpers.rb +60 -3
- data/lib/geneva_drive/removed_step_definition.rb +34 -0
- data/lib/geneva_drive/signal.rb +4 -2
- data/lib/geneva_drive/step_collection.rb +31 -7
- data/lib/geneva_drive/step_definition.rb +9 -0
- data/lib/geneva_drive/step_execution.rb +3 -2
- data/lib/geneva_drive/version.rb +1 -1
- data/lib/geneva_drive/workflow.rb +200 -39
- data/lib/geneva_drive.rb +65 -0
- data/test/dummy/config/database.yml +40 -5
- data/test/dummy/config/initializers/geneva_drive.rb +78 -0
- data/test/dummy/db/mysql/schema-8.1.rb +95 -0
- data/test/dummy/db/postgres/schema-8.1.rb +99 -0
- data/test/dummy/db/sqlite/schema-7.2.rb +96 -0
- data/test/dummy/db/sqlite/schema-8.0.rb +96 -0
- data/test/dummy/db/sqlite/schema-8.1.rb +96 -0
- data/test/dummy/db/sqlite/test.sqlite3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-0 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-1 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-2 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-4 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-5 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-6 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-7 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-8 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3-9 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_0 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_1 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_2 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_3 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_4 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_5 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_6 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_7 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_8 +0 -0
- data/test/dummy/db/sqlite/test.sqlite3_9 +0 -0
- data/test/dummy/db/sqlite3/schema-7.2.rb +96 -0
- data/test/dummy/db/sqlite3/schema-8.1.rb +96 -0
- data/test/dummy/db/sqlite3/test.sqlite3 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_0 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_1 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_2 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_3 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_4 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_5 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_6 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_7 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_8 +0 -0
- data/test/dummy/db/sqlite3/test.sqlite3_9 +0 -0
- data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
- data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
- data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
- data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
- data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
- data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
- data/test/dummy_install/db/schema.rb +75 -0
- data/test/dummy_install/log/test.log +1852 -0
- data/test/generators/install_generator_test.rb +56 -0
- data/test/geneva_drive_test.rb +10 -0
- data/test/migration_helpers_test.rb +62 -0
- data/test/sidekiq/sidekiq_dispatcher_test.rb +322 -0
- data/test/sidekiq/sidekiq_redis_test.rb +115 -0
- data/test/test_helper.rb +51 -13
- data/test/workflow/pause_resume_test.rb +15 -0
- data/test/workflow/removed_step_test.rb +294 -0
- data/test/workflow/resume_at_test.rb +170 -0
- metadata +62 -1
data/lib/geneva_drive.rb
CHANGED
|
@@ -39,6 +39,7 @@ module GenevaDrive
|
|
|
39
39
|
autoload :FlowControl, "geneva_drive/flow_control"
|
|
40
40
|
autoload :StepDefinition, "geneva_drive/step_definition"
|
|
41
41
|
autoload :ResumableStepDefinition, "geneva_drive/resumable_step_definition"
|
|
42
|
+
autoload :RemovedStepDefinition, "geneva_drive/removed_step_definition"
|
|
42
43
|
autoload :StepCollection, "geneva_drive/step_collection"
|
|
43
44
|
autoload :Workflow, "geneva_drive/workflow"
|
|
44
45
|
autoload :StepExecution, "geneva_drive/step_execution"
|
|
@@ -49,12 +50,20 @@ module GenevaDrive
|
|
|
49
50
|
autoload :ExceptionPolicy, "geneva_drive/exception_policy"
|
|
50
51
|
autoload :JobOptions, "geneva_drive/job_options"
|
|
51
52
|
autoload :CombinedExceptionPolicy, "geneva_drive/combined_exception_policy"
|
|
53
|
+
autoload :JsonColumn, "geneva_drive/json_column"
|
|
52
54
|
autoload :MigrationHelpers, "geneva_drive/migration_helpers"
|
|
53
55
|
autoload :TestHelpers, "geneva_drive/test_helpers"
|
|
54
56
|
|
|
57
|
+
# Dispatchers
|
|
58
|
+
autoload :ActiveJobDispatcher, "geneva_drive/dispatchers/active_job_dispatcher"
|
|
59
|
+
autoload :SidekiqDispatcher, "geneva_drive/dispatchers/sidekiq_dispatcher"
|
|
60
|
+
|
|
55
61
|
# Jobs
|
|
62
|
+
autoload :StepExecutionLookup, "geneva_drive/jobs/step_execution_lookup"
|
|
56
63
|
autoload :PerformStepJob, "geneva_drive/jobs/perform_step_job"
|
|
57
64
|
autoload :HousekeepingJob, "geneva_drive/jobs/housekeeping_job"
|
|
65
|
+
autoload :SidekiqPerformStepJob, "geneva_drive/jobs/sidekiq_perform_step_job"
|
|
66
|
+
autoload :SidekiqHousekeepingJob, "geneva_drive/jobs/sidekiq_housekeeping_job"
|
|
58
67
|
|
|
59
68
|
class << self
|
|
60
69
|
# How long to keep completed workflows before cleanup.
|
|
@@ -178,6 +187,62 @@ module GenevaDrive
|
|
|
178
187
|
Thread.current[:geneva_drive_inline_enqueue] = previous
|
|
179
188
|
end
|
|
180
189
|
|
|
190
|
+
# Turns "this step execution should run" into a job: `:active_job` (the
|
|
191
|
+
# default, see ActiveJobDispatcher) or `:sidekiq` (see SidekiqDispatcher).
|
|
192
|
+
# Set it in an initializer, before any workflow class loads, since step
|
|
193
|
+
# job options are validated against it when a workflow class is defined.
|
|
194
|
+
#
|
|
195
|
+
# @example Run step jobs as native Sidekiq jobs
|
|
196
|
+
# GenevaDrive.dispatcher = :sidekiq
|
|
197
|
+
#
|
|
198
|
+
# @return [#enqueue_step, #validate_job_options!]
|
|
199
|
+
def dispatcher
|
|
200
|
+
@dispatcher ||= GenevaDrive::ActiveJobDispatcher.new
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @param value [Symbol, #enqueue_step] `:active_job`, `:sidekiq`, or a
|
|
204
|
+
# dispatcher object
|
|
205
|
+
# @return [void]
|
|
206
|
+
# @raise [ArgumentError] for anything else
|
|
207
|
+
def dispatcher=(value)
|
|
208
|
+
@dispatcher = case value
|
|
209
|
+
when :active_job
|
|
210
|
+
GenevaDrive::ActiveJobDispatcher.new
|
|
211
|
+
when :sidekiq
|
|
212
|
+
GenevaDrive::SidekiqDispatcher.new
|
|
213
|
+
else
|
|
214
|
+
unless value.respond_to?(:enqueue_step) && value.respond_to?(:validate_job_options!)
|
|
215
|
+
raise ArgumentError,
|
|
216
|
+
"GenevaDrive.dispatcher must be :active_job, :sidekiq, or an object responding to " \
|
|
217
|
+
"enqueue_step and validate_job_options! (got #{value.inspect})"
|
|
218
|
+
end
|
|
219
|
+
value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Tells resumable steps running in this process to hand off at their next
|
|
224
|
+
# iteration boundary. Wired to Sidekiq's :quiet event by the engine, which
|
|
225
|
+
# fires on TSTP and at the start of every shutdown. Process-wide, unlike
|
|
226
|
+
# the thread-local flag the executor also honours.
|
|
227
|
+
#
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @api private
|
|
230
|
+
def request_exit!
|
|
231
|
+
@exit_requested = true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @return [Boolean] whether the host process asked running work to stop
|
|
235
|
+
# @api private
|
|
236
|
+
def exit_requested?
|
|
237
|
+
@exit_requested == true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# @return [void]
|
|
241
|
+
# @api private
|
|
242
|
+
def reset_exit_request!
|
|
243
|
+
@exit_requested = false
|
|
244
|
+
end
|
|
245
|
+
|
|
181
246
|
# Returns true if job enqueueing should be deferred to after transaction commit.
|
|
182
247
|
#
|
|
183
248
|
# @return [Boolean]
|
|
@@ -1,13 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
# Everything the database tasks write under db/ is adapter-specific, the
|
|
3
|
+
# schema dump above all: parallelize() builds every worker database out of
|
|
4
|
+
# that dump, and a PostgreSQL one carries jsonb columns SQLite cannot load.
|
|
5
|
+
# CI points DATABASE_URL at a different engine per leg, so each engine gets
|
|
6
|
+
# its own corner of db/ and the legs stop overwriting each other's artifacts.
|
|
7
|
+
#
|
|
8
|
+
# This has to be resolved here rather than in an initializer - db:prepare and
|
|
9
|
+
# the other database tasks hang off :load_config, which never runs those.
|
|
10
|
+
url = ENV["DATABASE_URL"]
|
|
11
|
+
adapter = if url
|
|
12
|
+
ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.new(url).to_hash[:adapter]
|
|
13
|
+
else
|
|
14
|
+
"sqlite3"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# MariaDB and MySQL are both the mysql2 adapter but do not produce the same
|
|
18
|
+
# dump - MariaDB has no JSON type - so the engine, when the runner names one,
|
|
19
|
+
# is what the artifacts get filed under.
|
|
20
|
+
engine = ENV.fetch("GENEVA_DRIVE_ENGINE", adapter)
|
|
21
|
+
artifacts = "db/#{engine}"
|
|
22
|
+
|
|
23
|
+
# The dump is keyed by Rails minor on top of that: it is written as
|
|
24
|
+
# `ActiveRecord::Schema[8.1].define`, and 7.2 refuses to load a version it
|
|
25
|
+
# has never heard of. The databases themselves are not version-specific -
|
|
26
|
+
# the migrations produce the same DDL - so only the dump gets the suffix.
|
|
27
|
+
schema_dump = "#{engine}/schema-#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}.rb"
|
|
28
|
+
|
|
29
|
+
# Rails only creates db/ itself, not the subdirectory the dump lives in.
|
|
30
|
+
require "fileutils"
|
|
31
|
+
FileUtils.mkdir_p(Rails.root.join(artifacts))
|
|
32
|
+
%>
|
|
33
|
+
# The test suite runs against SQLite by default so that a fresh clone can run
|
|
34
|
+
# `rake` with no database server. CI - and `bin/matrix` locally - override this
|
|
35
|
+
# with DATABASE_URL to also exercise PostgreSQL and MySQL.
|
|
2
36
|
default: &default
|
|
3
|
-
adapter:
|
|
4
|
-
encoding: unicode
|
|
37
|
+
adapter: sqlite3
|
|
5
38
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
39
|
+
timeout: 5000
|
|
40
|
+
schema_dump: <%= schema_dump %>
|
|
6
41
|
|
|
7
42
|
development:
|
|
8
43
|
<<: *default
|
|
9
|
-
database:
|
|
44
|
+
database: storage/development.sqlite3
|
|
10
45
|
|
|
11
46
|
test:
|
|
12
47
|
<<: *default
|
|
13
|
-
database:
|
|
48
|
+
database: <%= artifacts %>/test.sqlite3
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# How step jobs reach your queue.
|
|
4
|
+
# - :active_job (the default) enqueues GenevaDrive::PerformStepJob through
|
|
5
|
+
# Active Job, onto whatever adapter the app uses.
|
|
6
|
+
# - :sidekiq pushes GenevaDrive::SidekiqPerformStepJob, a plain Sidekiq::Job,
|
|
7
|
+
# straight through Sidekiq::Client - for apps that write Sidekiq jobs rather
|
|
8
|
+
# than Active Job ones. Schedule GenevaDrive::SidekiqHousekeepingJob then.
|
|
9
|
+
# Supports Sidekiq 7.3 and later 7.x, and 8.x.
|
|
10
|
+
#
|
|
11
|
+
# GenevaDrive.dispatcher = :active_job
|
|
12
|
+
|
|
13
|
+
# How long to keep completed/canceled workflows before automatic deletion.
|
|
14
|
+
# Set to nil to disable automatic cleanup.
|
|
15
|
+
# When enabled, finished and canceled workflows older than this threshold
|
|
16
|
+
# will be deleted along with their step execution records.
|
|
17
|
+
#
|
|
18
|
+
# GenevaDrive.delete_completed_workflows_after = 30.days
|
|
19
|
+
|
|
20
|
+
# How long a step execution can be in "in_progress" state before being
|
|
21
|
+
# considered stuck. This typically happens when a worker process crashes
|
|
22
|
+
# mid-execution. The HousekeepingJob will recover these based on the
|
|
23
|
+
# stuck_recovery_action setting.
|
|
24
|
+
#
|
|
25
|
+
# GenevaDrive.stuck_in_progress_threshold = 1.hour
|
|
26
|
+
|
|
27
|
+
# How long a step execution can be past its scheduled_for time while
|
|
28
|
+
# still in "scheduled" state before being considered stuck. This can
|
|
29
|
+
# happen if jobs fail to enqueue or are lost by the queue backend.
|
|
30
|
+
#
|
|
31
|
+
# GenevaDrive.stuck_scheduled_threshold = 15.minutes
|
|
32
|
+
|
|
33
|
+
# Action to take when recovering stuck step executions.
|
|
34
|
+
# - :reattempt - Mark as recovered and schedule a retry (default)
|
|
35
|
+
# - :cancel - Mark as recovered and cancel the workflow
|
|
36
|
+
#
|
|
37
|
+
# GenevaDrive.stuck_recovery_action = :reattempt
|
|
38
|
+
|
|
39
|
+
# Maximum number of workflows/step executions to process in a single
|
|
40
|
+
# housekeeping run. Prevents runaway processing if there's a large backlog.
|
|
41
|
+
#
|
|
42
|
+
# GenevaDrive.housekeeping_batch_size = 1000
|
|
43
|
+
|
|
44
|
+
# Maximum size in bytes of a resumable step cursor once serialized to JSON.
|
|
45
|
+
# The cursor is a position marker (an id, page number, or pagination token),
|
|
46
|
+
# not a place to store the data being processed - it is rewritten on every
|
|
47
|
+
# checkpoint and copied to every successor execution. Exceeding the limit
|
|
48
|
+
# raises GenevaDrive::CursorTooLargeError. Set to nil to disable the check.
|
|
49
|
+
#
|
|
50
|
+
# GenevaDrive.max_cursor_size = 128.kilobytes
|
|
51
|
+
|
|
52
|
+
# Maximum size in bytes of a signal payload once serialized to JSON.
|
|
53
|
+
# A payload describes an event (an order id, an amount, a decision), not the
|
|
54
|
+
# data the step is going to work on - that belongs on the hero. Exceeding the
|
|
55
|
+
# limit raises GenevaDrive::SignalPayloadTooLargeError from Workflow#signal!,
|
|
56
|
+
# before anything is persisted. Set to nil to disable the check.
|
|
57
|
+
#
|
|
58
|
+
# GenevaDrive.max_signal_payload_size = 128.kilobytes
|
|
59
|
+
|
|
60
|
+
# How long a step execution may sit in the "waiting" state before
|
|
61
|
+
# housekeeping counts it in the geneva_drive.waiting_overdue gauge.
|
|
62
|
+
# Waiting indefinitely is legitimate, so this is not a timeout - it keeps
|
|
63
|
+
# a stalled rendezvous from going unnoticed. Alert on the overdue gauge.
|
|
64
|
+
# Set to nil to disable the gauge.
|
|
65
|
+
#
|
|
66
|
+
# GenevaDrive.waiting_visibility_threshold = 7.days
|
|
67
|
+
|
|
68
|
+
# Whether to defer job enqueueing until after the database transaction commits.
|
|
69
|
+
# In production this ensures step execution records are visible to job workers
|
|
70
|
+
# before the job runs. In test environments (the default when Rails.env.test?)
|
|
71
|
+
# this is set to false so that jobs are enqueued immediately — transactional
|
|
72
|
+
# tests (especially with SQLite) never commit the outermost transaction, which
|
|
73
|
+
# can cause after_commit callbacks to misbehave or not fire at all.
|
|
74
|
+
#
|
|
75
|
+
# Set to true in tests if you need strict transaction semantics for a specific
|
|
76
|
+
# test suite, or if you are not using transactional tests.
|
|
77
|
+
#
|
|
78
|
+
# GenevaDrive.enqueue_after_commit = !Rails.env.test?
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
|
4
|
+
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
10
|
+
#
|
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Schema[8.1].define(version: 2026_10_25_000614) do
|
|
14
|
+
create_table "geneva_drive_signals", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
15
|
+
t.bigint "workflow_id", null: false
|
|
16
|
+
t.string "name", null: false
|
|
17
|
+
t.string "idempotency_key"
|
|
18
|
+
t.string "state", default: "pending", null: false
|
|
19
|
+
t.json "payload"
|
|
20
|
+
t.datetime "claimed_at"
|
|
21
|
+
t.datetime "consumed_at"
|
|
22
|
+
t.bigint "claimed", default: 0, null: false
|
|
23
|
+
t.bigint "consumed", default: 0, null: false
|
|
24
|
+
t.datetime "created_at", null: false
|
|
25
|
+
t.datetime "updated_at", null: false
|
|
26
|
+
t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
|
|
27
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
|
|
28
|
+
t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
create_table "geneva_drive_step_executions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
32
|
+
t.bigint "workflow_id", null: false
|
|
33
|
+
t.string "step_name", null: false
|
|
34
|
+
t.string "state", default: "scheduled", null: false
|
|
35
|
+
t.string "outcome"
|
|
36
|
+
t.datetime "scheduled_for", null: false
|
|
37
|
+
t.datetime "started_at"
|
|
38
|
+
t.datetime "completed_at"
|
|
39
|
+
t.datetime "failed_at"
|
|
40
|
+
t.datetime "canceled_at"
|
|
41
|
+
t.datetime "skipped_at"
|
|
42
|
+
t.string "error_class_name"
|
|
43
|
+
t.text "error_message"
|
|
44
|
+
t.text "error_backtrace"
|
|
45
|
+
t.string "job_id"
|
|
46
|
+
t.text "metadata", size: :long
|
|
47
|
+
t.datetime "created_at", null: false
|
|
48
|
+
t.datetime "updated_at", null: false
|
|
49
|
+
t.virtual "active_unique_key", type: :string, limit: 767, as: "(case when (`state` in (_utf8mb4'scheduled',_utf8mb4'in_progress')) then cast(`workflow_id` as char charset utf8mb4) else NULL end)", stored: true
|
|
50
|
+
t.datetime "finished_at"
|
|
51
|
+
t.json "cursor"
|
|
52
|
+
t.bigint "continues_from_id"
|
|
53
|
+
t.bigint "signal_id"
|
|
54
|
+
t.datetime "waiting_since"
|
|
55
|
+
t.index ["active_unique_key"], name: "index_geneva_drive_step_executions_one_active", unique: true
|
|
56
|
+
t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
|
|
57
|
+
t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
|
|
58
|
+
t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
|
|
59
|
+
t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
|
|
60
|
+
t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
|
|
61
|
+
t.index ["state", "started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at"
|
|
62
|
+
t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
|
|
63
|
+
t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
|
|
64
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
|
|
65
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
create_table "geneva_drive_workflows", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
69
|
+
t.string "type", null: false
|
|
70
|
+
t.string "hero_type"
|
|
71
|
+
t.bigint "hero_id"
|
|
72
|
+
t.string "state", default: "ready", null: false
|
|
73
|
+
t.string "current_step_name"
|
|
74
|
+
t.string "next_step_name"
|
|
75
|
+
t.boolean "allow_multiple", default: false, null: false
|
|
76
|
+
t.text "metadata", size: :long
|
|
77
|
+
t.datetime "started_at"
|
|
78
|
+
t.datetime "transitioned_at"
|
|
79
|
+
t.datetime "created_at", null: false
|
|
80
|
+
t.datetime "updated_at", null: false
|
|
81
|
+
t.virtual "ongoing_unique_key", type: :string, limit: 767, as: "(case when ((`state` not in (_utf8mb4'finished',_utf8mb4'canceled')) and (`allow_multiple` = 0)) then concat(`type`,_utf8mb4'-',`hero_type`,_utf8mb4'-',`hero_id`) else NULL end)", stored: true
|
|
82
|
+
t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
|
|
83
|
+
t.index ["ongoing_unique_key"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true
|
|
84
|
+
t.index ["state"], name: "index_geneva_drive_workflows_on_state"
|
|
85
|
+
t.index ["type"], name: "index_geneva_drive_workflows_on_type"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
89
|
+
t.string "email", null: false
|
|
90
|
+
t.string "name"
|
|
91
|
+
t.boolean "active", default: true
|
|
92
|
+
t.datetime "created_at", null: false
|
|
93
|
+
t.datetime "updated_at", null: false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
|
4
|
+
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
10
|
+
#
|
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Schema[8.1].define(version: 2026_10_25_000614) do
|
|
14
|
+
# These are extensions that must be enabled in order to support this database
|
|
15
|
+
enable_extension "pg_catalog.plpgsql"
|
|
16
|
+
|
|
17
|
+
create_table "geneva_drive_signals", force: :cascade do |t|
|
|
18
|
+
t.bigint "workflow_id", null: false
|
|
19
|
+
t.string "name", null: false
|
|
20
|
+
t.string "idempotency_key"
|
|
21
|
+
t.string "state", default: "pending", null: false
|
|
22
|
+
t.jsonb "payload"
|
|
23
|
+
t.datetime "claimed_at"
|
|
24
|
+
t.datetime "consumed_at"
|
|
25
|
+
t.bigint "claimed", default: 0, null: false
|
|
26
|
+
t.bigint "consumed", default: 0, null: false
|
|
27
|
+
t.datetime "created_at", null: false
|
|
28
|
+
t.datetime "updated_at", null: false
|
|
29
|
+
t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
|
|
30
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
|
|
31
|
+
t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
create_table "geneva_drive_step_executions", force: :cascade do |t|
|
|
35
|
+
t.bigint "workflow_id", null: false
|
|
36
|
+
t.string "step_name", null: false
|
|
37
|
+
t.string "state", default: "scheduled", null: false
|
|
38
|
+
t.string "outcome"
|
|
39
|
+
t.datetime "scheduled_for", null: false
|
|
40
|
+
t.datetime "started_at"
|
|
41
|
+
t.datetime "completed_at"
|
|
42
|
+
t.datetime "failed_at"
|
|
43
|
+
t.datetime "canceled_at"
|
|
44
|
+
t.datetime "skipped_at"
|
|
45
|
+
t.string "error_class_name"
|
|
46
|
+
t.text "error_message"
|
|
47
|
+
t.text "error_backtrace"
|
|
48
|
+
t.string "job_id"
|
|
49
|
+
t.jsonb "metadata"
|
|
50
|
+
t.datetime "created_at", null: false
|
|
51
|
+
t.datetime "updated_at", null: false
|
|
52
|
+
t.datetime "finished_at"
|
|
53
|
+
t.jsonb "cursor"
|
|
54
|
+
t.bigint "continues_from_id"
|
|
55
|
+
t.bigint "signal_id"
|
|
56
|
+
t.datetime "waiting_since"
|
|
57
|
+
t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
|
|
58
|
+
t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
|
|
59
|
+
t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
|
|
60
|
+
t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
|
|
61
|
+
t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "((state)::text = 'in_progress'::text)"
|
|
62
|
+
t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
|
|
63
|
+
t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
|
|
64
|
+
t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
|
|
65
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
|
|
66
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
|
|
67
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "((state)::text = ANY ((ARRAY['scheduled'::character varying, 'in_progress'::character varying])::text[]))"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
create_table "geneva_drive_workflows", force: :cascade do |t|
|
|
71
|
+
t.string "type", null: false
|
|
72
|
+
t.string "hero_type"
|
|
73
|
+
t.bigint "hero_id"
|
|
74
|
+
t.string "state", default: "ready", null: false
|
|
75
|
+
t.string "current_step_name"
|
|
76
|
+
t.string "next_step_name"
|
|
77
|
+
t.boolean "allow_multiple", default: false, null: false
|
|
78
|
+
t.jsonb "metadata"
|
|
79
|
+
t.datetime "started_at"
|
|
80
|
+
t.datetime "transitioned_at"
|
|
81
|
+
t.datetime "created_at", null: false
|
|
82
|
+
t.datetime "updated_at", null: false
|
|
83
|
+
t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
|
|
84
|
+
t.index ["state"], name: "index_geneva_drive_workflows_on_state"
|
|
85
|
+
t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "(((state)::text <> ALL ((ARRAY['finished'::character varying, 'canceled'::character varying])::text[])) AND (allow_multiple = false))"
|
|
86
|
+
t.index ["type"], name: "index_geneva_drive_workflows_on_type"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
create_table "users", force: :cascade do |t|
|
|
90
|
+
t.string "email", null: false
|
|
91
|
+
t.string "name"
|
|
92
|
+
t.boolean "active", default: true
|
|
93
|
+
t.datetime "created_at", null: false
|
|
94
|
+
t.datetime "updated_at", null: false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
98
|
+
add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
99
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
|
4
|
+
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
10
|
+
#
|
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Schema[7.2].define(version: 2026_10_25_090462) do
|
|
14
|
+
create_table "geneva_drive_signals", force: :cascade do |t|
|
|
15
|
+
t.integer "workflow_id", null: false
|
|
16
|
+
t.string "name", null: false
|
|
17
|
+
t.string "idempotency_key"
|
|
18
|
+
t.string "state", default: "pending", null: false
|
|
19
|
+
t.json "payload"
|
|
20
|
+
t.datetime "claimed_at"
|
|
21
|
+
t.datetime "consumed_at"
|
|
22
|
+
t.bigint "claimed", default: 0, null: false
|
|
23
|
+
t.bigint "consumed", default: 0, null: false
|
|
24
|
+
t.datetime "created_at", null: false
|
|
25
|
+
t.datetime "updated_at", null: false
|
|
26
|
+
t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
|
|
27
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
|
|
28
|
+
t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
create_table "geneva_drive_step_executions", force: :cascade do |t|
|
|
32
|
+
t.integer "workflow_id", null: false
|
|
33
|
+
t.string "step_name", null: false
|
|
34
|
+
t.string "state", default: "scheduled", null: false
|
|
35
|
+
t.string "outcome"
|
|
36
|
+
t.datetime "scheduled_for", null: false
|
|
37
|
+
t.datetime "started_at"
|
|
38
|
+
t.datetime "completed_at"
|
|
39
|
+
t.datetime "failed_at"
|
|
40
|
+
t.datetime "canceled_at"
|
|
41
|
+
t.datetime "skipped_at"
|
|
42
|
+
t.string "error_class_name"
|
|
43
|
+
t.text "error_message"
|
|
44
|
+
t.text "error_backtrace"
|
|
45
|
+
t.string "job_id"
|
|
46
|
+
t.text "metadata"
|
|
47
|
+
t.datetime "created_at", null: false
|
|
48
|
+
t.datetime "updated_at", null: false
|
|
49
|
+
t.datetime "finished_at"
|
|
50
|
+
t.json "cursor"
|
|
51
|
+
t.bigint "continues_from_id"
|
|
52
|
+
t.bigint "signal_id"
|
|
53
|
+
t.datetime "waiting_since"
|
|
54
|
+
t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
|
|
55
|
+
t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
|
|
56
|
+
t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
|
|
57
|
+
t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
|
|
58
|
+
t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "state = 'in_progress'"
|
|
59
|
+
t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
|
|
60
|
+
t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
|
|
61
|
+
t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
|
|
62
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
|
|
63
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
|
|
64
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "state IN ('scheduled', 'in_progress')"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
create_table "geneva_drive_workflows", force: :cascade do |t|
|
|
68
|
+
t.string "type", null: false
|
|
69
|
+
t.string "hero_type"
|
|
70
|
+
t.integer "hero_id"
|
|
71
|
+
t.string "state", default: "ready", null: false
|
|
72
|
+
t.string "current_step_name"
|
|
73
|
+
t.string "next_step_name"
|
|
74
|
+
t.boolean "allow_multiple", default: false, null: false
|
|
75
|
+
t.text "metadata"
|
|
76
|
+
t.datetime "started_at"
|
|
77
|
+
t.datetime "transitioned_at"
|
|
78
|
+
t.datetime "created_at", null: false
|
|
79
|
+
t.datetime "updated_at", null: false
|
|
80
|
+
t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
|
|
81
|
+
t.index ["state"], name: "index_geneva_drive_workflows_on_state"
|
|
82
|
+
t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "state NOT IN ('finished', 'canceled') AND allow_multiple = 0"
|
|
83
|
+
t.index ["type"], name: "index_geneva_drive_workflows_on_type"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
create_table "users", force: :cascade do |t|
|
|
87
|
+
t.string "email", null: false
|
|
88
|
+
t.string "name"
|
|
89
|
+
t.boolean "active", default: true
|
|
90
|
+
t.datetime "created_at", null: false
|
|
91
|
+
t.datetime "updated_at", null: false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
95
|
+
add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
96
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
|
4
|
+
#
|
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
|
9
|
+
# migrations use external dependencies or application code.
|
|
10
|
+
#
|
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Schema[8.0].define(version: 2026_10_25_000614) do
|
|
14
|
+
create_table "geneva_drive_signals", force: :cascade do |t|
|
|
15
|
+
t.integer "workflow_id", null: false
|
|
16
|
+
t.string "name", null: false
|
|
17
|
+
t.string "idempotency_key"
|
|
18
|
+
t.string "state", default: "pending", null: false
|
|
19
|
+
t.json "payload"
|
|
20
|
+
t.datetime "claimed_at"
|
|
21
|
+
t.datetime "consumed_at"
|
|
22
|
+
t.bigint "claimed", default: 0, null: false
|
|
23
|
+
t.bigint "consumed", default: 0, null: false
|
|
24
|
+
t.datetime "created_at", null: false
|
|
25
|
+
t.datetime "updated_at", null: false
|
|
26
|
+
t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
|
|
27
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
|
|
28
|
+
t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
create_table "geneva_drive_step_executions", force: :cascade do |t|
|
|
32
|
+
t.integer "workflow_id", null: false
|
|
33
|
+
t.string "step_name", null: false
|
|
34
|
+
t.string "state", default: "scheduled", null: false
|
|
35
|
+
t.string "outcome"
|
|
36
|
+
t.datetime "scheduled_for", null: false
|
|
37
|
+
t.datetime "started_at"
|
|
38
|
+
t.datetime "completed_at"
|
|
39
|
+
t.datetime "failed_at"
|
|
40
|
+
t.datetime "canceled_at"
|
|
41
|
+
t.datetime "skipped_at"
|
|
42
|
+
t.string "error_class_name"
|
|
43
|
+
t.text "error_message"
|
|
44
|
+
t.text "error_backtrace"
|
|
45
|
+
t.string "job_id"
|
|
46
|
+
t.text "metadata"
|
|
47
|
+
t.datetime "created_at", null: false
|
|
48
|
+
t.datetime "updated_at", null: false
|
|
49
|
+
t.datetime "finished_at"
|
|
50
|
+
t.json "cursor"
|
|
51
|
+
t.bigint "continues_from_id"
|
|
52
|
+
t.bigint "signal_id"
|
|
53
|
+
t.datetime "waiting_since"
|
|
54
|
+
t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
|
|
55
|
+
t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
|
|
56
|
+
t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
|
|
57
|
+
t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
|
|
58
|
+
t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "state = 'in_progress'"
|
|
59
|
+
t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
|
|
60
|
+
t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
|
|
61
|
+
t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
|
|
62
|
+
t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
|
|
63
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
|
|
64
|
+
t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "state IN ('scheduled', 'in_progress')"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
create_table "geneva_drive_workflows", force: :cascade do |t|
|
|
68
|
+
t.string "type", null: false
|
|
69
|
+
t.string "hero_type"
|
|
70
|
+
t.integer "hero_id"
|
|
71
|
+
t.string "state", default: "ready", null: false
|
|
72
|
+
t.string "current_step_name"
|
|
73
|
+
t.string "next_step_name"
|
|
74
|
+
t.boolean "allow_multiple", default: false, null: false
|
|
75
|
+
t.text "metadata"
|
|
76
|
+
t.datetime "started_at"
|
|
77
|
+
t.datetime "transitioned_at"
|
|
78
|
+
t.datetime "created_at", null: false
|
|
79
|
+
t.datetime "updated_at", null: false
|
|
80
|
+
t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
|
|
81
|
+
t.index ["state"], name: "index_geneva_drive_workflows_on_state"
|
|
82
|
+
t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "state NOT IN ('finished', 'canceled') AND allow_multiple = 0"
|
|
83
|
+
t.index ["type"], name: "index_geneva_drive_workflows_on_type"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
create_table "users", force: :cascade do |t|
|
|
87
|
+
t.string "email", null: false
|
|
88
|
+
t.string "name"
|
|
89
|
+
t.boolean "active", default: true
|
|
90
|
+
t.datetime "created_at", null: false
|
|
91
|
+
t.datetime "updated_at", null: false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
95
|
+
add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
|
|
96
|
+
end
|