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.
- checksums.yaml +4 -4
- data/README.md +8 -11
- data/lib/kaal/active_record_support.rb +82 -0
- data/lib/kaal/backend/mysql.rb +41 -0
- data/lib/kaal/backend/postgres.rb +41 -0
- data/lib/kaal/backend/sqlite.rb +41 -0
- data/lib/kaal/definition/database_engine.rb +88 -0
- data/lib/kaal/dispatch/database_engine.rb +120 -0
- data/lib/kaal/internal/active_record/base_record.rb +16 -0
- data/lib/kaal/internal/active_record/connection_support.rb +96 -0
- data/lib/kaal/internal/active_record/database_backend.rb +73 -0
- data/lib/kaal/internal/active_record/definition_record.rb +16 -0
- data/lib/kaal/internal/active_record/definition_registry.rb +81 -0
- data/lib/kaal/internal/active_record/dispatch_record.rb +16 -0
- data/lib/kaal/internal/active_record/dispatch_registry.rb +100 -0
- data/lib/kaal/internal/active_record/lock_record.rb +16 -0
- data/lib/kaal/internal/active_record/migration_templates.rb +108 -0
- data/lib/kaal/internal/active_record/mysql_backend.rb +71 -0
- data/lib/kaal/internal/active_record/postgres_backend.rb +69 -0
- data/lib/kaal/internal/active_record.rb +17 -0
- data/lib/kaal/internal/sequel/database_backend.rb +74 -0
- data/lib/kaal/internal/sequel/mysql_backend.rb +69 -0
- data/lib/kaal/internal/sequel/postgres_backend.rb +67 -0
- data/lib/kaal/internal/sequel.rb +12 -0
- data/lib/kaal/persistence/database.rb +35 -0
- data/lib/kaal/persistence/migration_templates.rb +97 -0
- data/lib/kaal/registry.rb +0 -2
- data/lib/kaal/sequel_support.rb +82 -0
- data/lib/kaal/version.rb +1 -1
- data/lib/kaal.rb +6 -0
- metadata +26 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f078489fc2106826b98a1890f15dd5aad38a627ef9db230bd11db3785dc66f86
|
|
4
|
+
data.tar.gz: 10ec39fb62e2082d7a8191153af335950fb14d584c0ed33737298baaa2b2f270
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
-
##
|
|
125
|
+
## SQL Backends
|
|
130
126
|
|
|
131
|
-
Use
|
|
127
|
+
Use the explicit SQL backends when you want persisted registries:
|
|
132
128
|
|
|
133
|
-
- `
|
|
134
|
-
- `
|
|
135
|
-
- `
|
|
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
|