worker_plugins 0.0.15 → 0.0.17
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/README.md +53 -0
- data/app/services/worker_plugins/application_service.rb +18 -0
- data/app/services/worker_plugins/delete_old_workplaces.rb +48 -0
- data/app/services/worker_plugins/delete_orphan_links.rb +39 -0
- data/app/services/worker_plugins/query_links_status.rb +24 -5
- data/app/services/worker_plugins/remove_query.rb +7 -23
- data/lib/worker_plugins/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0724aebdce87819ebafc787c475284e25e7a6ee31837040e8532b0c400c95db3
|
|
4
|
+
data.tar.gz: dc8cd4edddcdce80d22c2e3c10990f4fa966f112fe67f01c1bd58443c313079b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f58a6c6a65122966a57b948f78758761825e2eed97bb13fb4277b627ec4db11bade6856b2baf23e1f1056fb7169d545bfeaf6d565bbbddbb027bfb61c9d16f26
|
|
7
|
+
data.tar.gz: a4fb5385013ab116c856bd5b5986ce04bcccbfb18b342edb41f463d652e664ecdd0b01567e75cf321f0299d64d8adfff5b37c9d2ef2ed75f81da31286975884a
|
data/README.md
CHANGED
|
@@ -27,6 +27,59 @@ Optimally loop over resources on a workspace:
|
|
|
27
27
|
workspace.each_resource(types: ['User']) do |user|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Scheduled cleanup of unused workplaces
|
|
31
|
+
|
|
32
|
+
`WorkerPlugins::DeleteOldWorkplaces` removes workplaces that haven't seen activity in a given window — both the workplace row's `updated_at` is older than the cutoff *and* no link on it has been created or updated since. Deletion runs in batches via raw `delete_all` to skip per-row callbacks.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
result = WorkerPlugins::DeleteOldWorkplaces.execute!(older_than: 2.months)
|
|
36
|
+
# => {workplaces_deleted: <N>, links_deleted: <M>}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
|
|
41
|
+
- `older_than:` (required) — any object that responds to `.ago` (typically an `ActiveSupport::Duration` like `2.months` or `30.days`). The service computes the cutoff at call time.
|
|
42
|
+
- `batch_size:` (default `1000`) — how many stale workplaces to delete per round-trip.
|
|
43
|
+
|
|
44
|
+
The gem does not register a scheduler of its own. Wire the service into your application's background queue. Example with `sidekiq-scheduler`:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# config/sidekiq.yml
|
|
48
|
+
:scheduler:
|
|
49
|
+
:schedule:
|
|
50
|
+
DeleteOldWorkplaces:
|
|
51
|
+
cron: "0 40 3 * * *" # daily at 03:40 local time
|
|
52
|
+
args: ["WorkerPlugins::DeleteOldWorkplaces", {"older_than": "2.months"}]
|
|
53
|
+
class: ServiceScheduler # or whatever your project's service-dispatching worker is called
|
|
54
|
+
queue: low_priority
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If your `ServiceScheduler` only accepts YAML-serializable arguments, wrap the call in a thin application-side service:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
class Workplaces::DeleteOld < ApplicationService
|
|
61
|
+
def perform
|
|
62
|
+
WorkerPlugins::DeleteOldWorkplaces.execute!(older_than: 2.months)
|
|
63
|
+
succeed!
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
and schedule `Workplaces::DeleteOld` instead.
|
|
69
|
+
|
|
70
|
+
## Scheduled cleanup of orphan links
|
|
71
|
+
|
|
72
|
+
`WorkerPlugins::DeleteOrphanLinks` removes `worker_plugins_workplace_links` whose target row no longer exists — i.e. links that point at a resource that was destroyed without the link being cleaned up alongside. Run it periodically from a background job to keep the links table consistent and keep probes like `QueryLinksStatus#checked_count` honest.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
result = WorkerPlugins::DeleteOrphanLinks.execute!
|
|
76
|
+
# => {deleted_count: <N>}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Links whose `resource_type` doesn't resolve to a Ruby class (e.g. a model was renamed or removed) are left alone — cleaning those up requires human judgement.
|
|
80
|
+
|
|
81
|
+
Schedule the same way as `DeleteOldWorkplaces` — typically once a day off-hours.
|
|
82
|
+
|
|
30
83
|
## Release
|
|
31
84
|
|
|
32
85
|
Run the release task from a clean worktree:
|
|
@@ -36,4 +36,22 @@ class WorkerPlugins::ApplicationService < ServicePattern::Service
|
|
|
36
36
|
|
|
37
37
|
adapter_name.include?("mysql") || adapter_name.include?("trilogy")
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
# True when a relation applies no row-narrowing scope — so filtering
|
|
41
|
+
# workplace links with `resource_id IN (SELECT ... FROM <target_table>)` adds
|
|
42
|
+
# no semantic value and just forces the database to materialize every row
|
|
43
|
+
# of the target model. Call sites (RemoveQuery, QueryLinksStatus) use this
|
|
44
|
+
# to drop the subquery and count/delete by `resource_type` alone on large
|
|
45
|
+
# target models (e.g. 340k+ users).
|
|
46
|
+
def relation_unscoped?(relation)
|
|
47
|
+
relation.where_clause.empty? &&
|
|
48
|
+
relation.joins_values.empty? &&
|
|
49
|
+
relation.left_outer_joins_values.empty? &&
|
|
50
|
+
relation.group_values.empty? &&
|
|
51
|
+
relation.having_clause.empty? &&
|
|
52
|
+
relation.limit_value.nil? &&
|
|
53
|
+
relation.offset_value.nil? &&
|
|
54
|
+
relation.from_clause.value.nil? &&
|
|
55
|
+
relation.with_values.empty?
|
|
56
|
+
end
|
|
39
57
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class WorkerPlugins::DeleteOldWorkplaces < WorkerPlugins::ApplicationService
|
|
2
|
+
arguments :older_than
|
|
3
|
+
argument :batch_size, default: 1_000
|
|
4
|
+
|
|
5
|
+
# Deletes workplaces that haven't seen any activity since `older_than.ago` —
|
|
6
|
+
# both the workplace record itself is older than the cutoff *and* none of
|
|
7
|
+
# its links have been created / updated since. Links on deleted workplaces
|
|
8
|
+
# are removed with the parent via `dependent: :destroy`, but this service
|
|
9
|
+
# uses raw `delete_all` in batches to skip per-row callbacks and keep
|
|
10
|
+
# long-running cleanup jobs cheap.
|
|
11
|
+
#
|
|
12
|
+
# Intended to be scheduled by the consumer application from a Sidekiq
|
|
13
|
+
# worker (or equivalent) — the gem does not register a scheduler of its
|
|
14
|
+
# own.
|
|
15
|
+
def perform
|
|
16
|
+
cutoff = older_than.ago
|
|
17
|
+
workplaces_deleted = 0
|
|
18
|
+
links_deleted = 0
|
|
19
|
+
|
|
20
|
+
stale_workplaces(cutoff).in_batches(of: batch_size) do |batch|
|
|
21
|
+
batch_ids = batch.pluck(:id)
|
|
22
|
+
|
|
23
|
+
links_deleted += WorkerPlugins::WorkplaceLink
|
|
24
|
+
.where(workplace_id: batch_ids)
|
|
25
|
+
.delete_all
|
|
26
|
+
workplaces_deleted += WorkerPlugins::Workplace
|
|
27
|
+
.where(id: batch_ids)
|
|
28
|
+
.delete_all
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
succeed!(workplaces_deleted:, links_deleted:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stale_workplaces(cutoff)
|
|
35
|
+
workplaces_table = quote_table(WorkerPlugins::Workplace.table_name)
|
|
36
|
+
links_table = quote_table(WorkerPlugins::WorkplaceLink.table_name)
|
|
37
|
+
|
|
38
|
+
WorkerPlugins::Workplace
|
|
39
|
+
.where("#{workplaces_table}.#{quote_column(:updated_at)} < ?", cutoff)
|
|
40
|
+
.where(<<~SQL.squish, cutoff)
|
|
41
|
+
NOT EXISTS (
|
|
42
|
+
SELECT 1 FROM #{links_table}
|
|
43
|
+
WHERE #{links_table}.#{quote_column(:workplace_id)} = #{workplaces_table}.#{quote_column(:id)}
|
|
44
|
+
AND #{links_table}.#{quote_column(:updated_at)} >= ?
|
|
45
|
+
)
|
|
46
|
+
SQL
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class WorkerPlugins::DeleteOrphanLinks < WorkerPlugins::ApplicationService
|
|
2
|
+
# Deletes `worker_plugins_workplace_links` whose target row no longer
|
|
3
|
+
# exists — i.e. links pointing at a resource that was destroyed without
|
|
4
|
+
# the link being cleaned up alongside. Intended to be scheduled by the
|
|
5
|
+
# consumer application from a background job.
|
|
6
|
+
#
|
|
7
|
+
# Links whose `resource_type` doesn't resolve to a Ruby class (e.g. a
|
|
8
|
+
# model was renamed or removed) are left alone — cleaning those up is
|
|
9
|
+
# a separate concern that requires human judgement.
|
|
10
|
+
def perform
|
|
11
|
+
deleted_count = distinct_resource_types.sum do |resource_type|
|
|
12
|
+
delete_orphans_for(resource_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
succeed!(deleted_count:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def distinct_resource_types
|
|
19
|
+
WorkerPlugins::WorkplaceLink.distinct.pluck(:resource_type)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete_orphans_for(resource_type)
|
|
23
|
+
model_class = resource_type.safe_constantize
|
|
24
|
+
return 0 unless model_class
|
|
25
|
+
|
|
26
|
+
WorkerPlugins::WorkplaceLink
|
|
27
|
+
.where(resource_type:)
|
|
28
|
+
.where.not(resource_id: live_ids_query(model_class))
|
|
29
|
+
.delete_all
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def live_ids_query(model_class)
|
|
33
|
+
WorkerPlugins::SelectColumnWithTypeCast.execute!(
|
|
34
|
+
column_name_to_select: model_class.primary_key,
|
|
35
|
+
column_to_compare_with: WorkerPlugins::WorkplaceLink.column_for_attribute(:resource_id),
|
|
36
|
+
query: model_class.all
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -2,12 +2,8 @@ class WorkerPlugins::QueryLinksStatus < WorkerPlugins::ApplicationService
|
|
|
2
2
|
arguments :query, :workplace
|
|
3
3
|
|
|
4
4
|
def perform
|
|
5
|
-
checked_count = workplace
|
|
6
|
-
.workplace_links
|
|
7
|
-
.where(resource_type: query.klass.name, resource_id: query_with_selected_ids)
|
|
8
|
-
.count
|
|
9
|
-
|
|
10
5
|
query_count = query.count
|
|
6
|
+
checked_count = count_linked_rows(query_count)
|
|
11
7
|
|
|
12
8
|
succeed!(
|
|
13
9
|
all_checked: query_count == checked_count,
|
|
@@ -17,6 +13,29 @@ class WorkerPlugins::QueryLinksStatus < WorkerPlugins::ApplicationService
|
|
|
17
13
|
)
|
|
18
14
|
end
|
|
19
15
|
|
|
16
|
+
def count_linked_rows(query_count)
|
|
17
|
+
base_scope = workplace.workplace_links.where(resource_type: query.klass.name)
|
|
18
|
+
|
|
19
|
+
# Fast path for unscoped queries: a plain index-only COUNT against the
|
|
20
|
+
# `(workplace_id, resource_type, resource_id)` composite index resolves in
|
|
21
|
+
# ~50 ms even for workplaces with hundreds of thousands of links. Joining
|
|
22
|
+
# back to the target table to exclude orphaned links (whose resource row
|
|
23
|
+
# has since been destroyed) would take 10+ seconds on the same data
|
|
24
|
+
# regardless of which shape we pick — the DB still has to cross-reference
|
|
25
|
+
# every link against the target's primary key. Instead we clamp the raw
|
|
26
|
+
# count to `query_count`; `WorkerPlugins::DeleteOrphanLinks` (scheduled
|
|
27
|
+
# daily by consumers) keeps the orphan count at zero so the raw count
|
|
28
|
+
# equals the live-linked count in practice. When orphans do briefly exist
|
|
29
|
+
# between cleanup runs, clamping bounds the over-count at the query's
|
|
30
|
+
# total — `all_checked` / `some_checked` stay correct because they're
|
|
31
|
+
# computed off the clamped value.
|
|
32
|
+
if relation_unscoped?(query)
|
|
33
|
+
[base_scope.count, query_count].min
|
|
34
|
+
else
|
|
35
|
+
base_scope.where(resource_id: query_with_selected_ids).count
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
20
39
|
def query_with_selected_ids
|
|
21
40
|
WorkerPlugins::SelectColumnWithTypeCast.execute!(
|
|
22
41
|
column_name_to_select: query.klass.primary_key,
|
|
@@ -7,33 +7,17 @@ class WorkerPlugins::RemoveQuery < WorkerPlugins::ApplicationService
|
|
|
7
7
|
|
|
8
8
|
def links_scope
|
|
9
9
|
scope = workplace.workplace_links.where(resource_type: model_class.name)
|
|
10
|
-
|
|
10
|
+
# When the caller's query applies no scoping, the `resource_id IN (SELECT
|
|
11
|
+
# ... FROM <target_table>)` subquery would materialize every row of the
|
|
12
|
+
# target model — the `resource_type = ?` filter alone is enough. Orphaned
|
|
13
|
+
# links (whose resource row has since been deleted) are deleted alongside
|
|
14
|
+
# live ones, which matches caller intent ("remove everything matching")
|
|
15
|
+
# and is the correct thing to do with dead references anyway.
|
|
16
|
+
return scope if relation_unscoped?(@query)
|
|
11
17
|
|
|
12
18
|
scope.where(resource_id: query_with_selected_ids)
|
|
13
19
|
end
|
|
14
20
|
|
|
15
|
-
# If the caller's query has no meaningful scoping applied, the `resource_id
|
|
16
|
-
# IN (SELECT ... FROM <target_table>)` subquery would simply materialize
|
|
17
|
-
# every row of the target model — for 340k+ users that's a full-table scan
|
|
18
|
-
# with no semantic effect other than preserving orphaned links. The
|
|
19
|
-
# `resource_type = ?` filter alone is enough to pin the DELETE to this
|
|
20
|
-
# workplace's links of the given type, so we short-circuit the subquery in
|
|
21
|
-
# that case. Orphaned links (whose resource row has since been deleted) are
|
|
22
|
-
# deleted alongside live ones, which matches caller intent ("remove
|
|
23
|
-
# everything matching the query") and is the correct thing to do with
|
|
24
|
-
# dead references anyway.
|
|
25
|
-
def unscoped_query?
|
|
26
|
-
@query.where_clause.empty? &&
|
|
27
|
-
@query.joins_values.empty? &&
|
|
28
|
-
@query.left_outer_joins_values.empty? &&
|
|
29
|
-
@query.group_values.empty? &&
|
|
30
|
-
@query.having_clause.empty? &&
|
|
31
|
-
@query.limit_value.nil? &&
|
|
32
|
-
@query.offset_value.nil? &&
|
|
33
|
-
@query.from_clause.value.nil? &&
|
|
34
|
-
@query.with_values.empty?
|
|
35
|
-
end
|
|
36
|
-
|
|
37
21
|
def model_class
|
|
38
22
|
query.klass
|
|
39
23
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: worker_plugins
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.17
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kasper Stöckel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Rails framework for easily choosing and creating lists of objects and
|
|
14
14
|
execute plugins against them.
|
|
@@ -26,6 +26,8 @@ files:
|
|
|
26
26
|
- app/models/worker_plugins/workplace_link.rb
|
|
27
27
|
- app/services/worker_plugins/add_query.rb
|
|
28
28
|
- app/services/worker_plugins/application_service.rb
|
|
29
|
+
- app/services/worker_plugins/delete_old_workplaces.rb
|
|
30
|
+
- app/services/worker_plugins/delete_orphan_links.rb
|
|
29
31
|
- app/services/worker_plugins/query_links_status.rb
|
|
30
32
|
- app/services/worker_plugins/remove_query.rb
|
|
31
33
|
- app/services/worker_plugins/select_column_with_type_cast.rb
|