legionio 1.5.13 → 1.5.14

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: 4815cb38dfcd2dc20af4e57d1ec14f63a9bf7876477b70fc11696b775685addf
4
- data.tar.gz: 79f424e95bbe364be35ef6a1b5b11da3ef476dda096443a303b0d8136ffc79e5
3
+ metadata.gz: 213403cc9241316f3d19263526f688166a256ecb38dd2fb45b4f782c9067e9e0
4
+ data.tar.gz: e93b0000542370d69c6b46323e217d3cace04429cf061dd2ba748040a560175a
5
5
  SHA512:
6
- metadata.gz: 4edcd4fabaa737d35e6176668dc63cd6091533240f0ca038250b2b44652130590ad04ee144ed58df2eca1aa3d5313353b46a99069b42eefd2d224384694209a5
7
- data.tar.gz: 16d72dd0db3df555b27defbe22fe1a3b27e2aa11d11674f57d89bc8de593f624aeb5c5ae6fcb589a5f085b0eea9619b0fd6ef56349cf749a3819210f5cc7b7ca
6
+ metadata.gz: c272f3d6e719737682c354a8313d5f3b93797ac44da5fea75f7e0473ffd27a6075eb2ba67665f2e6dfa55e37f3c2c986ae66ae3dcc4cdfbaa78c0fb2517a7fe5
7
+ data.tar.gz: fb92bae4cf4fddf1a9a47b88eb611d13bb4dfe7162315a560a9c9b75fd5250f41cf30a80cef166e88488794ef16a7c0d462e6018de4e460409e89d35ad33f41a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.5.14] - 2026-03-25
4
+
5
+ ### Fixed
6
+ - Shutdown no longer hangs when network is unreachable — all component shutdowns wrapped in bounded timeouts via `shutdown_component` helper (#30)
7
+ - Reload path also wrapped with same timeout guards to prevent hangs during network-triggered reload (#30)
8
+
9
+ ### Added
10
+ - Network watchdog: background `Concurrent::TimerTask` monitors transport/data/cache connectivity, pauses actors after sustained failures, triggers `Legion.reload` when network restores (#30)
11
+ - `Legion::Extensions.pause_actors` suspends all `Every` timer tasks without destroying instances (#30)
12
+ - Watchdog is feature-flagged via `network.watchdog.enabled` (default: false), configurable threshold and interval (#30)
13
+
3
14
  ## [1.5.13] - 2026-03-25
4
15
 
5
16
  ### Fixed
@@ -96,6 +96,16 @@ module Legion
96
96
  Legion::Logging.info "Successfully shut down all actors (#{(Time.now - shutdown_start).round(1)}s)"
97
97
  end
98
98
 
99
+ def pause_actors
100
+ @running_instances&.each do |inst|
101
+ timer = inst.instance_variable_get(:@timer)
102
+ timer&.shutdown if timer.respond_to?(:shutdown)
103
+ rescue StandardError => e
104
+ Legion::Logging.error "pause_actors: #{e.class}: #{e.message}" if defined?(Legion::Logging)
105
+ end
106
+ Legion::Logging.warn 'All actors paused' if defined?(Legion::Logging)
107
+ end
108
+
99
109
  def load_extensions
100
110
  @extensions ||= []
101
111
  @loaded_extensions ||= []
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
3
4
  require_relative 'readiness'
4
5
  require_relative 'process_role'
5
6
 
@@ -130,6 +131,7 @@ module Legion
130
131
  api_settings = Legion::Settings[:api] || {}
131
132
  @api_enabled = api && api_settings.fetch(:enabled, true)
132
133
  setup_api if @api_enabled
134
+ setup_network_watchdog
133
135
  Legion::Settings[:client][:ready] = true
134
136
  Legion::Events.emit('service.ready')
135
137
  end
@@ -465,13 +467,14 @@ module Legion
465
467
  Legion::Settings[:client][:shutting_down] = true
466
468
  Legion::Events.emit('service.shutting_down')
467
469
 
470
+ shutdown_network_watchdog
468
471
  shutdown_audit_archiver
469
472
  shutdown_api
470
473
 
471
474
  Legion::Metrics.reset! if defined?(Legion::Metrics)
472
475
 
473
476
  if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
474
- Legion::Gaia.shutdown
477
+ shutdown_component('Gaia') { Legion::Gaia.shutdown }
475
478
  Legion::Readiness.mark_not_ready(:gaia)
476
479
  end
477
480
 
@@ -480,32 +483,33 @@ module Legion
480
483
  @cluster_leader = nil
481
484
  end
482
485
 
483
- Legion::Extensions.shutdown
486
+ ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
487
+ shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
484
488
  Legion::Readiness.mark_not_ready(:extensions)
485
489
 
486
490
  if Legion::Settings[:llm]&.dig(:connected)
487
- Legion::LLM.shutdown
491
+ shutdown_component('LLM') { Legion::LLM.shutdown }
488
492
  Legion::Readiness.mark_not_ready(:llm)
489
493
  end
490
494
 
491
495
  if defined?(Legion::Rbac) && Legion::Settings[:rbac]&.dig(:connected)
492
- Legion::Rbac.shutdown
496
+ shutdown_component('Rbac') { Legion::Rbac.shutdown }
493
497
  Legion::Readiness.mark_not_ready(:rbac)
494
498
  end
495
499
 
496
- Legion::Data.shutdown if Legion::Settings[:data][:connected]
500
+ shutdown_component('Data') { Legion::Data.shutdown } if Legion::Settings[:data][:connected]
497
501
  Legion::Readiness.mark_not_ready(:data)
498
502
 
499
503
  Legion::Leader.reset! if defined?(Legion::Leader)
500
504
 
501
- Legion::Cache.shutdown
505
+ shutdown_component('Cache') { Legion::Cache.shutdown }
502
506
  Legion::Readiness.mark_not_ready(:cache)
503
507
 
504
- Legion::Transport::Connection.shutdown
508
+ shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
505
509
  Legion::Readiness.mark_not_ready(:transport)
506
510
 
507
511
  shutdown_mtls_rotation
508
- Legion::Crypt.shutdown
512
+ shutdown_component('Crypt') { Legion::Crypt.shutdown }
509
513
  Legion::Readiness.mark_not_ready(:crypt)
510
514
 
511
515
  Legion::Settings[:client][:ready] = false
@@ -513,29 +517,34 @@ module Legion
513
517
  end
514
518
 
515
519
  def reload
520
+ return if @reloading
521
+
522
+ @reloading = true
516
523
  Legion::Logging.info 'Legion::Service.reload was called'
517
524
  Legion::Settings[:client][:ready] = false
518
525
 
526
+ shutdown_network_watchdog
519
527
  shutdown_api
520
528
 
521
529
  if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:started?) && Legion::Gaia.started?
522
- Legion::Gaia.shutdown
530
+ shutdown_component('Gaia') { Legion::Gaia.shutdown }
523
531
  Legion::Readiness.mark_not_ready(:gaia)
524
532
  end
525
533
 
526
- Legion::Extensions.shutdown
534
+ ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
535
+ shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
527
536
  Legion::Readiness.mark_not_ready(:extensions)
528
537
 
529
- Legion::Data.shutdown
538
+ shutdown_component('Data') { Legion::Data.shutdown }
530
539
  Legion::Readiness.mark_not_ready(:data)
531
540
 
532
- Legion::Cache.shutdown
541
+ shutdown_component('Cache') { Legion::Cache.shutdown }
533
542
  Legion::Readiness.mark_not_ready(:cache)
534
543
 
535
- Legion::Transport::Connection.shutdown
544
+ shutdown_component('Transport') { Legion::Transport::Connection.shutdown }
536
545
  Legion::Readiness.mark_not_ready(:transport)
537
546
 
538
- Legion::Crypt.shutdown
547
+ shutdown_component('Crypt') { Legion::Crypt.shutdown }
539
548
  Legion::Readiness.mark_not_ready(:crypt)
540
549
 
541
550
  Legion::Readiness.wait_until_not_ready(:transport, :data, :cache, :crypt)
@@ -569,9 +578,12 @@ module Legion
569
578
 
570
579
  Legion::Crypt.cs
571
580
  setup_api if @api_enabled
581
+ setup_network_watchdog
572
582
  Legion::Settings[:client][:ready] = true
573
583
  Legion::Events.emit('service.ready')
574
584
  Legion::Logging.info 'Legion has been reloaded'
585
+ ensure
586
+ @reloading = false
575
587
  end
576
588
 
577
589
  def load_extensions
@@ -630,6 +642,67 @@ module Legion
630
642
  nil
631
643
  end
632
644
 
645
+ def shutdown_component(name, timeout: 5, &)
646
+ Timeout.timeout(timeout, &)
647
+ rescue Timeout::Error
648
+ Legion::Logging.warn "#{name} shutdown timed out after #{timeout}s, forcing"
649
+ rescue StandardError => e
650
+ Legion::Logging.warn "#{name} shutdown error: #{e.class}: #{e.message}"
651
+ end
652
+
653
+ def setup_network_watchdog
654
+ return unless Legion::Settings.dig(:network, :watchdog, :enabled)
655
+
656
+ @consecutive_failures = Concurrent::AtomicFixnum.new(0)
657
+ threshold = Legion::Settings.dig(:network, :watchdog, :failure_threshold) || 5
658
+ interval = Legion::Settings.dig(:network, :watchdog, :check_interval) || 15
659
+
660
+ @network_watchdog = Concurrent::TimerTask.new(execution_interval: interval) do
661
+ if network_healthy?
662
+ prev = @consecutive_failures.value
663
+ @consecutive_failures.value = 0
664
+ if prev >= threshold
665
+ Legion::Logging.info '[Watchdog] Network restored, triggering reload'
666
+ Thread.new { Legion.reload } unless @reloading
667
+ end
668
+ else
669
+ count = @consecutive_failures.increment
670
+ Legion::Logging.warn "[Watchdog] Network check failed (#{count}/#{threshold})"
671
+ if count == threshold
672
+ Legion::Logging.error '[Watchdog] Network failure threshold reached, pausing actors'
673
+ Legion::Extensions.pause_actors if Legion::Extensions.respond_to?(:pause_actors)
674
+ end
675
+ end
676
+ rescue StandardError => e
677
+ Legion::Logging.debug "[Watchdog] check error: #{e.message}"
678
+ end
679
+ @network_watchdog.execute
680
+ Legion::Logging.info "[Watchdog] Network watchdog started (interval=#{interval}s, threshold=#{threshold})"
681
+ rescue StandardError => e
682
+ Legion::Logging.warn "Network watchdog setup failed: #{e.message}"
683
+ end
684
+
685
+ def shutdown_network_watchdog
686
+ @network_watchdog&.shutdown
687
+ @network_watchdog = nil
688
+ end
689
+
690
+ def network_healthy?
691
+ return true if defined?(Legion::Transport::Connection) && Legion::Transport::Connection.lite_mode?
692
+
693
+ checks = []
694
+ checks << Legion::Transport::Connection.session_open? if Legion::Settings[:transport][:connected]
695
+ if Legion::Settings[:data][:connected] && defined?(Legion::Data::Connection)
696
+ checks << (Legion::Data::Connection.sequel&.test_connection rescue false) # rubocop:disable Style/RescueModifier
697
+ end
698
+ checks << Legion::Cache.connected? if Legion::Settings[:cache][:connected] && defined?(Legion::Cache)
699
+ return true if checks.empty?
700
+
701
+ checks.any?
702
+ rescue StandardError
703
+ false
704
+ end
705
+
633
706
  private
634
707
 
635
708
  def port_in_use?(bind, port)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.5.13'
4
+ VERSION = '1.5.14'
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.13
4
+ version: 1.5.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity