kaal 0.3.0 → 0.5.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +43 -11
  3. data/lib/kaal/active_record_support.rb +82 -0
  4. data/lib/kaal/backend/adapter.rb +4 -0
  5. data/lib/kaal/backend/memory_adapter.rb +5 -0
  6. data/lib/kaal/backend/mysql.rb +63 -0
  7. data/lib/kaal/backend/postgres.rb +45 -0
  8. data/lib/kaal/backend/redis_adapter.rb +5 -0
  9. data/lib/kaal/backend/sqlite.rb +45 -0
  10. data/lib/kaal/cli.rb +1 -0
  11. data/lib/kaal/config/configuration.rb +33 -2
  12. data/lib/kaal/config/delayed_job_security_policy.rb +60 -0
  13. data/lib/kaal/config.rb +1 -0
  14. data/lib/kaal/core/coordinator.rb +68 -19
  15. data/lib/kaal/definition/database_engine.rb +88 -0
  16. data/lib/kaal/delayed_job/database_engine.rb +116 -0
  17. data/lib/kaal/delayed_job/dispatch_failure_logger.rb +31 -0
  18. data/lib/kaal/delayed_job/memory_engine.rb +79 -0
  19. data/lib/kaal/delayed_job/mysql_version_support.rb +43 -0
  20. data/lib/kaal/delayed_job/redis_engine.rb +119 -0
  21. data/lib/kaal/delayed_job/registry.rb +39 -0
  22. data/lib/kaal/dispatch/database_engine.rb +120 -0
  23. data/lib/kaal/internal/active_record/base_record.rb +16 -0
  24. data/lib/kaal/internal/active_record/connection_support.rb +96 -0
  25. data/lib/kaal/internal/active_record/database_backend.rb +78 -0
  26. data/lib/kaal/internal/active_record/definition_record.rb +16 -0
  27. data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
  28. data/lib/kaal/internal/active_record/delayed_job_record.rb +16 -0
  29. data/lib/kaal/internal/active_record/delayed_job_registry.rb +119 -0
  30. data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
  31. data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
  32. data/lib/kaal/internal/active_record/lock_record.rb +16 -0
  33. data/lib/kaal/internal/active_record/migration_templates.rb +138 -0
  34. data/lib/kaal/internal/active_record/mysql_backend.rb +89 -0
  35. data/lib/kaal/internal/active_record/postgres_backend.rb +73 -0
  36. data/lib/kaal/internal/active_record.rb +19 -0
  37. data/lib/kaal/internal/sequel/database_backend.rb +79 -0
  38. data/lib/kaal/internal/sequel/mysql_backend.rb +83 -0
  39. data/lib/kaal/internal/sequel/postgres_backend.rb +71 -0
  40. data/lib/kaal/internal/sequel.rb +13 -0
  41. data/lib/kaal/job_dispatcher.rb +108 -0
  42. data/lib/kaal/persistence/database.rb +39 -0
  43. data/lib/kaal/persistence/migration_templates.rb +129 -0
  44. data/lib/kaal/registry.rb +0 -2
  45. data/lib/kaal/runtime/scheduler_boot_loader.rb +2 -0
  46. data/lib/kaal/scheduler_file/job_applier.rb +28 -53
  47. data/lib/kaal/sequel_support.rb +82 -0
  48. data/lib/kaal/version.rb +1 -1
  49. data/lib/kaal.rb +117 -0
  50. metadata +36 -1
@@ -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,116 @@
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/delayed_job/registry'
9
+ require 'kaal/persistence/database'
10
+
11
+ module Kaal
12
+ module DelayedJob
13
+ # Sequel-backed delayed-job store persisted in kaal_delayed_jobs.
14
+ class DatabaseEngine < Registry
15
+ def initialize(database:, use_skip_locked: false)
16
+ super()
17
+ @database = Kaal::Persistence::Database.new(database)
18
+ @use_skip_locked = use_skip_locked
19
+ end
20
+
21
+ def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, connection: nil)
22
+ now = Time.now.utc
23
+ payload = {
24
+ job_id: job_id,
25
+ run_at: run_at,
26
+ job_class: job_class,
27
+ args: JSON.generate(args),
28
+ queue: queue,
29
+ created_at: now
30
+ }
31
+
32
+ dataset_for(connection).insert(payload)
33
+ self.class.normalize_row(payload)
34
+ rescue ::Sequel::UniqueConstraintViolation
35
+ raise DuplicateJobError, "Delayed job #{job_id.inspect} already exists"
36
+ end
37
+
38
+ def pop_due(now:, limit:)
39
+ return pop_due_with_skip_locked(now:, limit:) if @use_skip_locked
40
+
41
+ pop_due_with_delete_confirmation(now:, limit:)
42
+ end
43
+
44
+ def find_job(job_id, connection: @database.connection)
45
+ self.class.normalize_row(connection[:kaal_delayed_jobs].where(job_id: job_id).first)
46
+ end
47
+
48
+ def all_jobs
49
+ connection[:kaal_delayed_jobs].order(:run_at, :job_id).filter_map { |row| self.class.normalize_row(row) }
50
+ end
51
+
52
+ def claim_strategy
53
+ @use_skip_locked ? :skip_locked : :delete_confirmation
54
+ end
55
+
56
+ def self.normalize_row(row)
57
+ return nil unless row
58
+
59
+ {
60
+ job_id: row[:job_id],
61
+ run_at: row[:run_at],
62
+ job_class: row[:job_class],
63
+ args: parse_args(row[:args]),
64
+ queue: row[:queue],
65
+ created_at: row[:created_at]
66
+ }
67
+ rescue JSON::ParserError
68
+ nil
69
+ end
70
+
71
+ private
72
+
73
+ def pop_due_with_skip_locked(now:, limit:)
74
+ connection.transaction do
75
+ delayed_jobs_dataset = connection[:kaal_delayed_jobs]
76
+ due_rows = delayed_jobs_dataset.where { run_at <= now }.order(:run_at, :job_id).for_update.skip_locked.limit(limit).all
77
+ job_ids = due_rows.map { |row| row[:job_id] }
78
+ normalized_jobs = due_rows.filter_map { |row| self.class.normalize_row(row) }
79
+ delayed_jobs_dataset.where(job_id: job_ids).delete unless job_ids.empty?
80
+ normalized_jobs
81
+ end
82
+ end
83
+
84
+ def pop_due_with_delete_confirmation(now:, limit:)
85
+ connection.transaction do
86
+ delayed_jobs_dataset = connection[:kaal_delayed_jobs]
87
+ due_rows = delayed_jobs_dataset.where { run_at <= now }.order(:run_at, :job_id).limit(limit).all
88
+ due_rows.each_with_object([]) do |row, jobs|
89
+ deleted = delayed_jobs_dataset.where(job_id: row[:job_id]).delete
90
+ normalized_job = self.class.normalize_row(row)
91
+ jobs << normalized_job if deleted.positive? && normalized_job
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.parse_args(args_payload)
97
+ JSON.parse(args_payload || '[]')
98
+ end
99
+ private_class_method :parse_args
100
+
101
+ def dataset_for(connection)
102
+ return dataset unless connection
103
+
104
+ Kaal::Persistence::Database.new(connection).delayed_jobs_dataset
105
+ end
106
+
107
+ def dataset
108
+ @database.delayed_jobs_dataset
109
+ end
110
+
111
+ def connection
112
+ @database.connection
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
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 DelayedJob
9
+ # Shared delayed-dispatch failure logging for at-most-once dispatches.
10
+ module DispatchFailureLogger
11
+ module_function
12
+
13
+ def log_claimed_dispatch_failure(logger:, job:, error:)
14
+ return unless logger
15
+
16
+ message = "Delayed job #{job.fetch(:job_id)} dispatch failed after claim; " \
17
+ "job_class=#{job.fetch(:job_class).inspect} " \
18
+ "queue=#{job[:queue].inspect} " \
19
+ "run_at=#{job.fetch(:run_at)} " \
20
+ 'job was already claimed and will not be retried: ' \
21
+ "#{error.class}: #{error.message}"
22
+
23
+ if logger.respond_to?(:fatal)
24
+ logger.fatal(message)
25
+ else
26
+ logger.error(message)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ 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 'kaal/delayed_job/registry'
8
+ require 'kaal/support/hash_tools'
9
+
10
+ module Kaal
11
+ module DelayedJob
12
+ # In-memory delayed job store for single-process development and tests.
13
+ class MemoryEngine < Registry
14
+ include Kaal::Support::HashTools
15
+
16
+ def initialize
17
+ super
18
+ @jobs = {}
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, **)
23
+ @mutex.synchronize do
24
+ raise DuplicateJobError, "Delayed job #{job_id.inspect} already exists" if @jobs.key?(job_id)
25
+
26
+ job = build_job(job_id:, run_at:, job_class:, args:, queue:)
27
+ @jobs[job_id] = job
28
+ deep_dup(job)
29
+ end
30
+ end
31
+
32
+ def pop_due(now:, limit:)
33
+ @mutex.synchronize do
34
+ due_jobs = @jobs.values
35
+ .select { |job| job[:run_at] <= now }
36
+ .sort_by { |job| job.values_at(:run_at, :job_id) }
37
+ .first(limit)
38
+
39
+ due_jobs.each do |job|
40
+ job_id = job.fetch(:job_id)
41
+ @jobs.delete(job_id)
42
+ end
43
+ due_jobs.map { |job| deep_dup(job) }
44
+ end
45
+ end
46
+
47
+ def find_job(job_id)
48
+ @mutex.synchronize { deep_dup(@jobs[job_id]) }
49
+ end
50
+
51
+ def all_jobs
52
+ @mutex.synchronize do
53
+ @jobs.values.sort_by { |job| [job[:run_at], job[:job_id]] }.map { |job| deep_dup(job) }
54
+ end
55
+ end
56
+
57
+ def clear
58
+ @mutex.synchronize { @jobs.clear }
59
+ end
60
+
61
+ def claim_strategy
62
+ :atomic_pop
63
+ end
64
+
65
+ private
66
+
67
+ def build_job(job_id:, run_at:, job_class:, args:, queue:)
68
+ {
69
+ job_id: job_id,
70
+ run_at: run_at,
71
+ job_class: job_class,
72
+ args: deep_dup(args),
73
+ queue: queue,
74
+ created_at: Time.now.utc
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,43 @@
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 DelayedJob
9
+ # MySQL version helper for delayed-job claim strategy selection.
10
+ module MySQLVersionSupport
11
+ MINIMUM_SKIP_LOCKED_VERSION = 800_00
12
+
13
+ module_function
14
+
15
+ def skip_locked_supported?(version_string)
16
+ version_number(version_string) >= MINIMUM_SKIP_LOCKED_VERSION
17
+ end
18
+
19
+ def version_number(version_string)
20
+ major, minor, patch = version_components(version_string)
21
+ return 0 unless major && minor && patch
22
+
23
+ (major * 10_000) + (minor * 100) + patch
24
+ end
25
+
26
+ def version_components(version_string)
27
+ major, minor, patch = version_string.to_s.split('.', 3)
28
+ [
29
+ integer_prefix(major),
30
+ integer_prefix(minor),
31
+ integer_prefix(patch)
32
+ ]
33
+ end
34
+
35
+ def integer_prefix(value)
36
+ digits = value.to_s.each_char.take_while { |character| character.between?('0', '9') }.join
37
+ return nil if digits.empty?
38
+
39
+ digits.to_i
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,119 @@
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 'time'
9
+ require 'kaal/delayed_job/registry'
10
+
11
+ module Kaal
12
+ module DelayedJob
13
+ # Redis-backed delayed-job store using a sorted set plus payload hash.
14
+ class RedisEngine < Registry
15
+ def initialize(redis, namespace: 'kaal')
16
+ super()
17
+ @redis = redis
18
+ @namespace = namespace
19
+ end
20
+
21
+ def enqueue(job_id:, run_at:, job_class:, args:, queue: nil, **)
22
+ payload = JSON.generate(
23
+ job_id: job_id,
24
+ run_at: run_at.iso8601,
25
+ job_class: job_class,
26
+ args: args,
27
+ queue: queue,
28
+ created_at: Time.now.utc.iso8601
29
+ )
30
+
31
+ script = <<~LUA
32
+ if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then
33
+ return 0
34
+ end
35
+
36
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
37
+ redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])
38
+ return 1
39
+ LUA
40
+
41
+ result = @redis.eval(script, keys: [payloads_key, schedule_key], argv: [job_id, payload, run_at.to_f])
42
+ raise DuplicateJobError, "Delayed job #{job_id.inspect} already exists" unless [1, '1', true].include?(result)
43
+
44
+ find_job(job_id)
45
+ end
46
+
47
+ def pop_due(now:, limit:)
48
+ script = <<~LUA
49
+ local job_ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[1], 'LIMIT', 0, ARGV[2])
50
+ local payloads = {}
51
+
52
+ for _, job_id in ipairs(job_ids) do
53
+ if redis.call('ZREM', KEYS[2], job_id) == 1 then
54
+ local payload = redis.call('HGET', KEYS[1], job_id)
55
+ if payload then
56
+ redis.call('HDEL', KEYS[1], job_id)
57
+ table.insert(payloads, payload)
58
+ end
59
+ end
60
+ end
61
+
62
+ return payloads
63
+ LUA
64
+
65
+ Array(@redis.eval(script, keys: [payloads_key, schedule_key], argv: [now.to_f, limit])).filter_map do |raw|
66
+ self.class.deserialize(raw)
67
+ end
68
+ end
69
+
70
+ def find_job(job_id)
71
+ self.class.deserialize(@redis.hget(payloads_key, job_id))
72
+ end
73
+
74
+ def all_jobs
75
+ Array(@redis.zrange(schedule_key, 0, -1)).filter_map { |job_id| find_job(job_id) }
76
+ end
77
+
78
+ def claim_strategy
79
+ :atomic_pop
80
+ end
81
+
82
+ def self.deserialize(raw)
83
+ return nil unless raw
84
+
85
+ parsed = JSON.parse(raw)
86
+ run_at = parse_time(parsed['run_at'])
87
+ created_at = parse_time(parsed['created_at'])
88
+ return nil unless run_at && created_at
89
+
90
+ {
91
+ job_id: parsed['job_id'],
92
+ run_at: run_at,
93
+ job_class: parsed['job_class'],
94
+ args: parsed['args'] || [],
95
+ queue: parsed['queue'],
96
+ created_at: created_at
97
+ }
98
+ rescue JSON::ParserError
99
+ nil
100
+ end
101
+
102
+ def self.parse_time(value)
103
+ Time.iso8601(value.to_s)
104
+ rescue ArgumentError
105
+ nil
106
+ end
107
+
108
+ private
109
+
110
+ def payloads_key
111
+ "#{@namespace}:delayed_jobs:payloads"
112
+ end
113
+
114
+ def schedule_key
115
+ "#{@namespace}:delayed_jobs:schedule"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,39 @@
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 DelayedJob
9
+ # Raised when a delayed job with the same identifier already exists.
10
+ class DuplicateJobError < StandardError; end
11
+
12
+ # Base abstraction for delayed-job persistence.
13
+ class Registry
14
+ def enqueue(**)
15
+ raise NotImplementedError, "#{self.class.name} must implement #enqueue"
16
+ end
17
+
18
+ def pop_due(**)
19
+ raise NotImplementedError, "#{self.class.name} must implement #pop_due"
20
+ end
21
+
22
+ def find_job(_job_id)
23
+ raise NotImplementedError, "#{self.class.name} must implement #find_job"
24
+ end
25
+
26
+ def all_jobs
27
+ raise NotImplementedError, "#{self.class.name} must implement #all_jobs"
28
+ end
29
+
30
+ def claim_strategy
31
+ raise NotImplementedError, "#{self.class.name} must implement #claim_strategy"
32
+ end
33
+
34
+ def requires_dispatch_lock?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ 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