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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -11
  3. data/lib/kaal/active_record_support.rb +82 -0
  4. data/lib/kaal/backend/mysql.rb +41 -0
  5. data/lib/kaal/backend/postgres.rb +41 -0
  6. data/lib/kaal/backend/sqlite.rb +41 -0
  7. data/lib/kaal/definition/database_engine.rb +88 -0
  8. data/lib/kaal/dispatch/database_engine.rb +120 -0
  9. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  10. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  11. data/lib/kaal/internal/active_record/database_backend.rb +73 -0
  12. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  13. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  14. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  15. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  16. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  17. data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
  18. data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
  19. data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
  20. data/lib/kaal/internal/active_record.rb +17 -0
  21. data/lib/kaal/internal/sequel/database_backend.rb +74 -0
  22. data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
  23. data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
  24. data/lib/kaal/internal/sequel.rb +12 -0
  25. data/lib/kaal/persistence/database.rb +35 -0
  26. data/lib/kaal/persistence/migration_templates.rb +97 -0
  27. data/lib/kaal/registry.rb +0 -2
  28. data/lib/kaal/sequel_support.rb +82 -0
  29. data/lib/kaal/version.rb +1 -1
  30. data/lib/kaal.rb +6 -0
  31. 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