kaal 0.4.0 → 0.5.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/README.md +36 -1
- data/lib/kaal/active_record_support.rb +2 -2
- data/lib/kaal/backend/adapter.rb +4 -0
- data/lib/kaal/backend/memory_adapter.rb +5 -0
- data/lib/kaal/backend/mysql.rb +25 -3
- data/lib/kaal/backend/postgres.rb +6 -2
- data/lib/kaal/backend/redis_adapter.rb +5 -0
- data/lib/kaal/backend/sqlite.rb +4 -0
- data/lib/kaal/cli.rb +1 -0
- data/lib/kaal/config/configuration.rb +33 -2
- data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
- data/lib/kaal/config.rb +1 -0
- data/lib/kaal/core/coordinator.rb +68 -19
- data/lib/kaal/delayed_job/database_engine.rb +116 -0
- data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
- data/lib/kaal/delayed_job/memory_engine.rb +79 -0
- data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
- data/lib/kaal/delayed_job/redis_engine.rb +119 -0
- data/lib/kaal/delayed_job/registry.rb +39 -0
- data/lib/kaal/internal/active_record/database_backend.rb +5 -0
- data/lib/kaal/internal/active_record/delayed_job_record.rb +16 -0
- data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -0
- data/lib/kaal/internal/active_record/migration_templates.rb +33 -3
- data/lib/kaal/internal/active_record/mysql_backend.rb +23 -5
- data/lib/kaal/internal/active_record/postgres_backend.rb +4 -0
- data/lib/kaal/internal/active_record.rb +2 -0
- data/lib/kaal/internal/sequel/database_backend.rb +5 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +15 -1
- data/lib/kaal/internal/sequel/postgres_backend.rb +4 -0
- data/lib/kaal/internal/sequel.rb +1 -0
- data/lib/kaal/job_dispatcher.rb +108 -0
- data/lib/kaal/persistence/database.rb +4 -0
- data/lib/kaal/persistence/migration_templates.rb +35 -3
- data/lib/kaal/runtime/scheduler_boot_loader.rb +2 -0
- data/lib/kaal/scheduler_file/job_applier.rb +28 -53
- data/lib/kaal/sequel_support.rb +2 -2
- data/lib/kaal/version.rb +1 -1
- data/lib/kaal.rb +111 -0
- metadata +11 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'kaal/delayed_job/registry'
|
|
8
|
+
require 'kaal/support/hash_tools'
|
|
9
|
+
|
|
10
|
+
module Kaal
|
|
11
|
+
module DelayedJob
|
|
12
|
+
# In-memory delayed job store for single-process development and tests.
|
|
13
|
+
class MemoryEngine < Registry
|
|
14
|
+
include Kaal::Support::HashTools
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
@jobs = {}
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, **)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
raise DuplicateJobError, "Delayed job #{job_id.inspect} already exists" if @jobs.key?(job_id)
|
|
25
|
+
|
|
26
|
+
job = build_job(job_id:, run_at:, job_class:, args:, queue:)
|
|
27
|
+
@jobs[job_id] = job
|
|
28
|
+
deep_dup(job)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pop_due(now:, limit:)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
due_jobs = @jobs.values
|
|
35
|
+
.select { |job| job[:run_at] <= now }
|
|
36
|
+
.sort_by { |job| job.values_at(:run_at, :job_id) }
|
|
37
|
+
.first(limit)
|
|
38
|
+
|
|
39
|
+
due_jobs.each do |job|
|
|
40
|
+
job_id = job.fetch(:job_id)
|
|
41
|
+
@jobs.delete(job_id)
|
|
42
|
+
end
|
|
43
|
+
due_jobs.map { |job| deep_dup(job) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def find_job(job_id)
|
|
48
|
+
@mutex.synchronize { deep_dup(@jobs[job_id]) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def all_jobs
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@jobs.values.sort_by { |job| [job[:run_at], job[:job_id]] }.map { |job| deep_dup(job) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def clear
|
|
58
|
+
@mutex.synchronize { @jobs.clear }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def claim_strategy
|
|
62
|
+
:atomic_pop
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_job(job_id:, run_at:, job_class:, args:, queue:)
|
|
68
|
+
{
|
|
69
|
+
job_id: job_id,
|
|
70
|
+
run_at: run_at,
|
|
71
|
+
job_class: job_class,
|
|
72
|
+
args: deep_dup(args),
|
|
73
|
+
queue: queue,
|
|
74
|
+
created_at: Time.now.utc
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module DelayedJob
|
|
9
|
+
# MySQL version helper for delayed-job claim strategy selection.
|
|
10
|
+
module MySQLVersionSupport
|
|
11
|
+
MINIMUM_SKIP_LOCKED_VERSION = 800_00
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def skip_locked_supported?(version_string)
|
|
16
|
+
version_number(version_string) >= MINIMUM_SKIP_LOCKED_VERSION
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def version_number(version_string)
|
|
20
|
+
major, minor, patch = version_components(version_string)
|
|
21
|
+
return 0 unless major && minor && patch
|
|
22
|
+
|
|
23
|
+
(major * 10_000) + (minor * 100) + patch
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def version_components(version_string)
|
|
27
|
+
major, minor, patch = version_string.to_s.split('.', 3)
|
|
28
|
+
[
|
|
29
|
+
integer_prefix(major),
|
|
30
|
+
integer_prefix(minor),
|
|
31
|
+
integer_prefix(patch)
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def integer_prefix(value)
|
|
36
|
+
digits = value.to_s.each_char.take_while { |character| character.between?('0', '9') }.join
|
|
37
|
+
return nil if digits.empty?
|
|
38
|
+
|
|
39
|
+
digits.to_i
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'time'
|
|
9
|
+
require 'kaal/delayed_job/registry'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module DelayedJob
|
|
13
|
+
# Redis-backed delayed-job store using a sorted set plus payload hash.
|
|
14
|
+
class RedisEngine < Registry
|
|
15
|
+
def initialize(redis, namespace: 'kaal')
|
|
16
|
+
super()
|
|
17
|
+
@redis = redis
|
|
18
|
+
@namespace = namespace
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, **)
|
|
22
|
+
payload = JSON.generate(
|
|
23
|
+
job_id: job_id,
|
|
24
|
+
run_at: run_at.iso8601,
|
|
25
|
+
job_class: job_class,
|
|
26
|
+
args: args,
|
|
27
|
+
queue: queue,
|
|
28
|
+
created_at: Time.now.utc.iso8601
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
script = <<~LUA
|
|
32
|
+
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then
|
|
33
|
+
return 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
37
|
+
redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])
|
|
38
|
+
return 1
|
|
39
|
+
LUA
|
|
40
|
+
|
|
41
|
+
result = @redis.eval(script, keys: [payloads_key, schedule_key], argv: [job_id, payload, run_at.to_f])
|
|
42
|
+
raise DuplicateJobError, "Delayed job #{job_id.inspect} already exists" unless [1, '1', true].include?(result)
|
|
43
|
+
|
|
44
|
+
find_job(job_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pop_due(now:, limit:)
|
|
48
|
+
script = <<~LUA
|
|
49
|
+
local job_ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[1], 'LIMIT', 0, ARGV[2])
|
|
50
|
+
local payloads = {}
|
|
51
|
+
|
|
52
|
+
for _, job_id in ipairs(job_ids) do
|
|
53
|
+
if redis.call('ZREM', KEYS[2], job_id) == 1 then
|
|
54
|
+
local payload = redis.call('HGET', KEYS[1], job_id)
|
|
55
|
+
if payload then
|
|
56
|
+
redis.call('HDEL', KEYS[1], job_id)
|
|
57
|
+
table.insert(payloads, payload)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return payloads
|
|
63
|
+
LUA
|
|
64
|
+
|
|
65
|
+
Array(@redis.eval(script, keys: [payloads_key, schedule_key], argv: [now.to_f, limit])).filter_map do |raw|
|
|
66
|
+
self.class.deserialize(raw)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_job(job_id)
|
|
71
|
+
self.class.deserialize(@redis.hget(payloads_key, job_id))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def all_jobs
|
|
75
|
+
Array(@redis.zrange(schedule_key, 0, -1)).filter_map { |job_id| find_job(job_id) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def claim_strategy
|
|
79
|
+
:atomic_pop
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.deserialize(raw)
|
|
83
|
+
return nil unless raw
|
|
84
|
+
|
|
85
|
+
parsed = JSON.parse(raw)
|
|
86
|
+
run_at = parse_time(parsed['run_at'])
|
|
87
|
+
created_at = parse_time(parsed['created_at'])
|
|
88
|
+
return nil unless run_at && created_at
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
job_id: parsed['job_id'],
|
|
92
|
+
run_at: run_at,
|
|
93
|
+
job_class: parsed['job_class'],
|
|
94
|
+
args: parsed['args'] || [],
|
|
95
|
+
queue: parsed['queue'],
|
|
96
|
+
created_at: created_at
|
|
97
|
+
}
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.parse_time(value)
|
|
103
|
+
Time.iso8601(value.to_s)
|
|
104
|
+
rescue ArgumentError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def payloads_key
|
|
111
|
+
"#{@namespace}:delayed_jobs:payloads"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def schedule_key
|
|
115
|
+
"#{@namespace}:delayed_jobs:schedule"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module DelayedJob
|
|
9
|
+
# Raised when a delayed job with the same identifier already exists.
|
|
10
|
+
class DuplicateJobError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Base abstraction for delayed-job persistence.
|
|
13
|
+
class Registry
|
|
14
|
+
def enqueue(**)
|
|
15
|
+
raise NotImplementedError, "#{self.class.name} must implement #enqueue"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def pop_due(**)
|
|
19
|
+
raise NotImplementedError, "#{self.class.name} must implement #pop_due"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_job(_job_id)
|
|
23
|
+
raise NotImplementedError, "#{self.class.name} must implement #find_job"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def all_jobs
|
|
27
|
+
raise NotImplementedError, "#{self.class.name} must implement #all_jobs"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def claim_strategy
|
|
31
|
+
raise NotImplementedError, "#{self.class.name} must implement #claim_strategy"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def requires_dispatch_lock?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
|
7
7
|
require 'kaal/backend/adapter'
|
|
8
8
|
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/internal/active_record/delayed_job_registry'
|
|
9
10
|
|
|
10
11
|
module Kaal
|
|
11
12
|
module Internal
|
|
@@ -31,6 +32,10 @@ module Kaal
|
|
|
31
32
|
@definition_registry ||= DefinitionRegistry.new
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
def delayed_store
|
|
36
|
+
@delayed_store ||= DelayedJobRegistry.new
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
def acquire(key, ttl)
|
|
35
40
|
now = Time.now.utc
|
|
36
41
|
expires_at = now + ttl
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
module Kaal
|
|
8
|
+
module Internal
|
|
9
|
+
module ActiveRecord
|
|
10
|
+
# Active Record model for persisted delayed jobs.
|
|
11
|
+
class DelayedJobRecord < BaseRecord
|
|
12
|
+
self.table_name = 'kaal_delayed_jobs'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'kaal/delayed_job/registry'
|
|
9
|
+
|
|
10
|
+
module Kaal
|
|
11
|
+
module Internal
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
# Active Record-backed store for delayed jobs.
|
|
14
|
+
class DelayedJobRegistry < Kaal::DelayedJob::Registry
|
|
15
|
+
def initialize(connection: nil, model: DelayedJobRecord, use_skip_locked: false)
|
|
16
|
+
super()
|
|
17
|
+
ConnectionSupport.configure!(connection)
|
|
18
|
+
@model = model
|
|
19
|
+
@use_skip_locked = use_skip_locked
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, connection: nil)
|
|
23
|
+
now = Time.now.utc
|
|
24
|
+
attributes = {
|
|
25
|
+
job_id: job_id,
|
|
26
|
+
run_at: run_at,
|
|
27
|
+
job_class: job_class,
|
|
28
|
+
args: JSON.generate(args),
|
|
29
|
+
queue: queue,
|
|
30
|
+
created_at: now
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if connection
|
|
34
|
+
insert_with_connection(connection, attributes)
|
|
35
|
+
else
|
|
36
|
+
@model.create!(attributes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
self.class.normalize(@model.new(attributes))
|
|
40
|
+
rescue ::ActiveRecord::RecordNotUnique
|
|
41
|
+
raise Kaal::DelayedJob::DuplicateJobError, "Delayed job #{job_id.inspect} already exists"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pop_due(now:, limit:)
|
|
45
|
+
return pop_due_with_skip_locked(now:, limit:) if @use_skip_locked
|
|
46
|
+
|
|
47
|
+
pop_due_with_delete_confirmation(now:, limit:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def pop_due_with_skip_locked(now:, limit:)
|
|
53
|
+
@model.transaction do
|
|
54
|
+
due_records = @model.where('run_at <= ?', now).order(:run_at, :job_id).lock('FOR UPDATE SKIP LOCKED').limit(limit).to_a
|
|
55
|
+
job_ids = due_records.map(&:job_id)
|
|
56
|
+
@model.where(job_id: job_ids).delete_all unless job_ids.empty?
|
|
57
|
+
due_records.filter_map { |record| self.class.normalize(record) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pop_due_with_delete_confirmation(now:, limit:)
|
|
62
|
+
@model.transaction do
|
|
63
|
+
@model.where('run_at <= ?', now).order(:run_at, :job_id).limit(limit).each_with_object([]) do |record, jobs|
|
|
64
|
+
normalized_job = self.class.normalize(record)
|
|
65
|
+
jobs << normalized_job if @model.where(job_id: record.job_id).delete_all.positive? && normalized_job
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
public
|
|
71
|
+
|
|
72
|
+
def find_job(job_id)
|
|
73
|
+
self.class.normalize(@model.find_by(job_id: job_id))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def all_jobs
|
|
77
|
+
@model.order(:run_at, :job_id).filter_map { |record| self.class.normalize(record) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def claim_strategy
|
|
81
|
+
@use_skip_locked ? :skip_locked : :delete_confirmation
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.normalize(record)
|
|
85
|
+
return nil unless record
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
job_id: record.job_id,
|
|
89
|
+
run_at: record.run_at,
|
|
90
|
+
job_class: record.job_class,
|
|
91
|
+
args: parse_args(record.args),
|
|
92
|
+
queue: record.queue,
|
|
93
|
+
created_at: record.created_at
|
|
94
|
+
}
|
|
95
|
+
rescue JSON::ParserError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def insert_with_connection(connection, attributes)
|
|
102
|
+
table_name = @model.table_name
|
|
103
|
+
columns = attributes.keys
|
|
104
|
+
quoted_pairs = columns.map do |column|
|
|
105
|
+
[connection.quote_column_name(column), connection.quote(attributes.fetch(column))]
|
|
106
|
+
end
|
|
107
|
+
quoted_columns = quoted_pairs.map(&:first).join(', ')
|
|
108
|
+
quoted_values = quoted_pairs.map(&:last).join(', ')
|
|
109
|
+
connection.execute("INSERT INTO #{connection.quote_table_name(table_name)} (#{quoted_columns}) VALUES (#{quoted_values})")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.parse_args(args_payload)
|
|
113
|
+
JSON.parse(args_payload || '[]')
|
|
114
|
+
end
|
|
115
|
+
private_class_method :parse_args
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -17,17 +17,20 @@ module Kaal
|
|
|
17
17
|
{
|
|
18
18
|
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
19
19
|
'002_create_kaal_locks.rb' => locks_template,
|
|
20
|
-
'003_create_kaal_definitions.rb' => definitions_template('sqlite')
|
|
20
|
+
'003_create_kaal_definitions.rb' => definitions_template('sqlite'),
|
|
21
|
+
'004_create_kaal_delayed_jobs.rb' => delayed_jobs_template('sqlite')
|
|
21
22
|
}
|
|
22
23
|
when 'postgres'
|
|
23
24
|
{
|
|
24
25
|
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
25
|
-
'002_create_kaal_definitions.rb' => definitions_template('postgres')
|
|
26
|
+
'002_create_kaal_definitions.rb' => definitions_template('postgres'),
|
|
27
|
+
'003_create_kaal_delayed_jobs.rb' => delayed_jobs_template('postgres')
|
|
26
28
|
}
|
|
27
29
|
when 'mysql'
|
|
28
30
|
{
|
|
29
31
|
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
30
|
-
'002_create_kaal_definitions.rb' => definitions_template('mysql')
|
|
32
|
+
'002_create_kaal_definitions.rb' => definitions_template('mysql'),
|
|
33
|
+
'003_create_kaal_delayed_jobs.rb' => delayed_jobs_template('mysql')
|
|
31
34
|
}
|
|
32
35
|
else
|
|
33
36
|
{}
|
|
@@ -102,6 +105,33 @@ module Kaal
|
|
|
102
105
|
end
|
|
103
106
|
RUBY
|
|
104
107
|
end
|
|
108
|
+
|
|
109
|
+
def delayed_jobs_template(backend)
|
|
110
|
+
args_definition =
|
|
111
|
+
if backend == 'mysql'
|
|
112
|
+
't.text :args, null: false'
|
|
113
|
+
else
|
|
114
|
+
"t.text :args, null: false, default: '[]'"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
<<~RUBY
|
|
118
|
+
class CreateKaalDelayedJobs < ActiveRecord::Migration[7.1]
|
|
119
|
+
def change
|
|
120
|
+
create_table :kaal_delayed_jobs do |t|
|
|
121
|
+
t.string :job_id, null: false
|
|
122
|
+
t.datetime :run_at, null: false
|
|
123
|
+
t.string :job_class, null: false
|
|
124
|
+
#{args_definition}
|
|
125
|
+
t.string :queue
|
|
126
|
+
t.datetime :created_at, null: false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
add_index :kaal_delayed_jobs, :job_id, unique: true
|
|
130
|
+
add_index :kaal_delayed_jobs, :run_at
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
end
|
|
105
135
|
end
|
|
106
136
|
end
|
|
107
137
|
end
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# This source code is licensed under the MIT license found in the
|
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
|
7
7
|
require 'digest'
|
|
8
|
+
require 'kaal/delayed_job/mysql_version_support'
|
|
8
9
|
|
|
9
10
|
module Kaal
|
|
10
11
|
module Internal
|
|
@@ -14,13 +15,16 @@ module Kaal
|
|
|
14
15
|
include Kaal::Backend::DispatchLogging
|
|
15
16
|
|
|
16
17
|
MAX_LOCK_NAME_LENGTH = 64
|
|
18
|
+
UNSET_SKIP_LOCKED_SUPPORT = Object.new.freeze
|
|
17
19
|
|
|
18
|
-
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil
|
|
20
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil,
|
|
21
|
+
use_skip_locked: UNSET_SKIP_LOCKED_SUPPORT)
|
|
19
22
|
super()
|
|
20
23
|
ConnectionSupport.configure!(connection)
|
|
21
24
|
@dispatch_registry = dispatch_registry
|
|
22
25
|
@definition_registry = definition_registry
|
|
23
26
|
@namespace = namespace
|
|
27
|
+
@use_skip_locked = use_skip_locked
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def dispatch_registry
|
|
@@ -31,6 +35,10 @@ module Kaal
|
|
|
31
35
|
@definition_registry ||= DefinitionRegistry.new
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
def delayed_store
|
|
39
|
+
@delayed_store ||= DelayedJobRegistry.new(use_skip_locked: supports_skip_locked?)
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
def acquire(key, _ttl)
|
|
35
43
|
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
44
|
log_dispatch_attempt(key) if acquired
|
|
@@ -55,16 +63,26 @@ module Kaal
|
|
|
55
63
|
|
|
56
64
|
private
|
|
57
65
|
|
|
58
|
-
def scalar(sql,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
def scalar(sql, *binds)
|
|
67
|
+
sanitized_sql = if binds.empty?
|
|
68
|
+
sql
|
|
69
|
+
else
|
|
70
|
+
BaseRecord.send(:sanitize_sql_array, [sql, *binds])
|
|
71
|
+
end
|
|
72
|
+
result = BaseRecord.connection.exec_query(sanitized_sql)
|
|
62
73
|
result.first.values.first
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
def resolved_namespace
|
|
66
77
|
@namespace || Kaal.configuration.namespace
|
|
67
78
|
end
|
|
79
|
+
|
|
80
|
+
def supports_skip_locked?
|
|
81
|
+
return @use_skip_locked unless @use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
|
|
82
|
+
|
|
83
|
+
version_string = scalar('SELECT VERSION() AS version')
|
|
84
|
+
Kaal::DelayedJob::MySQLVersionSupport.skip_locked_supported?(version_string)
|
|
85
|
+
end
|
|
68
86
|
end
|
|
69
87
|
end
|
|
70
88
|
end
|
|
@@ -32,6 +32,10 @@ module Kaal
|
|
|
32
32
|
@definition_registry ||= DefinitionRegistry.new
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def delayed_store
|
|
36
|
+
@delayed_store ||= DelayedJobRegistry.new(use_skip_locked: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
def acquire(key, _ttl)
|
|
36
40
|
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
41
|
log_dispatch_attempt(key) if acquired
|
|
@@ -8,9 +8,11 @@ require 'kaal/internal/active_record/base_record'
|
|
|
8
8
|
require 'kaal/internal/active_record/connection_support'
|
|
9
9
|
require 'kaal/internal/active_record/definition_record'
|
|
10
10
|
require 'kaal/internal/active_record/dispatch_record'
|
|
11
|
+
require 'kaal/internal/active_record/delayed_job_record'
|
|
11
12
|
require 'kaal/internal/active_record/lock_record'
|
|
12
13
|
require 'kaal/internal/active_record/definition_registry'
|
|
13
14
|
require 'kaal/internal/active_record/dispatch_registry'
|
|
15
|
+
require 'kaal/internal/active_record/delayed_job_registry'
|
|
14
16
|
require 'kaal/internal/active_record/database_backend'
|
|
15
17
|
require 'kaal/internal/active_record/postgres_backend'
|
|
16
18
|
require 'kaal/internal/active_record/mysql_backend'
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# This source code is licensed under the MIT license found in the
|
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
|
7
7
|
require 'kaal/backend/dispatch_logging'
|
|
8
|
+
require 'kaal/delayed_job/database_engine'
|
|
8
9
|
require 'kaal/persistence/database'
|
|
9
10
|
|
|
10
11
|
module Kaal
|
|
@@ -28,6 +29,10 @@ module Kaal
|
|
|
28
29
|
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def delayed_store
|
|
33
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection)
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
def acquire(key, ttl)
|
|
32
37
|
now = Time.now.utc
|
|
33
38
|
expires_at = now + ttl
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# LICENSE file in the root directory of this source tree.
|
|
7
7
|
require 'digest'
|
|
8
8
|
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/delayed_job/mysql_version_support'
|
|
9
10
|
require 'kaal/persistence/database'
|
|
10
11
|
|
|
11
12
|
module Kaal
|
|
@@ -16,11 +17,13 @@ module Kaal
|
|
|
16
17
|
include Kaal::Backend::DispatchLogging
|
|
17
18
|
|
|
18
19
|
MAX_LOCK_NAME_LENGTH = 64
|
|
20
|
+
UNSET_SKIP_LOCKED_SUPPORT = Object.new.freeze
|
|
19
21
|
|
|
20
|
-
def initialize(database, namespace: nil)
|
|
22
|
+
def initialize(database, namespace: nil, use_skip_locked: UNSET_SKIP_LOCKED_SUPPORT)
|
|
21
23
|
super()
|
|
22
24
|
@database = Kaal::Persistence::Database.new(database)
|
|
23
25
|
@namespace = namespace
|
|
26
|
+
@use_skip_locked = use_skip_locked
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def dispatch_registry
|
|
@@ -31,6 +34,10 @@ module Kaal
|
|
|
31
34
|
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
def delayed_store
|
|
38
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection, use_skip_locked: supports_skip_locked?)
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
def acquire(key, _ttl)
|
|
35
42
|
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
43
|
log_dispatch_attempt(key) if acquired
|
|
@@ -63,6 +70,13 @@ module Kaal
|
|
|
63
70
|
def resolved_namespace
|
|
64
71
|
@namespace || Kaal.configuration.namespace
|
|
65
72
|
end
|
|
73
|
+
|
|
74
|
+
def supports_skip_locked?
|
|
75
|
+
return @use_skip_locked unless @use_skip_locked.equal?(UNSET_SKIP_LOCKED_SUPPORT)
|
|
76
|
+
|
|
77
|
+
version_string = scalar('SELECT VERSION() AS version')
|
|
78
|
+
Kaal::DelayedJob::MySQLVersionSupport.skip_locked_supported?(version_string)
|
|
79
|
+
end
|
|
66
80
|
end
|
|
67
81
|
end
|
|
68
82
|
end
|
|
@@ -32,6 +32,10 @@ module Kaal
|
|
|
32
32
|
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def delayed_store
|
|
36
|
+
@delayed_store ||= Kaal::DelayedJob::DatabaseEngine.new(database: @database.connection, use_skip_locked: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
def acquire(key, _ttl)
|
|
36
40
|
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
41
|
log_dispatch_attempt(key) if acquired
|
data/lib/kaal/internal/sequel.rb
CHANGED
|
@@ -8,5 +8,6 @@ require 'kaal/internal/sequel/database_backend'
|
|
|
8
8
|
require 'kaal/internal/sequel/postgres_backend'
|
|
9
9
|
require 'kaal/internal/sequel/mysql_backend'
|
|
10
10
|
require 'kaal/definition/database_engine'
|
|
11
|
+
require 'kaal/delayed_job/database_engine'
|
|
11
12
|
require 'kaal/dispatch/database_engine'
|
|
12
13
|
require 'kaal/persistence/database'
|