galaaz 2.1.2 → 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 +38 -3
- 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/ext/new_bridge/galaaz_gatekeeper_phase1.cpp +13 -3
- 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,37 @@
|
|
|
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
|
+
|
|
28
|
+
## 2.1.3
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Gatekeeper builds on **R 4.5+/4.6** (Omarchy): replace removed `Rf_findVar` with
|
|
33
|
+
`R_getVarEx` (fallback to `Rf_findVar` on older R).
|
|
34
|
+
|
|
3
35
|
## 2.1.2
|
|
4
36
|
|
|
5
37
|
### Fixed
|
|
@@ -13,8 +45,9 @@
|
|
|
13
45
|
|
|
14
46
|
### Notes
|
|
15
47
|
|
|
16
|
-
- Omarchy dogfood:
|
|
17
|
-
|
|
48
|
+
- Omarchy dogfood: `script/omarchy/install-galaaz.sh` installs latest from RubyGems
|
|
49
|
+
(optional pin: `GALAAZ_GEM_VERSION=x.y.z`). Re-copy into `~/.local/bin/omarchy-install-galaaz`
|
|
50
|
+
after updating the script.
|
|
18
51
|
|
|
19
52
|
## 2.1.1
|
|
20
53
|
|
|
@@ -77,4 +110,6 @@ Galaaz 2.0 drives **GNU R** from **JRuby** or **CRuby** over a process bridge. I
|
|
|
77
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).
|
|
78
111
|
- The gatekeeper must be compiled on the install machine (Rcpp + C++ toolchain); it is not a prebuilt binary in the gem.
|
|
79
112
|
- Multi-runtime Docker R (Phase 4.5) is a slow integration spec, not part of cold-install CI.
|
|
80
|
-
- 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}
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// -----------------------------------------------------------------------------
|
|
46
46
|
|
|
47
47
|
#include <Rcpp.h>
|
|
48
|
+
#include <Rversion.h>
|
|
48
49
|
#include <arpa/inet.h>
|
|
49
50
|
#include <chrono>
|
|
50
51
|
#include <cstdint>
|
|
@@ -61,6 +62,15 @@
|
|
|
61
62
|
#include <unistd.h>
|
|
62
63
|
#include <vector>
|
|
63
64
|
|
|
65
|
+
// R 4.5+: Rf_findVar is no longer a public API entry (hidden in R 4.6).
|
|
66
|
+
// R_getVarEx(sym, rho, inherits, ifnotfound) is the supported replacement.
|
|
67
|
+
static inline SEXP galaaz_find_var(SEXP sym, SEXP env) {
|
|
68
|
+
#if defined(R_VERSION) && R_VERSION >= R_Version(4, 5, 0)
|
|
69
|
+
return R_getVarEx(sym, env, TRUE, R_UnboundValue);
|
|
70
|
+
#else
|
|
71
|
+
return Rf_findVar(sym, env);
|
|
72
|
+
#endif
|
|
73
|
+
}
|
|
64
74
|
// Active bridge socket used by main loop and callback path.
|
|
65
75
|
static int g_bridge_fd = -1;
|
|
66
76
|
// Instance id of the currently serviced REQ; reused by callback CALL messages.
|
|
@@ -848,7 +858,7 @@ EvalResult eval_unbox_walk_cmd(const std::string& cmd, const Rcpp::Environment&
|
|
|
848
858
|
}
|
|
849
859
|
|
|
850
860
|
SEXP sym = Rf_install(handle.c_str());
|
|
851
|
-
SEXP root =
|
|
861
|
+
SEXP root = galaaz_find_var(sym, env);
|
|
852
862
|
if (root == R_UnboundValue) return {false, payload_error("unbox_walk unknown handle")};
|
|
853
863
|
|
|
854
864
|
std::vector<std::pair<SEXP, int>> stack;
|
|
@@ -1001,7 +1011,7 @@ EvalResult eval_unbox_materialize_cmd(const std::string& cmd, const Rcpp::Enviro
|
|
|
1001
1011
|
}
|
|
1002
1012
|
|
|
1003
1013
|
SEXP sym = Rf_install(handle.c_str());
|
|
1004
|
-
SEXP root =
|
|
1014
|
+
SEXP root = galaaz_find_var(sym, env);
|
|
1005
1015
|
if (root == R_UnboundValue) return {false, payload_error("unbox_materialize unknown handle")};
|
|
1006
1016
|
|
|
1007
1017
|
std::vector<uint8_t> value_bytes;
|
|
@@ -1176,7 +1186,7 @@ static EvalResult eval_pull_vector_cmd(const std::string& cmd, const Rcpp::Envir
|
|
|
1176
1186
|
}
|
|
1177
1187
|
|
|
1178
1188
|
SEXP sym = Rf_install(handle.c_str());
|
|
1179
|
-
SEXP vec =
|
|
1189
|
+
SEXP vec = galaaz_find_var(sym, env);
|
|
1180
1190
|
if (vec == R_UnboundValue) {
|
|
1181
1191
|
return {false, payload_error("pull_vector unknown handle")};
|
|
1182
1192
|
}
|
|
@@ -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
|
#----------------------------------------------------------------------------------------
|