galaaz 2.1.3 → 2.1.4

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: '085fd47fe76016fb5b078f3c44a02100a7d9af05c2f7cf9e19ded83058947c8e'
4
- data.tar.gz: 19f36b20968fd4456ad95024f2a33dbde60dfdf62a3bbda295d49e08a8c32bc7
3
+ metadata.gz: 03ba582ba7054d11d6ce78bd5bf50fc424aac1a391209dee52749dd6bb642677
4
+ data.tar.gz: a9997f60ab4446b6ecdbda2cfa32a1f9de03fb002b8d2bdc5df945043fe547ab
5
5
  SHA512:
6
- metadata.gz: c7db477fca2f6de435d9eb6bd5ccc5ee709a779e3a5816febc9a0eda9be51f5f18812af79889e7fd4ce1e5facd79a080e80a98829381b19670d6a380765554c4
7
- data.tar.gz: 6ba4144e49328b6bb70188ab6330ed086f969cb952076758db0207171c54c2ff41e6fb21c8666a6cd87b459f47b2786a31edf8f1f1b98343d7d536aae902de36
6
+ metadata.gz: fcbc2a6d680c73df54768ff9a623d11f3c4723f030416f8d0937183fef4247b66982aa5bc7c0600d97ade5b9a67e7a6b17eecd7c8e7f507d0528510086fd33db
7
+ data.tar.gz: fa9b17e4e135ea1a199b76103f5f86739312816dee9e3fae332fa9e0773b6eb80124bd1f37a65759afdd610bc5a7fb9d4eba2c928c08adfd730aaf37f17899b8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.4
4
+
5
+ ### Added
6
+
7
+ - **`R::Job`**: long CRAN installs (and arbitrary R via `R::Job.eval`) run in a **child
8
+ Rscript** process, not on the NewBridge gatekeeper. Logs and metadata live under
9
+ `~/.local/share/galaaz/jobs/` (override with `GALAAZ_JOBS_DIR`). One install at a
10
+ time (`install.lock`). Bridge R stays free while Ruby awaits the job. Clears stale
11
+ `00LOCK-<pkg>` dirs before install (left behind after kill-on-timeout).
12
+ - **Layer C:** `R::Job.eval` / `R::Job.script` for long arbitrary R. Child gets
13
+ `setwd(job.dir)`, `GALAAZ_JOB_DIR`, and `result_path` (default `result.rds`). After
14
+ success, `job.load_rds` does a short sync `readRDS` on the bridge. Block form
15
+ awaits and yields the job (`coef = R::Job.eval(code) { |j| j.load_rds }`).
16
+ Documented in the manual section **Background R jobs (`R::Job`)** (README /
17
+ `blogs/manual/manual.Rmd`).
18
+
19
+ ### Changed
20
+
21
+ - `R.install_rlibs` / `R.install_and_loads` use `R::Job.install` and **await until
22
+ finished** by default (no bridge 60s install timeout). Optional await limit via
23
+ `install_timeout_sec:` or `GALAAZ_INSTALL_TIMEOUT_SEC` (also `gknit
24
+ --install_timeout_sec`); on timeout the child process group is killed so compile
25
+ work cannot OOM the shell. gknit reports install-job timeouts in the internal
26
+ error summary.
27
+
3
28
  ## 2.1.3
4
29
 
5
30
  ### Fixed
@@ -85,4 +110,6 @@ Galaaz 2.0 drives **GNU R** from **JRuby** or **CRuby** over a process bridge. I
85
110
  - `grun`, `gknit_Rscript`, and the polyglot path in `gknit-draft` are leftover from the Graal era; use `galaaz-jruby` / `gknit` (or `GALAAZ_RUBY=ruby` for CRuby).
86
111
  - The gatekeeper must be compiled on the install machine (Rcpp + C++ toolchain); it is not a prebuilt binary in the gem.
87
112
  - Multi-runtime Docker R (Phase 4.5) is a slow integration spec, not part of cold-install CI.
88
- - The gknit installation-timeout spec is skipped pending a dedicated pass; SimpleCov is a development gem and is not installed in the smoke image.
113
+ - The gknit installation-timeout spec covers `R::Job` await timeouts
114
+ (`GALAAZ_INSTALL_TIMEOUT_SEC`); SimpleCov is a development gem and is not installed in
115
+ the smoke image.
data/README.md CHANGED
@@ -73,7 +73,7 @@ rewrite in another stack. **R-on-Rails** means:
73
73
  1. **Keep your science in R** — packages, formulas, plots, Bioconductor, the same engine as RStudio.
74
74
  2. **Learn enough Ruby/Rails** — routes, controllers, views, jobs, auth—not a second statistics career.
75
75
  3. **Call R from the app** — Galaaz loads R behind the scenes; long jobs can complete asynchronously
76
- while Rails stays responsive (see later sections on the bridge and `R::Async`).
76
+ while Rails stays responsive (see later sections on the bridge, `R::Async`, and `R::Job`).
77
77
  4. **Ship alone when you need to** — one developer can own both the analysis and the product UI,
78
78
  without waiting for a separate “stats engineer” and “Rails engineer.”
79
79
 
@@ -456,8 +456,13 @@ driver). For concurrency tests on the bridge itself, see `specs/bridge_concurren
456
456
 
457
457
  ## Long-running R calls and a completion block
458
458
 
459
- For R work that can take a long time, the bridge can avoid a Ruby-side **wait timeout** by
460
- scheduling the call and resuming in a **block** when the `RET` arrives.
459
+ For R work that can take a long time **on the bridge**, the bridge can avoid a Ruby-side
460
+ **wait timeout** by scheduling the call and resuming in a **block** when the `RET` arrives.
461
+
462
+ **Important distinction:** this keeps the **same** GNU R process busy. Other sync
463
+ `eval_r` / gknit chunks still wait on that R. For CRAN installs and other work that must
464
+ **not** monopolize the bridge (or that can OOM a small VM if abandoned mid-compile), use
465
+ **`R::Job`** in the next section instead.
461
466
 
462
467
  - **`R.eval_r_async(code, timeout: nil) { |result| ... }`** — string eval; on success, `result.value`
463
468
  is the same formatted string as **`R.eval_r`** (use `timeout: nil` for no Ruby-side limit).
@@ -514,6 +519,67 @@ the outcome to storage, and notify the client (poll, WebSocket, Turbo Stream, et
514
519
  Ruby pattern above is only to show **when** the result exists (inside the block, or after data
515
520
  written there is observed elsewhere). Runnable specs live in **`new_bridge_specs/eval_r_async_spec.rb`**.
516
521
 
522
+ ## Background R jobs (`R::Job`)
523
+
524
+ `R::Async` / `R.eval_r_async` free the **Ruby** thread while the **same** bridge R process
525
+ runs your code. That is enough for Rails-style “don’t block the request thread,” but not
526
+ enough for heavy `install.packages` or multi-minute model fits: the gatekeeper R is still
527
+ busy, other chunks time out, and abandoning the wait can leave compile work burning RAM.
528
+
529
+ **`R::Job`** runs that work in a **child `Rscript` process**. The bridge stays free. Logs and
530
+ metadata live under `~/.local/share/galaaz/jobs/` (override with `GALAAZ_JOBS_DIR`).
531
+
532
+ ### Package installs
533
+
534
+ `R.install_and_loads` / `R.install_rlibs` use `R::Job.install` and **await until the child
535
+ finishes** (default: no wall-clock limit). Optional limit: `GALAAZ_INSTALL_TIMEOUT_SEC` or
536
+ `install_timeout_sec:`. On timeout the child process group is killed so leftover
537
+ `make`/`gcc` cannot OOM the shell. Stale `00LOCK-*` dirs are cleared before the next install.
538
+ Only one install runs at a time (`install.lock`).
539
+
540
+ ``` ruby
541
+ # May take a long time the first time (e.g. caret); the bridge is not used for compile.
542
+ R.install_and_loads 'caret'
543
+ ```
544
+
545
+ ### Long arbitrary R (`eval` / `script`)
546
+
547
+ Prefer the **block** form (like `File.open`): await the child, yield the job, return the
548
+ block’s value. Without a block, the methods still await by default and return the `Job`.
549
+
550
+ ``` ruby
551
+ coef = R::Job.eval(<<~R) { |job| job.load_rds }
552
+ fit <- lm(mpg ~ wt, data = mtcars)
553
+ saveRDS(unname(coef(fit)), result_path)
554
+ R
555
+ puts coef
556
+ ```
557
+
558
+ ```
559
+ ## [1] 37.285126 -5.344472
560
+ ```
561
+
562
+ ``` ruby
563
+ # Without a block: awaits (wait: true is the default) and returns the Job
564
+ job = R::Job.eval(code)
565
+ job = R::Job.eval(code, wait: false) # start only; call job.wait later
566
+
567
+ # Script file; trailing args → commandArgs(trailingOnly=TRUE) in the child
568
+ res = R::Job.script('train.R', '5') { |job| job.load_rds }
569
+ ```
570
+
571
+ In the child: `setwd(job.dir)`, `.libPaths` includes the Galaaz user library,
572
+ `GALAAZ_JOB_DIR` is set, and **`result_path`** defaults to
573
+ `file.path(GALAAZ_JOB_DIR, "result.rds")`. Persist with `saveRDS(..., result_path)`, then
574
+ load on the bridge with **`job.load_rds`** (short sync `readRDS` → a normal Galaaz R object).
575
+
576
+ ### Choosing async vs Job
577
+
578
+ | Need | Use |
579
+ |------|-----|
580
+ | Don’t freeze a Ruby thread; short/medium R on the bridge is OK | `R::Async` / `R.eval_r_async` |
581
+ | Install CRAN packages, or long R that must not block the bridge | `R::Job` / `R.install_and_loads` |
582
+
517
583
  ## Galaaz + Rails (R-on-Rails) integration baseline
518
584
 
519
585
  This is the practical **R-on-Rails** starter: an R scientist’s analysis behind a small Rails
@@ -1873,7 +1939,8 @@ using even a very complex package as 'caret' is trivial with Galaaz.
1873
1939
 
1874
1940
  A word of advice: the 'caret' package has lots of dependencies and installing
1875
1941
  it in a Linux system is a time consuming operation. Method 'R.install_and_loads'
1876
- will install the package if it is not already installed and can take a while.
1942
+ will install the package if it is not already installed (via **`R::Job`**: a child
1943
+ `Rscript`, so the bridge stays free) and can take a while.
1877
1944
 
1878
1945
  ````
1879
1946
  ```{include model}
data/bin/gknit CHANGED
@@ -65,9 +65,12 @@ that are created during rendering.") do |opt|
65
65
  opts.on("--callback_timeout_ms TIMEOUT_MS", Integer, "Callback timeout in milliseconds for R<->Ruby calls inside chunk execution. Use for long installs/computation (e.g., 7200000 for 2h).") do |opt|
66
66
  options[:callback_timeout_ms] = opt
67
67
  end
68
- opts.on("--bridge_timeout_sec TIMEOUT_SEC", Integer, "Bridge request timeout in seconds for Ruby->R eval calls (default 60). Increase for long install.packages/compilation work.") do |opt|
68
+ opts.on("--bridge_timeout_sec TIMEOUT_SEC", Integer, "Bridge request timeout in seconds for Ruby->R eval calls (default 60). For CRAN installs via R::Job, use GALAAZ_INSTALL_TIMEOUT_SEC instead.") do |opt|
69
69
  options[:bridge_timeout_sec] = opt
70
70
  end
71
+ opts.on("--install_timeout_sec TIMEOUT_SEC", Integer, "Await limit in seconds for R::Job package installs (sets GALAAZ_INSTALL_TIMEOUT_SEC). Default: wait until the child finishes.") do |opt|
72
+ options[:install_timeout_sec] = opt
73
+ end
71
74
  end
72
75
 
73
76
  remaining_args = parser.permute(ARGV.dup)
@@ -153,6 +156,7 @@ input_dir = File.dirname(input_file)
153
156
  code = <<EOF
154
157
  ENV['GALAAZ_CALLBACK_TIMEOUT_MS'] = #{options[:callback_timeout_ms].to_s.inspect} if #{!options[:callback_timeout_ms].nil?}
155
158
  ENV['GALAAZ_BRIDGE_TIMEOUT_SEC'] = #{options[:bridge_timeout_sec].to_s.inspect} if #{!options[:bridge_timeout_sec].nil?}
159
+ ENV['GALAAZ_INSTALL_TIMEOUT_SEC'] = #{options[:install_timeout_sec].to_s.inspect} if #{!options[:install_timeout_sec].nil?}
156
160
  require %q(galaaz)
157
161
  require %q(gknit)
158
162
 
@@ -199,6 +203,11 @@ Dir.chdir(#{input_dir.inspect}) do
199
203
  STDERR.puts "Likely bridge timeout during long-running Ruby->R evaluation."
200
204
  STDERR.puts "Current bridge timeout: \#{bridge_sec} sec; callback timeout: \#{callback_ms} ms."
201
205
  STDERR.puts "Use gknit --bridge_timeout_sec <sec> and/or --callback_timeout_ms <ms> for longer operations."
206
+ elsif e.message.to_s.match?(/install job timed out/i)
207
+ install_sec = ENV['GALAAZ_INSTALL_TIMEOUT_SEC'] || '(none - wait until done)'
208
+ STDERR.puts "Likely R::Job install await timeout."
209
+ STDERR.puts "Current GALAAZ_INSTALL_TIMEOUT_SEC: \#{install_sec}."
210
+ STDERR.puts "Use gknit --install_timeout_sec <sec> (or unset it to wait forever), or install the package outside gknit."
202
211
  end
203
212
  if missing_pkg
204
213
  STDERR.puts "Likely missing LaTeX package: \#{missing_pkg} (install with tlmgr)."
@@ -218,6 +227,7 @@ libx = File.expand_path('../lib', File.dirname(__FILE__))
218
227
  require File.join(libx, 'galaaz_ruby')
219
228
  ENV['GALAAZ_CALLBACK_TIMEOUT_MS'] = options[:callback_timeout_ms].to_s if !options[:callback_timeout_ms].nil?
220
229
  ENV['GALAAZ_BRIDGE_TIMEOUT_SEC'] = options[:bridge_timeout_sec].to_s if !options[:bridge_timeout_sec].nil?
230
+ ENV['GALAAZ_INSTALL_TIMEOUT_SEC'] = options[:install_timeout_sec].to_s if !options[:install_timeout_sec].nil?
221
231
  exec "#{GalaazRuby.shell_invocation(libx)} -e #{Shellwords.escape(code)}"
222
232
 
223
233
  =begin
@@ -610,7 +610,8 @@ using even a very complex package as 'caret' is trivial with Galaaz.
610
610
 
611
611
  A word of advice: the 'caret' package has lots of dependencies and installing
612
612
  it in a Linux system is a time consuming operation. Method 'R.install_and_loads'
613
- will install the package if it is not already installed and can take a while.
613
+ will install the package if it is not already installed (via **`R::Job`**: a child
614
+ `Rscript`, so the bridge stays free) and can take a while.
614
615
 
615
616
  ````
616
617
  ```{include model}`r ''`
data/blogs/gknit/gknit.md CHANGED
@@ -1166,7 +1166,8 @@ using even a very complex package as 'caret' is trivial with Galaaz.
1166
1166
 
1167
1167
  A word of advice: the 'caret' package has lots of dependencies and installing
1168
1168
  it in a Linux system is a time consuming operation. Method 'R.install_and_loads'
1169
- will install the package if it is not already installed and can take a while.
1169
+ will install the package if it is not already installed (via **`R::Job`**: a child
1170
+ `Rscript`, so the bridge stays free) and can take a while.
1170
1171
 
1171
1172
  ````
1172
1173
  ```{include model}
@@ -371,8 +371,13 @@ driver). For concurrency tests on the bridge itself, see `specs/bridge_concurren
371
371
 
372
372
  ## Long-running R calls and a completion block
373
373
 
374
- For R work that can take a long time, the bridge can avoid a Ruby-side **wait timeout** by
375
- scheduling the call and resuming in a **block** when the `RET` arrives.
374
+ For R work that can take a long time **on the bridge**, the bridge can avoid a Ruby-side
375
+ **wait timeout** by scheduling the call and resuming in a **block** when the `RET` arrives.
376
+
377
+ **Important distinction:** this keeps the **same** GNU R process busy. Other sync
378
+ `eval_r` / gknit chunks still wait on that R. For CRAN installs and other work that must
379
+ **not** monopolize the bridge (or that can OOM a small VM if abandoned mid-compile), use
380
+ **`R::Job`** in the next section instead.
376
381
 
377
382
  - **`R.eval_r_async(code, timeout: nil) { |result| ... }`** — string eval; on success, `result.value`
378
383
  is the same formatted string as **`R.eval_r`** (use `timeout: nil` for no Ruby-side limit).
@@ -420,6 +425,63 @@ the outcome to storage, and notify the client (poll, WebSocket, Turbo Stream, et
420
425
  Ruby pattern above is only to show **when** the result exists (inside the block, or after data
421
426
  written there is observed elsewhere). Runnable specs live in **`new_bridge_specs/eval_r_async_spec.rb`**.
422
427
 
428
+ ## Background R jobs (`R::Job`)
429
+
430
+ `R::Async` / `R.eval_r_async` free the **Ruby** thread while the **same** bridge R process
431
+ runs your code. That is enough for Rails-style “don’t block the request thread,” but not
432
+ enough for heavy `install.packages` or multi-minute model fits: the gatekeeper R is still
433
+ busy, other chunks time out, and abandoning the wait can leave compile work burning RAM.
434
+
435
+ **`R::Job`** runs that work in a **child `Rscript` process**. The bridge stays free. Logs and
436
+ metadata live under `~/.local/share/galaaz/jobs/` (override with `GALAAZ_JOBS_DIR`).
437
+
438
+ ### Package installs
439
+
440
+ `R.install_and_loads` / `R.install_rlibs` use `R::Job.install` and **await until the child
441
+ finishes** (default: no wall-clock limit). Optional limit: `GALAAZ_INSTALL_TIMEOUT_SEC` or
442
+ `install_timeout_sec:`. On timeout the child process group is killed so leftover
443
+ `make`/`gcc` cannot OOM the shell. Stale `00LOCK-*` dirs are cleared before the next install.
444
+ Only one install runs at a time (`install.lock`).
445
+
446
+ ```{ruby r_job_install_note, eval=FALSE}
447
+ # May take a long time the first time (e.g. caret); the bridge is not used for compile.
448
+ R.install_and_loads 'caret'
449
+ ```
450
+
451
+ ### Long arbitrary R (`eval` / `script`)
452
+
453
+ Prefer the **block** form (like `File.open`): await the child, yield the job, return the
454
+ block’s value. Without a block, the methods still await by default and return the `Job`.
455
+
456
+ ```{ruby r_job_eval_block}
457
+ coef = R::Job.eval(<<~R) { |job| job.load_rds }
458
+ fit <- lm(mpg ~ wt, data = mtcars)
459
+ saveRDS(unname(coef(fit)), result_path)
460
+ R
461
+ puts coef
462
+ ```
463
+
464
+ ```{ruby r_job_eval_no_block, eval=FALSE}
465
+ # Without a block: awaits (wait: true is the default) and returns the Job
466
+ job = R::Job.eval(code)
467
+ job = R::Job.eval(code, wait: false) # start only; call job.wait later
468
+
469
+ # Script file; trailing args → commandArgs(trailingOnly=TRUE) in the child
470
+ res = R::Job.script('train.R', '5') { |job| job.load_rds }
471
+ ```
472
+
473
+ In the child: `setwd(job.dir)`, `.libPaths` includes the Galaaz user library,
474
+ `GALAAZ_JOB_DIR` is set, and **`result_path`** defaults to
475
+ `file.path(GALAAZ_JOB_DIR, "result.rds")`. Persist with `saveRDS(..., result_path)`, then
476
+ load on the bridge with **`job.load_rds`** (short sync `readRDS` → a normal Galaaz R object).
477
+
478
+ ### Choosing async vs Job
479
+
480
+ | Need | Use |
481
+ |------|-----|
482
+ | Don’t freeze a Ruby thread; short/medium R on the bridge is OK | `R::Async` / `R.eval_r_async` |
483
+ | Install CRAN packages, or long R that must not block the bridge | `R::Job` / `R.install_and_loads` |
484
+
423
485
  ## Galaaz + Rails (R-on-Rails) integration baseline
424
486
 
425
487
  This is the practical **R-on-Rails** starter: an R scientist’s analysis behind a small Rails
@@ -1163,7 +1225,8 @@ using even a very complex package as 'caret' is trivial with Galaaz.
1163
1225
 
1164
1226
  A word of advice: the 'caret' package has lots of dependencies and installing
1165
1227
  it in a Linux system is a time consuming operation. Method 'R.install_and_loads'
1166
- will install the package if it is not already installed and can take a while.
1228
+ will install the package if it is not already installed (via **`R::Job`**: a child
1229
+ `Rscript`, so the bridge stays free) and can take a while.
1167
1230
 
1168
1231
  ````
1169
1232
  ```{include model}`r ''`
@@ -73,7 +73,7 @@ rewrite in another stack. **R-on-Rails** means:
73
73
  1. **Keep your science in R** — packages, formulas, plots, Bioconductor, the same engine as RStudio.
74
74
  2. **Learn enough Ruby/Rails** — routes, controllers, views, jobs, auth—not a second statistics career.
75
75
  3. **Call R from the app** — Galaaz loads R behind the scenes; long jobs can complete asynchronously
76
- while Rails stays responsive (see later sections on the bridge and `R::Async`).
76
+ while Rails stays responsive (see later sections on the bridge, `R::Async`, and `R::Job`).
77
77
  4. **Ship alone when you need to** — one developer can own both the analysis and the product UI,
78
78
  without waiting for a separate “stats engineer” and “Rails engineer.”
79
79
 
@@ -456,8 +456,13 @@ driver). For concurrency tests on the bridge itself, see `specs/bridge_concurren
456
456
 
457
457
  ## Long-running R calls and a completion block
458
458
 
459
- For R work that can take a long time, the bridge can avoid a Ruby-side **wait timeout** by
460
- scheduling the call and resuming in a **block** when the `RET` arrives.
459
+ For R work that can take a long time **on the bridge**, the bridge can avoid a Ruby-side
460
+ **wait timeout** by scheduling the call and resuming in a **block** when the `RET` arrives.
461
+
462
+ **Important distinction:** this keeps the **same** GNU R process busy. Other sync
463
+ `eval_r` / gknit chunks still wait on that R. For CRAN installs and other work that must
464
+ **not** monopolize the bridge (or that can OOM a small VM if abandoned mid-compile), use
465
+ **`R::Job`** in the next section instead.
461
466
 
462
467
  - **`R.eval_r_async(code, timeout: nil) { |result| ... }`** — string eval; on success, `result.value`
463
468
  is the same formatted string as **`R.eval_r`** (use `timeout: nil` for no Ruby-side limit).
@@ -514,6 +519,67 @@ the outcome to storage, and notify the client (poll, WebSocket, Turbo Stream, et
514
519
  Ruby pattern above is only to show **when** the result exists (inside the block, or after data
515
520
  written there is observed elsewhere). Runnable specs live in **`new_bridge_specs/eval_r_async_spec.rb`**.
516
521
 
522
+ ## Background R jobs (`R::Job`)
523
+
524
+ `R::Async` / `R.eval_r_async` free the **Ruby** thread while the **same** bridge R process
525
+ runs your code. That is enough for Rails-style “don’t block the request thread,” but not
526
+ enough for heavy `install.packages` or multi-minute model fits: the gatekeeper R is still
527
+ busy, other chunks time out, and abandoning the wait can leave compile work burning RAM.
528
+
529
+ **`R::Job`** runs that work in a **child `Rscript` process**. The bridge stays free. Logs and
530
+ metadata live under `~/.local/share/galaaz/jobs/` (override with `GALAAZ_JOBS_DIR`).
531
+
532
+ ### Package installs
533
+
534
+ `R.install_and_loads` / `R.install_rlibs` use `R::Job.install` and **await until the child
535
+ finishes** (default: no wall-clock limit). Optional limit: `GALAAZ_INSTALL_TIMEOUT_SEC` or
536
+ `install_timeout_sec:`. On timeout the child process group is killed so leftover
537
+ `make`/`gcc` cannot OOM the shell. Stale `00LOCK-*` dirs are cleared before the next install.
538
+ Only one install runs at a time (`install.lock`).
539
+
540
+ ``` ruby
541
+ # May take a long time the first time (e.g. caret); the bridge is not used for compile.
542
+ R.install_and_loads 'caret'
543
+ ```
544
+
545
+ ### Long arbitrary R (`eval` / `script`)
546
+
547
+ Prefer the **block** form (like `File.open`): await the child, yield the job, return the
548
+ block’s value. Without a block, the methods still await by default and return the `Job`.
549
+
550
+ ``` ruby
551
+ coef = R::Job.eval(<<~R) { |job| job.load_rds }
552
+ fit <- lm(mpg ~ wt, data = mtcars)
553
+ saveRDS(unname(coef(fit)), result_path)
554
+ R
555
+ puts coef
556
+ ```
557
+
558
+ ```
559
+ ## [1] 37.285126 -5.344472
560
+ ```
561
+
562
+ ``` ruby
563
+ # Without a block: awaits (wait: true is the default) and returns the Job
564
+ job = R::Job.eval(code)
565
+ job = R::Job.eval(code, wait: false) # start only; call job.wait later
566
+
567
+ # Script file; trailing args → commandArgs(trailingOnly=TRUE) in the child
568
+ res = R::Job.script('train.R', '5') { |job| job.load_rds }
569
+ ```
570
+
571
+ In the child: `setwd(job.dir)`, `.libPaths` includes the Galaaz user library,
572
+ `GALAAZ_JOB_DIR` is set, and **`result_path`** defaults to
573
+ `file.path(GALAAZ_JOB_DIR, "result.rds")`. Persist with `saveRDS(..., result_path)`, then
574
+ load on the bridge with **`job.load_rds`** (short sync `readRDS` → a normal Galaaz R object).
575
+
576
+ ### Choosing async vs Job
577
+
578
+ | Need | Use |
579
+ |------|-----|
580
+ | Don’t freeze a Ruby thread; short/medium R on the bridge is OK | `R::Async` / `R.eval_r_async` |
581
+ | Install CRAN packages, or long R that must not block the bridge | `R::Job` / `R.install_and_loads` |
582
+
517
583
  ## Galaaz + Rails (R-on-Rails) integration baseline
518
584
 
519
585
  This is the practical **R-on-Rails** starter: an R scientist’s analysis behind a small Rails
@@ -1873,7 +1939,8 @@ using even a very complex package as 'caret' is trivial with Galaaz.
1873
1939
 
1874
1940
  A word of advice: the 'caret' package has lots of dependencies and installing
1875
1941
  it in a Linux system is a time consuming operation. Method 'R.install_and_loads'
1876
- will install the package if it is not already installed and can take a while.
1942
+ will install the package if it is not already installed (via **`R::Job`**: a child
1943
+ `Rscript`, so the bridge stays free) and can take a while.
1877
1944
 
1878
1945
  ````
1879
1946
  ```{include model}
@@ -24,7 +24,21 @@ module R
24
24
  File.expand_path('../../ext/new_bridge/galaaz_gatekeeper_phase1.cpp', __dir__)
25
25
  @client = NewBridge::SessionClient.new(source_path: source_path)
26
26
  @client.start(accept_timeout: 120)
27
+ bootstrap_session!
28
+ @ready = true
29
+ end
27
30
 
31
+ # After a hung/timed-out R eval (e.g. install.packages), kill R and start a fresh runtime
32
+ # so the compile does not keep running and OOM the host shell.
33
+ def restart_runtime!(accept_timeout: 120)
34
+ @ready = false
35
+ @client.restart!(accept_timeout: accept_timeout)
36
+ bootstrap_session!
37
+ @ready = true
38
+ self
39
+ end
40
+
41
+ def bootstrap_session!
28
42
  # Compatibility with the legacy ShadowBridge setup:
29
43
  # - `R.awt` / X11 for plotting (examples/sthda_ggplot).
30
44
  # - `missing_arg()` for Ruby :all in `[` / tbl subset (R::Support.parse_arg).
@@ -117,7 +131,6 @@ module R
117
131
  parent_id: callback_parent_id,
118
132
  timeout: init_timeout
119
133
  )
120
- @ready = true
121
134
  end
122
135
 
123
136
  def ready?
data/lib/R_interface/r.rb CHANGED
@@ -30,6 +30,7 @@ dir = File.dirname(File.expand_path('.', __FILE__))
30
30
 
31
31
  # Bridge and Support first
32
32
  require_relative 'rsupport'
33
+ require_relative 'r_job'
33
34
 
34
35
  # Operator modules next (so R::Object can include them)
35
36
  require_relative 'rbinary_operators'
@@ -134,68 +135,74 @@ module R
134
135
 
135
136
  def self.install_rlibs(*libs, install_timeout_sec: nil, callback_timeout_ms: nil, bridge_timeout_sec: nil)
136
137
  with_callback_timeout_ms(callback_timeout_ms) do
138
+ # Await wall-clock for the child Rscript job (nil = wait forever).
139
+ # Prefer install_timeout_sec / GALAAZ_INSTALL_TIMEOUT_SEC; bridge_timeout_sec
140
+ # is legacy and only used if the others are unset.
141
+ await_timeout = install_timeout_sec
142
+ if await_timeout.nil?
143
+ raw = ENV['GALAAZ_INSTALL_TIMEOUT_SEC']
144
+ unless raw.nil? || raw.to_s.strip.empty?
145
+ await_timeout = begin
146
+ Integer(raw)
147
+ rescue StandardError, ArgumentError
148
+ nil
149
+ end
150
+ end
151
+ end
152
+ await_timeout = bridge_timeout_sec if await_timeout.nil? && !bridge_timeout_sec.nil?
137
153
 
138
- # Use a fixed local library directory and ensure R can see it
139
- lib_dir = File.expand_path("~/R/x86_64-pc-linux-gnu-library/galaaz")
140
- FileUtils.mkdir_p(lib_dir) unless Dir.exist?(lib_dir)
141
- R.bridge.eval_r(".libPaths(c('#{lib_dir.gsub("'", "\\\\'")}', .libPaths()))", timeout: bridge_timeout_sec)
154
+ lib_dir = R::Job.default_lib_dir
155
+ FileUtils.mkdir_p(lib_dir)
156
+ R.bridge.eval_r(".libPaths(c('#{lib_dir.gsub("'", "\\\\'")}', .libPaths()))", timeout: 60)
142
157
 
143
158
  packages = R.c(*libs)
144
-
145
- # installed.packages() returns a matrix; package names are in the row names.
146
- # Using [:all, "Package"] on this matrix ends up calling [[ with a missing
147
- # row subscript in R, which raises "missing subscript". Instead, rely on
148
- # the row names vector for the list of installed packages.
149
- installed_mat = R.installed__packages(nil)
150
- installed_names = R.rownames(installed_mat)
151
-
152
- new_packages = packages[!(packages._ :in, installed_names)]
153
- new_packages_str = new_packages.to_s
154
-
155
- if(new_packages.length > 0 && new_packages_str != "character(0)")
156
- $stderr.puts "[RUBY] The following packages are missing and will be installed: #{new_packages_str}"
157
- $stderr.puts "[RUBY] Installing to: #{lib_dir}"
158
-
159
- # Get packages as Ruby array using :native mode
160
- pkg_list = []
161
- new_packages.each(:native) { |pkg| pkg_list << pkg }
162
- $stderr.puts "[RUBY] Package list: #{pkg_list.inspect}"
163
-
164
- # Install each package
165
- pkg_list.each do |pkg|
166
- $stderr.puts "[RUBY] Installing #{pkg}..."
167
- if install_timeout_sec
168
- timeout_i = Integer(install_timeout_sec)
169
- R.bridge.eval_r("options(timeout=#{timeout_i})", timeout: bridge_timeout_sec)
170
- end
171
- r_cmd = "install.packages('#{pkg.gsub("'", "\\\\'")}', repos='https://cloud.r-project.org', lib='#{lib_dir.gsub("'", "\\\\'")}', dependencies=NA)"
172
- $stderr.puts "[RUBY] R command: #{r_cmd}"
173
- result = R.bridge.eval_r(r_cmd, timeout: bridge_timeout_sec)
174
- $stderr.puts "[RUBY] Install output: #{result.inspect}"
175
- if result.to_s.include?("ANTICONF") || result.to_s.include?("Configuration failed")
176
- $stderr.puts "[RUBY] WARNING: Package #{pkg} failed to install due to missing system libraries."
177
- $stderr.puts "[RUBY] For kableExtra, you may need: libfontconfig1-dev libxml2-dev libfreetype6-dev"
159
+ installed_mat = R.installed__packages(nil)
160
+ installed_names = R.rownames(installed_mat)
161
+ new_packages = packages[!(packages._ :in, installed_names)]
162
+ new_packages_str = new_packages.to_s
163
+
164
+ if new_packages.length > 0 && new_packages_str != 'character(0)'
165
+ pkg_list = []
166
+ new_packages.each(:native) { |pkg| pkg_list << pkg }
167
+ $stderr.puts "[RUBY] The following packages are missing and will be installed via R::Job: #{pkg_list.inspect}"
168
+ $stderr.puts "[RUBY] Installing to: #{lib_dir}"
169
+ $stderr.puts "[RUBY] Bridge R stays free; awaiting child Rscript" +
170
+ (await_timeout ? " (timeout=#{await_timeout}s)" : ' (no await timeout)')
171
+
172
+ pkg_list.each do |pkg|
173
+ $stderr.puts "[RUBY] R::Job.install #{pkg}..."
174
+ begin
175
+ job = R::Job.install(pkg, lib_dir: lib_dir, wait: true, timeout: await_timeout)
176
+ job.raise_if_failed!
177
+ $stderr.puts "[RUBY] R::Job #{job.id} OK for #{pkg}"
178
+ rescue R::Job::Timeout => e
179
+ # Child Rscript process group already killed inside Job#wait.
180
+ raise "R package '#{pkg}' install job timed out. #{e.message}\n" \
181
+ "Install outside gknit if needed, then re-run:\n" \
182
+ " Rscript -e \"install.packages('#{pkg}', lib='#{lib_dir}', repos='#{R::Job::DEFAULT_REPOS}')\""
183
+ rescue R::Job::Failed => e
184
+ raise "Failed to install R package '#{pkg}' via R::Job.\n#{e.message}"
185
+ end
178
186
  end
179
- ensure
180
- begin
181
- R.bridge.eval_r("options(timeout=60)", timeout: bridge_timeout_sec)
182
- rescue StandardError
183
- nil
187
+
188
+ # Refresh bridge view of libraries after child install.
189
+ R.bridge.eval_r(".libPaths(c('#{lib_dir.gsub("'", "\\\\'")}', .libPaths()))", timeout: 60)
190
+ installed_mat = R.installed__packages(nil)
191
+ installed_names = R.rownames(installed_mat)
192
+ still_missing = packages[!(packages._ :in, installed_names)]
193
+ still_missing_str = still_missing.to_s
194
+ if still_missing.length > 0 && still_missing_str != 'character(0)'
195
+ raise "Failed to install packages: #{still_missing}. Check ~/.local/share/galaaz/jobs/*/job.log"
184
196
  end
185
197
  end
186
-
187
- # Re-check installed packages after install attempt
188
- installed_mat = R.installed__packages(nil)
189
- installed_names = R.rownames(installed_mat)
190
- still_missing = packages[!(packages._ :in, installed_names)]
191
- if still_missing.length > 0
192
- raise "Failed to install packages: #{still_missing.to_s}. Check stderr output above for [RUBY] debug messages."
193
- end
194
- end
195
-
196
198
  end
197
199
  end
198
200
 
201
+ # Start a CRAN install Job without awaiting (advanced). Prefer +install_and_loads+.
202
+ def self.install_async(*libs, lib_dir: R::Job.default_lib_dir)
203
+ R::Job.install(*libs, lib_dir: lib_dir, wait: false)
204
+ end
205
+
199
206
  #----------------------------------------------------------------------------------------
200
207
  #
201
208
  #----------------------------------------------------------------------------------------