joblin 0.1.11 → 0.2.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/joblin/background_task/api_access.rb +15 -2
  3. data/app/models/joblin/background_task/attachments.rb +2 -2
  4. data/app/models/joblin/background_task/retention_policy.rb +12 -4
  5. data/app/models/joblin/background_task.rb +8 -1
  6. data/db/migrate/20260709120000_add_indexes_to_joblin_background_tasks.rb +10 -0
  7. data/lib/joblin/batching/batch.rb +60 -33
  8. data/lib/joblin/batching/callback.rb +9 -1
  9. data/lib/joblin/batching/chain_builder.rb +3 -2
  10. data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +2 -2
  11. data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +3 -3
  12. data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +5 -5
  13. data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +13 -13
  14. data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +3 -3
  15. data/lib/joblin/batching/compat/sidekiq/web.rb +17 -1
  16. data/lib/joblin/batching/context_hash.rb +1 -1
  17. data/lib/joblin/batching/jobs/managed_batch_job.rb +8 -2
  18. data/lib/joblin/batching/pool.rb +12 -3
  19. data/lib/joblin/batching/pool_refill.lua +2 -1
  20. data/lib/joblin/batching/schedule_callback.lua +6 -0
  21. data/lib/joblin/lazy_access.rb +108 -19
  22. data/lib/joblin/uniqueness/compat/active_job.rb +1 -1
  23. data/lib/joblin/uniqueness/job_uniqueness.rb +2 -0
  24. data/lib/joblin/uniqueness/lock_context.rb +4 -0
  25. data/lib/joblin/uniqueness/on_conflict/log.rb +1 -0
  26. data/lib/joblin/uniqueness/on_conflict/reject.rb +8 -1
  27. data/lib/joblin/uniqueness/strategy/until_expired.rb +1 -0
  28. data/lib/joblin/uniqueness/unique_job_common.rb +15 -7
  29. data/lib/joblin/version.rb +1 -1
  30. data/spec/internal/log/test.log +6619 -0
  31. data/spec/lazy_access_spec.rb +76 -0
  32. data/spec/models/api_access_spec.rb +57 -0
  33. data/spec/uniqueness/on_conflict/reject_spec.rb +42 -0
  34. data/spec/uniqueness/strategy/until_expired_spec.rb +7 -0
  35. metadata +8 -1
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ # A real ActiveRecord subclass so dump/locate see a genuine GlobalID.
4
+ class LazyAccessTestTask < Joblin::BackgroundTask
5
+ def perform; end
6
+ end
7
+
8
+ RSpec.describe Joblin::LazyAccess do
9
+ let(:record) { LazyAccessTestTask.create! }
10
+ let(:gid) { record.to_gid.to_s }
11
+
12
+ describe ".dump" do
13
+ it "converts record values to gid strings and records them in the marker" do
14
+ dumped = described_class.dump({ "user" => record, "n" => 5 })
15
+
16
+ expect(dumped["user"]).to eq(gid)
17
+ expect(dumped["n"]).to eq(5)
18
+ expect(dumped["_joblin_"]).to eq({ "gid_keys" => ["user"] })
19
+ end
20
+
21
+ it "does NOT mark a plain string that merely looks like a gid" do
22
+ dumped = described_class.dump({ "note" => "gid://app/Whatever/1" })
23
+
24
+ expect(dumped["note"]).to eq("gid://app/Whatever/1")
25
+ expect(dumped).not_to have_key("_joblin_")
26
+ end
27
+
28
+ it "omits the marker entirely when there are no record values" do
29
+ expect(described_class.dump({ "a" => 1 })).to eq({ "a" => 1 })
30
+ end
31
+ end
32
+
33
+ describe ".load" do
34
+ it "does not locate anything eagerly and only resolves marked keys on access" do
35
+ dumped = described_class.dump({ "user" => record, "note" => "gid://app/Whatever/1" })
36
+
37
+ expect(GlobalID::Locator).to receive(:locate).once.and_return(record)
38
+
39
+ h = described_class.load(dumped)
40
+ # Unmarked gid-looking string is returned verbatim (no locate).
41
+ expect(h["note"]).to eq("gid://app/Whatever/1")
42
+ # Marked key resolves to the record.
43
+ expect(h["user"]).to eq(record)
44
+ end
45
+
46
+ it "hides the marker key from enumeration" do
47
+ dumped = described_class.dump({ "user" => record, "n" => 5 })
48
+ h = described_class.load(dumped)
49
+
50
+ expect(h.keys).to match_array(%w[user n])
51
+ expect(h.to_h).not_to have_key("_joblin_")
52
+ end
53
+ end
54
+
55
+ describe "round-trip" do
56
+ it "preserves gid marking when a loaded value is re-dumped without being accessed" do
57
+ dumped = described_class.dump({ "user" => record })
58
+ loaded = described_class.load(dumped)
59
+
60
+ # Re-dump without ever reading loaded["user"].
61
+ redumped = described_class.dump(loaded)
62
+
63
+ expect(redumped["user"]).to eq(gid)
64
+ expect(redumped["_joblin_"]).to eq({ "gid_keys" => ["user"] })
65
+ end
66
+ end
67
+
68
+ describe "Array access (regression for multi-arg [])" do
69
+ it "supports two-arg and range slicing" do
70
+ arr = described_class.load([10, 20, 30, 40])
71
+ expect(arr[1, 2]).to eq([20, 30])
72
+ expect(arr[1..2]).to eq([20, 30])
73
+ expect(arr[0]).to eq(10)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ # Defined with real `class` statements so the constant is named when
4
+ # allow_api_access! runs (the registry keys off `name`).
5
+ class ApiAllowedTask < Joblin::BackgroundTask
6
+ allow_api_access!
7
+ def perform; end
8
+ end
9
+
10
+ class ApiNotAllowedTask < Joblin::BackgroundTask
11
+ def perform; end
12
+ end
13
+
14
+ RSpec.describe Joblin::BackgroundTask::ApiAccess, type: :model do
15
+ describe '.allow_api_access!' do
16
+ it 'registers the class by name in api_accessible_types' do
17
+ expect(Joblin::BackgroundTask.api_accessible_types['ApiAllowedTask']).to eq(ApiAllowedTask)
18
+ end
19
+
20
+ it 'does not register a class that never opted in' do
21
+ expect(Joblin::BackgroundTask.api_accessible_types).not_to have_key('ApiNotAllowedTask')
22
+ end
23
+ end
24
+
25
+ describe '.build_from_api type resolution' do
26
+ it 'builds a registered type named in params[:type]' do
27
+ task = Joblin::BackgroundTask.build_from_api({ type: 'ApiAllowedTask' })
28
+ expect(task).to be_an_instance_of(ApiAllowedTask)
29
+ end
30
+
31
+ it 'raises RecordNotFound for a real class that never opted in' do
32
+ # ApiNotAllowedTask exists and would constantize fine — but it isn't in
33
+ # the registry, so it must not be buildable.
34
+ expect do
35
+ Joblin::BackgroundTask.build_from_api({ type: 'ApiNotAllowedTask' })
36
+ end.to raise_error(ActiveRecord::RecordNotFound)
37
+ end
38
+
39
+ it 'raises RecordNotFound for an unknown/hostile type without constantizing it' do
40
+ expect(String).not_to receive(:safe_constantize)
41
+ expect do
42
+ Joblin::BackgroundTask.build_from_api({ type: 'Kernel' })
43
+ end.to raise_error(ActiveRecord::RecordNotFound)
44
+ end
45
+
46
+ it 'raises RecordNotFound when params[:type] is missing' do
47
+ expect do
48
+ Joblin::BackgroundTask.build_from_api({})
49
+ end.to raise_error(ActiveRecord::RecordNotFound)
50
+ end
51
+
52
+ it 'ignores params[:type] when called on a concrete subclass' do
53
+ task = ApiAllowedTask.build_from_api({ type: 'Nonexistent' })
54
+ expect(task).to be_an_instance_of(ApiAllowedTask)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ require 'uniqueness/spec_helper'
2
+ require 'sidekiq/api'
3
+
4
+ RSpec.describe Joblin::Uniqueness::OnConflict::Reject do
5
+ include_context "on_conflict specs"
6
+ include_examples "OnConflict is compatible with", %i[while_executing]
7
+ include_examples "OnConflict is incompatible with", %i[until_executing until_executed until_and_while_executing until_expired]
8
+
9
+ let(:sidekiq_message) do
10
+ { 'class' => 'TestWorker', 'jid' => 'j1', 'queue' => 'default', 'args' => [] }
11
+ end
12
+
13
+ # Reject only supports Sidekiq-backed jobs, so override the shared plain
14
+ # LockContext with a SidekiqLockContext carrying a job payload.
15
+ let(:lock_context) do
16
+ Joblin::Uniqueness::Compat::Sidekiq::SidekiqLockContext.new(
17
+ { jid: 'j1', queue: 'default', job_clazz: 'TestWorker' },
18
+ job_instance: sidekiq_message,
19
+ )
20
+ end
21
+
22
+ it "kills the duplicate job via the DeadSet" do
23
+ dead_set = instance_double(Sidekiq::DeadSet)
24
+ expect(Sidekiq::DeadSet).to receive(:new).and_return(dead_set)
25
+ # DeadSet#kill accepts options on every supported Sidekiq (arity -2), so we
26
+ # always pass notify_failure: false to avoid firing death handlers for the
27
+ # rejected duplicate.
28
+ expect(dead_set).to receive(:kill).with(JSON.dump(sidekiq_message), notify_failure: false)
29
+
30
+ on_conflict.call
31
+ end
32
+
33
+ it "logs an error and does not touch the DeadSet for non-Sidekiq contexts" do
34
+ plain_ctx = Joblin::Uniqueness::LockContext.new({ jid: 'j1', queue: 'default', job_clazz: 'TestWorker' })
35
+ strategy = described_class.new(plain_ctx)
36
+
37
+ expect(Sidekiq::DeadSet).not_to receive(:new)
38
+ expect(Joblin::Uniqueness.logger).to receive(:error)
39
+
40
+ strategy.call
41
+ end
42
+ end
@@ -13,6 +13,13 @@ RSpec.describe Joblin::Uniqueness::Strategy::UntilExpired do
13
13
  end
14
14
 
15
15
  describe "#on_perform" do
16
+ it "runs the job body" do
17
+ process_one.on_enqueue {}
18
+ ran = false
19
+ process_one.on_perform { ran = true }
20
+ expect(ran).to be(true)
21
+ end
22
+
16
23
  it "does not unlock after processing" do
17
24
  process_one.on_enqueue {}
18
25
  process_one.on_perform {}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: joblin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Knapp
@@ -157,6 +157,7 @@ files:
157
157
  - app/models/joblin/concerns/job_working_dirs.rb
158
158
  - db/migrate/20250903184852_create_joblin_background_tasks.rb
159
159
  - db/migrate/20250916184852_add_results_to_joblin_background_tasks.rb
160
+ - db/migrate/20260709120000_add_indexes_to_joblin_background_tasks.rb
160
161
  - joblin.gemspec
161
162
  - lib/joblin.rb
162
163
  - lib/joblin/batching/batch.rb
@@ -239,6 +240,8 @@ files:
239
240
  - spec/internal/db/schema.rb
240
241
  - spec/internal/log/test.log
241
242
  - spec/internal/public/favicon.ico
243
+ - spec/lazy_access_spec.rb
244
+ - spec/models/api_access_spec.rb
242
245
  - spec/models/background_task_spec.rb
243
246
  - spec/spec_helper.rb
244
247
  - spec/uniqueness/compat/active_job_spec.rb
@@ -246,6 +249,7 @@ files:
246
249
  - spec/uniqueness/lock_context_spec.rb
247
250
  - spec/uniqueness/on_conflict/log_spec.rb
248
251
  - spec/uniqueness/on_conflict/raise_spec.rb
252
+ - spec/uniqueness/on_conflict/reject_spec.rb
249
253
  - spec/uniqueness/on_conflict/reschedule_spec.rb
250
254
  - spec/uniqueness/on_conflict_spec.rb
251
255
  - spec/uniqueness/spec_helper.rb
@@ -304,6 +308,8 @@ test_files:
304
308
  - spec/internal/db/schema.rb
305
309
  - spec/internal/log/test.log
306
310
  - spec/internal/public/favicon.ico
311
+ - spec/lazy_access_spec.rb
312
+ - spec/models/api_access_spec.rb
307
313
  - spec/models/background_task_spec.rb
308
314
  - spec/spec_helper.rb
309
315
  - spec/uniqueness/compat/active_job_spec.rb
@@ -311,6 +317,7 @@ test_files:
311
317
  - spec/uniqueness/lock_context_spec.rb
312
318
  - spec/uniqueness/on_conflict/log_spec.rb
313
319
  - spec/uniqueness/on_conflict/raise_spec.rb
320
+ - spec/uniqueness/on_conflict/reject_spec.rb
314
321
  - spec/uniqueness/on_conflict/reschedule_spec.rb
315
322
  - spec/uniqueness/on_conflict_spec.rb
316
323
  - spec/uniqueness/spec_helper.rb