Class: NewBridge::RInstanceManager
- Inherits:
-
Object
- Object
- NewBridge::RInstanceManager
- Defined in:
- lib/new_bridge/r_instance_manager.rb
Overview
Phase 4.3 (local): Manage a dynamic set of R instances.
This is the first slice of the planned production-grade instance manager:
- spawn local R processes on demand
- evaluate code via the spawned SessionClient instances
- stop one or all instances
Container provisioning and multi-version orchestration are deferred.
Defined Under Namespace
Classes: Error
Instance Attribute Summary collapse
-
#default_instance_id ⇒ Object
readonly
Returns the value of attribute default_instance_id.
-
#preferred_bridge_host ⇒ Object
readonly
Returns the value of attribute preferred_bridge_host.
Instance Method Summary collapse
-
#eval_all_versions(code, session_id: 'default', timeout: 60) ⇒ Object
Run the same code once per distinct version.
- #eval_r(code, session_id:, instance_id:, parent_id: nil, timeout: 60) ⇒ Object
-
#eval_with_version(code, version:, session_id: 'default', timeout: 60) ⇒ Object
Route to an instance by version label.
-
#initialize(source_path:, host: '127.0.0.1', default_r_cmd: 'R', session_client_class: SessionClient, out: $stdout, docker_access_checker: nil) ⇒ RInstanceManager
constructor
A new instance of RInstanceManager.
- #instance_ids ⇒ Object
-
#instances ⇒ Object
Lightweight metadata view suitable for management UIs and tests.
-
#kill_container(instance_id) ⇒ Object
Explicitly force-kill a container-backed runtime instance.
- #set_default_instance(instance_id) ⇒ Object
-
#spawn(instance_id:, runtime:, version:, r_cmd: @default_r_cmd, image: nil, container_name: nil, accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) ⇒ Object
Unified spawn API.
-
#spawn_container(instance_id:, image:, version:, container_name: nil, r_exec: 'R', accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) ⇒ Object
Spawn R in a container image.
-
#spawn_local(instance_id:, r_cmd: @default_r_cmd, version: nil) ⇒ SessionClient
Spawn a new local R gatekeeper process.
-
#stop(instance_id = nil, keep_default: false, force_container: false) ⇒ Object
Stop one instance or all instances.
- #stop_all(keep_default: false, force_container: false) ⇒ Object
Constructor Details
#initialize(source_path:, host: '127.0.0.1', default_r_cmd: 'R', session_client_class: SessionClient, out: $stdout, docker_access_checker: nil) ⇒ RInstanceManager
Returns a new instance of RInstanceManager.
20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/new_bridge/r_instance_manager.rb', line 20 def initialize(source_path:, host: '127.0.0.1', default_r_cmd: 'R', session_client_class: SessionClient, out: $stdout, docker_access_checker: nil) @source_path = File.(source_path) @host = host @default_r_cmd = default_r_cmd @session_client_class = session_client_class @out = out @docker_access_checker = docker_access_checker @clients = {} # instance_id => SessionClient @meta = {} # instance_id => { provider:, runtime:, version:, r_cmd:, status:, image: } @default_instance_id = nil @preferred_bridge_host = nil end |
Instance Attribute Details
#default_instance_id ⇒ Object (readonly)
Returns the value of attribute default_instance_id.
18 19 20 |
# File 'lib/new_bridge/r_instance_manager.rb', line 18 def default_instance_id @default_instance_id end |
#preferred_bridge_host ⇒ Object (readonly)
Returns the value of attribute preferred_bridge_host.
18 19 20 |
# File 'lib/new_bridge/r_instance_manager.rb', line 18 def preferred_bridge_host @preferred_bridge_host end |
Instance Method Details
#eval_all_versions(code, session_id: 'default', timeout: 60) ⇒ Object
Run the same code once per distinct version. Returns a map keyed by version label.
223 224 225 226 227 228 229 230 231 232 |
# File 'lib/new_bridge/r_instance_manager.rb', line 223 def eval_all_versions(code, session_id: 'default', timeout: 60) by_version = {} @meta.each do |id, m| v = m[:version] next if v.nil? || by_version.key?(v) by_version[v] = eval_r(code, session_id: session_id, instance_id: id, timeout: timeout) end by_version end |
#eval_r(code, session_id:, instance_id:, parent_id: nil, timeout: 60) ⇒ Object
197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/new_bridge/r_instance_manager.rb', line 197 def eval_r(code, session_id:, instance_id:, parent_id: nil, timeout: 60) c = @clients.fetch(instance_id) c.eval_r(code, session_id: session_id, instance_id: instance_id, parent_id: parent_id, timeout: timeout) rescue StandardError => e raise unless infrastructure_retryable_error?(e) raise unless @clients.key?(instance_id) @out.puts("[RInstanceManager] runtime failure on instance=#{instance_id}: #{e.class}: #{e.}") @out.puts("[RInstanceManager] attempting one restart+retry for instance=#{instance_id}") restart_instance(instance_id) c2 = @clients.fetch(instance_id) c2.eval_r(code, session_id: session_id, instance_id: instance_id, parent_id: parent_id, timeout: timeout) end |
#eval_with_version(code, version:, session_id: 'default', timeout: 60) ⇒ Object
Route to an instance by version label. If multiple instances share a version, first match is used (deterministic by insertion order).
213 214 215 216 217 218 219 |
# File 'lib/new_bridge/r_instance_manager.rb', line 213 def eval_with_version(code, version:, session_id: 'default', timeout: 60) entry = @meta.find { |_id, m| m[:version] == version } raise KeyError, "no instance for version=#{version}" unless entry instance_id, = entry eval_r(code, session_id: session_id, instance_id: instance_id, timeout: timeout) end |
#instance_ids ⇒ Object
173 174 175 |
# File 'lib/new_bridge/r_instance_manager.rb', line 173 def instance_ids @clients.keys end |
#instances ⇒ Object
Lightweight metadata view suitable for management UIs and tests. Example: [{ id: "a", provider: "local", version: "4.3.3", status: :healthy, r_cmd: "R" }]
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/new_bridge/r_instance_manager.rb', line 180 def instances @clients.keys.map do |id| m = @meta.fetch(id, {}) { id: id, provider: m[:provider] || 'local', runtime: m[:runtime] || 'local', version: m[:version], status: m[:status] || :healthy, r_cmd: m[:r_cmd], image: m[:image], container_name: m[:container_name], default: (id == @default_instance_id) } end end |
#kill_container(instance_id) ⇒ Object
Explicitly force-kill a container-backed runtime instance. Useful when user wants to ensure no container stays alive after a versioned run.
277 278 279 280 281 282 |
# File 'lib/new_bridge/r_instance_manager.rb', line 277 def kill_container(instance_id) m = @meta.fetch(instance_id) raise ArgumentError, "instance #{instance_id} is not container runtime" unless m[:runtime] == 'container' stop(instance_id, force_container: true) end |
#set_default_instance(instance_id) ⇒ Object
234 235 236 237 238 |
# File 'lib/new_bridge/r_instance_manager.rb', line 234 def set_default_instance(instance_id) raise KeyError, "unknown instance_id=#{instance_id}" unless @clients.key?(instance_id) @default_instance_id = instance_id end |
#spawn(instance_id:, runtime:, version:, r_cmd: @default_r_cmd, image: nil, container_name: nil, accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) ⇒ Object
Unified spawn API. runtime: 'local' or 'container'
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/new_bridge/r_instance_manager.rb', line 150 def spawn(instance_id:, runtime:, version:, r_cmd: @default_r_cmd, image: nil, container_name: nil, accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) case runtime.to_s when 'local' spawn_local(instance_id: instance_id, r_cmd: r_cmd, version: version) when 'container' raise ArgumentError, 'image is required for container runtime' if image.nil? || image.empty? spawn_container( instance_id: instance_id, image: image, version: version, container_name: container_name, accept_timeout: accept_timeout, bridge_host: bridge_host, bridge_hosts: bridge_hosts, relearn_bridge_host: relearn_bridge_host ) else raise ArgumentError, "unsupported runtime=#{runtime} (expected 'local' or 'container')" end end |
#spawn_container(instance_id:, image:, version:, container_name: nil, r_exec: 'R', accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) ⇒ Object
Spawn R in a container image.
Notes:
- This uses Docker and assumes the image can execute
Rwith Rcpp available. - We mount the project root into /workspace and source the gatekeeper from that path.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/new_bridge/r_instance_manager.rb', line 62 def spawn_container(instance_id:, image:, version:, container_name: nil, r_exec: 'R', accept_timeout: 30, bridge_host: nil, bridge_hosts: nil, relearn_bridge_host: false) raise ArgumentError, "instance already exists: #{instance_id}" if @clients.key?(instance_id) ensure_docker_daemon_access! source_dir = File.dirname(@source_path) project_root = File.('../..', source_dir) runtime_source = "/workspace/ext/new_bridge/#{File.basename(@source_path)}" base_name = container_name || "galaaz-#{instance_id}" host_candidates = if bridge_host [bridge_host] elsif bridge_hosts bridge_hosts else bridge_host_candidates(relearn: relearn_bridge_host) end @out.puts("[RInstanceManager] starting container instance #{instance_id} image=#{image} version=#{version}") @out.puts("[RInstanceManager] bridge host candidates: #{host_candidates.join(', ')}") c = nil last_error = nil selected_name = nil selected_host = nil selected_launch = nil host_candidates.each_with_index do |bridge_host, idx| name = "#{base_name}-#{idx}" @out.puts("[RInstanceManager] trying bridge host #{bridge_host} (attempt #{idx + 1}/#{host_candidates.size})") launch = [ 'docker', 'run', '--name', name, '--add-host', 'host.docker.internal:host-gateway', '-v', "#{project_root}:/workspace", image, r_exec ] c = @session_client_class.new( source_path: @source_path, runtime_source_path: runtime_source, host: '0.0.0.0', bridge_host: bridge_host, r_cmd: launch ) begin c.start(accept_timeout: accept_timeout) selected_name = name selected_host = bridge_host selected_launch = launch @out.puts("[RInstanceManager] container instance #{instance_id} connected via #{bridge_host}") break rescue StandardError => e last_error = e logs = docker_logs(name, tail: 120) @out.puts("[RInstanceManager] bridge host #{bridge_host} failed: #{e.}") if bridge_host == @preferred_bridge_host @out.puts("[RInstanceManager] cached bridge host failed, trying fallback candidates...") end @out.puts(logs) unless logs.nil? || logs.empty? docker_rm_force(name) c = nil end end if c.nil? raise Error, "container runtime start failed instance=#{instance_id} image=#{image}: #{last_error&.}" end if selected_host && selected_host != @preferred_bridge_host @preferred_bridge_host = selected_host @out.puts("[RInstanceManager] learned preferred bridge host: #{@preferred_bridge_host}") end @clients[instance_id] = c @meta[instance_id] = { provider: 'docker', runtime: 'container', version: version, image: image, bridge_host: selected_host, container_name: selected_name, r_cmd: selected_launch, status: :healthy } @default_instance_id ||= instance_id c end |
#spawn_local(instance_id:, r_cmd: @default_r_cmd, version: nil) ⇒ SessionClient
Spawn a new local R gatekeeper process.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/new_bridge/r_instance_manager.rb', line 39 def spawn_local(instance_id:, r_cmd: @default_r_cmd, version: nil) raise ArgumentError, "instance already exists: #{instance_id}" if @clients.key?(instance_id) @out.puts("[RInstanceManager] starting local instance #{instance_id} version=#{version || 'n/a'}") c = @session_client_class.new(source_path: @source_path, host: @host, r_cmd: r_cmd) c.start @clients[instance_id] = c @meta[instance_id] = { provider: 'local', runtime: 'local', version: version, r_cmd: r_cmd, status: :healthy } @default_instance_id ||= instance_id c end |
#stop(instance_id = nil, keep_default: false, force_container: false) ⇒ Object
Stop one instance or all instances.
- keep_default: when stopping all, leaves default instance alive.
- force_container: when stopping container instances, force-remove docker container.
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/new_bridge/r_instance_manager.rb', line 243 def stop(instance_id = nil, keep_default: false, force_container: false) if instance_id c = @clients.delete(instance_id) c&.stop m = @meta.delete(instance_id) cleanup_container(m, force: force_container) @default_instance_id = @clients.keys.first if @default_instance_id == instance_id return self end ids = @clients.keys ids.each do |id| next if keep_default && id == @default_instance_id c = @clients.delete(id) c&.stop m = @meta.delete(id) cleanup_container(m, force: force_container) end if @clients.empty? @default_instance_id = nil elsif @default_instance_id.nil? || !@clients.key?(@default_instance_id) @default_instance_id = @clients.keys.first end self end |
#stop_all(keep_default: false, force_container: false) ⇒ Object
271 272 273 |
# File 'lib/new_bridge/r_instance_manager.rb', line 271 def stop_all(keep_default: false, force_container: false) stop(nil, keep_default: keep_default, force_container: force_container) end |