worker_plugins 0.0.16 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 343e7440c6234a58f501c96d553f019c653c7eb32c8dc07847d4f02d9ee8020b
4
- data.tar.gz: 5100e6c819ee4f818082e87ef515e7362523b7e1c5b0c25ca386741e07e9d8ec
3
+ metadata.gz: 0724aebdce87819ebafc787c475284e25e7a6ee31837040e8532b0c400c95db3
4
+ data.tar.gz: dc8cd4edddcdce80d22c2e3c10990f4fa966f112fe67f01c1bd58443c313079b
5
5
  SHA512:
6
- metadata.gz: a658e1f90e8f9c33503e526b98a59ef578e9a366dc388d79abbf8efec602c60fe5c920afa4e0772a113de5e47f4d71eba4bd7346dfc38dcbefd31ac7808deca1
7
- data.tar.gz: c73827b981b2d50abc9f5bd3c388645fa382f0d052188f3ff85f5f3e0e8ac1d47e62ba01a2fa73a2d1e6d4c78475870f36cbb520723154491bffe44245536db6
6
+ metadata.gz: f58a6c6a65122966a57b948f78758761825e2eed97bb13fb4277b627ec4db11bade6856b2baf23e1f1056fb7169d545bfeaf6d565bbbddbb027bfb61c9d16f26
7
+ data.tar.gz: a4fb5385013ab116c856bd5b5986ce04bcccbfb18b342edb41f463d652e664ecdd0b01567e75cf321f0299d64d8adfff5b37c9d2ef2ed75f81da31286975884a
data/README.md CHANGED
@@ -67,6 +67,19 @@ end
67
67
 
68
68
  and schedule `Workplaces::DeleteOld` instead.
69
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
+
70
83
  ## Release
71
84
 
72
85
  Run the release task from a clean worktree:
@@ -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
@@ -3,7 +3,7 @@ class WorkerPlugins::QueryLinksStatus < WorkerPlugins::ApplicationService
3
3
 
4
4
  def perform
5
5
  query_count = query.count
6
- checked_count = count_linked_rows
6
+ checked_count = count_linked_rows(query_count)
7
7
 
8
8
  succeed!(
9
9
  all_checked: query_count == checked_count,
@@ -13,46 +13,26 @@ class WorkerPlugins::QueryLinksStatus < WorkerPlugins::ApplicationService
13
13
  )
14
14
  end
15
15
 
16
- def count_linked_rows
16
+ def count_linked_rows(query_count)
17
17
  base_scope = workplace.workplace_links.where(resource_type: query.klass.name)
18
18
 
19
- # When the query applies no scoping, the original `resource_id IN (SELECT
20
- # DISTINCT <target_table>.id FROM <target_table>)` subquery materialized
21
- # every row of the target model just to count 2+ seconds on a 340k-row
22
- # target. We drop the DISTINCT subquery and instead `INNER JOIN` the
23
- # target table on its primary key so the composite index on links drives
24
- # the scan and each matching link does a cheap PK probe to confirm the
25
- # target row still exists. Orphaned links (whose target has since been
26
- # deleted) are correctly excluded from the count, so `checked_count`
27
- # never exceeds `query_count`.
28
- return base_scope.joins(unscoped_target_join_sql).count if relation_unscoped?(query)
29
-
30
- base_scope.where(resource_id: query_with_selected_ids).count
31
- end
32
-
33
- def unscoped_target_join_sql
34
- target_table = quote_table(query.klass.table_name)
35
- target_pk = "#{target_table}.#{quote_column(query.klass.primary_key)}"
36
- resource_id_column = "#{quote_table(WorkerPlugins::WorkplaceLink.table_name)}.#{quote_column(:resource_id)}"
37
-
38
- "INNER JOIN #{target_table} ON #{target_pk} = #{resource_id_expression_for_join(resource_id_column)}"
39
- end
40
-
41
- # On MySQL / MariaDB and SQLite, implicit conversion handles comparing the
42
- # target's primary key against the VARCHAR `resource_id` column. Postgres
43
- # is strict about types and needs an explicit cast when they differ.
44
- def resource_id_expression_for_join(resource_id_column)
45
- return resource_id_column unless postgres?
46
-
47
- target_pk_type = query.klass.column_for_attribute(query.klass.primary_key).type
48
- resource_id_type = WorkerPlugins::WorkplaceLink.column_for_attribute(:resource_id).type
49
-
50
- return resource_id_column if target_pk_type == resource_id_type
51
-
52
- case target_pk_type
53
- when :uuid then "CAST(#{resource_id_column} AS UUID)"
54
- when :integer then "CAST(#{resource_id_column} AS BIGINT)"
55
- else resource_id_column
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
56
36
  end
57
37
  end
58
38
 
@@ -1,3 +1,3 @@
1
1
  module WorkerPlugins
2
- VERSION = "0.0.16".freeze
2
+ VERSION = "0.0.17".freeze
3
3
  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.16
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-23 00:00:00.000000000 Z
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.
@@ -27,6 +27,7 @@ files:
27
27
  - app/services/worker_plugins/add_query.rb
28
28
  - app/services/worker_plugins/application_service.rb
29
29
  - app/services/worker_plugins/delete_old_workplaces.rb
30
+ - app/services/worker_plugins/delete_orphan_links.rb
30
31
  - app/services/worker_plugins/query_links_status.rb
31
32
  - app/services/worker_plugins/remove_query.rb
32
33
  - app/services/worker_plugins/select_column_with_type_cast.rb