legionio 1.7.13 → 1.7.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: 42d28272b4f314a736fdd339106511ef743107d7df2b1a771d403a8312d66135
4
- data.tar.gz: aa616bda3a27ae36e9524a5dcdc4f027d730274cd04f4e45a1142accc638365c
3
+ metadata.gz: 86fc5b0df46813866069443960f455bfd54c68eab894e96e23c763ef1c3356e2
4
+ data.tar.gz: f3881012c84c5eaf039ae0c46243e9a97d5aa2ef53092a5b733a1cdeeacf9d08
5
5
  SHA512:
6
- metadata.gz: e1b1d5265860f317faf0da4a23e5e25373e290e4b4456acc0ceffdd3b6f3b8f65f97a5d78c0ca199f7882ab1776e9a17cc585a11dd8eb3ac8f75d6b9ef4b7772
7
- data.tar.gz: bd82d4424862d625639be08c340686b0eb830f1cd4002116c69dd525116576685761c0624b6dddfa19a789b6c98561444a88c89e8a8a002a846c3ca01494aa8a
6
+ metadata.gz: f191aabb91a76ac66d6d71890c2fcdc273f4cbd60f77768a024064b8f58386a6e2791073364c6deb93dfd95b5a8fb8485b0b26c7d0dad67eb5c3d40a1c337289
7
+ data.tar.gz: bb61732fe43b41f23e8bb96defb087c206d5d397bd165e3c6eef5c16f9b12e542f88de846639fb767300c5050960f0b7298d83be3d720ab80d5d11096a1008ae
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.7.14] - 2026-04-03
6
+
7
+ ### Fixed
8
+ - Actor boot ordering: once → poll → every → loop → subscriptions, preventing timer actors from competing with AMQP channel setup
9
+ - Builder now respects `remote_invocable? false` and skips auto-generated subscription actors for local-only extensions
10
+ - Catalog exchange cached and reused instead of creating a new channel + exchange_declare per transition
11
+ - Catalog SQLite persists batched into a single transaction at end of boot instead of per-transition writes from concurrent threads
12
+
5
13
  ## [1.7.13] - 2026-04-03
6
14
 
7
15
  ### Changed
@@ -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.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.7.13
4
+ version: 1.7.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity