legionio 1.7.13 → 1.7.15

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: 42d28272b4f314a736fdd339106511ef743107d7df2b1a771d403a8312d66135
4
- data.tar.gz: aa616bda3a27ae36e9524a5dcdc4f027d730274cd04f4e45a1142accc638365c
3
+ metadata.gz: de3b6cd94efb3c63cc92dfcd54303a89fa9517b9a525349b2a40750373e6cb2f
4
+ data.tar.gz: 8cbee81b7503b87a10502b47b308d614e1c22a2675f881c110c9cc8b753561c5
5
5
  SHA512:
6
- metadata.gz: e1b1d5265860f317faf0da4a23e5e25373e290e4b4456acc0ceffdd3b6f3b8f65f97a5d78c0ca199f7882ab1776e9a17cc585a11dd8eb3ac8f75d6b9ef4b7772
7
- data.tar.gz: bd82d4424862d625639be08c340686b0eb830f1cd4002116c69dd525116576685761c0624b6dddfa19a789b6c98561444a88c89e8a8a002a846c3ca01494aa8a
6
+ metadata.gz: bbaa15bc0e0c87a1f72bc8efc84f005e6a1f6b02bb56464436e7cc3d80ec67a9f691107561b3a817d2bb63b5be0f3c0c2b081a5595a491daa29db5328bffb2f2
7
+ data.tar.gz: af119a4f87785f6bc2f05b2c01977e7d352882f20fb1407b6aef7e3ed08f1d96ab5c37f8814f6bd7eb403b554cd9cfb7ea0aa71e46509703e3d34dfe6f6ced2a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.7.15] - 2026-04-03
6
+
7
+ ### Added
8
+ - Every actors now support `delay` method to defer timer start (used by lex-microsoft_teams)
9
+ - Request logger emits `[api][request-start]` on inbound, warns on responses > 5s
10
+
11
+ ### Changed
12
+ - `/api/reload` disabled (returns 418) to prevent accidental full-restart loops
13
+
14
+ ## [1.7.14] - 2026-04-03
15
+
16
+ ### Fixed
17
+ - Actor boot ordering: once → poll → every → loop → subscriptions, preventing timer actors from competing with AMQP channel setup
18
+ - Builder now respects `remote_invocable? false` and skips auto-generated subscription actors for local-only extensions
19
+ - Catalog exchange cached and reused instead of creating a new channel + exchange_declare per transition
20
+ - Catalog SQLite persists batched into a single transaction at end of boot instead of per-transition writes from concurrent threads
21
+
5
22
  ## [1.7.13] - 2026-04-03
6
23
 
7
24
  ### Changed
@@ -9,15 +9,18 @@ module Legion
9
9
  end
10
10
 
11
11
  def call(env)
12
+ method_path = "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
13
+ Legion::Logging.info "[api][request-start] #{method_path}"
12
14
  start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
13
15
  status, headers, body = @app.call(env)
14
16
  duration = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
15
17
 
16
- Legion::Logging.info "[api] #{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{status} #{duration}ms"
18
+ level = duration > 5000 ? :warn : :info
19
+ Legion::Logging.send(level, "[api] #{method_path} #{status} #{duration}ms")
17
20
  [status, headers, body]
18
21
  rescue StandardError => e
19
22
  duration = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
20
- Legion::Logging.error "[api] #{env['REQUEST_METHOD']} #{env['PATH_INFO']} 500 #{duration}ms - #{e.message}"
23
+ Legion::Logging.error "[api] #{method_path} 500 #{duration}ms - #{e.message}"
21
24
  raise
22
25
  end
23
26
  end
data/lib/legion/api.rb CHANGED
@@ -111,8 +111,9 @@ module Legion
111
111
  end
112
112
 
113
113
  post '/api/reload' do
114
- Thread.new { Legion.reload }
115
- json_response({ status: 'reloading' })
114
+ log.error "[api] reload attempted by #{request.ip} — blocked"
115
+ halt 418, { 'Content-Type' => 'application/json' },
116
+ Legion::JSON.dump({ error: 'reload is disabled', status: 418 })
116
117
  end
117
118
 
118
119
  # Global error handlers
@@ -34,7 +34,12 @@ module Legion
34
34
  end
35
35
  end
36
36
 
37
- @timer.execute
37
+ initial_delay = respond_to?(:delay) ? delay.to_f : 0
38
+ if initial_delay.positive?
39
+ Concurrent::ScheduledTask.execute(initial_delay) { @timer.execute }
40
+ else
41
+ @timer.execute
42
+ end
38
43
  rescue StandardError => e
39
44
  handle_exception(e)
40
45
  end
@@ -37,6 +37,11 @@ module Legion
37
37
  end
38
38
 
39
39
  def build_meta_actor_list
40
+ if lex_class.respond_to?(:remote_invocable?) && !lex_class.remote_invocable?
41
+ log.debug "[Actors] skipping meta actors for #{lex_class} (remote_invocable=false)"
42
+ return
43
+ end
44
+
40
45
  @runners.each do |runner, attr|
41
46
  next if @actors[runner.to_sym].is_a? Hash
42
47
 
@@ -61,6 +61,40 @@ module Legion
61
61
  @warned_missing_extension_catalog = false
62
62
  end
63
63
 
64
+ def flush_persisted_transitions
65
+ pending = nil
66
+ @pending_persists_mutex ||= Mutex.new
67
+ @pending_persists_mutex.synchronize do
68
+ return if @pending_persists.nil? || @pending_persists.empty?
69
+
70
+ pending = @pending_persists.dup
71
+ @pending_persists.clear
72
+ end
73
+
74
+ return unless defined?(Legion::Data::Local) &&
75
+ Legion::Data::Local.respond_to?(:connected?) &&
76
+ Legion::Data::Local.connected?
77
+
78
+ ensure_local_migration_registered!
79
+ return warn_missing_extension_catalog_once unless extension_catalog_table_available?
80
+
81
+ model = Legion::Data::Local.model(:extension_catalog)
82
+ now = Time.now
83
+ Legion::Data::Local.connection.transaction do
84
+ pending.each do |lex_name, new_state|
85
+ existing = model.where(lex_name: lex_name).first
86
+ if existing
87
+ existing.update(state: new_state.to_s, updated_at: now)
88
+ else
89
+ model.insert(lex_name: lex_name, state: new_state.to_s, created_at: now, updated_at: now)
90
+ end
91
+ end
92
+ end
93
+ Legion::Logging.info "Catalog persisted #{pending.size} transitions" if defined?(Legion::Logging)
94
+ rescue StandardError => e
95
+ Legion::Logging.warn { "Catalog flush failed: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
96
+ end
97
+
64
98
  private
65
99
 
66
100
  def entries
@@ -78,30 +112,25 @@ module Legion
78
112
  timestamp: Time.now.to_i
79
113
  )
80
114
 
81
- exchange = Legion::Transport::Exchange.new('legion.catalog')
82
- exchange.publish(payload, routing_key: "legion.catalog.#{lex_name}.#{new_state}",
83
- content_type: 'application/json', persistent: true)
115
+ catalog_exchange.publish(payload, routing_key: "legion.catalog.#{lex_name}.#{new_state}",
116
+ content_type: 'application/json', persistent: true)
84
117
  rescue StandardError => e
118
+ @catalog_exchange = nil
85
119
  Legion::Logging.warn { "Catalog publish failed for #{lex_name}=#{new_state}: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
86
120
  end
87
121
 
88
- def persist_transition(lex_name, new_state)
89
- return unless defined?(Legion::Data::Local) &&
90
- Legion::Data::Local.respond_to?(:connected?) &&
91
- Legion::Data::Local.connected?
122
+ def catalog_exchange
123
+ return @catalog_exchange if @catalog_exchange&.channel&.open?
92
124
 
93
- ensure_local_migration_registered!
94
- return warn_missing_extension_catalog_once unless extension_catalog_table_available?
125
+ @catalog_exchange = Legion::Transport::Exchange.new('legion.catalog')
126
+ end
95
127
 
96
- model = Legion::Data::Local.model(:extension_catalog)
97
- existing = model.where(lex_name: lex_name).first
98
- if existing
99
- existing.update(state: new_state.to_s, updated_at: Time.now)
100
- else
101
- model.insert(lex_name: lex_name, state: new_state.to_s, created_at: Time.now, updated_at: Time.now)
128
+ def persist_transition(lex_name, new_state)
129
+ @pending_persists_mutex ||= Mutex.new
130
+ @pending_persists_mutex.synchronize do
131
+ @pending_persists ||= {}
132
+ @pending_persists[lex_name] = new_state
102
133
  end
103
- rescue StandardError => e
104
- Legion::Logging.warn { "Catalog persist failed for #{lex_name}=#{new_state}: #{e.class}: #{e.message}" } if defined?(Legion::Logging)
105
134
  end
106
135
 
107
136
  def extension_catalog_table_available?
@@ -279,16 +279,18 @@ module Legion
279
279
 
280
280
  Legion::Logging.info "Hooking #{@pending_actors.size} deferred actors"
281
281
 
282
- sub_actors = []
283
- @pending_actors.each do |actor|
284
- if actor[:actor_class].ancestors.include?(Legion::Extensions::Actors::Subscription)
285
- sub_actors << actor
286
- else
287
- hook_actor(**actor)
288
- end
289
- end
282
+ groups = group_pending_actors
290
283
 
291
- hook_subscription_actors_pooled(sub_actors) unless sub_actors.empty?
284
+ %i[once poll every loop].each do |type|
285
+ next if groups[type].empty?
286
+
287
+ Legion::Logging.info "Starting #{type} actors (#{groups[type].size})"
288
+ groups[type].each { |actor| hook_actor(**actor) }
289
+ end
290
+ unless groups[:subscription].empty?
291
+ Legion::Logging.info "Starting subscription actors (#{groups[:subscription].size})"
292
+ hook_subscription_actors_pooled(groups[:subscription])
293
+ end
292
294
  dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
293
295
 
294
296
  @pending_actors.clear
@@ -301,6 +303,33 @@ module Legion
301
303
  "local:#{@local_tasks.count}"
302
304
  )
303
305
  @loaded_extensions&.each { |name| Catalog.transition(name, :running) }
306
+ Catalog.flush_persisted_transitions
307
+ end
308
+
309
+ ACTOR_TYPE_MAP = {
310
+ Once: :once,
311
+ Poll: :poll,
312
+ Every: :every,
313
+ Loop: :loop,
314
+ Subscription: :subscription
315
+ }.freeze
316
+
317
+ def group_pending_actors
318
+ groups = { once: [], poll: [], every: [], loop: [], subscription: [] }
319
+ @pending_actors.each do |actor|
320
+ type = resolve_actor_type(actor[:actor_class])
321
+ groups[type] << actor
322
+ end
323
+ groups
324
+ end
325
+
326
+ def resolve_actor_type(actor_class)
327
+ anc = actor_class.ancestors
328
+ ACTOR_TYPE_MAP.each do |const, type|
329
+ return type if anc.include?(Legion::Extensions::Actors.const_get(const))
330
+ end
331
+ Legion::Logging.warn "Unknown actor type for #{actor_class}, defaulting to loop"
332
+ :loop
304
333
  end
305
334
 
306
335
  def hook_actor(extension:, extension_name:, actor_class:, size: 1, **opts)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.7.13'
4
+ VERSION = '1.7.15'
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.7.13
4
+ version: 1.7.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity