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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +380 -13
  4. data/db/durable_schema.rb +57 -0
  5. data/lib/active_job/continuable/callbacks.rb +48 -0
  6. data/lib/active_job/continuable/configurable.rb +75 -0
  7. data/lib/active_job/continuation/callbacks.rb +18 -0
  8. data/lib/active_job/continuation/rescue_handlers_first.rb +29 -0
  9. data/lib/active_job/durable/args_mapper.rb +41 -0
  10. data/lib/active_job/durable/config.rb +100 -0
  11. data/lib/active_job/durable/continuation.rb +24 -0
  12. data/lib/active_job/durable/execution.rb +438 -0
  13. data/lib/active_job/durable/housekeeping_job.rb +16 -0
  14. data/lib/active_job/durable/record.rb +17 -0
  15. data/lib/active_job/durable/run.rb +165 -0
  16. data/lib/active_job/durable/step.rb +14 -0
  17. data/lib/active_job/durable/wake_job.rb +16 -0
  18. data/lib/active_job/durable.rb +251 -0
  19. data/lib/ajdc/railtie.rb +18 -0
  20. data/lib/ajdc/version.rb +1 -1
  21. data/lib/ajdc.rb +1 -0
  22. data/lib/generators/ajdc/install/USAGE +19 -0
  23. data/lib/generators/ajdc/install/install_generator.rb +66 -0
  24. data/lib/generators/ajdc/install/templates/create_active_job_durable_tables.rb.tt +56 -0
  25. data/lib/generators/ajdc/install/templates/initializer.rb.tt +4 -0
  26. data/skills/ajdc/SKILL.md +117 -0
  27. data/skills/ajdc/examples/README.md +11 -0
  28. data/skills/ajdc/examples/agent-loop.md +57 -0
  29. data/skills/ajdc/examples/halting.md +84 -0
  30. data/skills/ajdc/examples/pipeline.md +73 -0
  31. data/skills/ajdc/examples/signals.md +139 -0
  32. data/skills/ajdc/examples/timers.md +136 -0
  33. data/skills/ajdc/examples/uniqueness.md +87 -0
  34. data/skills/ajdc/references/api.md +157 -0
  35. data/skills/ajdc/references/decision-guide.md +130 -0
  36. data/skills/ajdc/references/installation.md +98 -0
  37. data/skills/ajdc/references/testing.md +152 -0
  38. data/skills/ajdc/references/transactions.md +68 -0
  39. 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.1
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: bundler
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: '4.0'
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: '4.0'
67
+ version: '2.0'
26
68
  - !ruby/object:Gem::Dependency
27
- name: combustion
69
+ name: bundler
28
70
  requirement: !ruby/object:Gem::Requirement
29
71
  requirements:
30
72
  - - ">="
31
73
  - !ruby/object:Gem::Version
32
- version: '1.1'
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: '1.1'
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