lex-health 0.1.7 → 0.2.1

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: 58aebf740f7b4a79bf0c97cc5bae4cec886fe731bda53c681e9526f3c5d6ebc7
4
- data.tar.gz: 71b495ec970cd3230614a9f6a812fdf9987bcb00c0a16784fe83cedfb23d9438
3
+ metadata.gz: d482b0f36c50c6fb14c9aecdb9accbbc689202f4f2977a6c81cd123379aa1745
4
+ data.tar.gz: 3e980332379f3748c013eb06e22cb5cb20a8b89f028708bbb94ba05b039cf0d6
5
5
  SHA512:
6
- metadata.gz: 0ebad0bf1c817a9bccba265b7c5b127c045b725ad8a25a966d141547f8ce535f0d5073155c41e0346b44f1d6b4409b15c0b8a72fc6d98a1357300889224356b9
7
- data.tar.gz: de77a45ce02799fe3f9720054585ef6d332f7d852732035c0c8511f7c92f4fd7ec64444c2bd7b920ec6ddb219139b65ed3e37544733b77213d9bbfe14b907865
6
+ metadata.gz: ceb0f962def03f38956dda15300e678262a57d6cfb4e844343ea76a39b03ac49242138f9c3310cf64c10a640acdac60251db2f8d7714907893aa8656815ee29d
7
+ data.tar.gz: e1ce74c59b555aec02f12bfe35f81fa6c31789b754e0b46ac9f53767a291567e5a784f8aa246a02c6db2b4258cd99497612f354c424e020c5ad0dbf4941ad5b1
data/.gitignore CHANGED
@@ -7,5 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ Gemfile.lock
11
+
10
12
  # rspec failure tracking
11
13
  .rspec_status
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1] - 2026-03-22
4
+
5
+ ### Changed
6
+ - Add legion-cache, legion-crypt, legion-data, legion-json, legion-logging, legion-settings, legion-transport as runtime dependencies
7
+ - Replace direct Legion::JSON.dump calls with json_dump helper in runners/health.rb
8
+ - Update spec_helper with real sub-gem helper stubs
9
+
10
+ ## [0.2.0] - 2026-03-18
11
+
12
+ ### Fixed
13
+ - `active` column now uses boolean `true` instead of integer `1` (PostgreSQL compatibility)
14
+ - Watchdog message routing key changed from `'health'` to `'node.health'` to match queue binding
15
+ - Added `require 'time'` for `Time.parse`
16
+ - Nil guard on `updated` timestamp in back-in-time comparison
17
+ - TOCTOU race condition on concurrent heartbeat inserts (rescue UniqueConstraintViolation)
18
+ - `delete` method nil guard for nonexistent nodes
19
+ - `mark_workers_offline` now clears `health_node` on expired workers
20
+
21
+ ### Changed
22
+ - Entry point `data_required?` is now `self.` (class method) matching framework expectation
23
+
24
+ ## [0.1.8] - 2026-03-17
25
+
26
+ ### Fixed
27
+ - Watchdog `expire` guards against missing `Legion::Data::Model::Node` constant before use, returning an error hash when the model is unavailable
28
+
3
29
  ## [0.1.7] - 2026-03-16
4
30
 
5
31
  ### Fixed
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ Legion Extension that reads heartbeat messages from cluster nodes and updates th
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-health
12
12
  **License**: MIT
13
- **Version**: 0.1.7
13
+ **Version**: 0.2.0
14
14
 
15
15
  ## Architecture
16
16
 
data/README.md CHANGED
@@ -15,9 +15,9 @@ gem install lex-health
15
15
 
16
16
  ## How It Works
17
17
 
18
- The Health runner receives heartbeat messages published by `lex-node` and upserts node records in the database. It uses timestamp comparison to prevent out-of-order updates from rolling back newer status.
18
+ The Health runner receives heartbeat messages published by `lex-node` and upserts node records in the database. It uses timestamp comparison to prevent out-of-order updates from rolling back newer status. On each update it also marks all digital workers reported by the node as `online` and any workers no longer reported as `unknown`.
19
19
 
20
- The Watchdog actor runs every 5 seconds and queries for healthy nodes whose last heartbeat timestamp is older than `expire_time` seconds (default: 60 seconds). Those nodes are transitioned to `unknown` by publishing `NodeHealth` messages.
20
+ The Watchdog actor runs every 5 seconds and queries for healthy nodes whose last heartbeat timestamp is older than `expire_time` seconds (default: 60 seconds). Expired nodes are transitioned to `unknown` and all digital workers hosted on that node are marked `offline`.
21
21
 
22
22
  ## Requirements
23
23
 
data/lex-health.gemspec CHANGED
@@ -28,6 +28,14 @@ Gem::Specification.new do |spec|
28
28
  end
29
29
  spec.require_paths = ['lib']
30
30
 
31
+ spec.add_dependency 'legion-cache', '>= 1.3.11'
32
+ spec.add_dependency 'legion-crypt', '>= 1.4.9'
33
+ spec.add_dependency 'legion-data', '>= 1.4.17'
34
+ spec.add_dependency 'legion-json', '>= 1.2.1'
35
+ spec.add_dependency 'legion-logging', '>= 1.3.2'
36
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
37
+ spec.add_dependency 'legion-transport', '>= 1.3.9'
38
+
31
39
  spec.add_development_dependency 'rake'
32
40
  spec.add_development_dependency 'rspec'
33
41
  spec.add_development_dependency 'rubocop'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Health
@@ -16,7 +18,7 @@ module Legion
16
18
  return { success: result, hostname: hostname, **opts }
17
19
  end
18
20
 
19
- if opts.key?(:timestamp) && !item.values[:updated].nil? && item.values[:updated] > Time.parse(opts[:timestamp])
21
+ if opts.key?(:timestamp) && item.values[:updated] && item.values[:updated] > Time.parse(opts[:timestamp])
20
22
  return { success: false,
21
23
  reason: 'entry already updated',
22
24
  hostname: hostname,
@@ -24,9 +26,9 @@ module Legion
24
26
  **opts }
25
27
  end
26
28
 
27
- update_hash = { active: 1, status: opts[:status], name: hostname, updated: Sequel::CURRENT_TIMESTAMP }
28
- update_hash[:metrics] = Legion::JSON.dump(opts[:metrics]) if opts[:metrics]
29
- update_hash[:hosted_worker_ids] = Legion::JSON.dump(opts[:hosted_worker_ids]) if opts[:hosted_worker_ids]
29
+ update_hash = { active: true, status: opts[:status], name: hostname, updated: Sequel::CURRENT_TIMESTAMP }
30
+ update_hash[:metrics] = json_dump(opts[:metrics]) if opts[:metrics]
31
+ update_hash[:hosted_worker_ids] = json_dump(opts[:hosted_worker_ids]) if opts[:hosted_worker_ids]
30
32
  update_hash[:version] = opts[:version] if opts[:version]
31
33
  item.update(update_hash)
32
34
 
@@ -36,19 +38,27 @@ module Legion
36
38
  end
37
39
 
38
40
  def insert(hostname:, status: 'unknown', **opts)
39
- insert = { active: 1, status: status, name: hostname }
41
+ insert = { active: true, status: status, name: hostname }
40
42
  insert[:datacenter_id] = opts[:datacenter_id] if opts.key? :datacenter_id
41
43
  insert[:environment_id] = opts[:environment_id] if opts.key? :environment_id
42
- insert[:active] = opts[:active] if opts.key? :active
43
- insert[:metrics] = Legion::JSON.dump(opts[:metrics]) if opts[:metrics]
44
- insert[:hosted_worker_ids] = Legion::JSON.dump(opts[:hosted_worker_ids]) if opts[:hosted_worker_ids]
44
+ insert[:metrics] = json_dump(opts[:metrics]) if opts[:metrics]
45
+ insert[:hosted_worker_ids] = json_dump(opts[:hosted_worker_ids]) if opts[:hosted_worker_ids]
45
46
  insert[:version] = opts[:version] if opts[:version]
46
47
 
47
- { success: true, hostname: hostname, node_id: Legion::Data::Model::Node.insert(insert), **insert }
48
+ node_id = begin
49
+ Legion::Data::Model::Node.insert(insert)
50
+ rescue Sequel::UniqueConstraintViolation
51
+ item = Legion::Data::Model::Node[name: hostname]
52
+ item&.id
53
+ end
54
+ { success: true, hostname: hostname, node_id: node_id, **insert }
48
55
  end
49
56
 
50
57
  def delete(node_id:, **_opts)
51
- Legion::Data::Model::Node[node_id].delete
58
+ node = Legion::Data::Model::Node[node_id]
59
+ return { success: false, error: 'node not found', node_id: node_id } if node.nil?
60
+
61
+ node.delete
52
62
  { success: true, node_id: node_id }
53
63
  end
54
64
 
@@ -8,6 +8,8 @@ module Legion
8
8
  include Legion::Extensions::Helpers::Lex
9
9
 
10
10
  def expire(expire_time: 60, **_opts)
11
+ return { success: false, reason: 'Legion::Data::Model::Node not available' } unless defined?(Legion::Data::Model::Node)
12
+
11
13
  cutoff = Time.now - expire_time
12
14
  nodes = []
13
15
  Legion::Data::Model::Node
@@ -36,7 +38,7 @@ module Legion
36
38
  Legion::Data::Model::DigitalWorker
37
39
  .where(health_node: node_name, health_status: 'online')
38
40
  .each do |worker|
39
- worker.update(health_status: 'offline')
41
+ worker.update(health_status: 'offline', health_node: nil)
40
42
  end
41
43
  rescue StandardError => e
42
44
  log.warn "worker offline marking failed: #{e.message}" if respond_to?(:log)
@@ -7,7 +7,7 @@ module Legion
7
7
  module Messages
8
8
  class Watchdog < Legion::Transport::Message
9
9
  def routing_key
10
- 'health'
10
+ 'node.health'
11
11
  end
12
12
 
13
13
  def expiration
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Health
6
- VERSION = '0.1.7'
6
+ VERSION = '0.2.1'
7
7
  end
8
8
  end
9
9
  end
@@ -10,10 +10,6 @@ module Legion
10
10
  def self.data_required?
11
11
  true
12
12
  end
13
-
14
- def data_required?
15
- true
16
- end
17
13
  end
18
14
  end
19
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-health
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,104 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-cache
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 1.3.11
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.3.11
26
+ - !ruby/object:Gem::Dependency
27
+ name: legion-crypt
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.4.9
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.4.9
40
+ - !ruby/object:Gem::Dependency
41
+ name: legion-data
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.4.17
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.4.17
54
+ - !ruby/object:Gem::Dependency
55
+ name: legion-json
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.2.1
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 1.2.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: legion-logging
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.3.2
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.2
82
+ - !ruby/object:Gem::Dependency
83
+ name: legion-settings
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.14
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 1.3.14
96
+ - !ruby/object:Gem::Dependency
97
+ name: legion-transport
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.3.9
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.3.9
12
110
  - !ruby/object:Gem::Dependency
13
111
  name: rake
14
112
  requirement: !ruby/object:Gem::Requirement
@@ -100,7 +198,6 @@ executables: []
100
198
  extensions: []
101
199
  extra_rdoc_files: []
102
200
  files:
103
- - ".circleci/config.yml"
104
201
  - ".github/workflows/ci.yml"
105
202
  - ".gitignore"
106
203
  - ".rspec"
@@ -109,7 +206,6 @@ files:
109
206
  - CLAUDE.md
110
207
  - Dockerfile
111
208
  - Gemfile
112
- - Gemfile.lock
113
209
  - LICENSE.txt
114
210
  - README.md
115
211
  - Rakefile
data/.circleci/config.yml DELETED
@@ -1,61 +0,0 @@
1
- version: 2.1
2
- orbs:
3
- ruby: circleci/ruby@0.2.1
4
-
5
- jobs:
6
- "rubocop":
7
- docker:
8
- - image: circleci/ruby:2.5-node
9
- steps:
10
- - checkout
11
- - ruby/load-cache
12
- - ruby/install-deps
13
- - run:
14
- name: Run Rubocop
15
- command: bundle exec rubocop
16
- - ruby/save-cache
17
- "ruby-two-five":
18
- docker:
19
- - image: circleci/ruby:2.5
20
- - image: memcached:1.5-alpine
21
- steps:
22
- - checkout
23
- - ruby/load-cache
24
- - ruby/install-deps
25
- - ruby/run-tests
26
- - ruby/save-cache
27
- "ruby-two-six":
28
- docker:
29
- - image: circleci/ruby:2.6
30
- - image: memcached:1.5-alpine
31
- steps:
32
- - checkout
33
- - ruby/load-cache
34
- - ruby/install-deps
35
- - ruby/run-tests
36
- - ruby/save-cache
37
- "ruby-two-seven":
38
- docker:
39
- - image: circleci/ruby:2.7
40
- - image: memcached:1.5-alpine
41
- steps:
42
- - checkout
43
- - ruby/load-cache
44
- - ruby/install-deps
45
- - ruby/run-tests
46
- - ruby/save-cache
47
-
48
- workflows:
49
- version: 2
50
- rubocop-rspec:
51
- jobs:
52
- - rubocop
53
- - ruby-two-five:
54
- requires:
55
- - rubocop
56
- - ruby-two-six:
57
- requires:
58
- - ruby-two-five
59
- - ruby-two-seven:
60
- requires:
61
- - ruby-two-five
data/Gemfile.lock DELETED
@@ -1,86 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- lex-health (0.1.7)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- addressable (2.8.9)
10
- public_suffix (>= 2.0.2, < 8.0)
11
- ast (2.4.3)
12
- bigdecimal (4.0.1)
13
- diff-lcs (1.6.2)
14
- json (2.19.1)
15
- json-schema (6.2.0)
16
- addressable (~> 2.8)
17
- bigdecimal (>= 3.1, < 5)
18
- language_server-protocol (3.17.0.5)
19
- lint_roller (1.1.0)
20
- mcp (0.8.0)
21
- json-schema (>= 4.1)
22
- parallel (1.27.0)
23
- parser (3.3.10.2)
24
- ast (~> 2.4.1)
25
- racc
26
- prism (1.9.0)
27
- public_suffix (7.0.5)
28
- racc (1.8.1)
29
- rainbow (3.1.1)
30
- rake (13.3.1)
31
- regexp_parser (2.11.3)
32
- rspec (3.13.2)
33
- rspec-core (~> 3.13.0)
34
- rspec-expectations (~> 3.13.0)
35
- rspec-mocks (~> 3.13.0)
36
- rspec-core (3.13.6)
37
- rspec-support (~> 3.13.0)
38
- rspec-expectations (3.13.5)
39
- diff-lcs (>= 1.2.0, < 2.0)
40
- rspec-support (~> 3.13.0)
41
- rspec-mocks (3.13.8)
42
- diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.13.0)
44
- rspec-support (3.13.7)
45
- rubocop (1.85.1)
46
- json (~> 2.3)
47
- language_server-protocol (~> 3.17.0.2)
48
- lint_roller (~> 1.1.0)
49
- mcp (~> 0.6)
50
- parallel (~> 1.10)
51
- parser (>= 3.3.0.2)
52
- rainbow (>= 2.2.2, < 4.0)
53
- regexp_parser (>= 2.9.3, < 3.0)
54
- rubocop-ast (>= 1.49.0, < 2.0)
55
- ruby-progressbar (~> 1.7)
56
- unicode-display_width (>= 2.4.0, < 4.0)
57
- rubocop-ast (1.49.1)
58
- parser (>= 3.3.7.2)
59
- prism (~> 1.7)
60
- rubocop-rspec (3.9.0)
61
- lint_roller (~> 1.1)
62
- rubocop (~> 1.81)
63
- ruby-progressbar (1.13.0)
64
- sequel (5.102.0)
65
- bigdecimal
66
- sqlite3 (2.9.2-arm64-darwin)
67
- sqlite3 (2.9.2-x86_64-linux-gnu)
68
- unicode-display_width (3.2.0)
69
- unicode-emoji (~> 4.1)
70
- unicode-emoji (4.2.0)
71
-
72
- PLATFORMS
73
- arm64-darwin-25
74
- x86_64-linux
75
-
76
- DEPENDENCIES
77
- lex-health!
78
- rake
79
- rspec
80
- rubocop
81
- rubocop-rspec
82
- sequel
83
- sqlite3
84
-
85
- BUNDLED WITH
86
- 2.6.9