lex-scheduler 0.2.0 → 0.3.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/.gitignore +1 -0
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +8 -6
- data/lib/legion/extensions/scheduler/client.rb +31 -0
- data/lib/legion/extensions/scheduler/data/migrations/001_schedule_table.rb +13 -21
- data/lib/legion/extensions/scheduler/data/migrations/002_schedule_log.rb +10 -16
- data/lib/legion/extensions/scheduler/data/migrations/005_add_payload_column.rb +1 -1
- data/lib/legion/extensions/scheduler/data/models/schedule_log.rb +2 -4
- data/lib/legion/extensions/scheduler/runners/schedule.rb +16 -4
- data/lib/legion/extensions/scheduler/transport/messages/refresh.rb +0 -9
- data/lib/legion/extensions/scheduler/transport/queues/schedule.rb +1 -1
- data/lib/legion/extensions/scheduler/version.rb +1 -1
- data/lib/legion/extensions/scheduler.rb +0 -4
- metadata +2 -5
- data/Gemfile.lock +0 -166
- data/lib/legion/extensions/scheduler/runners/emergency_promotion.rb +0 -41
- data/lib/legion/extensions/scheduler/runners/mode_scheduler.rb +0 -67
- data/lib/legion/extensions/scheduler/runners/mode_transition.rb +0 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17c661a4da86c3c66a0d66f54c6339e2724766bc2ae0231fadf5f167675e6c28
|
|
4
|
+
data.tar.gz: f73724797cd54b5932d57d0e83aae28f6f6cf2197312665771887cce22861371
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 320d897e6b2cb4f78f4b1cb52b927cd89ae88d6fb900a35612b0334f566ab96d24009d230f5f88f126367cd3d4065cd9161d2fa957d3e80adc0a52c3bd37bb10
|
|
7
|
+
data.tar.gz: c3950163ef797ffc297276efd96516eb39406741be6bc9d8d68e089e91a44b819685ba9c0f333ec0a4cbc5f3b3d0afc54d393aa96e3ea06877255cd78632c642
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-03-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Migrations 001/002 rewritten as Sequel DSL (cross-DB: SQLite, PostgreSQL, MySQL)
|
|
7
|
+
- Migration 005 column type `File` -> `String, text: true`
|
|
8
|
+
- ScheduleLog model class name (was defining duplicate `Schedule`)
|
|
9
|
+
- Queue TTL from 5ms to 5000ms (messages were expiring instantly)
|
|
10
|
+
- Nil guard on `last_run` (was TypeError on new schedules)
|
|
11
|
+
- Nil guard on function lookup (was NoMethodError on missing function)
|
|
12
|
+
- Removed dead cron guard (`Time.now < previous_time` was always false)
|
|
13
|
+
- ScheduleLog records now created after each dispatch
|
|
14
|
+
- Entry point `data_required?` is now class method only (framework requirement)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Standalone `Scheduler::Client` for programmatic schedule management
|
|
18
|
+
- Comprehensive spec coverage (92%+)
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- ModeScheduler, ModeTransition, EmergencyPromotion runners (dead code, no actor wiring)
|
|
22
|
+
- Dead `message_example` in Refresh message (copy-paste from lex-node)
|
|
23
|
+
|
|
3
24
|
## [0.2.0] - 2026-03-17
|
|
4
25
|
|
|
5
26
|
### Added
|
data/CLAUDE.md
CHANGED
data/Gemfile
CHANGED
|
@@ -4,9 +4,11 @@ source 'https://rubygems.org'
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
gem '
|
|
9
|
-
gem '
|
|
10
|
-
gem '
|
|
11
|
-
gem '
|
|
12
|
-
gem 'simplecov'
|
|
7
|
+
group :test do
|
|
8
|
+
gem 'rake'
|
|
9
|
+
gem 'rspec', '~> 3.13'
|
|
10
|
+
gem 'rubocop', '~> 1.75'
|
|
11
|
+
gem 'sequel'
|
|
12
|
+
gem 'simplecov'
|
|
13
|
+
gem 'sqlite3'
|
|
14
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fugit'
|
|
4
|
+
require_relative 'runners/schedule'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Scheduler
|
|
9
|
+
class Client
|
|
10
|
+
include Runners::Schedule
|
|
11
|
+
|
|
12
|
+
def initialize(data_model: nil, fugit: nil)
|
|
13
|
+
@data_model = data_model
|
|
14
|
+
@fugit = fugit || Fugit
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def models_class
|
|
18
|
+
@data_model || Data::Model
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log
|
|
22
|
+
@log ||= defined?(Legion::Logging) ? Legion::Logging : Logger.new($stdout)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def settings
|
|
26
|
+
{ options: {} }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Sequel.migration do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
updated datetime null,
|
|
18
|
-
constraint schedules_pk
|
|
19
|
-
primary key (id)
|
|
20
|
-
);'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
down do
|
|
24
|
-
drop_table :schedules
|
|
4
|
+
change do
|
|
5
|
+
create_table(:schedules) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
foreign_key :function_id, :functions, null: true
|
|
8
|
+
String :name, null: false
|
|
9
|
+
Integer :interval, null: true
|
|
10
|
+
String :cron, null: true, text: true
|
|
11
|
+
Integer :task_ttl, null: true
|
|
12
|
+
TrueClass :active, default: true
|
|
13
|
+
DateTime :last_run, null: true
|
|
14
|
+
DateTime :created, default: Sequel::CURRENT_TIMESTAMP
|
|
15
|
+
DateTime :updated, null: true
|
|
16
|
+
end
|
|
25
17
|
end
|
|
26
18
|
end
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Sequel.migration do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
constraint schedule_logs_pk primary key (id)
|
|
15
|
-
);'
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
down do
|
|
19
|
-
drop_table :schedule_logs
|
|
4
|
+
change do
|
|
5
|
+
create_table(:schedule_logs) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
foreign_key :schedule_id, :schedules, null: true
|
|
8
|
+
foreign_key :task_id, :tasks, null: true
|
|
9
|
+
foreign_key :function_id, :functions, null: true
|
|
10
|
+
TrueClass :success, null: true
|
|
11
|
+
String :status, null: true
|
|
12
|
+
DateTime :created, default: Sequel::CURRENT_TIMESTAMP
|
|
13
|
+
end
|
|
20
14
|
end
|
|
21
15
|
end
|
|
@@ -5,10 +5,8 @@ module Legion
|
|
|
5
5
|
module Scheduler
|
|
6
6
|
module Data
|
|
7
7
|
module Model
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
many_to_one :task
|
|
11
|
-
many_to_one :function
|
|
8
|
+
class ScheduleLog < Sequel::Model(:schedule_logs)
|
|
9
|
+
many_to_one :schedule, class: '::Legion::Extensions::Scheduler::Data::Model::Schedule'
|
|
12
10
|
end
|
|
13
11
|
end
|
|
14
12
|
end
|
|
@@ -25,25 +25,37 @@ module Legion
|
|
|
25
25
|
return unless Legion::Cache.get('scheduler_schedule_lock') == Legion::Settings[:client][:name]
|
|
26
26
|
|
|
27
27
|
models_class::Schedule.where(active: 1).each do |row|
|
|
28
|
+
last_run = row.values[:last_run] || Time.at(0)
|
|
29
|
+
|
|
28
30
|
if row.values[:interval].is_a?(Integer) && row.values[:interval].positive?
|
|
29
|
-
next if (Time.now -
|
|
31
|
+
next if (Time.now - last_run) < row.values[:interval]
|
|
30
32
|
elsif row.values[:cron].is_a? String
|
|
31
33
|
cron_class = Fugit.parse(row.values[:cron])
|
|
32
34
|
if cron_class.respond_to? :to_sec
|
|
33
|
-
next if (Time.now -
|
|
35
|
+
next if (Time.now - last_run) < cron_class.to_sec
|
|
34
36
|
elsif cron_class.respond_to? :previous_time
|
|
35
|
-
|
|
36
|
-
next if
|
|
37
|
+
prev = Time.parse(cron_class.previous_time.to_s)
|
|
38
|
+
next if last_run > prev
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
function = Legion::Data::Model::Function[row.values[:function_id]]
|
|
43
|
+
next unless function
|
|
41
44
|
|
|
42
45
|
send_task(transformation: row.values[:transformation],
|
|
43
46
|
function_id: row.values[:function_id],
|
|
44
47
|
expiration: row.values[:task_ttl],
|
|
45
48
|
function: function.values[:name],
|
|
46
49
|
**Legion::JSON.load(row.values[:payload]))
|
|
50
|
+
|
|
51
|
+
models_class::ScheduleLog.insert(
|
|
52
|
+
schedule_id: row.values[:id],
|
|
53
|
+
function_id: row.values[:function_id],
|
|
54
|
+
success: true,
|
|
55
|
+
status: 'dispatched',
|
|
56
|
+
created: Sequel::CURRENT_TIMESTAMP
|
|
57
|
+
)
|
|
58
|
+
|
|
47
59
|
row.update(last_run: Sequel::CURRENT_TIMESTAMP)
|
|
48
60
|
end
|
|
49
61
|
end
|
|
@@ -28,15 +28,6 @@ module Legion
|
|
|
28
28
|
runner_class: 'Legion::Extensions::Scheduler::Runners::Schedule'
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
|
-
|
|
32
|
-
def message_example
|
|
33
|
-
{ function: 'push_cluster_secret',
|
|
34
|
-
node_name: Legion::Settings[:client][:name],
|
|
35
|
-
queue_name: "node.#{Legion::Settings[:client][:name]}",
|
|
36
|
-
runner_class: 'Legion::Extensions::Node::Runners::Crypt',
|
|
37
|
-
# public_key: Base64.encode64(Legion::Crypt.public_key) }
|
|
38
|
-
public_key: Legion::Crypt.public_key }
|
|
39
|
-
end
|
|
40
31
|
end
|
|
41
32
|
end
|
|
42
33
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-scheduler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -38,7 +38,6 @@ files:
|
|
|
38
38
|
- CLAUDE.md
|
|
39
39
|
- Dockerfile
|
|
40
40
|
- Gemfile
|
|
41
|
-
- Gemfile.lock
|
|
42
41
|
- LICENSE.txt
|
|
43
42
|
- README.md
|
|
44
43
|
- Rakefile
|
|
@@ -47,6 +46,7 @@ files:
|
|
|
47
46
|
- lib/legion/extensions/scheduler.rb
|
|
48
47
|
- lib/legion/extensions/scheduler/actors/run_scheduler.rb
|
|
49
48
|
- lib/legion/extensions/scheduler/actors/schedule_task.rb
|
|
49
|
+
- lib/legion/extensions/scheduler/client.rb
|
|
50
50
|
- lib/legion/extensions/scheduler/data/migrations/001_schedule_table.rb
|
|
51
51
|
- lib/legion/extensions/scheduler/data/migrations/002_schedule_log.rb
|
|
52
52
|
- lib/legion/extensions/scheduler/data/migrations/003_schedule_indexes.rb
|
|
@@ -55,9 +55,6 @@ files:
|
|
|
55
55
|
- lib/legion/extensions/scheduler/data/migrations/006_add_transform_to_schedule.rb
|
|
56
56
|
- lib/legion/extensions/scheduler/data/models/schedule.rb
|
|
57
57
|
- lib/legion/extensions/scheduler/data/models/schedule_log.rb
|
|
58
|
-
- lib/legion/extensions/scheduler/runners/emergency_promotion.rb
|
|
59
|
-
- lib/legion/extensions/scheduler/runners/mode_scheduler.rb
|
|
60
|
-
- lib/legion/extensions/scheduler/runners/mode_transition.rb
|
|
61
58
|
- lib/legion/extensions/scheduler/runners/schedule.rb
|
|
62
59
|
- lib/legion/extensions/scheduler/transport/messages/refresh.rb
|
|
63
60
|
- lib/legion/extensions/scheduler/transport/messages/send_task.rb
|
data/Gemfile.lock
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
lex-scheduler (0.2.0)
|
|
5
|
-
fugit (>= 1.9)
|
|
6
|
-
|
|
7
|
-
GEM
|
|
8
|
-
remote: https://rubygems.org/
|
|
9
|
-
specs:
|
|
10
|
-
addressable (2.8.9)
|
|
11
|
-
public_suffix (>= 2.0.2, < 8.0)
|
|
12
|
-
amq-protocol (2.5.1)
|
|
13
|
-
ast (2.4.3)
|
|
14
|
-
aws-eventstream (1.4.0)
|
|
15
|
-
aws-sigv4 (1.12.1)
|
|
16
|
-
aws-eventstream (~> 1, >= 1.0.2)
|
|
17
|
-
base64 (0.3.0)
|
|
18
|
-
bigdecimal (4.0.1)
|
|
19
|
-
bunny (2.24.0)
|
|
20
|
-
amq-protocol (~> 2.3)
|
|
21
|
-
sorted_set (~> 1, >= 1.0.2)
|
|
22
|
-
concurrent-ruby (1.3.6)
|
|
23
|
-
concurrent-ruby-ext (1.3.6)
|
|
24
|
-
concurrent-ruby (= 1.3.6)
|
|
25
|
-
connection_pool (2.5.5)
|
|
26
|
-
daemons (1.4.1)
|
|
27
|
-
dalli (5.0.2)
|
|
28
|
-
logger
|
|
29
|
-
diff-lcs (1.6.2)
|
|
30
|
-
docile (1.4.1)
|
|
31
|
-
et-orbi (1.4.0)
|
|
32
|
-
tzinfo
|
|
33
|
-
fugit (1.12.1)
|
|
34
|
-
et-orbi (~> 1.4)
|
|
35
|
-
raabro (~> 1.4)
|
|
36
|
-
json (2.19.1)
|
|
37
|
-
json-schema (6.2.0)
|
|
38
|
-
addressable (~> 2.8)
|
|
39
|
-
bigdecimal (>= 3.1, < 5)
|
|
40
|
-
json_pure (2.8.1)
|
|
41
|
-
language_server-protocol (3.17.0.5)
|
|
42
|
-
legion-cache (1.2.0)
|
|
43
|
-
connection_pool (>= 2.2.3)
|
|
44
|
-
dalli (>= 2.7)
|
|
45
|
-
legion-logging
|
|
46
|
-
legion-settings
|
|
47
|
-
redis (>= 4.2)
|
|
48
|
-
legion-crypt (1.2.0)
|
|
49
|
-
vault (>= 0.15.0)
|
|
50
|
-
legion-json (1.2.0)
|
|
51
|
-
json_pure
|
|
52
|
-
multi_json
|
|
53
|
-
legion-logging (1.2.0)
|
|
54
|
-
rainbow (~> 3)
|
|
55
|
-
legion-settings (1.2.0)
|
|
56
|
-
legion-json (>= 1.2)
|
|
57
|
-
legion-transport (1.2.0)
|
|
58
|
-
bunny (>= 2.17.0)
|
|
59
|
-
concurrent-ruby (>= 1.1.7)
|
|
60
|
-
legion-json
|
|
61
|
-
legion-settings
|
|
62
|
-
legionio (1.2.1)
|
|
63
|
-
concurrent-ruby (>= 1.1.7)
|
|
64
|
-
concurrent-ruby-ext (>= 1.1.7)
|
|
65
|
-
daemons (>= 1.3.1)
|
|
66
|
-
legion-cache (>= 0.2.0)
|
|
67
|
-
legion-crypt (>= 0.2.0)
|
|
68
|
-
legion-json (>= 0.2.0)
|
|
69
|
-
legion-logging (>= 0.2.0)
|
|
70
|
-
legion-settings (>= 0.2.0)
|
|
71
|
-
legion-transport (>= 1.1.9)
|
|
72
|
-
lex-node
|
|
73
|
-
oj (>= 3.10)
|
|
74
|
-
thor (>= 1)
|
|
75
|
-
lex-node (0.2.0)
|
|
76
|
-
lint_roller (1.1.0)
|
|
77
|
-
logger (1.7.0)
|
|
78
|
-
mcp (0.8.0)
|
|
79
|
-
json-schema (>= 4.1)
|
|
80
|
-
multi_json (1.19.1)
|
|
81
|
-
net-http-persistent (4.0.8)
|
|
82
|
-
connection_pool (>= 2.2.4, < 4)
|
|
83
|
-
oj (3.16.16)
|
|
84
|
-
bigdecimal (>= 3.0)
|
|
85
|
-
ostruct (>= 0.2)
|
|
86
|
-
ostruct (0.6.3)
|
|
87
|
-
parallel (1.27.0)
|
|
88
|
-
parser (3.3.10.2)
|
|
89
|
-
ast (~> 2.4.1)
|
|
90
|
-
racc
|
|
91
|
-
prism (1.9.0)
|
|
92
|
-
public_suffix (7.0.5)
|
|
93
|
-
raabro (1.4.0)
|
|
94
|
-
racc (1.8.1)
|
|
95
|
-
rainbow (3.1.1)
|
|
96
|
-
rake (13.3.1)
|
|
97
|
-
rbtree (0.4.6)
|
|
98
|
-
redis (5.4.1)
|
|
99
|
-
redis-client (>= 0.22.0)
|
|
100
|
-
redis-client (0.27.0)
|
|
101
|
-
connection_pool
|
|
102
|
-
regexp_parser (2.11.3)
|
|
103
|
-
rspec (3.13.2)
|
|
104
|
-
rspec-core (~> 3.13.0)
|
|
105
|
-
rspec-expectations (~> 3.13.0)
|
|
106
|
-
rspec-mocks (~> 3.13.0)
|
|
107
|
-
rspec-core (3.13.6)
|
|
108
|
-
rspec-support (~> 3.13.0)
|
|
109
|
-
rspec-expectations (3.13.5)
|
|
110
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
111
|
-
rspec-support (~> 3.13.0)
|
|
112
|
-
rspec-mocks (3.13.8)
|
|
113
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
|
114
|
-
rspec-support (~> 3.13.0)
|
|
115
|
-
rspec-support (3.13.7)
|
|
116
|
-
rubocop (1.85.1)
|
|
117
|
-
json (~> 2.3)
|
|
118
|
-
language_server-protocol (~> 3.17.0.2)
|
|
119
|
-
lint_roller (~> 1.1.0)
|
|
120
|
-
mcp (~> 0.6)
|
|
121
|
-
parallel (~> 1.10)
|
|
122
|
-
parser (>= 3.3.0.2)
|
|
123
|
-
rainbow (>= 2.2.2, < 4.0)
|
|
124
|
-
regexp_parser (>= 2.9.3, < 3.0)
|
|
125
|
-
rubocop-ast (>= 1.49.0, < 2.0)
|
|
126
|
-
ruby-progressbar (~> 1.7)
|
|
127
|
-
unicode-display_width (>= 2.4.0, < 4.0)
|
|
128
|
-
rubocop-ast (1.49.1)
|
|
129
|
-
parser (>= 3.3.7.2)
|
|
130
|
-
prism (~> 1.7)
|
|
131
|
-
ruby-progressbar (1.13.0)
|
|
132
|
-
simplecov (0.22.0)
|
|
133
|
-
docile (~> 1.1)
|
|
134
|
-
simplecov-html (~> 0.11)
|
|
135
|
-
simplecov_json_formatter (~> 0.1)
|
|
136
|
-
simplecov-html (0.13.2)
|
|
137
|
-
simplecov_json_formatter (0.1.4)
|
|
138
|
-
sorted_set (1.1.0)
|
|
139
|
-
rbtree
|
|
140
|
-
thor (1.5.0)
|
|
141
|
-
tzinfo (2.0.6)
|
|
142
|
-
concurrent-ruby (~> 1.0)
|
|
143
|
-
unicode-display_width (3.2.0)
|
|
144
|
-
unicode-emoji (~> 4.1)
|
|
145
|
-
unicode-emoji (4.2.0)
|
|
146
|
-
vault (0.20.0)
|
|
147
|
-
aws-sigv4
|
|
148
|
-
base64
|
|
149
|
-
connection_pool (~> 2.4)
|
|
150
|
-
net-http-persistent (~> 4.0, >= 4.0.2)
|
|
151
|
-
|
|
152
|
-
PLATFORMS
|
|
153
|
-
arm64-darwin-25
|
|
154
|
-
ruby
|
|
155
|
-
|
|
156
|
-
DEPENDENCIES
|
|
157
|
-
bundler (>= 2)
|
|
158
|
-
legionio
|
|
159
|
-
lex-scheduler!
|
|
160
|
-
rake
|
|
161
|
-
rspec
|
|
162
|
-
rubocop
|
|
163
|
-
simplecov
|
|
164
|
-
|
|
165
|
-
BUNDLED WITH
|
|
166
|
-
2.6.9
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Scheduler
|
|
6
|
-
module Runners
|
|
7
|
-
module EmergencyPromotion
|
|
8
|
-
DEFAULT_PATTERNS = %w[extinction.* governance.consent_violation governance.shadow_ai_detected].freeze
|
|
9
|
-
|
|
10
|
-
def check_emergency(event_name:, **)
|
|
11
|
-
return { promoted: false, reason: 'not_emergency' } unless emergency_pattern?(event_name)
|
|
12
|
-
return { promoted: false, reason: 'already_active' } if current_mode == :active
|
|
13
|
-
|
|
14
|
-
result = transition_to(target_mode: :active, reason: "emergency:#{event_name}", force: true)
|
|
15
|
-
log_emergency(event_name)
|
|
16
|
-
{ promoted: result[:transitioned], event: event_name, transition: result }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def emergency_pattern?(event_name)
|
|
22
|
-
patterns = emergency_patterns
|
|
23
|
-
patterns.any? { |p| File.fnmatch?(p, event_name) }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def log_emergency(event_name)
|
|
27
|
-
return unless defined?(Legion::Logging) && Legion::Logging.respond_to?(:warn, true)
|
|
28
|
-
|
|
29
|
-
Legion::Logging.send(:warn, "[scheduler] Emergency promotion: #{event_name}")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def emergency_patterns
|
|
33
|
-
settings = scheduler_settings
|
|
34
|
-
patterns = settings[:emergency_patterns]
|
|
35
|
-
patterns.is_a?(Array) ? patterns : DEFAULT_PATTERNS
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Scheduler
|
|
6
|
-
module Runners
|
|
7
|
-
module ModeScheduler
|
|
8
|
-
MODES = {
|
|
9
|
-
active: { tick_interval: 1, cognitive: true, dream: false },
|
|
10
|
-
idle: { tick_interval: 5, cognitive: true, dream: false },
|
|
11
|
-
dream: { tick_interval: 10, cognitive: false, dream: true },
|
|
12
|
-
maintenance: { tick_interval: 0, cognitive: false, dream: false }
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
def evaluate_schedule(current_time: Time.now, **)
|
|
16
|
-
schedules = load_schedules
|
|
17
|
-
applicable = schedules.select { |s| matches_time?(s[:schedule], current_time) }
|
|
18
|
-
|
|
19
|
-
return { mode: :idle, reason: 'no_matching_schedule', mode_config: MODES[:idle] } if applicable.empty?
|
|
20
|
-
|
|
21
|
-
winner = applicable.max_by { |s| s[:priority] || 0 }
|
|
22
|
-
mode = winner[:mode].to_sym
|
|
23
|
-
{ mode: mode, reason: 'scheduled', schedule: winner, mode_config: MODES[mode] || MODES[:idle] }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def matches_time?(schedule, time)
|
|
29
|
-
case schedule
|
|
30
|
-
when 'default'
|
|
31
|
-
true
|
|
32
|
-
when /\Aweekday:(\d+)-(\d+)\z/
|
|
33
|
-
start_h = ::Regexp.last_match(1).to_i
|
|
34
|
-
end_h = ::Regexp.last_match(2).to_i
|
|
35
|
-
(1..5).include?(time.wday) && time.hour >= start_h && time.hour < end_h
|
|
36
|
-
when /\Aweekend:(\d+)-(\d+)\z/
|
|
37
|
-
start_h = ::Regexp.last_match(1).to_i
|
|
38
|
-
end_h = ::Regexp.last_match(2).to_i
|
|
39
|
-
[0, 6].include?(time.wday) && time.hour >= start_h && time.hour < end_h
|
|
40
|
-
when /\Adaily:(\d+)-(\d+)\z/
|
|
41
|
-
start_h = ::Regexp.last_match(1).to_i
|
|
42
|
-
end_h = ::Regexp.last_match(2).to_i
|
|
43
|
-
time.hour >= start_h && time.hour < end_h
|
|
44
|
-
else
|
|
45
|
-
false
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def load_schedules
|
|
50
|
-
settings = scheduler_settings
|
|
51
|
-
raw = settings[:mode_schedule]
|
|
52
|
-
return [] unless raw.is_a?(Array)
|
|
53
|
-
|
|
54
|
-
raw.map { |s| s.transform_keys(&:to_sym) }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def scheduler_settings
|
|
58
|
-
settings = Legion::Settings[:scheduler]
|
|
59
|
-
settings.is_a?(Hash) ? settings : {}
|
|
60
|
-
rescue StandardError
|
|
61
|
-
{}
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Scheduler
|
|
6
|
-
module Runners
|
|
7
|
-
module ModeTransition
|
|
8
|
-
def transition_to(target_mode:, reason: 'scheduled', force: false, **)
|
|
9
|
-
current = current_mode
|
|
10
|
-
target = target_mode.to_sym
|
|
11
|
-
|
|
12
|
-
return { transitioned: false, reason: 'already_in_mode' } if current == target
|
|
13
|
-
return { transitioned: false, reason: 'blocked_by_critical' } if !force && critical_tasks_active?
|
|
14
|
-
|
|
15
|
-
apply_mode(target)
|
|
16
|
-
emit_transition_event(from: current, to: target, reason: reason)
|
|
17
|
-
{ transitioned: true, from: current, to: target, reason: reason }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def current_mode(**)
|
|
21
|
-
if defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:mode)
|
|
22
|
-
Legion::Gaia.mode
|
|
23
|
-
else
|
|
24
|
-
:active
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def critical_tasks_active?
|
|
31
|
-
return false unless defined?(Legion::Data::Model::Task)
|
|
32
|
-
|
|
33
|
-
Legion::Data::Model::Task.where(status: 'running', priority: 'critical').any?
|
|
34
|
-
rescue StandardError
|
|
35
|
-
false
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def apply_mode(mode)
|
|
39
|
-
config = ModeScheduler::MODES[mode] || ModeScheduler::MODES[:idle]
|
|
40
|
-
return unless defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:heartbeat_interval=)
|
|
41
|
-
|
|
42
|
-
Legion::Gaia.heartbeat_interval = config[:tick_interval]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def emit_transition_event(from:, to:, reason:)
|
|
46
|
-
return unless defined?(Legion::Events)
|
|
47
|
-
|
|
48
|
-
Legion::Events.emit('scheduler.mode_changed', {
|
|
49
|
-
from: from, to: to, reason: reason, changed_at: Time.now.utc
|
|
50
|
-
})
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|