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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f271018748afb1f0673160c77fb130306881d834350f863459b0dd4bf9e50ca
4
- data.tar.gz: 30735d835cdf55297b2c3e930bf5208aa57f8e93b2ebecce34759922f1717778
3
+ metadata.gz: '0418444aa216ec13ed88dfb05f28df8928ffbfbfabcee2b1680fad3fcc0f6d11'
4
+ data.tar.gz: a377af093161703e150a689102bc9a8e460409b36ba27cf60d05d66932f8fb9f
5
5
  SHA512:
6
- metadata.gz: 350a4f591d15e80fbeec13d00cecc6dc5517fc217ecf403fb33fea6b4288a8c54d6c120ef944d2ce28780dd02be02b79e22d7c534a84f9c6fd23e861a3189514
7
- data.tar.gz: 06c021bdaa31ab1d1533893288c095385dd170b5910c42f843144f705faf0fdb45615ae98a7936de628b7599fac7ece3f9af3c96de2b63d96887554508912ad8
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
@@ -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 defined?(Legion::Lock)
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
- ttl_ms = singleton_ttl * 1000
41
+ ttl_secs = singleton_ttl
32
42
 
33
- unless @leader_token
34
- @leader_token = Legion::Lock.acquire("leader:#{role}", ttl: ttl_ms)
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
- extended = Legion::Lock.extend_lock("leader:#{role}", @leader_token, ttl: ttl_ms)
39
- unless extended
40
- @leader_token = Legion::Lock.acquire("leader:#{role}", ttl: ttl_ms)
41
- return unless @leader_token
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
- super
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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.5.3'
4
+ VERSION = '1.5.4'
5
5
  end
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.3
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