ajdc 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +380 -13
- data/db/durable_schema.rb +57 -0
- data/lib/active_job/continuable/callbacks.rb +48 -0
- data/lib/active_job/continuable/configurable.rb +75 -0
- data/lib/active_job/continuation/callbacks.rb +18 -0
- data/lib/active_job/continuation/rescue_handlers_first.rb +29 -0
- data/lib/active_job/durable/args_mapper.rb +41 -0
- data/lib/active_job/durable/config.rb +100 -0
- data/lib/active_job/durable/continuation.rb +24 -0
- data/lib/active_job/durable/execution.rb +438 -0
- data/lib/active_job/durable/housekeeping_job.rb +16 -0
- data/lib/active_job/durable/record.rb +17 -0
- data/lib/active_job/durable/run.rb +165 -0
- data/lib/active_job/durable/step.rb +14 -0
- data/lib/active_job/durable/wake_job.rb +16 -0
- data/lib/active_job/durable.rb +251 -0
- data/lib/ajdc/railtie.rb +18 -0
- data/lib/ajdc/version.rb +1 -1
- data/lib/ajdc.rb +1 -0
- data/lib/generators/ajdc/install/USAGE +19 -0
- data/lib/generators/ajdc/install/install_generator.rb +66 -0
- data/lib/generators/ajdc/install/templates/create_active_job_durable_tables.rb.tt +56 -0
- data/lib/generators/ajdc/install/templates/initializer.rb.tt +4 -0
- data/skills/ajdc/SKILL.md +117 -0
- data/skills/ajdc/examples/README.md +11 -0
- data/skills/ajdc/examples/agent-loop.md +57 -0
- data/skills/ajdc/examples/halting.md +84 -0
- data/skills/ajdc/examples/pipeline.md +73 -0
- data/skills/ajdc/examples/signals.md +139 -0
- data/skills/ajdc/examples/timers.md +136 -0
- data/skills/ajdc/examples/uniqueness.md +87 -0
- data/skills/ajdc/references/api.md +157 -0
- data/skills/ajdc/references/decision-guide.md +130 -0
- data/skills/ajdc/references/installation.md +98 -0
- data/skills/ajdc/references/testing.md +152 -0
- data/skills/ajdc/references/transactions.md +68 -0
- metadata +83 -7
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Transactions and AJ/DC
|
|
2
|
+
|
|
3
|
+
The run row lives in the app's database (or a separate `durable` database via
|
|
4
|
+
`ActiveJob::Durable.connects_to`). These rules follow from that.
|
|
5
|
+
|
|
6
|
+
## 1. `perform_later` inside a transaction
|
|
7
|
+
|
|
8
|
+
- The run row is created in the caller's transaction. The queue message is enqueued after all
|
|
9
|
+
transactions commit (Rails `enqueue_after_transaction_commit`). A rollback removes the row and
|
|
10
|
+
sends nothing.
|
|
11
|
+
- So: call `perform_later` inside the same transaction that creates or changes the entity
|
|
12
|
+
(`with_lock`, `transaction do ... end`). The run and the entity commit or roll back together.
|
|
13
|
+
This is the outbox pattern without an outbox table.
|
|
14
|
+
- `unique_by` is checked in that transaction too. Concurrent enqueues are decided by the unique
|
|
15
|
+
index on `[job_class, active_key]`; the loser gets `:skip`, `:reject` or `:replace` semantics.
|
|
16
|
+
- If the run row is in a separate database, the two databases do not share a transaction. Then
|
|
17
|
+
a rollback of the entity leaves an `enqueued` run whose job will find no entity; guard the
|
|
18
|
+
first step, or keep runs in the primary database when transactional creation matters.
|
|
19
|
+
|
|
20
|
+
## 2. `wake_up`, `resume!`, `cancel!` inside a transaction
|
|
21
|
+
|
|
22
|
+
- Each is one status-guarded `UPDATE` on the run row. It joins the caller's transaction like any
|
|
23
|
+
other write.
|
|
24
|
+
- The re-enqueue of the parked job happens after all transactions commit. Inside a rolled-back
|
|
25
|
+
transaction nothing is enqueued and the status update is undone.
|
|
26
|
+
- A guarded update that matches zero rows raises (`NotLive`, `NotResumable`, `NotCancellable`,
|
|
27
|
+
`NotWaiting`). Re-find the run right before the call; do not use a run loaded minutes earlier.
|
|
28
|
+
- `on_conflict: :replace` is `cancel!` of the live run plus the new run's row in one
|
|
29
|
+
transaction; a wake of the cancelled run is a no-op.
|
|
30
|
+
|
|
31
|
+
## 3. Writes the gem makes while the job runs
|
|
32
|
+
|
|
33
|
+
- Every step boundary and every checkpoint (`step.set!`, `step.advance!`, `step.checkpoint!`) is
|
|
34
|
+
a committed write to the run and step rows, outside your step's transaction.
|
|
35
|
+
- Do not wrap a whole step body in one long transaction and checkpoint inside it: the
|
|
36
|
+
checkpoint commits on its own connection state, but your work does not, and a crash leaves the
|
|
37
|
+
cursor ahead of the data. Commit per item, then checkpoint.
|
|
38
|
+
- Every run-row write is guarded on `status = 'running'`. A `cancel!` from elsewhere makes the
|
|
39
|
+
next checkpoint raise `ActiveJob::Durable::Cancelled`; the step body stops there.
|
|
40
|
+
- Attribute changes (`self.verdict = ...`) are written with the next checkpoint or step
|
|
41
|
+
boundary, not immediately.
|
|
42
|
+
|
|
43
|
+
## 4. Step bodies
|
|
44
|
+
|
|
45
|
+
- At-least-once per step. Make step bodies idempotent: `find_or_create_by`, `upsert_all`, a
|
|
46
|
+
guard on a fact column (`return if payout.completed?`).
|
|
47
|
+
- A step that both writes the entity and calls an external service: write the external call's
|
|
48
|
+
result to the entity in the same step, and check the entity first on re-run.
|
|
49
|
+
- Isolated steps run in separate executions; a shared instance variable does not survive. Set
|
|
50
|
+
`@record = record` at the top of `perform`, which re-runs on every execution, or use
|
|
51
|
+
`attribute`.
|
|
52
|
+
|
|
53
|
+
## 5. Locks
|
|
54
|
+
|
|
55
|
+
- `with_lock` on the entity plus `perform_later` inside it is fine and recommended for
|
|
56
|
+
start/cancel pairs (cancel an account: create the cancellation, start the run; reactivate:
|
|
57
|
+
destroy it, cancel the run).
|
|
58
|
+
- Do not hold an entity lock across a step boundary; the step boundary is a separate write and
|
|
59
|
+
the job may be re-enqueued between steps.
|
|
60
|
+
- SQLite: one writer. Checkpoints are short writes; keep step bodies' transactions short too, or
|
|
61
|
+
`limits_concurrency` heavy writers as the app already does.
|
|
62
|
+
|
|
63
|
+
## 6. Tests
|
|
64
|
+
|
|
65
|
+
Transactional tests wrap each test in a non-joinable transaction, so an app-level
|
|
66
|
+
`transaction do ... end` still commits its savepoint and `after_all_transactions_commit`
|
|
67
|
+
fires; `perform_later` inside `with_lock` enqueues in tests as in production. Verify with
|
|
68
|
+
`assert_enqueued_jobs`. See `testing.md`.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ajdc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vladimir Dementyev
|
|
@@ -10,33 +10,75 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activejob
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '8.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '8.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '8.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: sqlite3
|
|
14
56
|
requirement: !ruby/object:Gem::Requirement
|
|
15
57
|
requirements:
|
|
16
58
|
- - ">="
|
|
17
59
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
60
|
+
version: '2.0'
|
|
19
61
|
type: :development
|
|
20
62
|
prerelease: false
|
|
21
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
64
|
requirements:
|
|
23
65
|
- - ">="
|
|
24
66
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
67
|
+
version: '2.0'
|
|
26
68
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
69
|
+
name: bundler
|
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
|
29
71
|
requirements:
|
|
30
72
|
- - ">="
|
|
31
73
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
74
|
+
version: '2.0'
|
|
33
75
|
type: :development
|
|
34
76
|
prerelease: false
|
|
35
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
78
|
requirements:
|
|
37
79
|
- - ">="
|
|
38
80
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
81
|
+
version: '2.0'
|
|
40
82
|
- !ruby/object:Gem::Dependency
|
|
41
83
|
name: rake
|
|
42
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,9 +117,41 @@ files:
|
|
|
75
117
|
- CHANGELOG.md
|
|
76
118
|
- LICENSE.txt
|
|
77
119
|
- README.md
|
|
120
|
+
- db/durable_schema.rb
|
|
121
|
+
- lib/active_job/continuable/callbacks.rb
|
|
122
|
+
- lib/active_job/continuable/configurable.rb
|
|
123
|
+
- lib/active_job/continuation/callbacks.rb
|
|
124
|
+
- lib/active_job/continuation/rescue_handlers_first.rb
|
|
125
|
+
- lib/active_job/durable.rb
|
|
126
|
+
- lib/active_job/durable/args_mapper.rb
|
|
127
|
+
- lib/active_job/durable/config.rb
|
|
128
|
+
- lib/active_job/durable/continuation.rb
|
|
129
|
+
- lib/active_job/durable/execution.rb
|
|
130
|
+
- lib/active_job/durable/housekeeping_job.rb
|
|
131
|
+
- lib/active_job/durable/record.rb
|
|
132
|
+
- lib/active_job/durable/run.rb
|
|
133
|
+
- lib/active_job/durable/step.rb
|
|
134
|
+
- lib/active_job/durable/wake_job.rb
|
|
78
135
|
- lib/ajdc.rb
|
|
79
136
|
- lib/ajdc/railtie.rb
|
|
80
137
|
- lib/ajdc/version.rb
|
|
138
|
+
- lib/generators/ajdc/install/USAGE
|
|
139
|
+
- lib/generators/ajdc/install/install_generator.rb
|
|
140
|
+
- lib/generators/ajdc/install/templates/create_active_job_durable_tables.rb.tt
|
|
141
|
+
- lib/generators/ajdc/install/templates/initializer.rb.tt
|
|
142
|
+
- skills/ajdc/SKILL.md
|
|
143
|
+
- skills/ajdc/examples/README.md
|
|
144
|
+
- skills/ajdc/examples/agent-loop.md
|
|
145
|
+
- skills/ajdc/examples/halting.md
|
|
146
|
+
- skills/ajdc/examples/pipeline.md
|
|
147
|
+
- skills/ajdc/examples/signals.md
|
|
148
|
+
- skills/ajdc/examples/timers.md
|
|
149
|
+
- skills/ajdc/examples/uniqueness.md
|
|
150
|
+
- skills/ajdc/references/api.md
|
|
151
|
+
- skills/ajdc/references/decision-guide.md
|
|
152
|
+
- skills/ajdc/references/installation.md
|
|
153
|
+
- skills/ajdc/references/testing.md
|
|
154
|
+
- skills/ajdc/references/transactions.md
|
|
81
155
|
homepage: https://github.com/palkan/ajdc
|
|
82
156
|
licenses:
|
|
83
157
|
- MIT
|
|
@@ -88,6 +162,8 @@ metadata:
|
|
|
88
162
|
homepage_uri: https://github.com/palkan/ajdc
|
|
89
163
|
source_code_uri: https://github.com/palkan/ajdc
|
|
90
164
|
rubygems_mfa_required: 'true'
|
|
165
|
+
hyperdrive_targets: activejob
|
|
166
|
+
hyperdrive_artifacts: skill
|
|
91
167
|
rdoc_options: []
|
|
92
168
|
require_paths:
|
|
93
169
|
- lib
|