cuboid 0.3.6 → 0.5
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/README.md +195 -0
- data/cuboid.gemspec +4 -0
- data/lib/cuboid/application.rb +84 -3
- data/lib/cuboid/mcp/auth.rb +99 -0
- data/lib/cuboid/mcp/core_tools.rb +318 -0
- data/lib/cuboid/mcp/live.rb +166 -0
- data/lib/cuboid/mcp/server.rb +426 -0
- data/lib/cuboid/option_groups/paths.rb +40 -0
- data/lib/cuboid/processes/executables/base.rb +37 -0
- data/lib/cuboid/processes/executables/mcp.rb +20 -0
- data/lib/cuboid/processes/instances.rb +9 -1
- data/lib/cuboid/processes/manager.rb +22 -1
- data/lib/cuboid/rest/server/instance_helpers.rb +13 -79
- data/lib/cuboid/rest/server/routes/instances.rb +8 -13
- data/lib/cuboid/rest/server.rb +1 -1
- data/lib/cuboid/rpc/server/agent.rb +6 -1
- data/lib/cuboid/rpc/server/instance.rb +86 -66
- data/lib/cuboid/server/instance_helpers.rb +131 -0
- data/lib/version +1 -1
- data/spec/cuboid/mcp/auth_spec.rb +179 -0
- data/spec/cuboid/mcp/server_spec.rb +346 -0
- data/spec/cuboid/rest/server_spec.rb +3 -4
- data/spec/support/shared/option_group.rb +11 -1
- metadata +26 -2
|
@@ -1,97 +1,31 @@
|
|
|
1
|
+
require_relative '../../server/instance_helpers'
|
|
2
|
+
|
|
1
3
|
module Cuboid
|
|
2
4
|
module Rest
|
|
3
5
|
class Server
|
|
4
6
|
|
|
7
|
+
# Sinatra-coupled supplement to `Cuboid::Server::InstanceHelpers` —
|
|
8
|
+
# the methods that read `env` or call `handle_error` (a Sinatra helper
|
|
9
|
+
# defined on `Rest::Server`). Everything that doesn't need Sinatra
|
|
10
|
+
# stays on the shared module above.
|
|
5
11
|
module InstanceHelpers
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
@@agents = {}
|
|
9
|
-
|
|
10
|
-
def get_instance
|
|
11
|
-
if agent
|
|
12
|
-
options = {
|
|
13
|
-
owner: self.class.to_s,
|
|
14
|
-
helpers: {
|
|
15
|
-
owner: {
|
|
16
|
-
url: env['HTTP_HOST']
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (info = agent.spawn( options ))
|
|
22
|
-
connect_to_instance( info['url'], info['token'] )
|
|
23
|
-
end
|
|
24
|
-
else
|
|
25
|
-
Processes::Instances.spawn( application: Options.paths.application, daemonize: true )
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def agents
|
|
30
|
-
@@agents.keys
|
|
31
|
-
end
|
|
13
|
+
include ::Cuboid::Server::InstanceHelpers
|
|
32
14
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def unplug_agent( url )
|
|
39
|
-
connect_to_agent( url ).node.unplug
|
|
40
|
-
|
|
41
|
-
c = @@agents.delete( url )
|
|
42
|
-
c.close if c
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def connect_to_agent( url )
|
|
46
|
-
@@agents[url] ||= RPC::Client::Agent.new( url )
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def connect_to_instance( url, token )
|
|
50
|
-
RPC::Client::Instance.new( url, token )
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def update_from_scheduler
|
|
54
|
-
return if !scheduler
|
|
55
|
-
|
|
56
|
-
scheduler.running.each do |id, info|
|
|
57
|
-
instances[id] ||= connect_to_instance( info['url'], info['token'] )
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
(scheduler.failed.keys | scheduler.completed.keys).each do |id|
|
|
61
|
-
session.delete id
|
|
62
|
-
client = instances.delete( id )
|
|
63
|
-
client.close if client
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def scheduler
|
|
68
|
-
return if !Options.scheduler.url
|
|
69
|
-
@scheduler ||= connect_to_scheduler( Options.scheduler.url )
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def connect_to_scheduler( url )
|
|
73
|
-
RPC::Client::Scheduler.new( url )
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def instances
|
|
77
|
-
@@instances
|
|
15
|
+
# Forward the request host to the shared spawner so the Agent can
|
|
16
|
+
# log who asked for the instance.
|
|
17
|
+
def spawn( owner_url: env['HTTP_HOST'] )
|
|
18
|
+
super
|
|
78
19
|
end
|
|
79
20
|
|
|
80
21
|
def instance_for( id, &block )
|
|
81
|
-
cleanup = proc
|
|
82
|
-
instances.delete( id ).close
|
|
83
|
-
session.delete id
|
|
84
|
-
end
|
|
22
|
+
cleanup = proc { instances.delete( id ).close }
|
|
85
23
|
|
|
86
24
|
handle_error cleanup do
|
|
87
|
-
block.call
|
|
25
|
+
block.call instances[id]
|
|
88
26
|
end
|
|
89
27
|
end
|
|
90
28
|
|
|
91
|
-
def exists?( id )
|
|
92
|
-
instances.include? id
|
|
93
|
-
end
|
|
94
|
-
|
|
95
29
|
end
|
|
96
30
|
|
|
97
31
|
end
|
|
@@ -20,7 +20,7 @@ module Instances
|
|
|
20
20
|
|
|
21
21
|
options = ::JSON.load( request.body.read ) || {}
|
|
22
22
|
|
|
23
|
-
instance =
|
|
23
|
+
instance = self.spawn
|
|
24
24
|
max_utilization! if !instance
|
|
25
25
|
|
|
26
26
|
handle_error proc { (instance.shutdown rescue nil) } do
|
|
@@ -36,20 +36,19 @@ module Instances
|
|
|
36
36
|
app.get '/instances/:instance' do
|
|
37
37
|
ensure_instance!
|
|
38
38
|
|
|
39
|
-
session
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# The Sinatra session is per-cookie, but its id isn't
|
|
40
|
+
# exposed; lazy-init a per-client UUID and pass it as
|
|
41
|
+
# the RPC `session:` token so the engine tracks the
|
|
42
|
+
# error-line offset server-side.
|
|
43
|
+
require 'securerandom' unless defined?( SecureRandom )
|
|
44
|
+
session[:rpc_session_id] ||= SecureRandom.uuid
|
|
42
45
|
|
|
43
46
|
data = instance_for( params[:instance] ) do |instance|
|
|
44
47
|
instance.progress(
|
|
45
|
-
|
|
46
|
-
errors: session[params[:instance]][:seen_errors],
|
|
47
|
-
]
|
|
48
|
+
session: "#{session[:rpc_session_id]}:#{params[:instance]}"
|
|
48
49
|
)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
session[params[:instance]][:seen_errors] += data[:errors].size
|
|
52
|
-
|
|
53
52
|
json data
|
|
54
53
|
end
|
|
55
54
|
|
|
@@ -110,14 +109,10 @@ module Instances
|
|
|
110
109
|
app.delete '/instances/:instance' do
|
|
111
110
|
ensure_instance!
|
|
112
111
|
id = params[:instance]
|
|
113
|
-
|
|
114
|
-
instance = instances[id]
|
|
115
112
|
handle_error { (instance.shutdown rescue nil) }
|
|
116
113
|
|
|
117
114
|
instances.delete( id ).close
|
|
118
115
|
|
|
119
|
-
session.delete params[:instance]
|
|
120
|
-
|
|
121
116
|
json nil
|
|
122
117
|
end
|
|
123
118
|
|
data/lib/cuboid/rest/server.rb
CHANGED
|
@@ -18,7 +18,7 @@ class Server < Sinatra::Base
|
|
|
18
18
|
|
|
19
19
|
Dir.glob( "#{File.dirname( __FILE__ )}/server/routes/*.rb" ).each { |f| require f }
|
|
20
20
|
|
|
21
|
-
helpers InstanceHelpers
|
|
21
|
+
helpers ::Cuboid::Rest::Server::InstanceHelpers
|
|
22
22
|
|
|
23
23
|
register Sinatra::Namespace
|
|
24
24
|
Cuboid::Application.application.rest_services.each do |name, service|
|
|
@@ -320,12 +320,17 @@ class Agent
|
|
|
320
320
|
end
|
|
321
321
|
|
|
322
322
|
def spawn_instance( options = {}, &block )
|
|
323
|
+
# `detached: true` opts the spawned engine out of the
|
|
324
|
+
# base.rb parent-death watchdog: an agent restarting / dying
|
|
325
|
+
# must NOT take the engine with it (grid pattern — the
|
|
326
|
+
# instance is owned by whoever connects, not the agent).
|
|
323
327
|
Processes::Instances.spawn( options.merge(
|
|
324
328
|
address: @server.address,
|
|
325
329
|
port_range: Options.agent.instance_port_range,
|
|
326
330
|
token: Utilities.generate_token,
|
|
327
331
|
application: Options.paths.application,
|
|
328
|
-
daemonize: true
|
|
332
|
+
daemonize: true,
|
|
333
|
+
detached: true
|
|
329
334
|
)) do |client|
|
|
330
335
|
block.call(
|
|
331
336
|
'token' => client.token,
|
|
@@ -152,51 +152,38 @@ class Instance
|
|
|
152
152
|
# In addition, ask to **not** be served data you already have, like
|
|
153
153
|
# error messages.
|
|
154
154
|
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
# the
|
|
155
|
+
# Pass a `session:` token (any caller-chosen string) and the
|
|
156
|
+
# server returns only error lines past the previous offset
|
|
157
|
+
# under that token. Reuse the same token across polls for
|
|
158
|
+
# the same logical view; pick a fresh one to start fresh.
|
|
159
159
|
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
# This is done by telling the method how many error messages you already
|
|
163
|
-
# have and you will be served the errors from the error-log that are past
|
|
164
|
-
# that line.
|
|
165
|
-
# So, if you were to use a loop to get fresh progress data it would look
|
|
166
|
-
# like so:
|
|
167
|
-
#
|
|
168
|
-
# error_cnt = 0
|
|
169
|
-
# i = 0
|
|
160
|
+
# token = SecureRandom.uuid
|
|
170
161
|
# while sleep 1
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
# # Only request errors we don't already have
|
|
175
|
-
# errors = instance.progress( with: { errors: error_cnt } )[:errors]
|
|
176
|
-
# error_cnt += errors.size
|
|
177
|
-
#
|
|
178
|
-
# # You will only see new errors
|
|
179
|
-
# puts errors.join("\n")
|
|
162
|
+
# errors = instance.progress( session: token )[:errors]
|
|
163
|
+
# puts errors.join( "\n" )
|
|
180
164
|
# end
|
|
181
165
|
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
# @option options [Array<Symbol, Hash>] :with
|
|
185
|
-
# Specify data to include:
|
|
186
|
-
#
|
|
187
|
-
# * :errors -- Errors and the line offset to use for {#errors}.
|
|
188
|
-
# Pass as a hash, like: `{ errors: 10 }`
|
|
189
|
-
# @option options [Array<Symbol, Hash>] :without
|
|
190
|
-
# Specify data to exclude:
|
|
166
|
+
# Without `session`, callers must opt into errors via
|
|
167
|
+
# `with: [:errors]` and will receive the full set every poll.
|
|
191
168
|
#
|
|
192
|
-
#
|
|
169
|
+
# @param [Hash] options
|
|
170
|
+
# @option options [String, Symbol] :session
|
|
171
|
+
# Caller-chosen session token. When provided, the response
|
|
172
|
+
# carries only errors past the previously emitted offset.
|
|
173
|
+
# @option options [Array<Symbol>] :with
|
|
174
|
+
# Block names to include when no session is in use. Currently
|
|
175
|
+
# only `:errors` is delta-able.
|
|
176
|
+
# @option options [Array<Symbol>] :without
|
|
177
|
+
# Block names to exclude. One or more of `:statistics`,
|
|
178
|
+
# `:errors`. Takes precedence over `with:` and over the
|
|
179
|
+
# session-on-by-default blocks.
|
|
193
180
|
#
|
|
194
181
|
# @return [Hash]
|
|
195
182
|
# * `statistics` -- General runtime statistics (merged when part of Grid)
|
|
196
183
|
# (enabled by default)
|
|
197
184
|
# * `status` -- {#status}
|
|
198
185
|
# * `busy` -- {#busy?}
|
|
199
|
-
# * `errors` -- {#errors}
|
|
186
|
+
# * `errors` -- {#errors}
|
|
200
187
|
def progress( options = {} )
|
|
201
188
|
progress_handler( options.merge( as_hash: true ) )
|
|
202
189
|
end
|
|
@@ -226,6 +213,27 @@ class Instance
|
|
|
226
213
|
end
|
|
227
214
|
|
|
228
215
|
# Makes the server go bye-bye...Lights out!
|
|
216
|
+
#
|
|
217
|
+
# `shutdown` must reliably take the Ruby process with it. Stopping
|
|
218
|
+
# the reactor + RPC server alone leaves the Application's non-daemon
|
|
219
|
+
# threads (audit workers, browser cluster manager, etc.) blocking
|
|
220
|
+
# the runtime — historically this leaked engine subprocesses every
|
|
221
|
+
# time `kill_instance` was called over MCP, and showed up in the
|
|
222
|
+
# cuboid spec suite as leftover ruby processes after the run.
|
|
223
|
+
# The `instance.shutdown` RPC returned success but the daemonised
|
|
224
|
+
# process never actually exited.
|
|
225
|
+
#
|
|
226
|
+
# Two-stage exit:
|
|
227
|
+
# 1. Raise SystemExit on the **main thread** so the at_exit
|
|
228
|
+
# chain runs (Cuboid_<pid> tmpdir cleanup, live-plugin's
|
|
229
|
+
# `exited` push). SystemExit raised on a non-main thread
|
|
230
|
+
# only kills that thread — must hit the main one.
|
|
231
|
+
# 2. Watchdog SIGKILL after a grace window in case a
|
|
232
|
+
# non-daemon Application thread refuses to release. The
|
|
233
|
+
# Paths boot-sweep reaps the orphaned tmpdir on the next
|
|
234
|
+
# cuboid process launch even when at_exit didn't run.
|
|
235
|
+
SHUTDOWN_GRACE_SECONDS = 5.0
|
|
236
|
+
|
|
229
237
|
def shutdown( &block )
|
|
230
238
|
if @shutdown
|
|
231
239
|
block.call if block_given?
|
|
@@ -243,6 +251,17 @@ class Instance
|
|
|
243
251
|
@server.shutdown
|
|
244
252
|
@raktr.stop
|
|
245
253
|
block.call true if block_given?
|
|
254
|
+
|
|
255
|
+
# Stage 1 — graceful: SystemExit on the main thread so
|
|
256
|
+
# at_exit handlers run.
|
|
257
|
+
main = Thread.main
|
|
258
|
+
if main && main.alive? && main != Thread.current
|
|
259
|
+
main.raise( SystemExit.new( 0 ) ) rescue nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Stage 2 — watchdog: hammer if main can't unwind.
|
|
263
|
+
sleep SHUTDOWN_GRACE_SECONDS
|
|
264
|
+
Process.kill( 'KILL', Process.pid ) rescue nil
|
|
246
265
|
end
|
|
247
266
|
|
|
248
267
|
true
|
|
@@ -262,49 +281,50 @@ class Instance
|
|
|
262
281
|
[Process.pid]
|
|
263
282
|
end
|
|
264
283
|
|
|
265
|
-
def self.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
case w
|
|
269
|
-
when Array
|
|
270
|
-
w.compact.flatten.each do |q|
|
|
271
|
-
case q
|
|
272
|
-
when String, Symbol
|
|
273
|
-
parsed[q.to_sym] = nil
|
|
274
|
-
|
|
275
|
-
when Hash
|
|
276
|
-
parsed.merge!( q.my_symbolize_keys )
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
when String, Symbol
|
|
281
|
-
parsed[w.to_sym] = nil
|
|
282
|
-
|
|
283
|
-
when Hash
|
|
284
|
-
parsed.merge!( w.my_symbolize_keys )
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
parsed
|
|
284
|
+
def self.parse_block_names( raw )
|
|
285
|
+
return [] if raw.nil?
|
|
286
|
+
Array( raw ).flatten.compact.map(&:to_sym)
|
|
289
287
|
end
|
|
290
288
|
|
|
291
289
|
private
|
|
292
290
|
|
|
291
|
+
# Server-side state for `session:`-tracked progress polls.
|
|
292
|
+
# Keyed off a caller-supplied token so RPC clients don't have
|
|
293
|
+
# to re-transmit the error line offset on every poll.
|
|
294
|
+
def progress_sessions
|
|
295
|
+
@progress_sessions ||= {}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def progress_session_for( id )
|
|
299
|
+
progress_sessions[id] ||= { seen_errors: 0 }
|
|
300
|
+
end
|
|
301
|
+
|
|
293
302
|
def progress_handler( options = {}, &block )
|
|
294
|
-
|
|
295
|
-
|
|
303
|
+
options = options.my_symbolize_keys
|
|
304
|
+
|
|
305
|
+
session_id = options.delete( :session )
|
|
306
|
+
session = progress_session_for( session_id ) if session_id
|
|
296
307
|
|
|
297
|
-
options
|
|
308
|
+
with = self.class.parse_block_names( options[:with] )
|
|
309
|
+
without = self.class.parse_block_names( options[:without] )
|
|
310
|
+
|
|
311
|
+
# Under a session, errors are on by default; without a
|
|
312
|
+
# session, callers opt in via `with: [:errors]`.
|
|
313
|
+
include_errors = !without.include?( :errors ) && (session || with.include?( :errors ))
|
|
314
|
+
|
|
315
|
+
wrapper_options = {
|
|
298
316
|
as_hash: options[:as_hash],
|
|
299
317
|
statistics: !without.include?( :statistics )
|
|
300
318
|
}
|
|
319
|
+
wrapper_options[:errors] = session ? session[:seen_errors] : 0 if include_errors
|
|
301
320
|
|
|
302
|
-
|
|
303
|
-
options[:errors] = with[:errors]
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
@application.progress( options ) do |data|
|
|
321
|
+
@application.progress( wrapper_options ) do |data|
|
|
307
322
|
data[:busy] = busy?
|
|
323
|
+
|
|
324
|
+
if session && data[:errors]
|
|
325
|
+
session[:seen_errors] += data[:errors].size
|
|
326
|
+
end
|
|
327
|
+
|
|
308
328
|
block.call( data )
|
|
309
329
|
end
|
|
310
330
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Cuboid
|
|
2
|
+
module Server
|
|
3
|
+
|
|
4
|
+
# Shared registry + lookup helpers for the running engine instances
|
|
5
|
+
# any front-end (REST, MCP, scheduler-sync) drives. The two
|
|
6
|
+
# class-variables (`@@instances`, `@@agents`) are intentionally
|
|
7
|
+
# module-level so every includer sees the same map without explicit
|
|
8
|
+
# cross-process plumbing.
|
|
9
|
+
#
|
|
10
|
+
# `spawn` here picks an Agent if one is configured (so grid mode keeps
|
|
11
|
+
# working) or falls back to local `Processes::Instances.spawn`.
|
|
12
|
+
# Sinatra-only surface — `instance_for`, REST-side scheduler-session
|
|
13
|
+
# cleanup, and the env-derived owner URL on `spawn` — lives on
|
|
14
|
+
# `Cuboid::Rest::Server::InstanceHelpers`, which mixes this in.
|
|
15
|
+
module InstanceHelpers
|
|
16
|
+
|
|
17
|
+
@@instances = {}
|
|
18
|
+
@@agents = {}
|
|
19
|
+
|
|
20
|
+
def self.instances
|
|
21
|
+
@@instances
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Spawn a new engine instance. If an Agent URL is configured the
|
|
25
|
+
# instance is provisioned via the Agent (grid path); otherwise we
|
|
26
|
+
# fork a local one via `Processes::Instances.spawn`.
|
|
27
|
+
#
|
|
28
|
+
# `owner_url` is forwarded to the Agent as `helpers.owner.url` —
|
|
29
|
+
# purely metadata identifying who asked. Sinatra/REST callers pass
|
|
30
|
+
# `env['HTTP_HOST']`; MCP and other non-Rack callers can leave it
|
|
31
|
+
# nil or pass whatever they have. Module-level so callers without
|
|
32
|
+
# an includer context (e.g. `MCP::CoreTools::SpawnInstance`) can
|
|
33
|
+
# use it as `Cuboid::Server::InstanceHelpers.spawn`.
|
|
34
|
+
def self.spawn( owner_url: nil )
|
|
35
|
+
if (a = agent)
|
|
36
|
+
options = {
|
|
37
|
+
owner: name,
|
|
38
|
+
helpers: { owner: { url: owner_url } }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (info = a.spawn( options ))
|
|
42
|
+
connect_to_instance( info['url'], info['token'] )
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
::Cuboid::Processes::Instances.spawn(
|
|
46
|
+
application: ::Cuboid::Options.paths.application,
|
|
47
|
+
daemonize: true
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.agent
|
|
53
|
+
return if !::Cuboid::Options.agent.url
|
|
54
|
+
@@agents[::Cuboid::Options.agent.url] ||=
|
|
55
|
+
::Cuboid::RPC::Client::Agent.new( ::Cuboid::Options.agent.url )
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.connect_to_agent( url )
|
|
59
|
+
@@agents[url] ||= ::Cuboid::RPC::Client::Agent.new( url )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.connect_to_instance( url, token )
|
|
63
|
+
::Cuboid::RPC::Client::Instance.new( url, token )
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def agents
|
|
67
|
+
@@agents.keys
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def agent
|
|
71
|
+
InstanceHelpers.agent
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def spawn( owner_url: nil )
|
|
75
|
+
InstanceHelpers.spawn( owner_url: owner_url )
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unplug_agent( url )
|
|
79
|
+
InstanceHelpers.connect_to_agent( url ).node.unplug
|
|
80
|
+
|
|
81
|
+
c = @@agents.delete( url )
|
|
82
|
+
c.close if c
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def connect_to_agent( url )
|
|
86
|
+
InstanceHelpers.connect_to_agent( url )
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def connect_to_instance( url, token )
|
|
90
|
+
InstanceHelpers.connect_to_instance( url, token )
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Pulls scheduler-tracked running instances into the local map and
|
|
94
|
+
# closes/removes any that the scheduler reports failed or completed.
|
|
95
|
+
# Sinatra-side session cleanup for the same IDs is the responsibility
|
|
96
|
+
# of `Cuboid::Rest::Server::InstanceHelpers#update_from_scheduler`,
|
|
97
|
+
# which calls super then prunes its session.
|
|
98
|
+
def update_from_scheduler
|
|
99
|
+
return if !scheduler
|
|
100
|
+
|
|
101
|
+
scheduler.running.each do |id, info|
|
|
102
|
+
instances[id] ||= connect_to_instance( info['url'], info['token'] )
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
(scheduler.failed.keys | scheduler.completed.keys).each do |id|
|
|
106
|
+
client = instances.delete( id )
|
|
107
|
+
client.close if client
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def scheduler
|
|
112
|
+
return if !Options.scheduler.url
|
|
113
|
+
@scheduler ||= connect_to_scheduler( Options.scheduler.url )
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def connect_to_scheduler( url )
|
|
117
|
+
RPC::Client::Scheduler.new( url )
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def instances
|
|
121
|
+
InstanceHelpers.instances
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def exists?( id )
|
|
125
|
+
instances.include? id
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5
|