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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54a0eb1cebfc4adc18c4b4ed105d47bb26a84d3512fbce891243a356bca5715a
4
- data.tar.gz: dd1fb18c3060c4688ea0f96c5628cd40001fb318b7745d411c46108fa06b45b3
3
+ metadata.gz: f078489fc2106826b98a1890f15dd5aad38a627ef9db230bd11db3785dc66f86
4
+ data.tar.gz: 10ec39fb62e2082d7a8191153af335950fb14d584c0ed33737298baaa2b2f270
5
5
  SHA512:
6
- metadata.gz: fda7173306897889b750707be46778bfc0b4893ce87759f30f060a37879212eab2731316a31c875acdcf0e7aad9db1caddbc11e7c0b0b16e3707edbfd1b1eec3
7
- data.tar.gz: a8d64f8ca728665167e8be7ad5e1e6a417066f6016ff2b838aa47cde5aa3369045b1ea2c3aaf326ce4b00cc94d0f519ebe464978660ad13cc29f6db90cf3c834
6
+ metadata.gz: 5be3591420c49e0149f58e6d76abdb3bfc7a7c10f2249754fb427f89e9ce414bad044d44fc17ed8016b4766182558a2a513598dcc212a0d899872f31007da19b
7
+ data.tar.gz: 60857c850fe9ca808fd198bcd8480ee63962bc1df5de69bab5953168aa7641f1a739c74d970c67b20a7f71d1858abe2a043ac004191b473a94aee7380cf8ce50
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Distributed cron scheduling for plain Ruby.
4
4
 
5
- `kaal` is the core engine gem. It owns scheduler/runtime behavior, the registry APIs, and the plain Ruby CLI. SQL persistence lives in adapter gems such as `kaal-sequel` and `kaal-activerecord`.
5
+ `kaal` is the core engine gem. It owns scheduler/runtime behavior, the registry APIs, the plain Ruby CLI, and the optional SQL backend surfaces.
6
6
 
7
7
  ## Installation
8
8
 
@@ -29,11 +29,7 @@ Supported backends:
29
29
  - `memory`
30
30
  - `redis`
31
31
 
32
- If you want SQL persistence instead, add one of:
33
-
34
- - `kaal-sequel` for Sequel-backed SQL in plain Ruby
35
- - `kaal-activerecord` for Active Record-backed SQL in plain Ruby
36
- - `kaal-rails` for Rails
32
+ If you want SQL persistence instead, add the runtime libraries your app uses, such as `sequel`, `activerecord`, `sqlite3`, `pg`, or `mysql2`, then configure one of the explicit `Kaal::Backend::*` SQL backends.
37
33
 
38
34
  ## Configuration
39
35
 
@@ -126,10 +122,11 @@ Kaal.register(
126
122
  Kaal.start!
127
123
  ```
128
124
 
129
- ## Adapter Gems
125
+ ## SQL Backends
130
126
 
131
- Use adapter gems when you want persisted SQL registries:
127
+ Use the explicit SQL backends when you want persisted registries:
132
128
 
133
- - `kaal-sequel` for Sequel-backed persistence
134
- - `kaal-activerecord` for Active Record-backed persistence
135
- - `kaal-rails` for Rails plugin integration over `kaal-activerecord`
129
+ - `Kaal::Backend::SQLite`
130
+ - `Kaal::Backend::Postgres`
131
+ - `Kaal::Backend::MySQL`
132
+ - `kaal-rails` for Rails-native install and auto-wiring
@@ -0,0 +1,82 @@
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 'fileutils'
8
+
9
+ module Kaal
10
+ # Active Record migration/install support for SQL-backed Kaal backends.
11
+ module ActiveRecord
12
+ module_function
13
+
14
+ def install_postgres_migration(target_dir:, migration_name: 'Create Kaal Postgres Backend')
15
+ install_migrations(target_dir:, backend: 'postgres', migration_name:)
16
+ end
17
+
18
+ def install_mysql_migration(target_dir:, migration_name: 'Create Kaal MySQL Backend')
19
+ install_migrations(target_dir:, backend: 'mysql', migration_name:)
20
+ end
21
+
22
+ def install_sqlite_migration(target_dir:, migration_name: 'Create Kaal SQLite Backend')
23
+ install_migrations(target_dir:, backend: 'sqlite', migration_name:)
24
+ end
25
+
26
+ def install_migrations(target_dir:, backend:, migration_name: nil, time_source: -> { Time.now.utc })
27
+ class_name = normalize_migration_name(migration_name, fallback: default_migration_class_for(backend))
28
+ base_path = File.expand_path(target_dir)
29
+ FileUtils.mkdir_p(base_path)
30
+ templates = Kaal::Internal::ActiveRecord::MigrationTemplates.for_backend(backend)
31
+
32
+ templates.map.with_index do |(_name, contents), index|
33
+ suffix = underscore(class_name)
34
+ suffix = "#{suffix}_#{migration_suffixes_for(backend).fetch(index)}" if templates.length > 1
35
+ path = File.expand_path("#{(time_source.call + index).strftime('%Y%m%d%H%M%S')}_#{suffix}.rb", base_path)
36
+ File.write(path, contents)
37
+ path
38
+ end
39
+ end
40
+
41
+ def require_activerecord!
42
+ require 'active_record'
43
+ require 'active_support/inflector'
44
+ rescue LoadError => e
45
+ raise LoadError,
46
+ "#{e.message}. Add `gem 'activerecord'` to your Gemfile to use Active Record-backed Kaal SQL support.",
47
+ cause: e
48
+ end
49
+
50
+ def normalize_migration_name(name, fallback:)
51
+ normalized = name.to_s.each_char.with_object(+'') do |char, buffer|
52
+ if alphanumeric?(char)
53
+ buffer << char
54
+ elsif !buffer.empty? && !buffer.end_with?(' ')
55
+ buffer << ' '
56
+ end
57
+ end.split.map!(&:capitalize).join
58
+ normalized.empty? ? fallback : normalized
59
+ end
60
+
61
+ def underscore(value)
62
+ require_activerecord!
63
+ ::ActiveSupport::Inflector.underscore(value)
64
+ end
65
+
66
+ def default_migration_class_for(backend)
67
+ "CreateKaal#{backend.capitalize}Backend"
68
+ end
69
+
70
+ def migration_suffixes_for(backend)
71
+ return %w[dispatches locks definitions] if backend.to_s == 'sqlite'
72
+
73
+ %w[dispatches definitions]
74
+ end
75
+
76
+ def alphanumeric?(char)
77
+ char.between?('a', 'z') ||
78
+ char.between?('A', 'Z') ||
79
+ char.between?('0', '9')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,41 @@
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 Backend
9
+ # MySQL-backed backend for either Sequel or Active Record persistence.
10
+ class MySQL < Adapter
11
+ def initialize(database: nil, connection: nil, namespace: nil, **)
12
+ super()
13
+ @engine = if database
14
+ Kaal::Sequel.require_sequel!
15
+ require 'kaal/internal/sequel'
16
+ Kaal::Internal::Sequel::MySQLBackend.new(database, namespace:)
17
+ else
18
+ Kaal::ActiveRecord.require_activerecord!
19
+ require 'kaal/internal/active_record'
20
+ Kaal::Internal::ActiveRecord::MySQLBackend.new(connection, namespace:, **)
21
+ end
22
+ end
23
+
24
+ def dispatch_registry
25
+ @engine.dispatch_registry
26
+ end
27
+
28
+ def definition_registry
29
+ @engine.definition_registry
30
+ end
31
+
32
+ def acquire(key, ttl)
33
+ @engine.acquire(key, ttl)
34
+ end
35
+
36
+ def release(key)
37
+ @engine.release(key)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
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 Backend
9
+ # PostgreSQL-backed backend for either Sequel or Active Record persistence.
10
+ class Postgres < Adapter
11
+ def initialize(database: nil, connection: nil, namespace: nil, **)
12
+ super()
13
+ @engine = if database
14
+ Kaal::Sequel.require_sequel!
15
+ require 'kaal/internal/sequel'
16
+ Kaal::Internal::Sequel::PostgresBackend.new(database, namespace:)
17
+ else
18
+ Kaal::ActiveRecord.require_activerecord!
19
+ require 'kaal/internal/active_record'
20
+ Kaal::Internal::ActiveRecord::PostgresBackend.new(connection, namespace:, **)
21
+ end
22
+ end
23
+
24
+ def dispatch_registry
25
+ @engine.dispatch_registry
26
+ end
27
+
28
+ def definition_registry
29
+ @engine.definition_registry
30
+ end
31
+
32
+ def acquire(key, ttl)
33
+ @engine.acquire(key, ttl)
34
+ end
35
+
36
+ def release(key)
37
+ @engine.release(key)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
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 Backend
9
+ # SQLite-backed backend for either Sequel or Active Record persistence.
10
+ class SQLite < Adapter
11
+ def initialize(database: nil, connection: nil, namespace: nil, **)
12
+ super()
13
+ @engine = if database
14
+ Kaal::Sequel.require_sequel!
15
+ require 'kaal/internal/sequel'
16
+ Kaal::Internal::Sequel::DatabaseBackend.new(database, namespace:)
17
+ else
18
+ Kaal::ActiveRecord.require_activerecord!
19
+ require 'kaal/internal/active_record'
20
+ Kaal::Internal::ActiveRecord::DatabaseBackend.new(connection, namespace:, **)
21
+ end
22
+ end
23
+
24
+ def dispatch_registry
25
+ @engine.dispatch_registry
26
+ end
27
+
28
+ def definition_registry
29
+ @engine.definition_registry
30
+ end
31
+
32
+ def acquire(key, ttl)
33
+ @engine.acquire(key, ttl)
34
+ end
35
+
36
+ def release(key)
37
+ @engine.release(key)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,88 @@
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
+ require 'kaal/persistence/database'
11
+
12
+ module Kaal
13
+ module Definition
14
+ # Sequel-backed definition registry persisted in kaal_definitions.
15
+ class DatabaseEngine < Registry
16
+ def initialize(database:)
17
+ super()
18
+ @database = Kaal::Persistence::Database.new(database)
19
+ end
20
+
21
+ def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
22
+ rows = dataset.where(key: key)
23
+ existing = rows.first
24
+ now = Time.now.utc
25
+ payload = {
26
+ key: key,
27
+ cron: cron,
28
+ enabled: enabled,
29
+ source: source,
30
+ metadata: JSON.generate(metadata || {}),
31
+ created_at: existing ? existing[:created_at] : now,
32
+ updated_at: now,
33
+ disabled_at: PersistenceHelpers.disabled_at_for(existing, enabled, now)
34
+ }
35
+
36
+ if existing
37
+ rows.update(payload)
38
+ else
39
+ dataset.insert(payload)
40
+ end
41
+
42
+ find_definition(key)
43
+ end
44
+
45
+ def remove_definition(key)
46
+ rows = dataset.where(key: key)
47
+ row = rows.first
48
+ return nil unless row
49
+
50
+ rows.delete
51
+ self.class.normalize_row(row)
52
+ end
53
+
54
+ def find_definition(key)
55
+ self.class.normalize_row(dataset.where(key: key).first)
56
+ end
57
+
58
+ def all_definitions
59
+ dataset.order(:key).all.map { |row| self.class.normalize_row(row) }
60
+ end
61
+
62
+ def enabled_definitions
63
+ dataset.where(enabled: true).order(:key).all.map { |row| self.class.normalize_row(row) }
64
+ end
65
+
66
+ def self.normalize_row(row)
67
+ return nil unless row
68
+
69
+ {
70
+ key: row[:key],
71
+ cron: row[:cron],
72
+ enabled: row[:enabled] ? true : false,
73
+ source: row[:source],
74
+ metadata: PersistenceHelpers.parse_metadata(row[:metadata]),
75
+ created_at: row[:created_at],
76
+ updated_at: row[:updated_at],
77
+ disabled_at: row[:disabled_at]
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ def dataset
84
+ @database.definitions_dataset
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,120 @@
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
+ require 'kaal/persistence/database'
9
+
10
+ module Kaal
11
+ module Dispatch
12
+ # Sequel-backed dispatch registry stored in kaal_dispatches.
13
+ class DatabaseEngine < Registry
14
+ def initialize(database:, namespace: nil)
15
+ super()
16
+ @database = Kaal::Persistence::Database.new(database)
17
+ @namespace = namespace
18
+ end
19
+
20
+ def log_dispatch(key, fire_time, node_id, status = 'dispatched')
21
+ now = Time.now.utc
22
+ storage_key = namespaced_key(key)
23
+ attributes = {
24
+ key: storage_key,
25
+ fire_time: fire_time,
26
+ dispatched_at: now,
27
+ node_id: node_id,
28
+ status: status
29
+ }
30
+ dispatches_dataset = dataset
31
+ update_values = { dispatched_at: now, node_id: node_id, status: status }
32
+ begin
33
+ dispatches_dataset.insert_conflict(
34
+ target: %i[key fire_time],
35
+ update: update_values
36
+ ).insert(attributes)
37
+ rescue NoMethodError => e
38
+ raise unless e.name == :insert_conflict
39
+
40
+ begin
41
+ dispatches_dataset.insert(attributes)
42
+ rescue ::Sequel::UniqueConstraintViolation
43
+ dispatches_dataset.where(key: storage_key, fire_time: fire_time).update(update_values)
44
+ end
45
+ end
46
+
47
+ find_dispatch(key, fire_time)
48
+ end
49
+
50
+ def find_dispatch(key, fire_time)
51
+ self.class.normalize_row(dataset.where(key: namespaced_key(key), fire_time: fire_time).first, namespace: @namespace)
52
+ end
53
+
54
+ def find_by_key(key)
55
+ query(key: namespaced_key(key))
56
+ end
57
+
58
+ def find_by_node(node_id)
59
+ query(node_id: node_id)
60
+ end
61
+
62
+ def find_by_status(status)
63
+ query(status: status)
64
+ end
65
+
66
+ def cleanup(recovery_window: 86_400)
67
+ cutoff_time = Time.now.utc - recovery_window
68
+ cleanup_dataset.where { fire_time < cutoff_time }.delete
69
+ end
70
+
71
+ def self.normalize_row(row, namespace: nil)
72
+ return nil unless row
73
+
74
+ {
75
+ key: strip_namespace(row[:key], namespace:),
76
+ fire_time: row[:fire_time],
77
+ dispatched_at: row[:dispatched_at],
78
+ node_id: row[:node_id],
79
+ status: row[:status]
80
+ }
81
+ end
82
+
83
+ def self.strip_namespace(key, namespace:)
84
+ return key if namespace.to_s.empty?
85
+
86
+ prefix = "#{namespace}:"
87
+ key.start_with?(prefix) ? key.delete_prefix(prefix) : key
88
+ end
89
+
90
+ private
91
+
92
+ def dataset
93
+ @database.dispatches_dataset
94
+ end
95
+
96
+ def namespaced_key(key)
97
+ return key if @namespace.to_s.empty?
98
+
99
+ "#{@namespace}:#{key}"
100
+ end
101
+
102
+ def query(filters)
103
+ query_dataset(filters).reverse_order(:fire_time).all.map { |row| self.class.normalize_row(row, namespace: @namespace) }
104
+ end
105
+
106
+ def query_dataset(filters)
107
+ relation = dataset.where(filters)
108
+ return relation if @namespace.to_s.empty? || filters.key?(:key)
109
+
110
+ relation.where(::Sequel.lit('key LIKE ?', "#{@namespace}:%"))
111
+ end
112
+
113
+ def cleanup_dataset
114
+ return dataset if @namespace.to_s.empty?
115
+
116
+ dataset.where(::Sequel.lit('key LIKE ?', "#{@namespace}:%"))
117
+ end
118
+ end
119
+ end
120
+ 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
+ # Shared abstract Active Record base class for Kaal tables.
11
+ class BaseRecord < ::ActiveRecord::Base
12
+ self.abstract_class = true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,96 @@
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
+ # Establishes and reuses the Active Record connection for adapter models.
11
+ module ConnectionSupport
12
+ CONFIGURE_MUTEX = Mutex.new
13
+
14
+ module_function
15
+
16
+ def configure!(connection = nil)
17
+ return BaseRecord unless connection
18
+
19
+ CONFIGURE_MUTEX.synchronize do
20
+ current_config = current_connection_config
21
+ target_config = normalize_connection_config(connection)
22
+ return BaseRecord if configs_match?(current_config, target_config) && connection_active?
23
+
24
+ BaseRecord.establish_connection(connection)
25
+ end
26
+ BaseRecord
27
+ end
28
+
29
+ def normalize_connection_config(connection)
30
+ config = extract_connection_config(connection)
31
+ return connection unless config
32
+
33
+ config.each_with_object({}) do |(key, value), normalized|
34
+ normalized_key = key.to_sym
35
+ normalized[normalized_key] = normalize_connection_value(normalized_key, value)
36
+ end
37
+ end
38
+
39
+ def current_connection_config
40
+ db_config = BaseRecord.connection_db_config
41
+ normalize_connection_config(extract_connection_config(db_config))
42
+ rescue ::ActiveRecord::ConnectionNotEstablished
43
+ nil
44
+ end
45
+
46
+ def extract_connection_config(connection)
47
+ case connection
48
+ when Hash
49
+ connection
50
+ when String
51
+ { url: connection }
52
+ else
53
+ config = connection.configuration_hash
54
+ url = begin
55
+ connection.url
56
+ rescue NoMethodError
57
+ nil
58
+ end
59
+ url ? config.merge(url: url) : config
60
+ end
61
+ rescue NoMethodError
62
+ nil
63
+ end
64
+
65
+ def normalize_connection_value(key, value)
66
+ case key
67
+ when :adapter
68
+ value.to_s.downcase
69
+ when :port
70
+ integer_like?(value) ? value.to_i : value
71
+ else
72
+ value
73
+ end
74
+ end
75
+
76
+ def integer_like?(value)
77
+ value.is_a?(Integer) || value.to_s.match?(/\A\d+\z/)
78
+ end
79
+
80
+ def configs_match?(current_config, target_config)
81
+ return true if current_config == target_config
82
+
83
+ current_url = current_config.is_a?(Hash) ? current_config[:url] : nil
84
+ target_url = target_config.is_a?(Hash) ? target_config[:url] : nil
85
+ !!(current_url && target_url && current_url == target_url)
86
+ end
87
+
88
+ def connection_active?
89
+ BaseRecord.connection.active?
90
+ rescue ::ActiveRecord::ConnectionNotEstablished, StandardError
91
+ false
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,73 @@
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/adapter'
8
+ require 'kaal/backend/dispatch_logging'
9
+
10
+ module Kaal
11
+ module Internal
12
+ module ActiveRecord
13
+ # Table-backed lock engine used for SQLite-style Active Record storage.
14
+ class DatabaseBackend < Kaal::Backend::Adapter
15
+ include Kaal::Backend::DispatchLogging
16
+
17
+ def initialize(connection = nil, lock_model: LockRecord, dispatch_registry: nil, definition_registry: nil, namespace: nil)
18
+ super()
19
+ ConnectionSupport.configure!(connection)
20
+ @lock_model = lock_model
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
+ now = Time.now.utc
36
+ expires_at = now + ttl
37
+
38
+ 2.times do |attempt|
39
+ cleanup_expired_locks if attempt.positive?
40
+
41
+ begin
42
+ @lock_model.create!(key: key, acquired_at: now, expires_at: expires_at)
43
+ log_dispatch_attempt(key)
44
+ return true
45
+ rescue ::ActiveRecord::RecordNotUnique
46
+ next
47
+ end
48
+ end
49
+
50
+ false
51
+ rescue StandardError => e
52
+ raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
53
+ end
54
+
55
+ def release(key)
56
+ @lock_model.where(key: key).delete_all.positive?
57
+ rescue StandardError => e
58
+ raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
59
+ end
60
+
61
+ def cleanup_expired_locks
62
+ @lock_model.where(expires_at: ...Time.now.utc).delete_all
63
+ end
64
+
65
+ private
66
+
67
+ def resolved_namespace
68
+ @namespace || Kaal.configuration.namespace
69
+ end
70
+ end
71
+ end
72
+ end
73
+ 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 scheduler definitions.
11
+ class DefinitionRecord < BaseRecord
12
+ self.table_name = 'kaal_definitions'
13
+ end
14
+ end
15
+ end
16
+ end