kaal 0.2.1 → 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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -287
  3. data/Rakefile +4 -2
  4. data/config/kaal.rb +15 -0
  5. data/config/scheduler.yml +12 -0
  6. data/{lib/tasks/kaal_tasks.rake → exe/kaal} +5 -3
  7. data/lib/kaal/active_record_support.rb +82 -0
  8. data/lib/kaal/backend/adapter.rb +0 -1
  9. data/lib/kaal/backend/dispatch_attempt_logger.rb +33 -0
  10. data/lib/kaal/backend/dispatch_logging.rb +36 -23
  11. data/lib/kaal/backend/dispatch_registry_accessor.rb +43 -0
  12. data/lib/kaal/backend/memory_adapter.rb +7 -5
  13. data/lib/kaal/backend/mysql.rb +41 -0
  14. data/lib/kaal/backend/postgres.rb +41 -0
  15. data/lib/kaal/backend/redis_adapter.rb +6 -6
  16. data/lib/kaal/backend/sqlite.rb +41 -0
  17. data/lib/kaal/cli.rb +230 -0
  18. data/lib/kaal/{configuration.rb → config/configuration.rb} +0 -1
  19. data/lib/kaal/{scheduler_config_error.rb → config/scheduler_config_error.rb} +0 -1
  20. data/lib/kaal/config/scheduler_time_zone_resolver.rb +50 -0
  21. data/lib/kaal/config.rb +19 -0
  22. data/lib/kaal/{coordinator.rb → core/coordinator.rb} +42 -62
  23. data/lib/kaal/core/enabled_entry_enumerator.rb +51 -0
  24. data/lib/kaal/core/occurrence_finder.rb +38 -0
  25. data/lib/kaal/core.rb +18 -0
  26. data/lib/kaal/definition/database_engine.rb +54 -16
  27. data/lib/kaal/definition/memory_engine.rb +11 -18
  28. data/lib/kaal/definition/persistence_helpers.rb +31 -0
  29. data/lib/kaal/definition/redis_engine.rb +9 -6
  30. data/lib/kaal/definition/registry.rb +24 -2
  31. data/lib/kaal/definitions/registration_service.rb +62 -0
  32. data/lib/kaal/definitions/registry_accessor.rb +33 -0
  33. data/lib/kaal/dispatch/database_engine.rb +87 -61
  34. data/lib/kaal/dispatch/memory_engine.rb +3 -4
  35. data/lib/kaal/dispatch/redis_engine.rb +2 -3
  36. data/lib/kaal/dispatch/registry.rb +0 -1
  37. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  38. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  39. data/lib/kaal/internal/active_record/database_backend.rb +73 -0
  40. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  41. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  42. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  43. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  44. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  45. data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
  46. data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
  47. data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
  48. data/lib/kaal/internal/active_record.rb +17 -0
  49. data/lib/kaal/internal/sequel/database_backend.rb +74 -0
  50. data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
  51. data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
  52. data/lib/kaal/internal/sequel.rb +12 -0
  53. data/lib/kaal/persistence/database.rb +35 -0
  54. data/lib/kaal/persistence/migration_templates.rb +97 -0
  55. data/lib/kaal/register_conflict_support.rb +0 -1
  56. data/lib/kaal/registry.rb +0 -3
  57. data/lib/kaal/runtime/runtime_context.rb +41 -0
  58. data/lib/kaal/runtime/scheduler_boot_loader.rb +52 -0
  59. data/lib/kaal/runtime/signal_handler_chain.rb +42 -0
  60. data/lib/kaal/runtime/signal_handler_installer.rb +39 -0
  61. data/lib/kaal/runtime.rb +20 -0
  62. data/lib/kaal/scheduler_file/hash_transform.rb +22 -0
  63. data/lib/kaal/scheduler_file/helper_bundle.rb +28 -0
  64. data/lib/kaal/scheduler_file/job_applier.rb +242 -0
  65. data/lib/kaal/scheduler_file/job_normalizer.rb +90 -0
  66. data/lib/kaal/scheduler_file/loader.rb +152 -0
  67. data/lib/kaal/scheduler_file/payload_loader.rb +95 -0
  68. data/lib/kaal/{scheduler_placeholder_support.rb → scheduler_file/placeholder_support.rb} +0 -1
  69. data/lib/kaal/scheduler_file.rb +18 -0
  70. data/lib/kaal/sequel_support.rb +82 -0
  71. data/lib/kaal/support/hash_tools.rb +93 -0
  72. data/lib/kaal/{cron_humanizer.rb → utils/cron_humanizer.rb} +19 -1
  73. data/lib/kaal/{cron_utils.rb → utils/cron_utils.rb} +0 -1
  74. data/lib/kaal/{idempotency_key_generator.rb → utils/idempotency_key_generator.rb} +3 -3
  75. data/lib/kaal/utils.rb +18 -0
  76. data/lib/kaal/version.rb +1 -2
  77. data/lib/kaal.rb +83 -397
  78. metadata +87 -42
  79. data/app/models/kaal/cron_definition.rb +0 -76
  80. data/app/models/kaal/cron_dispatch.rb +0 -50
  81. data/app/models/kaal/cron_lock.rb +0 -38
  82. data/lib/generators/kaal/install/install_generator.rb +0 -72
  83. data/lib/generators/kaal/install/templates/create_kaal_definitions.rb.tt +0 -21
  84. data/lib/generators/kaal/install/templates/create_kaal_dispatches.rb.tt +0 -20
  85. data/lib/generators/kaal/install/templates/create_kaal_locks.rb.tt +0 -17
  86. data/lib/generators/kaal/install/templates/kaal.rb.tt +0 -31
  87. data/lib/generators/kaal/install/templates/scheduler.yml.tt +0 -22
  88. data/lib/kaal/backend/mysql_adapter.rb +0 -170
  89. data/lib/kaal/backend/postgres_adapter.rb +0 -134
  90. data/lib/kaal/backend/sqlite_adapter.rb +0 -116
  91. data/lib/kaal/railtie.rb +0 -183
  92. data/lib/kaal/rake_tasks.rb +0 -184
  93. data/lib/kaal/scheduler_file_loader.rb +0 -321
  94. data/lib/kaal/scheduler_hash_transform.rb +0 -45
@@ -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
@@ -0,0 +1,12 @@
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/sequel/database_backend'
8
+ require 'kaal/internal/sequel/postgres_backend'
9
+ require 'kaal/internal/sequel/mysql_backend'
10
+ require 'kaal/definition/database_engine'
11
+ require 'kaal/dispatch/database_engine'
12
+ require 'kaal/persistence/database'
@@ -0,0 +1,35 @@
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 Persistence
9
+ # Thin wrapper around a Sequel connection to keep table access consistent.
10
+ class Database
11
+ attr_reader :connection
12
+
13
+ def initialize(connection)
14
+ Kaal::Sequel.require_sequel!
15
+ @connection = if connection.is_a?(::Sequel::Database)
16
+ connection
17
+ else
18
+ ::Sequel.connect(connection)
19
+ end
20
+ end
21
+
22
+ def definitions_dataset
23
+ connection[:kaal_definitions]
24
+ end
25
+
26
+ def dispatches_dataset
27
+ connection[:kaal_dispatches]
28
+ end
29
+
30
+ def locks_dataset
31
+ connection[:kaal_locks]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,97 @@
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 Persistence
9
+ # Sequel migration templates emitted by `kaal init`.
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
20
+ }
21
+ when 'postgres', 'mysql'
22
+ {
23
+ '001_create_kaal_dispatches.rb' => dispatches_template,
24
+ '002_create_kaal_definitions.rb' => definitions_template
25
+ }
26
+ else
27
+ {}
28
+ end
29
+ end
30
+
31
+ def dispatches_template
32
+ <<~RUBY
33
+ Sequel.migration do
34
+ change do
35
+ create_table?(:kaal_dispatches) do
36
+ primary_key :id
37
+ String :key, null: false
38
+ Time :fire_time, null: false
39
+ Time :dispatched_at, null: false
40
+ String :node_id, null: false
41
+ String :status, null: false, default: 'dispatched', size: 50
42
+ end
43
+
44
+ add_index :kaal_dispatches, [:key, :fire_time], unique: true
45
+ add_index :kaal_dispatches, :key
46
+ add_index :kaal_dispatches, :node_id
47
+ add_index :kaal_dispatches, :status
48
+ add_index :kaal_dispatches, :fire_time
49
+ end
50
+ end
51
+ RUBY
52
+ end
53
+
54
+ def locks_template
55
+ <<~RUBY
56
+ Sequel.migration do
57
+ change do
58
+ create_table?(:kaal_locks) do
59
+ primary_key :id
60
+ String :key, null: false
61
+ Time :acquired_at, null: false
62
+ Time :expires_at, null: false
63
+ end
64
+
65
+ add_index :kaal_locks, :key, unique: true
66
+ add_index :kaal_locks, :expires_at
67
+ end
68
+ end
69
+ RUBY
70
+ end
71
+
72
+ def definitions_template
73
+ <<~RUBY
74
+ Sequel.migration do
75
+ change do
76
+ create_table?(:kaal_definitions) do
77
+ primary_key :id
78
+ String :key, null: false
79
+ String :cron, null: false
80
+ TrueClass :enabled, null: false, default: true
81
+ String :source, null: false
82
+ String :metadata, text: true, null: false, default: '{}'
83
+ Time :disabled_at
84
+ Time :created_at, null: false
85
+ Time :updated_at, null: false
86
+ end
87
+
88
+ add_index :kaal_definitions, :key, unique: true
89
+ add_index :kaal_definitions, :enabled
90
+ add_index :kaal_definitions, :source
91
+ end
92
+ end
93
+ RUBY
94
+ end
95
+ end
96
+ end
97
+ end
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
7
  module Kaal
9
8
  # Register conflict handling helpers shared by Kaal singleton methods.
10
9
  module RegisterConflictSupport
data/lib/kaal/registry.rb CHANGED
@@ -4,7 +4,6 @@
4
4
  #
5
5
  # This source code is licensed under the MIT license found in the
6
6
  # LICENSE file in the root directory of this source tree.
7
-
8
7
  module Kaal
9
8
  ##
10
9
  # Thread-safe registry for storing and managing registered cron jobs.
@@ -21,9 +20,7 @@ module Kaal
21
20
 
22
21
  ##
23
22
  # Entry class representing a single registered cron job
24
- # rubocop:disable Style/RedundantStructKeywordInit
25
23
  Entry = Struct.new(:key, :cron, :enqueue, keyword_init: true)
26
- # rubocop:enable Style/RedundantStructKeywordInit
27
24
 
28
25
  ##
29
26
  # Initialize a new Registry instance.
@@ -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
+ require 'pathname'
8
+
9
+ module Kaal
10
+ # Resolves environment and path information for plain-Ruby runtime loading.
11
+ class RuntimeContext
12
+ DEFAULT_ENVIRONMENT_NAME = 'development'
13
+ ENVIRONMENT_KEYS = %w[KAAL_ENV RAILS_ENV APP_ENV RACK_ENV].freeze
14
+
15
+ attr_reader :environment_name, :root_path
16
+
17
+ def self.default(env: ENV, root_path: Dir.pwd)
18
+ new(root_path: root_path, environment_name: environment_name_from(env))
19
+ end
20
+
21
+ def self.environment_name_from(env)
22
+ ENVIRONMENT_KEYS.each do |key|
23
+ value = env[key].to_s.strip
24
+ return value unless value.empty?
25
+ end
26
+
27
+ DEFAULT_ENVIRONMENT_NAME
28
+ end
29
+
30
+ def initialize(root_path:, environment_name:)
31
+ @root_path = Pathname.new(root_path)
32
+ @environment_name = environment_name.to_s
33
+ end
34
+
35
+ def resolve_path(path)
36
+ return path.to_s if Pathname.new(path).absolute?
37
+
38
+ root_path.join(path).to_s
39
+ end
40
+ end
41
+ end