finalizers 0.0.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{MIT-LICENSE → LICENSE.txt} +1 -1
- data/README.md +159 -10
- data/Rakefile +11 -0
- data/app/jobs/eraser_job.rb +15 -0
- data/app/jobs/finalizers/application_job.rb +11 -0
- data/app/jobs/retry_job_error.rb +2 -0
- data/app/models/finalizers/model.rb +192 -0
- data/lib/finalizers/version.rb +1 -1
- data/lib/finalizers.rb +4 -4
- metadata +31 -11
- data/app/assets/config/finalizers_manifest.js +0 -0
- data/config/routes.rb +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02dec5224fb743376e9f9af2f17a522512a728dc1b80e55d002b01a81784649f
|
4
|
+
data.tar.gz: 231c643d2047c478961ff7679b04fd60d6372954ed3ca6491eaa0cefe0b03911
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 583b9dd07abcf74ff42b431a5ffd85233d5ebbe337af5d9ecaef5c7043ec74786b1642ed8ce2260cb189a8aabd1faf3d247e83de2012fefb8a0e711060366a78
|
7
|
+
data.tar.gz: 8beeefa591f84c3672750133935279add71e40d01b34c93862bf51d03cd8d56a140862fb018ace9fea808254ac11b252cbbe6d1172b3b711643859c70cb4b095
|
data/{MIT-LICENSE → LICENSE.txt}
RENAMED
data/README.md
CHANGED
@@ -1,28 +1,177 @@
|
|
1
1
|
# Finalizers
|
2
|
-
Short description and motivation.
|
3
2
|
|
4
|
-
|
5
|
-
How to use my plugin.
|
3
|
+
Add finalizers to your ActiveRecord models. Useful for cleaning up child dependencies in the database as well as associated external resources (APIs, etc).
|
6
4
|
|
7
|
-
|
8
|
-
|
5
|
+
* Finalizers and the eventual `destroy` run in background jobs, keeping controllers quick and responsive
|
6
|
+
* Finalizers may cleanup other database records, remote APIs, or anything else relevant
|
7
|
+
* Finalizers may also confirm any arbitrary dependency, making them extremly flexible
|
8
|
+
* Quickly define database-based dependencies with `erase_dependents`
|
9
|
+
* Supports cascading deletes
|
10
|
+
* Replaces `has_many ... dependent: :async`
|
11
|
+
* Background jobs are fully retryable, easily handling delays in satisfying finalizer dependencies and checks
|
12
|
+
* Dynamically determine when models shouldn't be deleted at all using `erasable?`
|
13
|
+
* Easily check if erasable and delete (erase) in controllers with `safe_erase`
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
## Basics and Usage
|
18
|
+
|
19
|
+
Each model used with Finalizers requires a `state` string field.
|
20
|
+
|
21
|
+
Finalizers is also aware and accommodative of `state_at` (when `state` was last changed) and `delete_at` (for scheduling a future delete), but expects those to be implemented separately.
|
22
|
+
|
23
|
+
A quick heads up: Finalizers depends on `rescue_like_a_pro` which changes how `retry_on` and `discard_on` are processed for *all* ActiveJob children. `rescue_like_a_pro` changes ActiveJob to handle exceptions based on specificity instead of last defintion, which most will find more intuitive. For basic usage, likely nothing will change. For advanced exception handling, it may warrant a review of your Job classes (which can often be simplified as a result).
|
24
|
+
|
25
|
+
|
26
|
+
#### Installation
|
27
|
+
|
28
|
+
As always, add to your Gemfile and run `bundle install` (or however you like to do such things):
|
9
29
|
|
10
30
|
```ruby
|
11
31
|
gem "finalizers"
|
12
32
|
```
|
13
33
|
|
14
|
-
And then execute:
|
15
34
|
```bash
|
16
35
|
$ bundle
|
17
36
|
```
|
18
37
|
|
19
|
-
|
20
|
-
|
21
|
-
|
38
|
+
|
39
|
+
#### Models
|
40
|
+
|
41
|
+
Add the required `state` field using a migration. It just needs to be a simple string long enough to hold `"deleted"` and any other values you wish to you.
|
42
|
+
|
43
|
+
Then, add `include Finalizers::Model` to the model and define an `erasable?` method.
|
44
|
+
|
45
|
+
To automatically cascade erase operations onto child classes (ie: `has_one` or `has_many`), use `erase_dependents`.
|
46
|
+
|
47
|
+
To add custom finalizers, use `add_finalizer`.
|
48
|
+
|
49
|
+
---
|
50
|
+
|
51
|
+
Finalizers add new `erase` and `erase!` methods to your model. You should generally use these instead of `destroy`.
|
52
|
+
|
53
|
+
`destroy` and `destroy!` continue to exist and will still destroy immediately, without running finalizers or handling dependent records. To prevent accidentally calling them and thus bypassing your finalizers, the `:force` argument must be added: `destroy(force: true)`. This is often still useful in tests.
|
54
|
+
|
55
|
+
For controllers and all other 'normal' actions, use `erase`, `erase!`, or `safe_erase`. `safe_erase` is designed especially for controllers. See below.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Vehicle < ApplicationRecord
|
59
|
+
# Must have `state` attribute
|
60
|
+
# May have `state_at` attribute. If present, will be updated when `state` is updated.
|
61
|
+
# May have `delete_at` attribute. If present, will be cleared when `erase` is called.
|
62
|
+
include Finalizers::Model
|
63
|
+
|
64
|
+
add_finalizer :delete_from_remote
|
65
|
+
# In addition to ensuring dependents are destroyed (see erase_dependents below),
|
66
|
+
# additional work can be required to complete before this record is destroyed.
|
67
|
+
# This is especially useful for cleaning up data in another system, but isn't
|
68
|
+
# limited to that.
|
69
|
+
# See `#delete_from_remote` below for more discussion.
|
70
|
+
add_finalizer do
|
71
|
+
# alternate syntax to define work inline if preferred
|
72
|
+
raise RetryJobError, "#{self.class.name} #{id} still running" if running?
|
73
|
+
end
|
74
|
+
|
75
|
+
has_many :wheels
|
76
|
+
# Hint: to further protect against accidental use of #destroy (and bypassing your
|
77
|
+
# finalizers), add a foreign key restriction to your schema and then add
|
78
|
+
# `:restrict_with_exception` to the has_many definition:
|
79
|
+
# has_many :wheels, dependent: :restrict_with_exception
|
80
|
+
erase_dependents :wheels
|
81
|
+
# This does 2 things:
|
82
|
+
# a) When Vehicle is erased (state := 'deleted'), causes all child Wheels to be
|
83
|
+
# erased too. (And, if Wheel has_one :tire, this will cascade as well.)
|
84
|
+
# b) Adds a finalizer to verify that the associated children are destroyed
|
85
|
+
# before proceeding. That means that children will always have access to the
|
86
|
+
# parent while performing their own finalization.
|
87
|
+
# Note: any dependent classes here must also include Finalizers::Model.
|
88
|
+
|
89
|
+
def erasable?
|
90
|
+
true # Allow `safe_erase` to proceed
|
91
|
+
# false # Prevent `safe_erase` from proceeding
|
92
|
+
end
|
93
|
+
# This only affects `safe_erase`. Using `erase` or `erase~` will work regardless.
|
94
|
+
# Returning false is particularly useful if an object shouldn't be allowed to be
|
95
|
+
# erased because it's in use, is a global object, etc.
|
96
|
+
|
97
|
+
# Finalizer callback, as configured above.
|
98
|
+
def delete_from_remote
|
99
|
+
# If another finalizer fails (including erase_dependents), this finalizer may be
|
100
|
+
# called more than once so it should be idempotent.
|
101
|
+
# You may use (and update) a tracking field (`remote_uuid` here), or may simply
|
102
|
+
# repeat the operation.
|
103
|
+
if remote_uuid
|
104
|
+
RemoteService.delete id: remote_uuid
|
105
|
+
update_columns remote_uuid: nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Exceptions will cause the finalizer to fail and be retried, so they should
|
109
|
+
# usually be passed through. You can raise RetryJobError if another exception
|
110
|
+
# isn't already in play.
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
22
114
|
```
|
23
115
|
|
116
|
+
|
117
|
+
#### Controllers
|
118
|
+
|
119
|
+
In `SomeController#destroy`, use `safe_erase` instead of `destroy`. `safe_erase` returns a boolean and will add an error message when false, so it allows making `#destroy` work like `#update`. Optionally, you may erase via `#update` by setting `@model.state = 'deleted'`.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
def destroy
|
123
|
+
if @model.safe_erase
|
124
|
+
render @model, notice: 'Resource deleted.'
|
125
|
+
else
|
126
|
+
render 'errors', locals: {obj: @model}, status: 422
|
127
|
+
# however you normally render errors
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
|
133
|
+
#### Error reporting
|
134
|
+
|
135
|
+
Finalizers uses `RetryJobError` internally to help manage flow. It is recommended to exclude it from any exception reporting tool (Honeybadger, Sentry, etc).
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
## Advanced usage
|
140
|
+
|
141
|
+
#### Overriding the default EraserJob
|
142
|
+
|
143
|
+
Just create your own version of the job in your app. Zeitwerk should prefer the app's version over the gem's.
|
144
|
+
|
145
|
+
Be sure to keep the existing signature for `perform`:
|
146
|
+
```ruby
|
147
|
+
def perform(obj)
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
#### Extending the default EraserJob
|
152
|
+
|
153
|
+
Like overriding, create your own version of the job and require the original job before reopening it:
|
154
|
+
```ruby
|
155
|
+
require "#{Finalizers::Engine.root}/app/jobs/eraser_job"
|
156
|
+
class EraserJob
|
157
|
+
# add extensions here
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
## History and Compatibility
|
164
|
+
|
165
|
+
Extracted from production code.
|
166
|
+
|
167
|
+
Tested w/Rails 7.x and GoodJob 3.x.
|
168
|
+
|
169
|
+
|
170
|
+
|
24
171
|
## Contributing
|
25
|
-
|
172
|
+
Pull requests welcomed. If unsure whether a proposed addition is in scope, feel free to open an Issue for discussion (not required though).
|
173
|
+
|
174
|
+
|
26
175
|
|
27
176
|
## License
|
28
177
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -6,3 +6,14 @@ load "rails/tasks/engine.rake"
|
|
6
6
|
load "rails/tasks/statistics.rake"
|
7
7
|
|
8
8
|
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
|
11
|
+
require "rake/testtask"
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
14
|
+
t.libs << 'test'
|
15
|
+
t.pattern = 'test/**/*_test.rb'
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class EraserJob < Finalizers::ApplicationJob
|
2
|
+
queue_with_priority 10
|
3
|
+
|
4
|
+
retry_on Exception, wait: 20.seconds, jitter: 15.seconds, attempts: :unlimited, priority: 20
|
5
|
+
|
6
|
+
def perform(obj)
|
7
|
+
if obj.state == 'deleted'
|
8
|
+
obj.finalize_and_destroy!
|
9
|
+
end
|
10
|
+
rescue RetryJobError => ex
|
11
|
+
logger.warn "#{ex.message} (attempt=#{executions})"
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Finalizers
|
2
|
+
class ApplicationJob < ActiveJob::Base
|
3
|
+
|
4
|
+
# Automatically retry jobs that encountered a deadlock
|
5
|
+
retry_on ActiveRecord::Deadlocked, wait: 10.seconds, attempts: :unlimited, jitter: 3.seconds
|
6
|
+
|
7
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
8
|
+
discard_on ActiveJob::DeserializationError
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# usage:
|
2
|
+
# class Book # mongoid or activerecord
|
3
|
+
# include Concerns::Finalizers
|
4
|
+
# has_many :editions
|
5
|
+
# # may add dependent: :restrict_with_exception to help ensure proper operation
|
6
|
+
# # of erase/finalization. similarly, may still use dependent: :destroy to
|
7
|
+
# # bypass erase/finalization system, but this is discouraged except perhaps in
|
8
|
+
# # tests [and delete() may be a better choice yet].
|
9
|
+
# erase_dependents :editions # replaces dependent: :destroy in has_many/etc
|
10
|
+
# # this should be used for dependents that themselves should also erase in the
|
11
|
+
# # background (and optionally define finalizers of their own).
|
12
|
+
# # will delay erasing the present object until finalizers for dependents have
|
13
|
+
# # completed.
|
14
|
+
#
|
15
|
+
# add_finalizer :do_something
|
16
|
+
# add_finalizer do
|
17
|
+
# do_something_else || throw(:abort)
|
18
|
+
# # alt: on error, raise RetryJobError instead
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# b = Book.create
|
22
|
+
# b.erase # alt: b.update state: 'deleted'
|
23
|
+
# b.state # => 'deleted'
|
24
|
+
# b.editions.first.state # => 'deleted
|
25
|
+
#
|
26
|
+
# the background job will then run finalizers for each Book and Edition being erased.
|
27
|
+
# like Rails' standard callbacks, if a finalizer fails, `throw(:abort)`,
|
28
|
+
# `raise RetryJobError, 'specific message'`, or raise any other exception.
|
29
|
+
# RetryJobError and throw(:abort) are excluded from sentry exception notifications.
|
30
|
+
# other exceptions will pass through as normal.
|
31
|
+
#
|
32
|
+
# requires a `state` field on the model.
|
33
|
+
# will call before/after destroy hooks when the object is finally destroyed, after
|
34
|
+
# completing finalizers.
|
35
|
+
#
|
36
|
+
# finalizers only run after verifying that dependent objects have been erased (as
|
37
|
+
# instructed by calling `erase_dependents`). this means that parent objects are not
|
38
|
+
# finalized or destroyed until all children are gone. this ensures child objects
|
39
|
+
# still have access to a functioning (undestroyed) parent to complete their own
|
40
|
+
# finalizers.
|
41
|
+
# the lifecycle looks like this:
|
42
|
+
# check dependents for not-yet-finalized objects
|
43
|
+
# run finalizers
|
44
|
+
# run before_destroy callbacks
|
45
|
+
# destroy self
|
46
|
+
# run after_destroy callbacks
|
47
|
+
#
|
48
|
+
# finalizers should be idempotent as they may run more than once in the event of a
|
49
|
+
# failure and subsequent retry. they may persist changes to the model to help manage
|
50
|
+
# idempotence.
|
51
|
+
#
|
52
|
+
# note that erase() simply updates :state and will execute normal save and update
|
53
|
+
# callbacks.
|
54
|
+
# like destroy(), erase() does not run validations. to conditionally trigger an
|
55
|
+
# erase, use update(state: 'deleted') instead, which will not bypass validations.
|
56
|
+
|
57
|
+
|
58
|
+
module Finalizers::Model
|
59
|
+
extend ActiveSupport::Concern
|
60
|
+
|
61
|
+
included do
|
62
|
+
define_model_callbacks :finalize, only: [:before]
|
63
|
+
class << self
|
64
|
+
alias_method :add_finalizer, :before_finalize
|
65
|
+
end
|
66
|
+
|
67
|
+
after_save do
|
68
|
+
if state == 'deleted' && state_previously_was != 'deleted'
|
69
|
+
EraserJob.perform_later self
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
scope :not_deleted, ->{ where.not(state: 'deleted') }
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
module ClassMethods
|
78
|
+
|
79
|
+
# if child models don't include Finalizers::Model, but are self-deleting (perhaps they
|
80
|
+
# represent an in-progress work task and are deleted when finished), then use
|
81
|
+
# wait_for_no_dependents to defer destroying the current model until the children are gone.
|
82
|
+
# if they are not self-deleting, use standard options like dependent: :destroy or :delete_all.
|
83
|
+
# finally, if children do include Finalizers::Model, use erase_dependents instead.
|
84
|
+
def wait_for_no_dependents(*assoc_list, erase_if_found: false)
|
85
|
+
add_finalizer prepend: true do
|
86
|
+
assoc_list.each do |assoc|
|
87
|
+
proxy = send(assoc)
|
88
|
+
# has_many's :dependent checks use .size, so match that here
|
89
|
+
# .size uses the cache_counter column, if available, else queries the db
|
90
|
+
if proxy.respond_to?(:size)
|
91
|
+
count = proxy.size
|
92
|
+
_perform_erase_dependents assoc if erase_if_found && count > 0 && proxy.not_deleted.any?
|
93
|
+
elsif proxy
|
94
|
+
count = 1
|
95
|
+
_perform_erase_dependents assoc if erase_if_found && !proxy.deleted?
|
96
|
+
else
|
97
|
+
count = 0
|
98
|
+
end
|
99
|
+
if count > 0
|
100
|
+
raise RetryJobError, "#{self.class.name} #{id} still has #{count} dependent #{assoc}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# if child models include Finalizers::Model, then use erase_dependents to perform a cascading
|
107
|
+
# erase. use this instead of has_many or has_one's depdendent: :destroy (etc).
|
108
|
+
def erase_dependents(*assoc_list)
|
109
|
+
wait_for_no_dependents(*assoc_list, erase_if_found: true)
|
110
|
+
before_update do
|
111
|
+
if state == 'deleted' && state_was != 'deleted'
|
112
|
+
assoc_list.each do |assoc|
|
113
|
+
_perform_erase_dependents assoc
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
def deleted? ; state=='deleted' ; end
|
123
|
+
|
124
|
+
def destroy(force: false)
|
125
|
+
raise 'Called destroy() directly instead of using erase()' unless force
|
126
|
+
super()
|
127
|
+
end
|
128
|
+
def destroy!(force: false)
|
129
|
+
raise 'Called destroy!() directly instead of using erase!()' unless force
|
130
|
+
destroy(force: true) || _raise_record_not_destroyed
|
131
|
+
end
|
132
|
+
|
133
|
+
# should run callbacks, but not validations
|
134
|
+
# intent is to parallel destroy()'s behavior
|
135
|
+
def erase
|
136
|
+
self.state = 'deleted'
|
137
|
+
self.state_at = Time.current if respond_to?(:state_at=) && state_changed?
|
138
|
+
self.delete_at = nil if respond_to?(:delete_at=)
|
139
|
+
save validate: false
|
140
|
+
end
|
141
|
+
def erase!
|
142
|
+
self.state = 'deleted'
|
143
|
+
self.state_at = Time.current if respond_to?(:state_at=) && state_changed?
|
144
|
+
self.delete_at = nil if respond_to?(:delete_at=)
|
145
|
+
save! validate: false
|
146
|
+
end
|
147
|
+
|
148
|
+
# must define on model
|
149
|
+
# def erasable?
|
150
|
+
# ...
|
151
|
+
# end
|
152
|
+
|
153
|
+
def safe_erase
|
154
|
+
if erasable?
|
155
|
+
erase
|
156
|
+
else
|
157
|
+
errors.add :base, "#{self.class.model_name.human} in use"
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# called by EraserJob
|
164
|
+
# may call directly for testing
|
165
|
+
def finalize_and_destroy!
|
166
|
+
# finalizers execute outside of the destroy transaction (and callback sequence).
|
167
|
+
# this intentionally allows finalizers to do whatever action and /persist/ that finalized state
|
168
|
+
# so if a later finalizer aborts, the previous ones don't have to repeat themselves.
|
169
|
+
# finalizers should still be idempotent though, as a finalizer could re-run due to an error
|
170
|
+
# persisting that state, server failure, etc.
|
171
|
+
run_callbacks :finalize do
|
172
|
+
destroy force: true
|
173
|
+
end
|
174
|
+
raise RetryJobError, "#{self.class.name} #{id} finalizers did not complete" unless destroyed?
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def _perform_erase_dependents(assoc)
|
182
|
+
# both activerecord and mongoid use non-! variants for their :dependent
|
183
|
+
# implementations, silently ignoring failures. match that behavior here.
|
184
|
+
proxy = send(assoc)
|
185
|
+
if proxy.respond_to?(:each)
|
186
|
+
proxy.not_deleted.each(&:erase)
|
187
|
+
elsif proxy
|
188
|
+
proxy.erase unless proxy.deleted?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
data/lib/finalizers/version.rb
CHANGED
data/lib/finalizers.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finalizers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thomas morgan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,34 +16,54 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '7.
|
19
|
+
version: '7.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '7.
|
27
|
-
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rescue_like_a_pro
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
description: Adds finalizers to ActiveRecord models to clean up both database child
|
42
|
+
dependencies and external resources (APIs, etc). Finalizers run in background jobs
|
43
|
+
and are fully retryable.
|
28
44
|
email:
|
29
45
|
- tm@iprog.com
|
30
46
|
executables: []
|
31
47
|
extensions: []
|
32
48
|
extra_rdoc_files: []
|
33
49
|
files:
|
34
|
-
-
|
50
|
+
- LICENSE.txt
|
35
51
|
- README.md
|
36
52
|
- Rakefile
|
37
|
-
- app/
|
38
|
-
-
|
53
|
+
- app/jobs/eraser_job.rb
|
54
|
+
- app/jobs/finalizers/application_job.rb
|
55
|
+
- app/jobs/retry_job_error.rb
|
56
|
+
- app/models/finalizers/model.rb
|
39
57
|
- lib/finalizers.rb
|
40
58
|
- lib/finalizers/engine.rb
|
41
59
|
- lib/finalizers/version.rb
|
42
60
|
- lib/tasks/finalizers_tasks.rake
|
43
|
-
homepage:
|
61
|
+
homepage: https://github.com/zarqman/finalizers
|
44
62
|
licenses:
|
45
63
|
- MIT
|
46
|
-
metadata:
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/zarqman/finalizers
|
66
|
+
source_code_uri: https://github.com/zarqman/finalizers
|
47
67
|
post_install_message:
|
48
68
|
rdoc_options: []
|
49
69
|
require_paths:
|
@@ -62,5 +82,5 @@ requirements: []
|
|
62
82
|
rubygems_version: 3.4.10
|
63
83
|
signing_key:
|
64
84
|
specification_version: 4
|
65
|
-
summary:
|
85
|
+
summary: Adds finalizers to ActiveRecord models
|
66
86
|
test_files: []
|
File without changes
|
data/config/routes.rb
DELETED