kaal-activerecord 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a7ad9284b605c193a77c6f998fc3f8d38b309075801bc25bad95f8b5d835a027
4
+ data.tar.gz: 671a2dfd4e3ea39ea43889c57258b6b0030170449bb63603d86a51c735e870fc
5
+ SHA512:
6
+ metadata.gz: e5af0962d3d26f4b0e36835744adb1c2ee206c1e7bf95520e429d8ec7952a233cdb560d456b5e9fa9fd2e32ecfe32935498d0457428165c955180441d7b52fc5
7
+ data.tar.gz: 9d94b680eb0217568aa310fa48a070288f69eac29797d4912b8d8e1157de783d805965166b7b59e7d909dd0151709b6279b14975c8a11fa8cc89ec7f0fbbdd60
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Codevedas Inc. and the Kaal Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Kaal::ActiveRecord
2
+
3
+ Active Record-backed datastore adapter for Kaal.
4
+
5
+ `kaal-activerecord` depends on `kaal` and owns the Active Record persistence layer:
6
+
7
+ - Active Record models for Kaal tables
8
+ - Active Record-backed definition registry
9
+ - Active Record-backed dispatch registry
10
+ - SQLite table-lock adapter
11
+ - PostgreSQL advisory-lock adapter
12
+ - MySQL named-lock adapter
13
+ - Rails-friendly migration templates
14
+
15
+ ## Install
16
+
17
+ Plain Ruby:
18
+
19
+ ```ruby
20
+ gem 'kaal'
21
+ gem 'kaal-activerecord'
22
+ ```
23
+
24
+ Rails applications normally use `kaal-rails`, which already depends on this gem.
25
+
26
+ ## Usage
27
+
28
+ SQLite in plain Ruby:
29
+
30
+ ```ruby
31
+ require 'kaal'
32
+ require 'kaal/active_record'
33
+
34
+ Kaal::ActiveRecord::ConnectionSupport.configure!(
35
+ adapter: 'sqlite3',
36
+ database: 'db/kaal.sqlite3'
37
+ )
38
+
39
+ Kaal.configure do |config|
40
+ config.backend = Kaal::ActiveRecord::DatabaseAdapter.new
41
+ config.scheduler_config_path = 'config/scheduler.yml'
42
+ end
43
+ ```
44
+
45
+ PostgreSQL in plain Ruby:
46
+
47
+ ```ruby
48
+ config.backend = Kaal::ActiveRecord::PostgresAdapter.new
49
+ ```
50
+
51
+ MySQL in plain Ruby:
52
+
53
+ ```ruby
54
+ config.backend = Kaal::ActiveRecord::MySQLAdapter.new
55
+ ```
56
+
57
+ Use this gem directly when you want Active Record-backed SQL outside Rails. For Rails apps, use `kaal-rails`.
58
+
59
+ ## Tables
60
+
61
+ The Active Record adapter persists against:
62
+
63
+ - `kaal_definitions`
64
+ - `kaal_dispatches`
65
+ - `kaal_locks`
66
+
67
+ `kaal_locks` is used for SQLite. PostgreSQL and MySQL rely on advisory or named locks and persist definitions and dispatches without a dedicated locks table.
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ bin/rspec-unit
73
+ bin/rspec-e2e sqlite
74
+ bin/rubocop
75
+ bin/reek
76
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
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 'bundler/setup'
8
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
9
+ load 'rails/tasks/engine.rake'
10
+ require 'bundler/gem_tasks'
@@ -0,0 +1,19 @@
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 ActiveRecord
9
+ # Shared abstract ApplicationRecord class for Kaal tables.
10
+ class ApplicationRecord < ::ActiveRecord::Base
11
+ self.abstract_class = true
12
+ end
13
+
14
+ # Shared abstract Active Record base class for Kaal tables.
15
+ class BaseRecord < ApplicationRecord
16
+ self.abstract_class = true
17
+ end
18
+ end
19
+ 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
+ module Kaal
8
+ module ActiveRecord
9
+ # Establishes and reuses the Active Record connection for adapter models.
10
+ module ConnectionSupport
11
+ CONFIGURE_MUTEX = Mutex.new
12
+
13
+ module_function
14
+
15
+ def configure!(connection = nil)
16
+ return BaseRecord unless connection
17
+
18
+ CONFIGURE_MUTEX.synchronize do
19
+ current_config = current_connection_config
20
+ target_config = normalize_connection_config(connection)
21
+ return BaseRecord if configs_match?(current_config, target_config)
22
+
23
+ BaseRecord.establish_connection(connection)
24
+ end
25
+ BaseRecord
26
+ end
27
+
28
+ def normalize_connection_config(connection)
29
+ config = extract_connection_config(connection)
30
+ return connection unless config
31
+
32
+ config.each_with_object({}) do |(key, value), normalized|
33
+ normalized_key = key.to_sym
34
+ normalized[normalized_key] = normalize_connection_value(normalized_key, value)
35
+ end
36
+ end
37
+
38
+ def current_connection_config
39
+ db_config = BaseRecord.connection_db_config
40
+ normalize_connection_config(extract_connection_config(db_config))
41
+ rescue ::ActiveRecord::ConnectionNotEstablished
42
+ nil
43
+ end
44
+
45
+ def extract_connection_config(connection)
46
+ case connection
47
+ when Hash
48
+ connection
49
+ when String
50
+ { url: connection }
51
+ else
52
+ config = connection.configuration_hash
53
+ url = begin
54
+ connection.url
55
+ rescue NoMethodError
56
+ nil
57
+ end
58
+ url ? config.merge(url: url) : config
59
+ end
60
+ rescue NoMethodError
61
+ nil
62
+ end
63
+
64
+ def normalize_connection_value(key, value)
65
+ case key
66
+ when :adapter
67
+ value.to_s.downcase
68
+ when :port
69
+ integer_like?(value) ? value.to_i : value
70
+ else
71
+ value
72
+ end
73
+ end
74
+
75
+ def integer_like?(value)
76
+ value.is_a?(Integer) || value.to_s.match?(/\A\d+\z/)
77
+ end
78
+
79
+ def configs_match?(current_config, target_config)
80
+ return true if current_config == target_config
81
+
82
+ current_url = current_config.is_a?(Hash) ? current_config[:url] : nil
83
+ target_url = target_config.is_a?(Hash) ? target_config[:url] : nil
84
+ !!(current_url && target_url && current_url == target_url)
85
+ end
86
+ end
87
+ end
88
+ 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 ActiveRecord
12
+ # Table-backed lock adapter used for SQLite-style Active Record storage.
13
+ class DatabaseAdapter < Kaal::Backend::Adapter
14
+ include Kaal::Backend::DispatchLogging
15
+
16
+ def initialize(connection = nil, lock_model: LockRecord, dispatch_registry: nil, definition_registry: nil, namespace: nil)
17
+ super()
18
+ ConnectionSupport.configure!(connection)
19
+ @lock_model = lock_model
20
+ @dispatch_registry = dispatch_registry
21
+ @definition_registry = definition_registry
22
+ @namespace = namespace
23
+ end
24
+
25
+ def dispatch_registry
26
+ @dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
27
+ end
28
+
29
+ def definition_registry
30
+ @definition_registry ||= DefinitionRegistry.new
31
+ end
32
+
33
+ def acquire(key, ttl)
34
+ now = Time.now.utc
35
+ expires_at = now + ttl
36
+
37
+ 2.times do |attempt|
38
+ cleanup_expired_locks if attempt.positive?
39
+
40
+ begin
41
+ @lock_model.create!(key: key, acquired_at: now, expires_at: expires_at)
42
+ log_dispatch_attempt(key)
43
+ return true
44
+ rescue ::ActiveRecord::RecordNotUnique
45
+ next
46
+ end
47
+ end
48
+
49
+ false
50
+ rescue StandardError => e
51
+ raise Kaal::Backend::LockAdapterError, "Database acquire failed for #{key}: #{e.message}"
52
+ end
53
+
54
+ def release(key)
55
+ @lock_model.where(key: key).delete_all.positive?
56
+ rescue StandardError => e
57
+ raise Kaal::Backend::LockAdapterError, "Database release failed for #{key}: #{e.message}"
58
+ end
59
+
60
+ def cleanup_expired_locks
61
+ @lock_model.where(expires_at: ...Time.now.utc).delete_all
62
+ end
63
+
64
+ private
65
+
66
+ def resolved_namespace
67
+ @namespace || Kaal.configuration.namespace
68
+ end
69
+ end
70
+
71
+ SQLiteAdapter = DatabaseAdapter
72
+ end
73
+ end
@@ -0,0 +1,14 @@
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 ActiveRecord
9
+ # Active Record model for persisted scheduler definitions.
10
+ class DefinitionRecord < BaseRecord
11
+ self.table_name = 'kaal_definitions'
12
+ end
13
+ end
14
+ end
@@ -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 'json'
8
+ require 'kaal/definition/registry'
9
+ require 'kaal/definition/persistence_helpers'
10
+
11
+ module Kaal
12
+ module ActiveRecord
13
+ # Active Record-backed registry for scheduler definitions.
14
+ class DefinitionRegistry < Kaal::Definition::Registry
15
+ def initialize(connection: nil, model: DefinitionRecord)
16
+ super()
17
+ ConnectionSupport.configure!(connection)
18
+ @model = model
19
+ end
20
+
21
+ def upsert_definition(key:, cron:, enabled: true, source: 'code', metadata: {})
22
+ record = @model.find_or_initialize_by(key: key)
23
+ existing = record.persisted? ? { enabled: record.enabled, disabled_at: record.disabled_at } : nil
24
+ now = Time.now.utc
25
+ record.cron = cron
26
+ record.enabled = enabled
27
+ record.source = source
28
+ record.metadata = JSON.generate(metadata || {})
29
+ record.created_at ||= now
30
+ record.updated_at = now
31
+ record.disabled_at = Kaal::Definition::PersistenceHelpers.disabled_at_for(existing, enabled, now)
32
+ record.save!
33
+ normalize(record)
34
+ end
35
+
36
+ def remove_definition(key)
37
+ record = @model.find_by(key: key)
38
+ return nil unless record
39
+
40
+ normalized = normalize(record)
41
+ record.destroy!
42
+ normalized
43
+ end
44
+
45
+ def find_definition(key)
46
+ normalize(@model.find_by(key: key))
47
+ end
48
+
49
+ def all_definitions
50
+ @model.order(:key).map { |record| normalize(record) }
51
+ end
52
+
53
+ def enabled_definitions
54
+ @model.where(enabled: true).order(:key).map { |record| normalize(record) }
55
+ end
56
+
57
+ private
58
+
59
+ def normalize(record)
60
+ return nil unless record
61
+
62
+ normalize_definition_record(record)
63
+ end
64
+
65
+ def normalize_definition_record(record)
66
+ {
67
+ key: record.key,
68
+ cron: record.cron,
69
+ enabled: record.enabled ? true : false,
70
+ source: record.source,
71
+ metadata: Kaal::Definition::PersistenceHelpers.parse_metadata(record.metadata),
72
+ created_at: record.created_at,
73
+ updated_at: record.updated_at,
74
+ disabled_at: record.disabled_at
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,14 @@
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 ActiveRecord
9
+ # Active Record model for persisted dispatch audit entries.
10
+ class DispatchRecord < BaseRecord
11
+ self.table_name = 'kaal_dispatches'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,98 @@
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 ActiveRecord
11
+ # Active Record-backed registry for dispatch audit records.
12
+ class DispatchRegistry < Kaal::Dispatch::Registry
13
+ def initialize(connection: nil, model: DispatchRecord, namespace: nil)
14
+ super()
15
+ ConnectionSupport.configure!(connection)
16
+ @model = model
17
+ @namespace = namespace
18
+ end
19
+
20
+ def log_dispatch(key, fire_time, node_id, status = 'dispatched')
21
+ record = @model.find_or_initialize_by(key: namespaced_key(key), fire_time: fire_time)
22
+ record.dispatched_at = Time.now.utc
23
+ record.node_id = node_id
24
+ record.status = status
25
+ record.save!
26
+ normalize(record)
27
+ end
28
+
29
+ def find_dispatch(key, fire_time)
30
+ normalize(@model.find_by(key: namespaced_key(key), fire_time: fire_time))
31
+ end
32
+
33
+ def find_by_key(key)
34
+ query(key: namespaced_key(key))
35
+ end
36
+
37
+ def find_by_node(node_id)
38
+ query(node_id: node_id)
39
+ end
40
+
41
+ def find_by_status(status)
42
+ query(status: status)
43
+ end
44
+
45
+ def cleanup(recovery_window: 86_400)
46
+ cutoff_time = Time.now.utc - recovery_window
47
+ cleanup_scope.where(fire_time: ...cutoff_time).delete_all
48
+ end
49
+
50
+ private
51
+
52
+ def query(filters)
53
+ query_scope(filters).order(fire_time: :desc).map { |record| normalize(record) }
54
+ end
55
+
56
+ def namespaced_key(key)
57
+ "#{namespace_prefix}#{key}"
58
+ end
59
+
60
+ def normalize(record)
61
+ return nil unless record
62
+
63
+ {
64
+ key: strip_namespace(record.key),
65
+ fire_time: record.fire_time,
66
+ dispatched_at: record.dispatched_at,
67
+ node_id: record.node_id,
68
+ status: record.status
69
+ }
70
+ end
71
+
72
+ def strip_namespace(key)
73
+ key.delete_prefix(namespace_prefix)
74
+ end
75
+
76
+ def query_scope(filters)
77
+ relation = @model.where(filters)
78
+ return relation if filters.key?(:key)
79
+
80
+ namespace_scope(relation)
81
+ end
82
+
83
+ def cleanup_scope
84
+ namespace_scope(@model)
85
+ end
86
+
87
+ def namespace_scope(relation)
88
+ return relation if namespace_prefix.empty?
89
+
90
+ relation.where('key LIKE ?', "#{namespace_prefix}%")
91
+ end
92
+
93
+ def namespace_prefix
94
+ @namespace.to_s.empty? ? '' : "#{@namespace}:"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,14 @@
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 ActiveRecord
9
+ # Active Record model for table-backed scheduler locks.
10
+ class LockRecord < BaseRecord
11
+ self.table_name = 'kaal_locks'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,106 @@
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 ActiveRecord
9
+ # Rails migration templates for Active Record-backed Kaal tables.
10
+ module MigrationTemplates
11
+ module_function
12
+
13
+ def for_backend(backend)
14
+ case backend.to_s
15
+ when 'sqlite'
16
+ {
17
+ '001_create_kaal_dispatches.rb' => dispatches_template,
18
+ '002_create_kaal_locks.rb' => locks_template,
19
+ '003_create_kaal_definitions.rb' => definitions_template('sqlite')
20
+ }
21
+ when 'postgres'
22
+ {
23
+ '001_create_kaal_dispatches.rb' => dispatches_template,
24
+ '002_create_kaal_definitions.rb' => definitions_template('postgres')
25
+ }
26
+ when 'mysql'
27
+ {
28
+ '001_create_kaal_dispatches.rb' => dispatches_template,
29
+ '002_create_kaal_definitions.rb' => definitions_template('mysql')
30
+ }
31
+ else
32
+ {}
33
+ end
34
+ end
35
+
36
+ def dispatches_template
37
+ <<~RUBY
38
+ class CreateKaalDispatches < ActiveRecord::Migration[7.1]
39
+ def change
40
+ create_table :kaal_dispatches do |t|
41
+ t.string :key, null: false
42
+ t.datetime :fire_time, null: false
43
+ t.datetime :dispatched_at, null: false
44
+ t.string :node_id, null: false
45
+ t.string :status, null: false, default: 'dispatched', limit: 50
46
+ end
47
+
48
+ add_index :kaal_dispatches, [:key, :fire_time], unique: true
49
+ add_index :kaal_dispatches, :key
50
+ add_index :kaal_dispatches, :node_id
51
+ add_index :kaal_dispatches, :status
52
+ add_index :kaal_dispatches, :fire_time
53
+ end
54
+ end
55
+ RUBY
56
+ end
57
+
58
+ def locks_template
59
+ <<~RUBY
60
+ class CreateKaalLocks < ActiveRecord::Migration[7.1]
61
+ def change
62
+ create_table :kaal_locks do |t|
63
+ t.string :key, null: false
64
+ t.datetime :acquired_at, null: false
65
+ t.datetime :expires_at, null: false
66
+ end
67
+
68
+ add_index :kaal_locks, :key, unique: true
69
+ add_index :kaal_locks, :expires_at
70
+ end
71
+ end
72
+ RUBY
73
+ end
74
+
75
+ def definitions_template(backend)
76
+ metadata_definition =
77
+ if backend == 'mysql'
78
+ 't.text :metadata, null: false'
79
+ else
80
+ "t.text :metadata, null: false, default: '{}'"
81
+ end
82
+
83
+ <<~RUBY
84
+ class CreateKaalDefinitions < ActiveRecord::Migration[7.1]
85
+ def change
86
+ create_table :kaal_definitions do |t|
87
+ t.string :key, null: false
88
+ t.string :cron, null: false
89
+ t.boolean :enabled, null: false, default: true
90
+ t.string :source, null: false
91
+ #{metadata_definition}
92
+ t.datetime :disabled_at
93
+ t.datetime :created_at, null: false
94
+ t.datetime :updated_at, null: false
95
+ end
96
+
97
+ add_index :kaal_definitions, :key, unique: true
98
+ add_index :kaal_definitions, :enabled
99
+ add_index :kaal_definitions, :source
100
+ end
101
+ end
102
+ RUBY
103
+ end
104
+ end
105
+ end
106
+ 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 ActiveRecord
11
+ # MySQL named-lock adapter paired with Active Record registries.
12
+ class MySQLAdapter < Kaal::Backend::Adapter
13
+ include Kaal::Backend::DispatchLogging
14
+
15
+ MAX_LOCK_NAME_LENGTH = 64
16
+
17
+ def initialize(connection = nil, dispatch_registry: nil, definition_registry: nil, namespace: nil)
18
+ super()
19
+ ConnectionSupport.configure!(connection)
20
+ @dispatch_registry = dispatch_registry
21
+ @definition_registry = definition_registry
22
+ @namespace = namespace
23
+ end
24
+
25
+ def dispatch_registry
26
+ @dispatch_registry ||= DispatchRegistry.new(namespace: resolved_namespace)
27
+ end
28
+
29
+ def definition_registry
30
+ @definition_registry ||= DefinitionRegistry.new
31
+ end
32
+
33
+ def acquire(key, _ttl)
34
+ acquired = scalar('SELECT GET_LOCK(?, 0) AS lock_result', self.class.normalize_lock_name(key)) == 1
35
+ log_dispatch_attempt(key) if acquired
36
+ acquired
37
+ rescue StandardError => e
38
+ raise Kaal::Backend::LockAdapterError, "MySQL acquire failed for #{key}: #{e.message}"
39
+ end
40
+
41
+ def release(key)
42
+ scalar('SELECT RELEASE_LOCK(?) AS lock_result', self.class.normalize_lock_name(key)) == 1
43
+ rescue StandardError => e
44
+ raise Kaal::Backend::LockAdapterError, "MySQL release failed for #{key}: #{e.message}"
45
+ end
46
+
47
+ def self.normalize_lock_name(key)
48
+ return key if key.length <= MAX_LOCK_NAME_LENGTH
49
+
50
+ digest = Digest::SHA256.hexdigest(key)
51
+ prefix_length = MAX_LOCK_NAME_LENGTH - 17
52
+ "#{key[0...prefix_length]}:#{digest[0...16]}"
53
+ end
54
+
55
+ private
56
+
57
+ def scalar(sql, value)
58
+ result = BaseRecord.connection.exec_query(
59
+ BaseRecord.send(:sanitize_sql_array, [sql, value])
60
+ )
61
+ result.first.values.first
62
+ end
63
+
64
+ def resolved_namespace
65
+ @namespace || Kaal.configuration.namespace
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
+
9
+ module Kaal
10
+ module ActiveRecord
11
+ # PostgreSQL advisory-lock adapter paired with Active Record registries.
12
+ class PostgresAdapter < Kaal::Backend::Adapter
13
+ include Kaal::Backend::DispatchLogging
14
+
15
+ SIGNED_64_MAX = 9_223_372_036_854_775_807
16
+ UNSIGNED_64_RANGE = 18_446_744_073_709_551_616
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 pg_try_advisory_lock(?) AS acquired', self.class.calculate_lock_id(key)) == true
36
+ log_dispatch_attempt(key) if acquired
37
+ acquired
38
+ rescue StandardError => e
39
+ raise Kaal::Backend::LockAdapterError, "PostgreSQL acquire failed for #{key}: #{e.message}"
40
+ end
41
+
42
+ def release(key)
43
+ scalar('SELECT pg_advisory_unlock(?) AS released', self.class.calculate_lock_id(key)) == true
44
+ rescue StandardError => e
45
+ raise Kaal::Backend::LockAdapterError, "PostgreSQL release failed for #{key}: #{e.message}"
46
+ end
47
+
48
+ def self.calculate_lock_id(key)
49
+ hash = Digest::MD5.digest(key).unpack1('Q>')
50
+ hash > SIGNED_64_MAX ? hash - UNSIGNED_64_RANGE : hash
51
+ end
52
+
53
+ private
54
+
55
+ def scalar(sql, value)
56
+ result = BaseRecord.connection.exec_query(
57
+ BaseRecord.send(:sanitize_sql_array, [sql, value])
58
+ )
59
+ result.first.values.first
60
+ end
61
+
62
+ def resolved_namespace
63
+ @namespace || Kaal.configuration.namespace
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
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 'rails/railtie'
8
+
9
+ module Kaal
10
+ module ActiveRecord
11
+ # Minimal Railtie so the adapter can integrate with Rails loading.
12
+ class Railtie < ::Rails::Railtie
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
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/active_record/database_adapter'
@@ -0,0 +1,11 @@
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 ActiveRecord
9
+ VERSION = '0.3.0'
10
+ end
11
+ end
@@ -0,0 +1,28 @@
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 'active_record'
8
+ require 'kaal'
9
+ require 'kaal/active_record/version'
10
+ require 'kaal/active_record/railtie'
11
+ require 'kaal/active_record/connection_support'
12
+ require 'kaal/active_record/base_record'
13
+ require 'kaal/active_record/definition_record'
14
+ require 'kaal/active_record/dispatch_record'
15
+ require 'kaal/active_record/lock_record'
16
+ require 'kaal/active_record/definition_registry'
17
+ require 'kaal/active_record/dispatch_registry'
18
+ require 'kaal/active_record/database_adapter'
19
+ require 'kaal/active_record/postgres_adapter'
20
+ require 'kaal/active_record/mysql_adapter'
21
+ require 'kaal/active_record/sqlite_adapter'
22
+ require 'kaal/active_record/migration_templates'
23
+
24
+ module Kaal
25
+ # Active Record-backed datastore adapter namespace for Kaal.
26
+ module ActiveRecord
27
+ end
28
+ end
@@ -0,0 +1,8 @@
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
+
8
+ # This file intentionally does not define any rake tasks for kaal-activerecord.
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kaal-activerecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Nitesh Purohit
8
+ - Codevedas Inc.
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: kaal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '9.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '7.1'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '9.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rails-i18n
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '7.0'
61
+ description: " Kaal-ActiveRecord provides seamless integration of Kaal with ActiveRecord,
62
+ allowing you to use ActiveRecord models for scheduling and managing cron jobs in
63
+ a distributed environment.\n"
64
+ email:
65
+ - nitesh.purohit.it@gmail.com
66
+ - team@codevedas.com
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - lib/kaal/active_record.rb
75
+ - lib/kaal/active_record/base_record.rb
76
+ - lib/kaal/active_record/connection_support.rb
77
+ - lib/kaal/active_record/database_adapter.rb
78
+ - lib/kaal/active_record/definition_record.rb
79
+ - lib/kaal/active_record/definition_registry.rb
80
+ - lib/kaal/active_record/dispatch_record.rb
81
+ - lib/kaal/active_record/dispatch_registry.rb
82
+ - lib/kaal/active_record/lock_record.rb
83
+ - lib/kaal/active_record/migration_templates.rb
84
+ - lib/kaal/active_record/mysql_adapter.rb
85
+ - lib/kaal/active_record/postgres_adapter.rb
86
+ - lib/kaal/active_record/railtie.rb
87
+ - lib/kaal/active_record/sqlite_adapter.rb
88
+ - lib/kaal/active_record/version.rb
89
+ - lib/tasks/kaal/active_record_tasks.rake
90
+ homepage: https://github.com/Code-Vedas/kaal
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ bug_tracker_uri: https://github.com/Code-Vedas/kaal/issues
95
+ changelog_uri: https://github.com/Code-Vedas/kaal/blob/main/CHANGELOG.md
96
+ documentation_uri: https://kaal.codevedas.com
97
+ homepage_uri: https://github.com/Code-Vedas/kaal
98
+ source_code_uri: https://github.com/Code-Vedas/kaal.git
99
+ funding_uri: https://github.com/sponsors/Code-Vedas
100
+ support_uri: https://kaal.codevedas.com/support
101
+ rubygems_uri: https://rubygems.org/gems/kaal-activerecord
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.6.7
118
+ specification_version: 4
119
+ summary: ActiveRecord integration for Kaal, a distributed cron scheduler for Ruby.
120
+ test_files: []