galaaz 2.1.0 → 2.1.1

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: 0fc6f307a98f073e4ad49a39f5b4fcfddbd948c9054722caea814c16d6be62fa
4
- data.tar.gz: d899ca8013e16d158cb1c035a3e8d1393b3d377782fad502b09d1e81d9eb868a
3
+ metadata.gz: 671c2fe1fb15dbd12a7f491e52c318d01ebe9625e78d9acdaaa3dffbe3efbd3d
4
+ data.tar.gz: 2d4d63f915f0b8a09cff53df98802f7bcaff0298f9247ef38b583b2a0e902730
5
5
  SHA512:
6
- metadata.gz: a73ce81b2b86550af39562adc7d1566746c5239d0b74688c8d1d1cef52bb5371ab5449cb3dbe3d6b14b2608e1c33e3289937f745a9749c7c425f026a4d8f8659
7
- data.tar.gz: 3d84d7e16f678ed8b13920585099df2ae641293ae74f65e7920d7bd7ee42c971deaa9c7dd1985343fb2eadf17be582d3922eecf3d20d009bfc45180f762b7b26
6
+ metadata.gz: 1dc8f434731a05f495909d5f8bc70949f7ec7ae8034fbe8dd6a026f6b9592d426d573dffc0f13326ddfb7c830112bd6922fcdb8ca717e091d8722e219ea43de6
7
+ data.tar.gz: d594128365977ac52a3a2d15f6e3dd5f1b97ee312c0c6232883f6fb8838fb7c1440f2272c3302e53853026befd7e625f6e4124a8d3ddd32959f7bf665cb8cda7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Added
6
+
7
+ - CLI on `bin/galaaz`: `setup`, `blogs init`, `doctor`, `add` (knit / arrow / tex / bio /
8
+ examples / ledger / demo). Legacy unknown args still forward to rake.
9
+ - Omarchy dogfood helpers under `script/omarchy/` (install/remove scripts + menu overlay).
10
+ - `galaaz doctor` reports both CRuby and JRuby when available (mise-aware).
11
+ - Package lists `r_requires/knit.txt`, `knit-extras.txt`, `arrow.txt` for add-on installs.
12
+ - Cold-install CRuby path uses `galaaz setup` + `galaaz blogs init`.
13
+
14
+ ### Notes
15
+
16
+ - Omarchy / stranger install remains `gem install galaaz` then `galaaz setup` (no
17
+ `bundle install` for core use). See `Documentation/PLAN_OMARCHY_INTEGRATION.md`.
18
+
3
19
  ## 2.1.0
4
20
 
5
21
  Galaaz **2.1** keeps the **galaaz2_0** integration line as the main development branch and marks
@@ -10,14 +26,16 @@ Galaaz **2.1** keeps the **galaaz2_0** integration line as the main development
10
26
  - First-class **CRuby** NewBridge path alongside JRuby (engine-aware launchers, cold-install /
11
27
  CI coverage for both).
12
28
  - Docs: treat JRuby and CRuby as equal supported runtimes for the bridge.
13
- - [Documentation/ROADMAP_ARROW_RUBY_R.md](Documentation/ROADMAP_ARROW_RUBY_R.md) — roadmap for
14
- Ruby↔R Arrow IPC / mmap handoff (stage B) then shared-memory bus (stage C).
29
+ - [Documentation/ROADMAP_ARROW_RUBY_R.md](Documentation/ROADMAP_ARROW_RUBY_R.md) — stages A (copy),
30
+ B1/B2 (IPC/mmap file; shipped), C (shared-memory bus; future).
31
+ - Stage **B1/B2** APIs: `Galaaz::ArrowIpc`, `R::Arrow.open_ipc`, `R::Arrow.write_ipc` (CRuby
32
+ red-arrow + system Arrow GLib; JRuby Arrow Java + `JAVA_OPTS` nio opens).
15
33
 
16
34
  ### Notes
17
35
 
18
36
  - Git branch **`galaaz2_0`** remains the integration branch name; the gem version is **2.1.0**.
19
- - Apache Arrow **zero-copy shared RAM** is still future work (see the roadmap); today’s helpers
20
- build R-side tables after a handoff, then use proxy / Remote Control calls.
37
+ - Apache Arrow **zero-copy shared RAM** (Stage C) is still future work. Stage A copies into R;
38
+ Stage B uses an IPC file and only the path crosses NewBridge. See the roadmap.
21
39
 
22
40
  ## 2.0.0
23
41
 
data/README.md CHANGED
@@ -59,9 +59,11 @@ CRuby when you prefer MRI. R remains the same **GNU R** you use interactively—
59
59
  compiled extensions and Bioconductor. Earlier GraalVM / TruffleRuby / FastR experiments
60
60
  are no longer the focus.
61
61
 
62
- The bridge handles **communication and typing** between the two worlds; large tables can
63
- also flow through **Apache Arrow** on the R side when you use the optional helpers described
64
- later in this manual.
62
+ The bridge handles **communication and typing** between the two worlds. Large tables can use
63
+ **Apache Arrow** in two shipped modes (described later): **Stage A** copies Ruby batches into an
64
+ R-side Arrow table (`R::Arrow.from_ruby_batches`); **Stage B** writes an Arrow IPC file and only
65
+ the **path** crosses NewBridge (`Galaaz::ArrowIpc` + `R::Arrow.open_ipc` / `write_ipc`). Shared-heap
66
+ zero-copy is **Stage C** and is not shipped.
65
67
 
66
68
  ## R-on-Rails: the one-person app for R scientists
67
69
 
@@ -340,7 +342,7 @@ The supported install is **`gem install` + compile the gatekeeper**. You do not
340
342
  make -C "${gem_dir}/ext/new_bridge" all
341
343
  ```
342
344
 
343
- 5. Ensure **`R`** starts GNU R and can install packages (network access to CRAN when you first call `R.install_and_loads`). For **Apache Arrow** on Java 9+, pass `-J--add-opens=java.base/java.nio=ALL-UNNAMED` to JRuby (from a checkout, `bin/galaaz-jruby` does this; on CRuby this flag is not needed).
345
+ 5. Ensure **`R`** starts GNU R and can install packages (network access to CRAN when you first call `R.install_and_loads`). For **Apache Arrow** on **JRuby** (Java 9+), the child JVM needs `--add-opens=java.base/java.nio=ALL-UNNAMED` via **`JAVA_OPTS`** (from a checkout, `bin/galaaz-jruby` and `mise.toml` set this; a leading `jruby -J... -S bundle exec` does **not** pass `-J` to rspec). On **CRuby**, install Apache Arrow GLib (`libarrow-glib-dev` from the [Apache Arrow APT](https://arrow.apache.org/install/) repo) and `gem install red-arrow` matching `pkg-config --modversion arrow-glib`. Do not install the unrelated Rubygems package named `arrow`.
344
346
 
345
347
  For **gKnit**, **knitr**, **rmarkdown**, and LaTeX (PDF output), install the corresponding R packages, **Pandoc**, and a TeX distribution if you need PDF; the repository includes helpers such as **`bin/install-tinytex`** where appropriate.
346
348
 
@@ -442,13 +444,15 @@ A practical pattern is:
442
444
 
443
445
  1. Use threads (or a connection pool) to read from **multiple databases or shards** in parallel.
444
446
  2. Merge the rows in Ruby under a `Mutex` if you collect into one structure.
445
- 3. Hand the merged table to R **once** (for example with `R::Arrow.from_ruby_batches` and dplyr,
446
- or by building a data frame) so heavy statistics run in R with fewer bridge round-trips.
447
+ 3. Hand the merged table to R **once**: **`R::Arrow.from_ruby_batches`** (Stage A: copy into R) or
448
+ **`Galaaz::ArrowIpc.write` / `write_batches`** then **`R::Arrow.open_ipc`** (Stage B: IPC file;
449
+ only the path crosses the bridge). Then run dplyr in R.
447
450
 
448
451
  A runnable sketch lives in
449
452
  `examples/multithread_shards_to_r/shards_to_r.rb` (simulated shard queries; swap in your DB
450
453
  driver). For concurrency tests on the bridge itself, see `specs/bridge_concurrent_spec.rb` and
451
- `specs/arrow_from_ruby_batches_spec.rb`.
454
+ `specs/arrow_from_ruby_batches_spec.rb`. Stage B IPC tests: `specs/arrow_ipc_handoff_spec.rb`,
455
+ `specs/arrow_ipc_export_spec.rb`.
452
456
 
453
457
  ## Long-running R calls and a completion block
454
458
 
@@ -4025,35 +4029,77 @@ ans = flights[:all, E.list(R[:arr_delay], R[:dep_delay])]
4025
4029
  # Apache Arrow
4026
4030
 
4027
4031
  [Apache Arrow](https://arrow.apache.org/) is a **columnar** in-memory format used heavily in R
4028
- and Python for analytics. In Galaaz, **Ruby does not hold an Arrow C++ table itself**; instead you
4029
- build ordinary Ruby structures (arrays of row hashes), and **`R::Arrow.from_ruby_batches`** creates
4030
- a real **Arrow `Table` inside GNU R**. From there you use R’s **`arrow`** and **`dplyr`** packages
4031
- as usual: **`group_by`** on the Arrow table, **`summarise`** for aggregates, then **`collect()`** to
4032
- materialize a tibble when you need in-memory R rows.
4033
-
4034
- That pattern matches production use: **JRuby threads** (or sequential code) assemble many rows in
4035
- Ruby; you pay **one** bridge-heavy handoff to R; **dplyr** runs vectorised work on the Arrow table
4036
- in R.
4032
+ and Python for analytics. GNU R still runs in a **separate process**. Ruby does **not** hold a
4033
+ shared Arrow C++ table with R. Stages:
4034
+
4035
+ 1. **Stage A (copy over the bridge):** Ruby row hashes **`R::Arrow.from_ruby_batches`** builds
4036
+ an Arrow `Table` **inside GNU R**. You get a **proxy**.
4037
+ 2. **Stage B1 (Ruby → R IPC file):** **`Galaaz::ArrowIpc.write`** / **`write_batches`** writes an
4038
+ Arrow IPC file (prefer **`/dev/shm`**); **`R::Arrow.open_ipc(path)`** opens it in R. Only the
4039
+ **path** crosses NewBridge. This is **mmap/IPC file handoff**, not a shared heap.
4040
+ 3. **Stage B2 (R → Ruby IPC file):** **`R::Arrow.write_ipc(obj)`** writes uncompressed IPC; Ruby
4041
+ reads with **`Galaaz::ArrowIpc.read`** (column hash) or **`read_batches`** (row hashes). Call
4042
+ **`Galaaz::ArrowIpc.release(path)`** when finished.
4043
+ 4. **Stage C (not shipped):** named shared-memory bus. Do not claim 0 ms shared RAM until then.
4044
+ See **`Documentation/ROADMAP_ARROW_RUBY_R.md`**.
4045
+
4046
+ After ingest, use R’s **`arrow`** / **`dplyr`** on the proxy (`group_by`, `summarise`, `collect`)
4047
+ and unbox only KPIs you need in Ruby.
4048
+
4049
+ **Optional Ruby backends for Stage B**
4050
+
4051
+ * **CRuby:** Apache **red-arrow** — `gem install red-arrow` pinned to the same major as
4052
+ `pkg-config --modversion arrow-glib`, plus system **Arrow GLib** (`libarrow-glib-dev` from the
4053
+ [Apache Arrow APT](https://arrow.apache.org/install/) repo). Do **not** install the unrelated
4054
+ legacy Rubygems package named `arrow`. `bundle exec` still sees a user-installed `red-arrow`
4055
+ via Galaaz’s load-path helper.
4056
+ * **JRuby:** Apache Arrow **Java** JARs — **`GALAAZ_ARROW_JARS`**, `~/arrow_jars`, or
4057
+ `jar-dependencies`. Export **`JAVA_OPTS=--add-opens=java.base/java.nio=ALL-UNNAMED`** on the
4058
+ **child** JVM (`bin/galaaz-jruby`, `mise.toml`). `jruby -J... -S bundle exec rspec` does **not**
4059
+ pass `-J` to rspec.
4060
+
4061
+ **R packages:** **`arrow`** and **`dplyr`**. B2 writes IPC with **`compression: 'uncompressed'`**
4062
+ so JRuby Arrow Java can read without extra compression JARs.
4063
+
4064
+ **Tests:** `specs/arrow_from_ruby_batches_spec.rb` (A);
4065
+ `specs/arrow_ipc_handoff_spec.rb`, `specs/arrow_ipc_export_spec.rb` (B, sync);
4066
+ `new_bridge_specs/arrow_ipc_async_spec.rb`, `new_bridge_specs/arrow_ipc_export_async_spec.rb` (B, async).
4067
+
4068
+ ## `R::Arrow` and `Galaaz::ArrowIpc`
4069
+
4070
+ * **`R::Arrow.from_ruby_batches`** — Stage A ingest.
4071
+ * **`R::Arrow.open_ipc(path)`** — Stage B1: IPC file → R Table proxy.
4072
+ * **`R::Arrow.write_ipc(obj, path = nil)`** — Stage B2: R Table/tibble → IPC path (scratch if omitted).
4073
+ * **`Galaaz::ArrowIpc.write` / `write_batches` / `read` / `read_batches` / `allocate_path` / `release` / `available?`**
4074
+ * **`R::Arrow.table_from(df)`** — wrap an R `data.frame` / tibble as an Arrow table.
4075
+ * **`R::Arrow.read_feather` / `write_feather`**, **`read_parquet`**, **`dataset(path)`** — file and
4076
+ dataset IO on paths visible to R.
4037
4077
 
4038
- **Prerequisites:** install R packages **`arrow`** and **`dplyr`**. Run scripts with
4039
- **`bin/galaaz-jruby`** (or the same JVM flags as in **`docs/testing.md`**) so the Arrow JNI stack is
4040
- available.
4078
+ ## Example: Stage B round-trip (IPC file)
4041
4079
 
4042
- ## Other `R::Arrow` helpers
4080
+ Requires `Galaaz::ArrowIpc.available?` (red-arrow or Arrow JARs) and R **`arrow`**. Not knitted
4081
+ below so a machine without the optional backend still builds this manual.
4043
4082
 
4044
- The Ruby module **`R::Arrow`** (see `lib/R_interface/r_arrow.rb`) also includes:
4083
+ ```ruby
4084
+ path = Galaaz::ArrowIpc.write(id: [1, 2, 3], grp: %w[a a b], value: [1.0, 2.0, 3.5])
4085
+ tbl = R::Arrow.open_ipc(path)
4086
+ Galaaz::ArrowIpc.release(path)
4045
4087
 
4046
- * **`R::Arrow.table_from(df)`** — wrap an R `data.frame` / tibble as an Arrow table.
4047
- * **`R::Arrow.read_feather` / `write_feather`**, **`read_parquet`**, **`dataset(path)`** — file and
4048
- dataset IO on paths visible to R.
4088
+ summed = R.dplyr___summarise(R.dplyr___group_by(tbl, :grp), total: E.sum(:value))
4089
+ out_path = R::Arrow.write_ipc(summed)
4090
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
4091
+ Galaaz::ArrowIpc.release(out_path)
4092
+ # rows => [{:grp=>"a", :total=>3.0}, {:grp=>"b", :total=>3.5}] (illustrative)
4093
+ ```
4049
4094
 
4050
4095
  ## Example: many Ruby rows → Arrow in R → grouped statistics
4051
4096
 
4052
4097
  The repository test **`slow-specs/arrow_large_pipeline_spec.rb`** builds **200k rows** in parallel
4053
- (eight threads × 25,000 rows), pushes them through **`R::Arrow.from_ruby_batches`**, then checks that
4054
- **dplyr** group summaries match a Ruby reference calculation. The same logic appears below at a
4055
- **smaller scale** so this manual can knit quickly; increase `thread_count` and `rows_per_thread`
4056
- when experimenting locally.
4098
+ (eight threads × 25,000 rows), pushes them through **`R::Arrow.from_ruby_batches`** (Stage A), then
4099
+ checks that **dplyr** group summaries match a Ruby reference calculation. The same logic appears
4100
+ below at a **smaller scale** so this manual can knit quickly; increase `thread_count` and
4101
+ `rows_per_thread` when experimenting locally. For the same ingest **without** copying every cell
4102
+ over NewBridge, use Stage B (`write_batches` + `open_ipc`) instead of `from_ruby_batches`.
4057
4103
 
4058
4104
 
4059
4105
  ``` ruby
@@ -4119,10 +4165,10 @@ end
4119
4165
  ```
4120
4166
 
4121
4167
  **What to notice:** (1) Ruby only sees **`Hash`** rows and Ruby **`Thread`** objects; (2) a single
4122
- **`from_ruby_batches`** call creates the Arrow table in R; (3) **`dplyr___group_by`** /
4168
+ **`from_ruby_batches`** call **copies** those columns into an Arrow table in R; (3) **`dplyr___group_by`** /
4123
4169
  **`dplyr___summarise`** / **`dplyr___collect`** mirror **`dplyr::group_by`** /
4124
4170
  **`dplyr::summarise`** / **`dplyr::collect`** on an Arrow-backed table. For a lighter test, see
4125
- **`specs/arrow_from_ruby_batches_spec.rb`**; for the full-size benchmark, run
4171
+ **`specs/arrow_from_ruby_batches_spec.rb`**; for the full-size Stage A benchmark, run
4126
4172
  **`bin/run_slow_rspec slow-specs/arrow_large_pipeline_spec.rb`**.
4127
4173
 
4128
4174
  # Bioconductor and DESeq2
@@ -4260,8 +4306,9 @@ Practical tips:
4260
4306
  glue.
4261
4307
  * **Reuse one process**: running many short scripts cold-starts Ruby, the JVM, and R each time;
4262
4308
  a long-lived process or repeated calls in one run amortize setup (see benchmarks below).
4263
- * **Batch data**: merge shards in Ruby, then call **`R::Arrow.from_ruby_batches`** (or build one
4264
- data frame) instead of millions of tiny R calls.
4309
+ * **Batch data**: merge shards in Ruby, then **`R::Arrow.from_ruby_batches`** (Stage A) or
4310
+ **`Galaaz::ArrowIpc`** + **`R::Arrow.open_ipc`** (Stage B) instead of millions of tiny R calls.
4311
+ When Ruby needs a bulky result table back, **`R::Arrow.write_ipc`** + **`Galaaz::ArrowIpc.read_batches`**.
4265
4312
 
4266
4313
  For measured discussion (including DESeq2-style workloads and warm comparisons), see
4267
4314
  **`docs/performance.md`** and **`docs/deseq2_airway_benchmark.md`** in the Galaaz repository.
data/bin/galaaz CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- Dir.chdir(File.dirname(File.expand_path('..', __FILE__)))
4
+ # Galaaz CLI: setup / blogs / doctor / add.
5
+ # Unknown commands still forward to rake (legacy example runners).
4
6
 
5
- system ("rake #{ARGV[0]}")
7
+ root = File.expand_path('..', __dir__)
8
+ lib = File.join(root, 'lib')
9
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
10
+
11
+ require 'galaaz/cli'
12
+
13
+ exit Galaaz::CLI.run(ARGV)
data/bin/galaaz-jruby CHANGED
File without changes
data/bin/galaaz-ruby CHANGED
File without changes
@@ -1,6 +1,22 @@
1
- # Galaaz required JVM flags for JRuby (Apache Arrow on Java 9+).
1
+ # Galaaz required JVM flags for JRuby (Apache Arrow memory on Java 9+).
2
2
  # Source from bin/*.sh: source "$ROOT/bin/galaaz_jruby_env.inc.sh"
3
3
  #
4
- # Source of truth in Ruby/Rake: lib/galaaz_jruby.rb (GalaazJRuby::REQUIRED_JRUBY_J_ARGS).
4
+ # Source of truth in Ruby/Rake: lib/galaaz_jruby.rb (GalaazJRuby::JAVA_NIO_ADD_OPENS).
5
5
  # Optional extra -J flags: export GALAAZ_JRUBY_OPTS="-J-Xmx4g" (space-separated).
6
- GALAAZ_REQUIRED_JRUBY_J_ARGS='-J--add-opens=java.base/java.nio=ALL-UNNAMED'
6
+ #
7
+ # JAVA_OPTS is exported so *child* JRuby processes (bundle exec rspec, mise exec)
8
+ # get the same opens flag. -J args on the outer command are not inherited by
9
+ # bundle exec's second JVM.
10
+ GALAAZ_JAVA_NIO_ADD_OPENS='--add-opens=java.base/java.nio=ALL-UNNAMED'
11
+ GALAAZ_REQUIRED_JRUBY_J_ARGS="-J${GALAAZ_JAVA_NIO_ADD_OPENS}"
12
+
13
+ case " ${JAVA_OPTS:-} " in
14
+ *" ${GALAAZ_JAVA_NIO_ADD_OPENS} "*) ;;
15
+ *)
16
+ if [ -n "${JAVA_OPTS:-}" ]; then
17
+ export JAVA_OPTS="${GALAAZ_JAVA_NIO_ADD_OPENS} ${JAVA_OPTS}"
18
+ else
19
+ export JAVA_OPTS="${GALAAZ_JAVA_NIO_ADD_OPENS}"
20
+ fi
21
+ ;;
22
+ esac
@@ -7,6 +7,7 @@
7
7
  #
8
8
  # JRuby and CRuby are both first-class. Override with GALAAZ_RUBY=jruby, GALAAZ_RUBY=ruby,
9
9
  # or a full path. Extra JRuby flags: GALAAZ_JRUBY_OPTS="-J-Xmx4g" (ignored on CRuby).
10
+ # CRuby red-arrow uses system Arrow GLib (Apache Arrow APT / libarrow-glib-dev).
10
11
 
11
12
  GALAAZ_RUBY_BIN="${GALAAZ_RUBY:-ruby}"
12
13
 
@@ -29,6 +30,7 @@ if [[ "$_galaaz_is_jruby" -eq 1 ]]; then
29
30
  # shellcheck source=galaaz_jruby_env.inc.sh
30
31
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/galaaz_jruby_env.inc.sh"
31
32
  GALAAZ_RUBY_J_ARGS="$GALAAZ_REQUIRED_JRUBY_J_ARGS ${GALAAZ_JRUBY_OPTS:-}"
33
+ # JAVA_OPTS is exported by galaaz_jruby_env.inc.sh for bundle-exec child JVMs
32
34
  else
33
35
  GALAAZ_REQUIRED_JRUBY_J_ARGS=''
34
36
  GALAAZ_RUBY_J_ARGS=''
data/bin/gbookdown CHANGED
File without changes
@@ -68,9 +68,9 @@ Rails never went away—it matured. Several factors make Rails compelling again:
68
68
 
69
69
  1. **Rails (Ruby)**: Handles web requests, authentication, authorization, database operations, background jobs, email, APIs, caching, sessions, and orchestration
70
70
  2. **R**: Handles statistical modeling, machine learning, visualization, and data analysis
71
- 3. **Galaaz**: The seamless bridge that makes this integration feel like a single system—using Apache Arrow for zero-copy data transfer at scale
71
+ 3. **Galaaz**: The seamless bridge that makes this integration feel like a single system—proxies for analytics in R, Apache Arrow for bulky tables (Stage A copy, Stage B IPC/mmap file; shared-heap zero-copy is Stage C and not shipped)
72
72
 
73
- **The Value Proposition**: Keep everything you love about R—the packages, the syntax, the statistical rigor—while gaining everything Rails provides for production applications. Move data between languages at memory speed, not serialization speed.
73
+ **The Value Proposition**: Keep everything you love about R—the packages, the syntax, the statistical rigor—while gaining everything Rails provides for production applications. For large tables, prefer Arrow **IPC files** (path across the bridge, bytes on disk or `/dev/shm`) over CSV/JSON serialization. Do **not** claim Ruby and R share one physical Arrow heap until Stage C ships.
74
74
 
75
75
  ---
76
76
 
@@ -86,25 +86,24 @@ Earlier versions of Galaaz explored GraalVM's polyglot capabilities with FastR (
86
86
  ├─────────────────────────────────────────────────────────────────┤
87
87
  │ │
88
88
  │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
89
- │ │ JRuby │ ←─────→ │ Galaaz Bridge│ ←─────→ │ GNU R │ │
90
- │ │ (JVM) (Java) │ (JNI/R API) │ (C/R) │ Process │ │
91
- │ └──────────────┘ └──────────────┘ └──────────┘ │
92
- │ │ │ │
89
+ │ │ JRuby/CRuby │ ←─────→ │ Galaaz Bridge│ ←─────→ │ GNU R │ │
90
+ │ │ path + │ (NewBridge) │ C/R │ Process │ │
91
+ │ └──────────────┘ cmds └──────────────┘ └──────────┘ │
93
92
  │ │ │ │
94
93
  │ ▼ ▼ │
95
94
  │ ┌──────────────┐ ┌──────────┐│
96
95
  │ │ Rails App │ │ Arrow ││
97
- │ │ Multi-thread Shared ││
98
- │ │ ActiveRecord │ │ Memory ││
96
+ │ │ (threads / bulky tables: IPC file Table in ││
97
+ │ │ processes) │ ──── mmap / /dev/shm (Stage B) ──│ R (proxy)││
99
98
  │ └──────────────┘ └──────────┘│
100
99
  │ │
101
100
  └─────────────────────────────────────────────────────────────────┘
102
101
  ```
103
102
 
104
103
  **How it works:**
105
- 1. **JRuby** runs on the JVM with true multi-threading, running Rails and application code
106
- 2. **Galaaz Bridge** communicates with a GNU R process via R's C API (or alternative mechanisms)
107
- 3. **Apache Arrow** (optional) provides efficient, shared-memory data transfer for large datasets
104
+ 1. **JRuby or CRuby** runs Rails and application code (JRuby for true JVM threads; CRuby for MRI). Both talk the same NewBridge protocol.
105
+ 2. **Galaaz Bridge** communicates with a GNU R process (NewBridge / gatekeeper)—commands and small values, not bulk bytes when you use Stage B.
106
+ 3. **Apache Arrow** (optional): **Stage A** copies Ruby batches into an R-side Arrow Table (`R::Arrow.from_ruby_batches`). **Stage B** writes an Arrow IPC file; only the **path** crosses the bridge (`Galaaz::ArrowIpc` + `R::Arrow.open_ipc` / `write_ipc`). **Stage C** (shared-heap zero-copy) is not shipped—see `Documentation/ROADMAP_ARROW_RUBY_R.md`.
108
107
  4. **GNU R** is the actual R interpreter—full compatibility with all R packages (ggplot2, dplyr, Bioconductor, etc.)
109
108
 
110
109
  **Why this is better than FastR/GraalVM:**
@@ -448,13 +447,22 @@ end
448
447
 
449
448
  ---
450
449
 
451
- ### Section 6: Apache Arrow - Zero-Copy Data at Scale
450
+ ### Section 6: Apache Arrow bulky tables without CSV/JSON
451
+
452
+ **Key Message**: Traditional bridges often serialize through text (CSV, JSON). Galaaz uses Apache Arrow in **two shipped modes**. Neither is a shared Ruby+R heap. Maintainer detail: `Documentation/ROADMAP_ARROW_RUBY_R.md`. User manual: `# Apache Arrow` in `README.md` / `blogs/manual/manual.md`.
452
453
 
453
- **Key Message**: Traditional bridges serialize data (JSON, CSV) to pass between languages. Galaaz uses Apache Arrow for zero-copy, memory-mapped data transfer—enabling huge datasets to move from Ruby to R instantly.
454
+ | Stage | API | What actually happens |
455
+ |-------|-----|------------------------|
456
+ | **A** (shipped) | `R::Arrow.from_ruby_batches` / `table_from` | Columns are **copied** into GNU R; Ruby holds a **proxy**. dplyr then runs in R. |
457
+ | **B1** (shipped) | `Galaaz::ArrowIpc.write` → `R::Arrow.open_ipc(path)` | Ruby writes an Arrow **IPC file** (prefer `/dev/shm`); NewBridge carries only the **path**. R memory-maps / reads the file into an Arrow Table. |
458
+ | **B2** (shipped) | `R::Arrow.write_ipc` → `Galaaz::ArrowIpc.read` / `read_batches` | R writes IPC (uncompressed so JRuby Arrow Java can read); Ruby reads columns or row hashes for DB/API. |
459
+ | **C** (future) | not shipped | Named shared segment; do **not** claim 0 ms shared RAM until this exists and is measured. |
460
+
461
+ **Writers/readers (Stage B):** CRuby uses **red-arrow** (`gem install red-arrow` matching `pkg-config --modversion arrow-glib`, plus Apache Arrow APT / `libarrow-glib-dev`). JRuby uses **Apache Arrow Java** JARs (`GALAAZ_ARROW_JARS` or `~/arrow_jars`) and `JAVA_OPTS` `--add-opens=java.base/java.nio=ALL-UNNAMED` on the **child** JVM (`bin/galaaz-jruby` / `mise.toml`). Do not install the unrelated Rubygems gem named `arrow`.
454
462
 
455
463
  **The Problem: Data Transfer Overhead**
456
464
 
457
- In traditional polyglot systems (rpy2, reticulate), passing data between languages requires serialization:
465
+ In traditional polyglot systems (rpy2, reticulate), passing data between languages often means a text round-trip:
458
466
 
459
467
  ```python
460
468
  # Python → R via JSON/CSV serialization (SLOW)
@@ -470,36 +478,31 @@ ro.globalenv['r_df'] = pandas2ri.py2rpy(df)
470
478
  This approach:
471
479
  - Copies all data through a text format
472
480
  - Loses type information (factors become strings, dates become strings)
473
- - Consumes 2-3x memory during transfer
474
- - Becomes a bottleneck at ~100K+ rows
481
+ - Consumes extra memory during transfer
482
+ - Becomes a bottleneck at large row counts
475
483
 
476
- **The Galaaz Solution: Apache Arrow**
484
+ **Stage A: copy into R, then Remote Control**
477
485
 
478
- Apache Arrow is a columnar, in-memory format designed for zero-copy data sharing across languages. With Galaaz:
486
+ Assemble row hashes in Ruby (threads OK on JRuby), then one ingest. Analytics stay on the R proxy; unbox KPIs only.
479
487
 
480
488
  ```ruby
481
489
  require 'galaaz'
482
490
 
483
- # 1. Build data in Ruby—using parallel threads for multi-source ingestion
484
491
  batches = []
485
492
  mutex = Mutex.new
486
493
  threads = []
487
494
 
488
- # Simulate parallel data ingestion from multiple sources
489
495
  [0, 1, 2, 3].each do |tid|
490
496
  threads << Thread.new do
491
- # Each thread queries its own database/shard/API
492
- local_data = fetch_from_source(tid) # Returns array of hashes
497
+ local_data = fetch_from_source(tid) # array of hashes
493
498
  mutex.synchronize { batches << local_data }
494
499
  end
495
500
  end
496
501
  threads.each(&:join)
497
502
 
498
- # 2. Convert to Arrow Table—zero-copy transfer to R
499
- # This is the key: data moves to R without serialization!
503
+ # Copy into an Arrow Table *inside GNU R* (not shared heap with Ruby)
500
504
  table = R::Arrow.from_ruby_batches(batches)
501
505
 
502
- # 3. Use R's dplyr directly on the Arrow table
503
506
  R.install_and_loads('dplyr', 'arrow')
504
507
 
505
508
  grouped = R.dplyr___group_by(table, :region)
@@ -510,57 +513,59 @@ summarised = R.dplyr___summarise(
510
513
  total: E.sum(:value)
511
514
  )
512
515
  results = R.dplyr___collect(summarised)
513
-
514
516
  puts results
515
- #> # A tibble: 4 × 4
516
- #> region count avg_value total
517
- #> <chr> <int> <dbl> <dbl>
518
- #> 1 North 25000 5.5 137500
519
- #> 2 South 25000 5.5 137500
520
- #> 3 East 25000 5.5 137500
521
- #> 4 West 25000 5.5 137500
522
517
  ```
523
518
 
524
- **Why This Matters:**
519
+ **Stage B: IPC file; path on the bridge**
520
+
521
+ Use this when the table is large enough that copying every cell over NewBridge/MsgPack is the wrong tax. Scratch files prefer `/dev/shm`.
522
+
523
+ ```ruby
524
+ require 'galaaz'
525
+
526
+ # B1: Ruby → file → R
527
+ path = Galaaz::ArrowIpc.write_batches(batches) # or .write(col => array, ...)
528
+ tbl = R::Arrow.open_ipc(path)
529
+ Galaaz::ArrowIpc.release(path) # unlink after R has opened (default B1 lifetime)
530
+
531
+ grouped = R.dplyr___group_by(tbl, :region)
532
+ summarised = R.dplyr___summarise(grouped, total: E.sum(:value))
533
+
534
+ # B2: R → file → Ruby (e.g. rows for ActiveRecord / JSON)
535
+ out_path = R::Arrow.write_ipc(summarised)
536
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
537
+ Galaaz::ArrowIpc.release(out_path)
538
+
539
+ render json: { analytics: rows }
540
+ ```
541
+
542
+ **Why this matters (honest):**
525
543
 
526
- - **No serialization cost**: Data stays in Arrow's columnar format throughout
527
- - **Type preservation**: Factors, dates, timestamps remain intact
528
- - **Memory efficiency**: No intermediate copies; Ruby and R share the same memory
529
- - **Scale**: Tested with millions of rows (see slow-specs/arrow_large_pipeline_spec.rb with 200K+ rows)
530
- - **Lazy evaluation**: Arrow datasets can be filtered/aggregated before materializing
544
+ - **Columnar Arrow**, not CSV, for the bulky payload
545
+ - **Type-preserving** numeric/string columns on the B1/B2 contract (int32/int64, float64, utf8; nulls kept)
546
+ - **Stage A** still copies; **Stage B** still materializes a file R (or Ruby) then reads—R may mmap the IPC file
547
+ - **Not** “Ruby and R map the same live buffer” until Stage C
548
+ - **Scale**: Stage A pipeline in `slow-specs/arrow_large_pipeline_spec.rb` (200K+ rows); Stage B specs in `specs/arrow_ipc_handoff_spec.rb`, `specs/arrow_ipc_export_spec.rb`, and the matching `new_bridge_specs/arrow_ipc_*_async_spec.rb`
549
+ - **Parquet/Feather/dataset** stay R-side file APIs (`R::Arrow.write_parquet`, `dataset`)
531
550
 
532
- **Real-World Example: Multi-DB Aggregation**:
551
+ **Real-world sketch: shards in Ruby, analytics in R**
533
552
 
534
553
  ```ruby
535
- # Aggregate data from multiple PostgreSQL shards in Ruby,
536
- # then analyze in R with zero-copy transfer
554
+ # Parallel ingest in Ruby, then one Arrow handoff (A or B), then dplyr in R.
537
555
 
538
556
  require 'galaaz'
539
557
  require 'active_record'
540
558
 
541
- # Connect to multiple database shards
542
- SHARDS = ['shard_1', 'shard_2', 'shard_3', 'shard_4'].map do |shard_name|
543
- ActiveRecord::Base.establish_connection(
544
- adapter: 'postgresql',
545
- host: "#{shard_name}.db.internal",
546
- database: 'analytics'
547
- )
548
- end
549
-
550
- # Parallel data collection from all shards
551
559
  batches = []
552
560
  mutex = Mutex.new
553
561
 
554
562
  SHARDS.each do |shard|
555
563
  Thread.new do
556
- # Query this shard
557
564
  records = shard.connection.select_all(<<-SQL).cast_values
558
565
  SELECT user_id, region, event_type, value, created_at
559
566
  FROM events
560
567
  WHERE created_at > NOW() - INTERVAL '7 days'
561
568
  SQL
562
-
563
- # Convert to array of hashes for Arrow
564
569
  batch = records.map do |row|
565
570
  {
566
571
  user_id: row[0],
@@ -570,60 +575,37 @@ SHARDS.each do |shard|
570
575
  created_at: row[4]
571
576
  }
572
577
  end
573
-
574
578
  mutex.synchronize { batches << batch }
575
579
  end
576
580
  end
577
581
 
578
- # Zero-copy transfer to R as Arrow Table
579
- table = R::Arrow.from_ruby_batches(batches)
582
+ R.install_and_loads('dplyr', 'arrow')
580
583
 
581
- # Now use R's full power on the complete dataset
582
- R.install_and_loads('dplyr', 'arrow', 'lubridate')
584
+ # Prefer B1 when the merged table is large:
585
+ path = Galaaz::ArrowIpc.write_batches(batches)
586
+ table = R::Arrow.open_ipc(path)
587
+ Galaaz::ArrowIpc.release(path)
583
588
 
584
- # R's dplyr works directly on Arrow tables (lazy evaluation)
585
- analysis = table \
586
- .dplyr___filter(E.created_at > (E.now() - E.days(7))) \
587
- .dplyr___group_by(:region, :event_type) \
588
- .dplyr___summarise(
589
+ analysis = R.dplyr___collect(
590
+ R.dplyr___summarise(
591
+ R.dplyr___group_by(table, :region, :event_type),
589
592
  count: E.n(),
590
- total_value: E.sum(:value),
591
- avg_value: E.mean(:value),
592
- unique_users: E.n_distinct(:user_id)
593
+ total_value: E.sum(:value)
593
594
  )
595
+ )
594
596
 
595
- # Materialize results when ready
596
- results = R.dplyr___collect(analysis)
597
-
598
- # Convert back to Ruby objects for API response
599
- results_hash = results.to_ruby
600
- render json: { analytics: results_hash }
597
+ out_path = R::Arrow.write_ipc(analysis)
598
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
599
+ Galaaz::ArrowIpc.release(out_path)
600
+ render json: { analytics: rows }
601
601
  ```
602
602
 
603
- **Parquet and Feather: Persistence Without Conversion**:
603
+ **Parquet and Feather (R-visible paths):**
604
604
 
605
605
  ```ruby
606
- # Save Ruby data as Parquet (columnar, compressed)
607
- # Then read directly into R without parsing
608
-
609
- # 1. Build large dataset in Ruby
610
- data = (1..1_000_000).map do |i|
611
- {
612
- id: i,
613
- category: ["A", "B", "C", "D"][i % 4],
614
- value: rand * 100,
615
- timestamp: Time.now - (i % 86400)
616
- }
617
- end
618
-
619
- # 2. Convert to R data.frame, then write as Parquet
620
606
  df = R.data__frame(data)
621
607
  R::Arrow.write_parquet(df, '/data/events.parquet')
622
-
623
- # 3. Later, read directly into R as a dataset (lazy, memory-mapped)
624
608
  dataset = R::Arrow.dataset('/data/events.parquet')
625
-
626
- # Query without loading entire file
627
609
  summary = R.dplyr___collect(
628
610
  dataset \
629
611
  .dplyr___filter(R[:value] > 50) \
@@ -632,28 +614,13 @@ summary = R.dplyr___collect(
632
614
  )
633
615
  ```
634
616
 
635
- **Large-Scale Test Results** (from `slow-specs/arrow_large_pipeline_spec.rb`):
636
-
637
- ```ruby
638
- # Test: 8 threads × 25,000 rows = 200,000 rows
639
- # With weighted aggregations in R
640
- thread_count = 8
641
- rows_per_thread = 25_000
642
- group_count = 10
643
-
644
- # Ruby parallel batch construction... 8 threads
645
- # Arrow table creation from batches → instant
646
- # R dplyr group_by + summarise → native speed
647
- # Results verified accurate against Ruby reference implementation
648
- ```
649
-
650
- **Key Insight**: With Arrow, the boundary between Ruby and R disappears for data. You can build data pipelines in Ruby (with its superior concurrency and database libraries) and analyze in R (with its statistical ecosystem)—with **zero overhead** at the language boundary.
617
+ **Key Insight**: Build pipelines in Ruby (concurrency, ActiveRecord); run statistics in R. Pay **one** bulky handoff (copy or IPC file), then **move commands** on proxies. That is the production story today—not a shared Arrow heap.
651
618
 
652
619
  ---
653
620
 
654
621
  ### Section 7: Parallelism and Process Orchestration
655
622
 
656
- **Key Message**: R is single-threaded. Galaaz solves this by orchestrating multiple R processes from Ruby's multi-threaded environment—now with Arrow for efficient data distribution.
623
+ **Key Message**: R is single-threaded. Galaaz solves this by orchestrating multiple R processes from Ruby—Arrow for bulky ingest/export, proxies for analytics per worker.
657
624
 
658
625
  **The Architecture**:
659
626
  ```
@@ -892,7 +859,7 @@ Galaaz 2.0 represents the maturation of the Ruby-R bridge:
892
859
  - **JRuby + GNU R architecture**: Moved from experimental GraalVM/FastR to battle-tested JRuby and standard GNU R for full package compatibility and production stability
893
860
  - **New bridge architecture**: More robust, faster, better error handling
894
861
  - **Process management**: Built-in support for R process pools
895
- - **Apache Arrow integration**: Zero-copy data transfer for large datasets—build data pipelines in Ruby's multi-threaded environment, analyze in R with zero serialization overhead
862
+ - **Apache Arrow**: Stage A copy (`from_ruby_batches`) and Stage B IPC/mmap (`Galaaz::ArrowIpc`, `open_ipc` / `write_ipc`). Shared-heap zero-copy is Stage C. Build pipelines in Ruby, analyze in R, unbox KPIs.
896
863
  - **gKnit improvements**: Better R Markdown integration
897
864
  - **Rails integration**: First-class support for Rails patterns
898
865
 
@@ -907,7 +874,7 @@ For the full blog post, include references to:
907
874
  - **NSE/dplyr**: `blogs/nse_dplyr/nse_dplyr.md`
908
875
  - **Object-Oriented**: `blogs/oh_my/oh_my.md`
909
876
  - **Plotting Tutorial**: `blogs/ruby_plot/ruby_plot.md`
910
- - **Apache Arrow**: `specs/arrow_semantics_spec.rb`, `specs/arrow_from_ruby_batches_spec.rb`, `slow-specs/arrow_large_pipeline_spec.rb`
877
+ - **Apache Arrow**: `Documentation/ROADMAP_ARROW_RUBY_R.md`; `specs/arrow_ipc_handoff_spec.rb`, `specs/arrow_ipc_export_spec.rb`, `new_bridge_specs/arrow_ipc_async_spec.rb`, `new_bridge_specs/arrow_ipc_export_async_spec.rb`; Stage A: `specs/arrow_from_ruby_batches_spec.rb`, `slow-specs/arrow_large_pipeline_spec.rb`
911
878
  - **Specs**: `specs/r_nse.spec.rb`, `specs/r_vector_functions.spec.rb`
912
879
 
913
880
  ---