legionio 1.5.3 → 1.5.4
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/CHANGELOG.md +12 -0
- data/legionio_local.db +0 -0
- data/lib/legion/cluster/lock.rb +32 -0
- data/lib/legion/extensions/actors/singleton.rb +35 -9
- data/lib/legion/service.rb +20 -0
- data/lib/legion/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0418444aa216ec13ed88dfb05f28df8928ffbfbfabcee2b1680fad3fcc0f6d11'
|
|
4
|
+
data.tar.gz: a377af093161703e150a689102bc9a8e460409b36ba27cf60d05d66932f8fb9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 026fc5fe5f7340f0a26bfac07738a95f8af2ae2fdb66877a105fcd3ddc0ac7cc5fec71f541dfcfa4fd8a59d39df2f90eace55a8f7f66be9dd63ef99f37f585ee
|
|
7
|
+
data.tar.gz: 21a74bee63dcb4dcba2c945b5ed7791b5d8158780454290cf207dc5af7aa84985503d77db1ec70b0233e69251e2441a449dcb22416ef792ff7d40bee2ecdab77
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.4] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Cluster::Leader` wired into `Service` boot behind `cluster.leader_election` feature flag (default: off)
|
|
7
|
+
- `Actors::Singleton` upgraded to dual-backend (Redis + PG advisory locks via `Cluster::Lock`)
|
|
8
|
+
- `Singleton` gating controlled by `cluster.singleton_enabled` feature flag (default: off — every node runs, no behavior change)
|
|
9
|
+
- `Cluster::Lock.extend_lock` method (Redis: Lua TTL extend; PG: always true; none: false)
|
|
10
|
+
- `Singleton` mixin added to lex-health watchdog and lex-metering cleanup/cost_optimizer actors
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `@cluster_leader.stop` called on `Service#shutdown` (before extensions shutdown)
|
|
14
|
+
|
|
3
15
|
## [1.5.3] - 2026-03-24
|
|
4
16
|
|
|
5
17
|
### Added
|
data/legionio_local.db
ADDED
|
Binary file
|
data/lib/legion/cluster/lock.rb
CHANGED
|
@@ -56,6 +56,17 @@ module Legion
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def extend_lock(name:, token: nil, ttl: 30)
|
|
60
|
+
case backend
|
|
61
|
+
when :redis
|
|
62
|
+
extend_lock_redis(name: name, token: token, ttl: ttl)
|
|
63
|
+
when :postgres
|
|
64
|
+
true
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
def with_lock(name:, ttl: 30, timeout: 5)
|
|
60
71
|
acquired = acquire(name: name, ttl: ttl, timeout: timeout)
|
|
61
72
|
return unless acquired
|
|
@@ -124,6 +135,27 @@ module Legion
|
|
|
124
135
|
false
|
|
125
136
|
end
|
|
126
137
|
|
|
138
|
+
def extend_lock_redis(name:, token:, ttl:)
|
|
139
|
+
tok = token || fetch_token(name)
|
|
140
|
+
return false unless tok
|
|
141
|
+
|
|
142
|
+
client = Legion::Cache::Redis.client
|
|
143
|
+
key = redis_key(name)
|
|
144
|
+
lua = <<~LUA
|
|
145
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
146
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[2])
|
|
147
|
+
return 1
|
|
148
|
+
else
|
|
149
|
+
return 0
|
|
150
|
+
end
|
|
151
|
+
LUA
|
|
152
|
+
result = client.call('EVAL', lua, 1, key, tok, (ttl * 1000).to_s)
|
|
153
|
+
result == 1
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
Legion::Logging.debug "Lock#extend_lock_redis failed for name=#{name}: #{e.message}" if defined?(Legion::Logging)
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
|
|
127
159
|
def release_postgres(name:)
|
|
128
160
|
key = lock_key(name)
|
|
129
161
|
db = Legion::Data.connection
|
|
@@ -24,24 +24,50 @@ module Legion
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
+
def singleton_enabled?
|
|
28
|
+
return false unless defined?(Legion::Settings)
|
|
29
|
+
|
|
30
|
+
cluster = Legion::Settings[:cluster]
|
|
31
|
+
cluster.is_a?(Hash) && cluster[:singleton_enabled] == true
|
|
32
|
+
rescue StandardError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
27
36
|
def skip_or_run(&)
|
|
28
|
-
return super unless
|
|
37
|
+
return super unless singleton_enabled?
|
|
38
|
+
return super unless defined?(Legion::Lock) || defined?(Legion::Cluster::Lock)
|
|
29
39
|
|
|
30
40
|
role = singleton_role
|
|
31
|
-
|
|
41
|
+
ttl_secs = singleton_ttl
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
@leader_token =
|
|
43
|
+
if @leader_token.nil?
|
|
44
|
+
@leader_token = acquire_singleton_lock(role, ttl_secs)
|
|
35
45
|
return unless @leader_token
|
|
46
|
+
else
|
|
47
|
+
extended = extend_singleton_lock(role, @leader_token, ttl_secs)
|
|
48
|
+
unless extended
|
|
49
|
+
@leader_token = acquire_singleton_lock(role, ttl_secs)
|
|
50
|
+
return unless @leader_token
|
|
51
|
+
end
|
|
36
52
|
end
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def acquire_singleton_lock(role, ttl_secs)
|
|
58
|
+
if defined?(Legion::Cluster::Lock)
|
|
59
|
+
Legion::Cluster::Lock.acquire(name: "leader:#{role}", ttl: ttl_secs)
|
|
60
|
+
else
|
|
61
|
+
Legion::Lock.acquire("leader:#{role}", ttl: ttl_secs * 1000)
|
|
42
62
|
end
|
|
63
|
+
end
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
def extend_singleton_lock(role, token, ttl_secs)
|
|
66
|
+
if defined?(Legion::Cluster::Lock)
|
|
67
|
+
Legion::Cluster::Lock.extend_lock(name: "leader:#{role}", token: token, ttl: ttl_secs)
|
|
68
|
+
else
|
|
69
|
+
Legion::Lock.extend_lock("leader:#{role}", token, ttl: ttl_secs * 1000)
|
|
70
|
+
end
|
|
45
71
|
end
|
|
46
72
|
end
|
|
47
73
|
end
|
data/lib/legion/service.rb
CHANGED
|
@@ -60,6 +60,7 @@ module Legion
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
setup_rbac if data
|
|
63
|
+
setup_cluster if data
|
|
63
64
|
|
|
64
65
|
if llm
|
|
65
66
|
setup_llm
|
|
@@ -147,6 +148,20 @@ module Legion
|
|
|
147
148
|
Legion::Logging.warn "Legion::Rbac failed to load: #{e.message}"
|
|
148
149
|
end
|
|
149
150
|
|
|
151
|
+
def setup_cluster
|
|
152
|
+
cluster_settings = Legion::Settings[:cluster]
|
|
153
|
+
return unless cluster_settings.is_a?(Hash) && cluster_settings[:leader_election] == true
|
|
154
|
+
|
|
155
|
+
require 'legion/cluster'
|
|
156
|
+
return unless defined?(Legion::Cluster::Leader)
|
|
157
|
+
|
|
158
|
+
@cluster_leader = Legion::Cluster::Leader.new
|
|
159
|
+
@cluster_leader.start
|
|
160
|
+
Legion::Logging.info('Cluster leader election started')
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
Legion::Logging.warn("Cluster leader setup failed: #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
|
|
150
165
|
def setup_settings
|
|
151
166
|
require 'legion/settings'
|
|
152
167
|
directories = Legion::Settings::Loader.default_directories
|
|
@@ -375,6 +390,11 @@ module Legion
|
|
|
375
390
|
Legion::Readiness.mark_not_ready(:gaia)
|
|
376
391
|
end
|
|
377
392
|
|
|
393
|
+
if @cluster_leader
|
|
394
|
+
@cluster_leader.stop
|
|
395
|
+
@cluster_leader = nil
|
|
396
|
+
end
|
|
397
|
+
|
|
378
398
|
Legion::Extensions.shutdown
|
|
379
399
|
Legion::Readiness.mark_not_ready(:extensions)
|
|
380
400
|
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -396,6 +396,7 @@ files:
|
|
|
396
396
|
- exe/legion
|
|
397
397
|
- exe/legionio
|
|
398
398
|
- legionio.gemspec
|
|
399
|
+
- legionio_local.db
|
|
399
400
|
- lib/legion.rb
|
|
400
401
|
- lib/legion/alerts.rb
|
|
401
402
|
- lib/legion/api.rb
|