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 +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +71 -4
- data/bin/gknit +11 -1
- data/blogs/gknit/gknit.Rmd +2 -1
- data/blogs/gknit/gknit.md +2 -1
- data/blogs/manual/manual.Rmd +66 -3
- data/blogs/manual/manual.md +71 -4
- data/lib/R_interface/new_bridge_adapter.rb +14 -1
- data/lib/R_interface/r.rb +60 -53
- data/lib/R_interface/r_job.rb +429 -0
- data/lib/galaaz/cli.rb +15 -1
- data/lib/gknit/diagnostics.rb +3 -1
- data/lib/new_bridge/session_client.rb +45 -0
- data/specs/gknit_install_timeout_report_spec.rb +27 -7
- data/specs/r_job_spec.rb +81 -0
- data/version.rb +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03ba582ba7054d11d6ce78bd5bf50fc424aac1a391209dee52749dd6bb642677
|
|
4
|
+
data.tar.gz: a9997f60ab4446b6ecdbda2cfa32a1f9de03fb002b8d2bdc5df945043fe547ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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::
|
|
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
|
|
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
|
|
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).
|
|
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
|
data/blogs/gknit/gknit.Rmd
CHANGED
|
@@ -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
|
|
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
|
|
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}
|
data/blogs/manual/manual.Rmd
CHANGED
|
@@ -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
|
|
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
|
|
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 ''`
|
data/blogs/manual/manual.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::
|
|
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
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
#----------------------------------------------------------------------------------------
|