legionio 1.8.16 → 1.9.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 +4 -4
- data/CHANGELOG.md +36 -0
- data/lib/legion/api/extensions.rb +49 -15
- data/lib/legion/api/identity_audit.rb +17 -4
- data/lib/legion/api/stats.rb +6 -4
- data/lib/legion/cli.rb +3 -0
- data/lib/legion/extensions/actors/subscription.rb +11 -0
- data/lib/legion/extensions/handle.rb +106 -0
- data/lib/legion/extensions/handle_registry.rb +71 -0
- data/lib/legion/extensions.rb +158 -51
- data/lib/legion/identity/broker.rb +145 -28
- data/lib/legion/identity/grant.rb +24 -0
- data/lib/legion/identity/middleware.rb +16 -7
- data/lib/legion/identity/process.rb +36 -4
- data/lib/legion/identity/request.rb +3 -1
- data/lib/legion/identity/resolver.rb +442 -0
- data/lib/legion/identity/trust.rb +36 -0
- data/lib/legion/identity.rb +15 -0
- data/lib/legion/ingress.rb +18 -4
- data/lib/legion/service.rb +19 -93
- data/lib/legion/tools/discovery.rb +12 -1
- data/lib/legion/tools/registry.rb +18 -1
- data/lib/legion/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45c36457fa18952ae68b86d8b1d230a5166bdf119338ce9c9e23791b413f822b
|
|
4
|
+
data.tar.gz: 380124a7e0321717dc18751d5de61bf83b2cd255d50efa5cf07a920c7d115f5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4bbc31826f903cbc0753fb79ed0745b1322d0acc86009a7b88586cd7ca4f520bc5e97d81f7c777bb7ba72be89d7d894565403cc413eda5b6934eadf35161f1b3
|
|
7
|
+
data.tar.gz: b546b6d4bd2f78e5a5d87c4acc253c1d849bd59adbd45af28781f16ab56c1895c389145576bf9ddae0af14b0907e32a4ee560817c34d2b2a83d11ab1a05bb10a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.9.1] - 2026-04-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Extension runtime handles now expose authoritative active/latest versions, reload state, pending-reload status, hot-reload eligibility, and owned runtime resources through `/api/extension_catalog`.
|
|
9
|
+
- Extension dispatch quiescing now blocks API, ingress, and subscription runner dispatch while an extension is stopping or actively reloading.
|
|
10
|
+
- `Legion::Tools::Registry.unregister_extension` removes callable tools owned by an extension during unload/reload cleanup.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Runtime handle `loaded?` no longer reports `stopped` or `failed` extensions as loaded.
|
|
14
|
+
- Extension registration publication now happens after extension autobuild and runtime side effects complete, avoiding durable registration of failed loads.
|
|
15
|
+
- Extension runtime handles now transition to loaded only after `require` and extension side effects succeed, and multi-segment extension modules keep their hyphenated lex identity.
|
|
16
|
+
|
|
17
|
+
## [1.9.0] - 2026-04-24
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `Legion::Identity::Resolver` — composite identity resolution chain with parallel provider execution, DB persistence, and transport event publishing
|
|
21
|
+
- `Legion::Identity::Trust` — trust level enum (verified, authenticated, configured, cached, unverified)
|
|
22
|
+
- `Legion::Identity::Grant` — frozen value object for credential access auditing
|
|
23
|
+
- `Identity::Process` extended with trust, aliases, providers, profile composite state
|
|
24
|
+
- `Identity::Broker` upgraded to `[provider, qualifier]` tuple-keyed multi-instance storage with `for_context` routing and bounded async audit queue
|
|
25
|
+
- `Resolver.upgrade!` for post-boot identity trust escalation with canonical_name change support
|
|
26
|
+
- Settings client name updated from resolved identity for correct queue naming
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `setup_identity` gate relaxed to run with DB-only nodes (not just transport)
|
|
30
|
+
- `register_credential_providers` gate relaxed for DB-only nodes
|
|
31
|
+
- Reload lifecycle: `Resolver.reset!` preserves providers, re-resolves with existing registrations
|
|
32
|
+
- Middleware `system_principal` uses Resolver identity when available
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- `Request.from_auth_context` canonical normalization now matches DB constraint `^[a-z0-9][a-z0-9_-]*$`
|
|
36
|
+
- `/api/identity/audit` reads from `identity_audit_log` table instead of `AuditRecord`
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
- Legacy tree-walk identity discovery (`resolve_identity_providers`, `find_identity_providers`, `collect_identity_providers`)
|
|
40
|
+
- `identity_provider?` and `register_identity_provider` from extensions.rb
|
|
5
41
|
|
|
6
42
|
## [1.8.16] - 2026-04-22
|
|
7
43
|
|
|
@@ -22,33 +22,21 @@ module Legion
|
|
|
22
22
|
|
|
23
23
|
def self.register_extension_routes(app)
|
|
24
24
|
app.get '/api/extension_catalog' do
|
|
25
|
-
entries =
|
|
26
|
-
{ name: name, state: entry[:state].to_s,
|
|
27
|
-
registered_at: entry[:registered_at]&.iso8601,
|
|
28
|
-
started_at: entry[:started_at]&.iso8601 }
|
|
29
|
-
end
|
|
25
|
+
entries = Routes::Extensions.extension_entries
|
|
30
26
|
entries = entries.select { |e| e[:state] == params[:state] } if params[:state]
|
|
31
27
|
json_response(entries)
|
|
32
28
|
end
|
|
33
29
|
|
|
34
30
|
app.get '/api/extension_catalog/:name' do
|
|
35
31
|
name = params[:name]
|
|
36
|
-
entry =
|
|
32
|
+
entry = Routes::Extensions.extension_entry(name)
|
|
37
33
|
halt_not_found("extension '#{name}' not found") unless entry
|
|
38
34
|
|
|
39
35
|
ext_mod = find_extension_module(name)
|
|
40
|
-
version = ext_mod&.const_defined?(:VERSION) ? ext_mod::VERSION : nil
|
|
41
36
|
|
|
42
37
|
runners = ext_mod ? runner_summaries(ext_mod) : []
|
|
43
38
|
|
|
44
|
-
json_response(
|
|
45
|
-
name: name,
|
|
46
|
-
state: entry[:state].to_s,
|
|
47
|
-
version: version,
|
|
48
|
-
registered_at: entry[:registered_at]&.iso8601,
|
|
49
|
-
started_at: entry[:started_at]&.iso8601,
|
|
50
|
-
runners: runners
|
|
51
|
-
}.compact)
|
|
39
|
+
json_response(entry.merge(runners: runners).compact)
|
|
52
40
|
end
|
|
53
41
|
end
|
|
54
42
|
|
|
@@ -154,6 +142,52 @@ module Legion
|
|
|
154
142
|
end
|
|
155
143
|
|
|
156
144
|
class << self
|
|
145
|
+
def extension_entries
|
|
146
|
+
handles = if Legion::Extensions.respond_to?(:extension_handles)
|
|
147
|
+
Legion::Extensions.extension_handles
|
|
148
|
+
else
|
|
149
|
+
[]
|
|
150
|
+
end
|
|
151
|
+
return handles.map { |handle| serialize_handle(handle) } unless handles.empty?
|
|
152
|
+
|
|
153
|
+
Legion::Extensions::Catalog.all.filter_map do |name, entry|
|
|
154
|
+
serialize_catalog_entry(name, entry)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def extension_entry(name)
|
|
159
|
+
handle = Legion::Extensions.extension_handle(name) if Legion::Extensions.respond_to?(:extension_handle)
|
|
160
|
+
return serialize_handle(handle) if handle
|
|
161
|
+
|
|
162
|
+
serialize_catalog_entry(name, Legion::Extensions::Catalog.entry(name))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def serialize_handle(handle)
|
|
166
|
+
{
|
|
167
|
+
name: handle.lex_name,
|
|
168
|
+
state: handle.state.to_s,
|
|
169
|
+
active_version: handle.active_version&.to_s,
|
|
170
|
+
latest_installed_version: handle.latest_installed_version&.to_s,
|
|
171
|
+
reload_state: handle.reload_state.to_s,
|
|
172
|
+
pending_reload: handle.pending_reload?,
|
|
173
|
+
hot_reloadable: handle.hot_reloadable,
|
|
174
|
+
loaded_at: handle.loaded_at&.iso8601,
|
|
175
|
+
last_error: handle.last_error,
|
|
176
|
+
routes: handle.routes,
|
|
177
|
+
tools: handle.tools,
|
|
178
|
+
absorbers: handle.absorbers,
|
|
179
|
+
owned_runners: handle.runners
|
|
180
|
+
}.compact
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def serialize_catalog_entry(name, entry)
|
|
184
|
+
return nil unless entry
|
|
185
|
+
|
|
186
|
+
{ name: name, state: entry[:state].to_s,
|
|
187
|
+
registered_at: entry[:registered_at]&.iso8601,
|
|
188
|
+
started_at: entry[:started_at]&.iso8601 }
|
|
189
|
+
end
|
|
190
|
+
|
|
157
191
|
private :register_available_route, :register_extension_routes,
|
|
158
192
|
:register_runner_routes, :register_function_routes, :register_invoke_route
|
|
159
193
|
end
|
|
@@ -8,12 +8,23 @@ module Legion
|
|
|
8
8
|
app.helpers IdentityAuditHelpers
|
|
9
9
|
|
|
10
10
|
app.get '/api/identity/audit' do
|
|
11
|
-
|
|
11
|
+
require_data!
|
|
12
|
+
halt 503, json_error('unavailable', 'identity audit log not available') unless defined?(Legion::Data::Model::IdentityAuditLog)
|
|
12
13
|
|
|
13
|
-
dataset = Legion::Data::Model::
|
|
14
|
+
dataset = Legion::Data::Model::IdentityAuditLog.dataset
|
|
14
15
|
|
|
15
16
|
principal = params[:principal]
|
|
16
|
-
|
|
17
|
+
if principal && defined?(Legion::Data::Model::Principal)
|
|
18
|
+
principal_record = Legion::Data::Model::Principal.where(canonical_name: principal).first
|
|
19
|
+
halt 404, json_error('not_found', "principal '#{principal}' not found") unless principal_record
|
|
20
|
+
dataset = dataset.where(principal_id: principal_record.id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
provider = params[:provider]
|
|
24
|
+
dataset = dataset.where(provider_name: provider) if provider
|
|
25
|
+
|
|
26
|
+
event_type = params[:event_type]
|
|
27
|
+
dataset = dataset.where(event_type: event_type) if event_type
|
|
17
28
|
|
|
18
29
|
since = params[:since]
|
|
19
30
|
if since
|
|
@@ -23,7 +34,9 @@ module Legion
|
|
|
23
34
|
|
|
24
35
|
records = dataset.order(Sequel.desc(:created_at)).limit(100).all
|
|
25
36
|
json_collection(records.map do |r|
|
|
26
|
-
{ id: r.id,
|
|
37
|
+
{ id: r.id, event_type: r.event_type, provider_name: r.provider_name,
|
|
38
|
+
trust_level: r.trust_level, detail: r.detail,
|
|
39
|
+
node_id: r.node_id, session_id: r.session_id, created_at: r.created_at }
|
|
27
40
|
end)
|
|
28
41
|
end
|
|
29
42
|
end
|
data/lib/legion/api/stats.rb
CHANGED
|
@@ -20,21 +20,23 @@ module Legion
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
loaded: :@loaded_extensions,
|
|
23
|
+
EXTENSION_TASK_IVARS = {
|
|
25
24
|
discovered: :@extensions,
|
|
26
25
|
subscription: :@subscription_tasks,
|
|
27
26
|
every: :@timer_tasks,
|
|
28
27
|
poll: :@poll_tasks,
|
|
29
28
|
once: :@once_tasks,
|
|
30
29
|
loop: :@loop_tasks,
|
|
31
|
-
|
|
30
|
+
actors: :@running_instances
|
|
32
31
|
}.freeze
|
|
33
32
|
|
|
34
33
|
class << self
|
|
35
34
|
def collect_extensions
|
|
36
35
|
ext = Legion::Extensions
|
|
37
|
-
|
|
36
|
+
{
|
|
37
|
+
loaded: ext.extension_handle_registry.loaded.count,
|
|
38
|
+
running: ext.extension_handle_registry.running.count
|
|
39
|
+
}.merge(EXTENSION_TASK_IVARS.transform_values { |ivar| ext.instance_variable_get(ivar)&.count || 0 })
|
|
38
40
|
rescue StandardError => e
|
|
39
41
|
{ error: e.message }
|
|
40
42
|
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -267,6 +267,9 @@ module Legion
|
|
|
267
267
|
desc 'init', 'Initialize a new Legion workspace'
|
|
268
268
|
subcommand 'init', Legion::CLI::Init
|
|
269
269
|
|
|
270
|
+
desc 'detect SUBCOMMAND', 'Scan environment and recommend extensions'
|
|
271
|
+
subcommand 'detect', Legion::CLI::Detect
|
|
272
|
+
|
|
270
273
|
# --- Interactive & shortcuts ---
|
|
271
274
|
desc 'knowledge SUBCOMMAND', 'Search and manage the document knowledge base'
|
|
272
275
|
subcommand 'knowledge', Legion::CLI::Knowledge
|
|
@@ -214,6 +214,11 @@ module Legion
|
|
|
214
214
|
end
|
|
215
215
|
|
|
216
216
|
def dispatch_runner(message, runner_cls, function, check_subtask, generate_task)
|
|
217
|
+
unless extension_dispatch_allowed?
|
|
218
|
+
log.warn "[Subscription] rejecting #{lex_name}/#{function}: extension is not accepting new work" if defined?(log)
|
|
219
|
+
return { success: false, status: 'task.blocked', error: { code: 'extension_quiescing' } }
|
|
220
|
+
end
|
|
221
|
+
|
|
217
222
|
run_block = lambda {
|
|
218
223
|
ctx = message.merge(runner_class: runner_cls.to_s, function: function.to_s)
|
|
219
224
|
Legion::Context.with_task_context(ctx) do
|
|
@@ -232,6 +237,12 @@ module Legion
|
|
|
232
237
|
end
|
|
233
238
|
end
|
|
234
239
|
|
|
240
|
+
def extension_dispatch_allowed?
|
|
241
|
+
return true unless defined?(Legion::Extensions) && Legion::Extensions.respond_to?(:dispatch_allowed?)
|
|
242
|
+
|
|
243
|
+
Legion::Extensions.dispatch_allowed?(lex_name)
|
|
244
|
+
end
|
|
245
|
+
|
|
235
246
|
def reject_or_retry(delivery_info, metadata, payload)
|
|
236
247
|
headers = metadata&.headers || {}
|
|
237
248
|
retry_count = RetryPolicy.extract_retry_count(headers)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
class Handle
|
|
6
|
+
STATES = %i[registered loaded starting running stopping stopped failed].freeze
|
|
7
|
+
LOADED_STATES = %i[loaded starting running stopping].freeze
|
|
8
|
+
DISPATCHABLE_STATES = %i[loaded starting running].freeze
|
|
9
|
+
RELOAD_STATES = %i[idle pending updating rolling_back failed].freeze
|
|
10
|
+
DISPATCH_BLOCKING_RELOAD_STATES = %i[updating rolling_back].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :lex_name, :gem_name, :active_version, :state, :reload_state, :hot_reloadable,
|
|
13
|
+
:latest_installed_version, :spec, :gem_dir, :loaded_features, :actors, :routes, :tools, :absorbers,
|
|
14
|
+
:runners, :loaded_at, :last_error
|
|
15
|
+
|
|
16
|
+
def initialize(**attrs)
|
|
17
|
+
lex_name = attrs.fetch(:lex_name)
|
|
18
|
+
spec = attrs[:spec]
|
|
19
|
+
@lex_name = lex_name.to_s
|
|
20
|
+
@gem_name = (attrs[:gem_name] || lex_name).to_s
|
|
21
|
+
@spec = spec
|
|
22
|
+
@active_version = normalize_version(attrs[:active_version] || spec&.version)
|
|
23
|
+
@latest_installed_version = normalize_version(attrs[:latest_installed_version] || attrs[:installed_version] || @active_version)
|
|
24
|
+
@state = normalize_state(attrs.fetch(:state, :registered))
|
|
25
|
+
@reload_state = normalize_reload_state(attrs.fetch(:reload_state, :idle))
|
|
26
|
+
@hot_reloadable = attrs[:hot_reloadable] == true
|
|
27
|
+
@gem_dir = attrs[:gem_dir] || spec&.gem_dir
|
|
28
|
+
@loaded_features = Array(attrs.fetch(:loaded_features, [])).dup.freeze
|
|
29
|
+
@actors = Array(attrs.fetch(:actors, [])).dup.freeze
|
|
30
|
+
@routes = Array(attrs.fetch(:routes, [])).dup.freeze
|
|
31
|
+
@tools = Array(attrs.fetch(:tools, [])).dup.freeze
|
|
32
|
+
@absorbers = Array(attrs.fetch(:absorbers, [])).dup.freeze
|
|
33
|
+
@runners = Array(attrs.fetch(:runners, [])).dup.freeze
|
|
34
|
+
@loaded_at = attrs.fetch(:loaded_at, Time.now)
|
|
35
|
+
@last_error = attrs[:last_error]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def loaded?
|
|
39
|
+
LOADED_STATES.include?(state)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def running?
|
|
43
|
+
state == :running
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def pending_reload?
|
|
47
|
+
return false if active_version.nil? || latest_installed_version.nil?
|
|
48
|
+
|
|
49
|
+
latest_installed_version > active_version
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dispatchable?
|
|
53
|
+
DISPATCHABLE_STATES.include?(state) && !DISPATCH_BLOCKING_RELOAD_STATES.include?(reload_state)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def with(**attrs)
|
|
57
|
+
self.class.new(**to_h, **attrs)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{
|
|
62
|
+
lex_name: lex_name,
|
|
63
|
+
gem_name: gem_name,
|
|
64
|
+
active_version: active_version,
|
|
65
|
+
latest_installed_version: latest_installed_version,
|
|
66
|
+
state: state,
|
|
67
|
+
reload_state: reload_state,
|
|
68
|
+
hot_reloadable: hot_reloadable,
|
|
69
|
+
spec: spec,
|
|
70
|
+
gem_dir: gem_dir,
|
|
71
|
+
loaded_features: loaded_features,
|
|
72
|
+
actors: actors,
|
|
73
|
+
routes: routes,
|
|
74
|
+
tools: tools,
|
|
75
|
+
absorbers: absorbers,
|
|
76
|
+
runners: runners,
|
|
77
|
+
loaded_at: loaded_at,
|
|
78
|
+
last_error: last_error
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def normalize_version(value)
|
|
85
|
+
return nil if value.nil?
|
|
86
|
+
return value if value.is_a?(Gem::Version)
|
|
87
|
+
|
|
88
|
+
Gem::Version.new(value.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def normalize_state(value)
|
|
92
|
+
normalized = value.to_sym
|
|
93
|
+
return normalized if STATES.include?(normalized)
|
|
94
|
+
|
|
95
|
+
raise ArgumentError, "unknown extension state: #{value.inspect}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize_reload_state(value)
|
|
99
|
+
normalized = value.to_sym
|
|
100
|
+
return normalized if RELOAD_STATES.include?(normalized)
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, "unknown extension reload state: #{value.inspect}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/handle'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
class HandleRegistry
|
|
8
|
+
def initialize
|
|
9
|
+
@handles = {}
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(lex_name, **attrs)
|
|
14
|
+
key = normalize_name(lex_name)
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
current = @handles[key]
|
|
17
|
+
@handles[key] = current ? current.with(**attrs) : Handle.new(lex_name: key, **attrs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def transition(lex_name, state)
|
|
22
|
+
update(lex_name, state: state)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update(lex_name, **attrs)
|
|
26
|
+
key = normalize_name(lex_name)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
current = @handles[key] || Handle.new(lex_name: key)
|
|
29
|
+
@handles[key] = current.with(**attrs)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(lex_name)
|
|
34
|
+
@mutex.synchronize { @handles[normalize_name(lex_name)] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def all
|
|
38
|
+
@mutex.synchronize { @handles.values.dup }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def running
|
|
42
|
+
all.select(&:running?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def loaded
|
|
46
|
+
all.select(&:loaded?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dispatch_allowed?(lex_name)
|
|
50
|
+
handle = fetch(lex_name)
|
|
51
|
+
return true unless handle
|
|
52
|
+
|
|
53
|
+
handle.dispatchable?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def delete(lex_name)
|
|
57
|
+
@mutex.synchronize { @handles.delete(normalize_name(lex_name)) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reset!
|
|
61
|
+
@mutex.synchronize { @handles.clear }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def normalize_name(lex_name)
|
|
67
|
+
lex_name.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|