kaal 0.3.0 → 0.4.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 +8 -11
- data/lib/kaal/active_record_support.rb +82 -0
- data/lib/kaal/backend/mysql.rb +41 -0
- data/lib/kaal/backend/postgres.rb +41 -0
- data/lib/kaal/backend/sqlite.rb +41 -0
- data/lib/kaal/definition/database_engine.rb +88 -0
- data/lib/kaal/dispatch/database_engine.rb +120 -0
- data/lib/kaal/internal/active_record/base_record.rb +16 -0
- data/lib/kaal/internal/active_record/connection_support.rb +96 -0
- data/lib/kaal/internal/active_record/database_backend.rb +73 -0
- data/lib/kaal/internal/active_record/definition_record.rb +16 -0
- data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
- data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
- data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
- data/lib/kaal/internal/active_record/lock_record.rb +16 -0
- data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
- data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
- data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
- data/lib/kaal/internal/active_record.rb +17 -0
- data/lib/kaal/internal/sequel/database_backend.rb +74 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
- data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
- data/lib/kaal/internal/sequel.rb +12 -0
- data/lib/kaal/persistence/database.rb +35 -0
- data/lib/kaal/persistence/migration_templates.rb +97 -0
- data/lib/kaal/registry.rb +0 -2
- data/lib/kaal/sequel_support.rb +82 -0
- data/lib/kaal/version.rb +1 -1
- data/lib/kaal.rb +6 -0
- metadata +26 -1
|
@@ -0,0 +1,81 @@
|
|
|
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/definition/registry'
|
|
9
|
+
require 'kaal/definition/persistence_helpers'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module ActiveRecord
|
|
14
|
+
# Active Record-backed registry for scheduler definitions.
|
|
15
|
+
class DefinitionRegistry < Kaal::Definition::Registry
|
|
16
|
+
def initialize(connection: nil, model: DefinitionRecord)
|
|
17
|
+
super()
|
|
18
|
+
ConnectionSupport.configure!(connection)
|
|
19
|
+
@model = model
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
|
|
23
|
+
record = @model.find_or_initialize_by(key: key)
|
|
24
|
+
existing = record.persisted? ? { enabled: record.enabled, disabled_at: record.disabled_at } : nil
|
|
25
|
+
now = Time.now.utc
|
|
26
|
+
record.cron = cron
|
|
27
|
+
record.enabled = enabled
|
|
28
|
+
record.source = source
|
|
29
|
+
record.metadata = JSON.generate(metadata || {})
|
|
30
|
+
record.created_at ||= now
|
|
31
|
+
record.updated_at = now
|
|
32
|
+
record.disabled_at = Kaal::Definition::PersistenceHelpers.disabled_at_for(existing, enabled, now)
|
|
33
|
+
record.save!
|
|
34
|
+
normalize(record)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def remove_definition(key)
|
|
38
|
+
record = @model.find_by(key: key)
|
|
39
|
+
return nil unless record
|
|
40
|
+
|
|
41
|
+
normalized = normalize(record)
|
|
42
|
+
record.destroy!
|
|
43
|
+
normalized
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_definition(key)
|
|
47
|
+
normalize(@model.find_by(key: key))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all_definitions
|
|
51
|
+
@model.order(:key).map { |record| normalize(record) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def enabled_definitions
|
|
55
|
+
@model.where(enabled: true).order(:key).map { |record| normalize(record) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize(record)
|
|
61
|
+
return nil unless record
|
|
62
|
+
|
|
63
|
+
normalize_definition_record(record)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_definition_record(record)
|
|
67
|
+
{
|
|
68
|
+
key: record.key,
|
|
69
|
+
cron: record.cron,
|
|
70
|
+
enabled: record.enabled ? true : false,
|
|
71
|
+
source: record.source,
|
|
72
|
+
metadata: Kaal::Definition::PersistenceHelpers.parse_metadata(record.metadata),
|
|
73
|
+
created_at: record.created_at,
|
|
74
|
+
updated_at: record.updated_at,
|
|
75
|
+
disabled_at: record.disabled_at
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -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 dispatch audit entries.
|
|
11
|
+
class DispatchRecord < BaseRecord
|
|
12
|
+
self.table_name = 'kaal_dispatches'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
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/dispatch/registry'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Internal
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
# Active Record-backed registry for dispatch audit records.
|
|
13
|
+
class DispatchRegistry < Kaal::Dispatch::Registry
|
|
14
|
+
def initialize(connection: nil, model: DispatchRecord, namespace: nil)
|
|
15
|
+
super()
|
|
16
|
+
ConnectionSupport.configure!(connection)
|
|
17
|
+
@model = model
|
|
18
|
+
@namespace = namespace
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log_dispatch(key, fire_time, node_id, status = 'dispatched')
|
|
22
|
+
record = @model.find_or_initialize_by(key: namespaced_key(key), fire_time: fire_time)
|
|
23
|
+
record.dispatched_at = Time.now.utc
|
|
24
|
+
record.node_id = node_id
|
|
25
|
+
record.status = status
|
|
26
|
+
record.save!
|
|
27
|
+
normalize(record)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_dispatch(key, fire_time)
|
|
31
|
+
normalize(@model.find_by(key: namespaced_key(key), fire_time: fire_time))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_by_key(key)
|
|
35
|
+
query(key: namespaced_key(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def find_by_node(node_id)
|
|
39
|
+
query(node_id: node_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_by_status(status)
|
|
43
|
+
query(status: status)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cleanup(recovery_window: 86_400)
|
|
47
|
+
cutoff_time = Time.now.utc - recovery_window
|
|
48
|
+
cleanup_scope.where(fire_time: ...cutoff_time).delete_all
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def query(filters)
|
|
54
|
+
query_scope(filters).order(fire_time: :desc).map { |record| normalize(record) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def namespaced_key(key)
|
|
58
|
+
"#{namespace_prefix}#{key}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def normalize(record)
|
|
62
|
+
return nil unless record
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
key: strip_namespace(record.key),
|
|
66
|
+
fire_time: record.fire_time,
|
|
67
|
+
dispatched_at: record.dispatched_at,
|
|
68
|
+
node_id: record.node_id,
|
|
69
|
+
status: record.status
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def strip_namespace(key)
|
|
74
|
+
key.delete_prefix(namespace_prefix)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def query_scope(filters)
|
|
78
|
+
relation = @model.where(filters)
|
|
79
|
+
return relation if filters.key?(:key)
|
|
80
|
+
|
|
81
|
+
namespace_scope(relation)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cleanup_scope
|
|
85
|
+
namespace_scope(@model)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def namespace_scope(relation)
|
|
89
|
+
return relation if namespace_prefix.empty?
|
|
90
|
+
|
|
91
|
+
relation.where('key LIKE ?', "#{namespace_prefix}%")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def namespace_prefix
|
|
95
|
+
@namespace.to_s.empty? ? '' : "#{@namespace}:"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -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 table-backed scheduler locks.
|
|
11
|
+
class LockRecord < BaseRecord
|
|
12
|
+
self.table_name = 'kaal_locks'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
# Rails migration templates for Active Record-backed Kaal tables.
|
|
11
|
+
module MigrationTemplates
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def for_backend(backend)
|
|
15
|
+
case backend.to_s
|
|
16
|
+
when 'sqlite'
|
|
17
|
+
{
|
|
18
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
19
|
+
'002_create_kaal_locks.rb' => locks_template,
|
|
20
|
+
'003_create_kaal_definitions.rb' => definitions_template('sqlite')
|
|
21
|
+
}
|
|
22
|
+
when 'postgres'
|
|
23
|
+
{
|
|
24
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
25
|
+
'002_create_kaal_definitions.rb' => definitions_template('postgres')
|
|
26
|
+
}
|
|
27
|
+
when 'mysql'
|
|
28
|
+
{
|
|
29
|
+
'001_create_kaal_dispatches.rb' => dispatches_template,
|
|
30
|
+
'002_create_kaal_definitions.rb' => definitions_template('mysql')
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dispatches_template
|
|
38
|
+
<<~RUBY
|
|
39
|
+
class CreateKaalDispatches < ActiveRecord::Migration[7.1]
|
|
40
|
+
def change
|
|
41
|
+
create_table :kaal_dispatches do |t|
|
|
42
|
+
t.string :key, null: false
|
|
43
|
+
t.datetime :fire_time, null: false
|
|
44
|
+
t.datetime :dispatched_at, null: false
|
|
45
|
+
t.string :node_id, null: false
|
|
46
|
+
t.string :status, null: false, default: 'dispatched', limit: 50
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
add_index :kaal_dispatches, [:key, :fire_time], unique: true
|
|
50
|
+
add_index :kaal_dispatches, :key
|
|
51
|
+
add_index :kaal_dispatches, :node_id
|
|
52
|
+
add_index :kaal_dispatches, :status
|
|
53
|
+
add_index :kaal_dispatches, :fire_time
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def locks_template
|
|
60
|
+
<<~RUBY
|
|
61
|
+
class CreateKaalLocks < ActiveRecord::Migration[7.1]
|
|
62
|
+
def change
|
|
63
|
+
create_table :kaal_locks do |t|
|
|
64
|
+
t.string :key, null: false
|
|
65
|
+
t.datetime :acquired_at, null: false
|
|
66
|
+
t.datetime :expires_at, null: false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
add_index :kaal_locks, :key, unique: true
|
|
70
|
+
add_index :kaal_locks, :expires_at
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def definitions_template(backend)
|
|
77
|
+
metadata_definition =
|
|
78
|
+
if backend == 'mysql'
|
|
79
|
+
't.text :metadata, null: false'
|
|
80
|
+
else
|
|
81
|
+
"t.text :metadata, null: false, default: '{}'"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
<<~RUBY
|
|
85
|
+
class CreateKaalDefinitions < ActiveRecord::Migration[7.1]
|
|
86
|
+
def change
|
|
87
|
+
create_table :kaal_definitions do |t|
|
|
88
|
+
t.string :key, null: false
|
|
89
|
+
t.string :cron, null: false
|
|
90
|
+
t.boolean :enabled, null: false, default: true
|
|
91
|
+
t.string :source, null: false
|
|
92
|
+
#{metadata_definition}
|
|
93
|
+
t.datetime :disabled_at
|
|
94
|
+
t.datetime :created_at, null: false
|
|
95
|
+
t.datetime :updated_at, null: false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
add_index :kaal_definitions, :key, unique: true
|
|
99
|
+
add_index :kaal_definitions, :enabled
|
|
100
|
+
add_index :kaal_definitions, :source
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
RUBY
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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 'digest'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Internal
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
# MySQL named-lock engine paired with Active Record registries.
|
|
13
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
14
|
+
include Kaal::Backend::DispatchLogging
|
|
15
|
+
|
|
16
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
17
|
+
|
|
18
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil)
|
|
19
|
+
super()
|
|
20
|
+
ConnectionSupport.configure!(connection)
|
|
21
|
+
@dispatch_registry = dispatch_registry
|
|
22
|
+
@definition_registry = definition_registry
|
|
23
|
+
@namespace = namespace
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dispatch_registry
|
|
27
|
+
@dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def definition_registry
|
|
31
|
+
@definition_registry ||= DefinitionRegistry.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def acquire(key, _ttl)
|
|
35
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
|
+
log_dispatch_attempt(key) if acquired
|
|
37
|
+
acquired
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def release(key)
|
|
43
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.normalize_lock_name(key)
|
|
49
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
50
|
+
|
|
51
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
52
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
53
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scalar(sql, value)
|
|
59
|
+
result = BaseRecord.connection.exec_query(
|
|
60
|
+
BaseRecord.send(:sanitize_sql_array, [sql, value])
|
|
61
|
+
)
|
|
62
|
+
result.first.values.first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolved_namespace
|
|
66
|
+
@namespace || Kaal.configuration.namespace
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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 'digest'
|
|
8
|
+
|
|
9
|
+
module Kaal
|
|
10
|
+
module Internal
|
|
11
|
+
module ActiveRecord
|
|
12
|
+
# PostgreSQL advisory-lock engine paired with Active Record registries.
|
|
13
|
+
class PostgresBackend < Kaal::Backend::Adapter
|
|
14
|
+
include Kaal::Backend::DispatchLogging
|
|
15
|
+
|
|
16
|
+
SIGNED_64_MAX = 9_223_372_036_854_775_807
|
|
17
|
+
UNSIGNED_64_RANGE = 18_446_744_073_709_551_616
|
|
18
|
+
|
|
19
|
+
def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil)
|
|
20
|
+
super()
|
|
21
|
+
ConnectionSupport.configure!(connection)
|
|
22
|
+
@dispatch_registry = dispatch_registry
|
|
23
|
+
@definition_registry = definition_registry
|
|
24
|
+
@namespace = namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch_registry
|
|
28
|
+
@dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def definition_registry
|
|
32
|
+
@definition_registry ||= DefinitionRegistry.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def acquire(key, _ttl)
|
|
36
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
|
+
log_dispatch_attempt(key) if acquired
|
|
38
|
+
acquired
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def release(key)
|
|
44
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.calculate_lock_id(key)
|
|
50
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
51
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def scalar(sql, value)
|
|
57
|
+
result = BaseRecord.connection.exec_query(
|
|
58
|
+
BaseRecord.send(:sanitize_sql_array, [sql, value])
|
|
59
|
+
)
|
|
60
|
+
result.first.values.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolved_namespace
|
|
64
|
+
@namespace || Kaal.configuration.namespace
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
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/internal/active_record/base_record'
|
|
8
|
+
require 'kaal/internal/active_record/connection_support'
|
|
9
|
+
require 'kaal/internal/active_record/definition_record'
|
|
10
|
+
require 'kaal/internal/active_record/dispatch_record'
|
|
11
|
+
require 'kaal/internal/active_record/lock_record'
|
|
12
|
+
require 'kaal/internal/active_record/definition_registry'
|
|
13
|
+
require 'kaal/internal/active_record/dispatch_registry'
|
|
14
|
+
require 'kaal/internal/active_record/database_backend'
|
|
15
|
+
require 'kaal/internal/active_record/postgres_backend'
|
|
16
|
+
require 'kaal/internal/active_record/mysql_backend'
|
|
17
|
+
require 'kaal/internal/active_record/migration_templates'
|
|
@@ -0,0 +1,74 @@
|
|
|
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/backend/dispatch_logging'
|
|
8
|
+
require 'kaal/persistence/database'
|
|
9
|
+
|
|
10
|
+
module Kaal
|
|
11
|
+
module Internal
|
|
12
|
+
module Sequel
|
|
13
|
+
# Shared table-backed Sequel SQL engine used by the public SQLite backend.
|
|
14
|
+
class DatabaseBackend < Kaal::Backend::Adapter
|
|
15
|
+
include Kaal::Backend::DispatchLogging
|
|
16
|
+
|
|
17
|
+
def initialize(database, namespace: nil)
|
|
18
|
+
super()
|
|
19
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
20
|
+
@namespace = namespace
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dispatch_registry
|
|
24
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def definition_registry
|
|
28
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def acquire(key, ttl)
|
|
32
|
+
now = Time.now.utc
|
|
33
|
+
expires_at = now + ttl
|
|
34
|
+
|
|
35
|
+
2.times do |attempt|
|
|
36
|
+
cleanup_expired_locks if attempt.positive?
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
dataset.insert(key: key, acquired_at: now, expires_at: expires_at)
|
|
40
|
+
log_dispatch_attempt(key)
|
|
41
|
+
return true
|
|
42
|
+
rescue ::Sequel::UniqueConstraintViolation
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
false
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def release(key)
|
|
53
|
+
dataset.where(key: key).delete.positive?
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cleanup_expired_locks
|
|
59
|
+
dataset.where { expires_at < Time.now.utc }.delete
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def dataset
|
|
65
|
+
@database.locks_dataset
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolved_namespace
|
|
69
|
+
@namespace || Kaal.configuration.namespace
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
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 'digest'
|
|
8
|
+
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/persistence/database'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module Sequel
|
|
14
|
+
# MySQL named-lock engine backed by Sequel.
|
|
15
|
+
class MySQLBackend < Kaal::Backend::Adapter
|
|
16
|
+
include Kaal::Backend::DispatchLogging
|
|
17
|
+
|
|
18
|
+
MAX_LOCK_NAME_LENGTH = 64
|
|
19
|
+
|
|
20
|
+
def initialize(database, namespace: nil)
|
|
21
|
+
super()
|
|
22
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
23
|
+
@namespace = namespace
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dispatch_registry
|
|
27
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def definition_registry
|
|
31
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def acquire(key, _ttl)
|
|
35
|
+
acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
36
|
+
log_dispatch_attempt(key) if acquired
|
|
37
|
+
acquired
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def release(key)
|
|
43
|
+
scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.normalize_lock_name(key)
|
|
49
|
+
return key if key.length <= MAX_LOCK_NAME_LENGTH
|
|
50
|
+
|
|
51
|
+
digest = Digest::SHA256.hexdigest(key)
|
|
52
|
+
prefix_length = MAX_LOCK_NAME_LENGTH - 17
|
|
53
|
+
"#{key[0...prefix_length]}:#{digest[0...16]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def scalar(sql, *binds)
|
|
59
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
60
|
+
row.values.first
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolved_namespace
|
|
64
|
+
@namespace || Kaal.configuration.namespace
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
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 'digest'
|
|
8
|
+
require 'kaal/backend/dispatch_logging'
|
|
9
|
+
require 'kaal/persistence/database'
|
|
10
|
+
|
|
11
|
+
module Kaal
|
|
12
|
+
module Internal
|
|
13
|
+
module Sequel
|
|
14
|
+
# PostgreSQL advisory-lock engine backed by Sequel.
|
|
15
|
+
class PostgresBackend < Kaal::Backend::Adapter
|
|
16
|
+
include Kaal::Backend::DispatchLogging
|
|
17
|
+
|
|
18
|
+
SIGNED_64_MAX = 9_223_372_036_854_775_807
|
|
19
|
+
UNSIGNED_64_RANGE = 18_446_744_073_709_551_616
|
|
20
|
+
|
|
21
|
+
def initialize(database, namespace: nil)
|
|
22
|
+
super()
|
|
23
|
+
@database = Kaal::Persistence::Database.new(database)
|
|
24
|
+
@namespace = namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def dispatch_registry
|
|
28
|
+
@dispatch_registry ||= Kaal::Dispatch::DatabaseEngine.new(database: @database.connection, namespace: resolved_namespace)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def definition_registry
|
|
32
|
+
@definition_registry ||= Kaal::Definition::DatabaseEngine.new(database: @database.connection)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def acquire(key, _ttl)
|
|
36
|
+
acquired = scalar('SELECT pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
|
|
37
|
+
log_dispatch_attempt(key) if acquired
|
|
38
|
+
acquired
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def release(key)
|
|
44
|
+
scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.calculate_lock_id(key)
|
|
50
|
+
hash = Digest::MD5.digest(key).unpack1('Q>')
|
|
51
|
+
hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def scalar(sql, *binds)
|
|
57
|
+
row = @database.connection.fetch(sql, *binds).first
|
|
58
|
+
row.values.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolved_namespace
|
|
62
|
+
@namespace || Kaal.configuration.namespace
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|