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.
- checksums.yaml +4 -4
- data/app/models/joblin/background_task/api_access.rb +15 -2
- data/app/models/joblin/background_task/attachments.rb +2 -2
- data/app/models/joblin/background_task/retention_policy.rb +12 -4
- data/app/models/joblin/background_task.rb +8 -1
- data/db/migrate/20260709120000_add_indexes_to_joblin_background_tasks.rb +10 -0
- data/lib/joblin/batching/batch.rb +60 -33
- data/lib/joblin/batching/callback.rb +9 -1
- data/lib/joblin/batching/chain_builder.rb +3 -2
- data/lib/joblin/batching/compat/sidekiq/web/views/_batches_table.erb +2 -2
- data/lib/joblin/batching/compat/sidekiq/web/views/_jobs_table.erb +3 -3
- data/lib/joblin/batching/compat/sidekiq/web/views/batch.erb +5 -5
- data/lib/joblin/batching/compat/sidekiq/web/views/pool.erb +13 -13
- data/lib/joblin/batching/compat/sidekiq/web/views/pools.erb +3 -3
- data/lib/joblin/batching/compat/sidekiq/web.rb +17 -1
- data/lib/joblin/batching/context_hash.rb +1 -1
- data/lib/joblin/batching/jobs/managed_batch_job.rb +8 -2
- data/lib/joblin/batching/pool.rb +12 -3
- data/lib/joblin/batching/pool_refill.lua +2 -1
- data/lib/joblin/batching/schedule_callback.lua +6 -0
- data/lib/joblin/lazy_access.rb +108 -19
- data/lib/joblin/uniqueness/compat/active_job.rb +1 -1
- data/lib/joblin/uniqueness/job_uniqueness.rb +2 -0
- data/lib/joblin/uniqueness/lock_context.rb +4 -0
- data/lib/joblin/uniqueness/on_conflict/log.rb +1 -0
- data/lib/joblin/uniqueness/on_conflict/reject.rb +8 -1
- data/lib/joblin/uniqueness/strategy/until_expired.rb +1 -0
- data/lib/joblin/uniqueness/unique_job_common.rb +15 -7
- data/lib/joblin/version.rb +1 -1
- data/spec/internal/log/test.log +6619 -0
- data/spec/lazy_access_spec.rb +76 -0
- data/spec/models/api_access_spec.rb +57 -0
- data/spec/uniqueness/on_conflict/reject_spec.rb +42 -0
- data/spec/uniqueness/strategy/until_expired_spec.rb +7 -0
- 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.
|
|
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
|