railst 0.2.1 → 0.2.2
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/lib/railst/cli.rb +14 -4
- data/lib/railst/templates.rb +158 -107
- data/lib/railst/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39b34a69f1512f12b3d0cafab0626efaf8d26e420a744c6c7ee8cd81c8c4b416
|
|
4
|
+
data.tar.gz: eeffce1eb4528445d31f36c5e5c61c4a411fab859a8663141ac3ebf7468cb814
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 991cebc2dbc0908279c391ad311ac7f1e3231078b0d4f1bcb56411f891b10c387b48c8f28ec3d65d5e810f0832727e775d6d3ecb6a3898bfc5d7d7722d2f53ef
|
|
7
|
+
data.tar.gz: 22e2646d19cb39ac9a3779b9c359c32f6cebab0185efff2773c6e6cde76cd0dcafa89fec90de6ae177b549162e7793760f32f2a870b1b276a667722846e99ad4
|
data/lib/railst/cli.rb
CHANGED
|
@@ -440,13 +440,17 @@ module Railst
|
|
|
440
440
|
|
|
441
441
|
Templates.inject_into(target_dir, project_root)
|
|
442
442
|
|
|
443
|
-
# Append rb_sys into Gemfile if not present
|
|
443
|
+
# Append rb_sys and railst into Gemfile if not present
|
|
444
444
|
gemfile = target_dir.join("Gemfile")
|
|
445
445
|
if gemfile.exist?
|
|
446
446
|
gem_content = gemfile.read
|
|
447
|
-
|
|
447
|
+
missing_gems = []
|
|
448
|
+
missing_gems << ' gem "railst", "~> 0.2"' unless gem_content.include?("railst")
|
|
449
|
+
missing_gems << ' gem "rb_sys", "~> 0.9"' unless gem_content.include?("rb_sys")
|
|
450
|
+
|
|
451
|
+
unless missing_gems.empty?
|
|
448
452
|
File.open(gemfile, "a") do |f|
|
|
449
|
-
f.puts "\n# Rust
|
|
453
|
+
f.puts "\n# Rust on Rails development & build tooling\ngroup :development, :test do\n#{missing_gems.join("\n")}\nend\n"
|
|
450
454
|
end
|
|
451
455
|
end
|
|
452
456
|
end
|
|
@@ -463,7 +467,13 @@ module Railst
|
|
|
463
467
|
|
|
464
468
|
Dir.chdir(target_dir) do
|
|
465
469
|
Kernel.system(Environment.toolchains_env, "bundle", "install")
|
|
466
|
-
Kernel.system(Environment.toolchains_env, "bin/rake", "rust:build")
|
|
470
|
+
success = Kernel.system(Environment.toolchains_env, "bin/rake", "rust:build")
|
|
471
|
+
unless success
|
|
472
|
+
puts ""
|
|
473
|
+
puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Failed to compile native Rust backend."
|
|
474
|
+
puts ""
|
|
475
|
+
exit 1
|
|
476
|
+
end
|
|
467
477
|
end
|
|
468
478
|
|
|
469
479
|
puts ""
|
data/lib/railst/templates.rb
CHANGED
|
@@ -92,7 +92,7 @@ module Railst
|
|
|
92
92
|
crate-type = ["cdylib"]
|
|
93
93
|
|
|
94
94
|
[dependencies]
|
|
95
|
-
magnus = "0.9"
|
|
95
|
+
magnus = { version = "0.9", features = ["rb-sys"] }
|
|
96
96
|
rb-sys = "0.9"
|
|
97
97
|
rayon = "1.10"
|
|
98
98
|
serde = { version = "1.0", features = ["derive"] }
|
|
@@ -113,30 +113,53 @@ module Railst
|
|
|
113
113
|
|
|
114
114
|
def self.lib_rs_content
|
|
115
115
|
<<~RUST
|
|
116
|
-
use magnus::{Error, Ruby};
|
|
117
|
-
|
|
118
|
-
pub mod dynamic_hash;
|
|
119
116
|
pub mod data_processor;
|
|
117
|
+
pub mod dynamic_hash;
|
|
120
118
|
pub mod gvl;
|
|
121
119
|
|
|
120
|
+
use magnus::{function, prelude::*, Error, RHash, Ruby};
|
|
121
|
+
|
|
122
|
+
fn hello(name: String) -> String {
|
|
123
|
+
format!("Hello {} from Rust (Magnus)!", name)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn calculate(ruby: &Ruby, a: f64, b: f64, op: String) -> Result<f64, Error> {
|
|
127
|
+
match op.as_str() {
|
|
128
|
+
"add" | "+" => Ok(a + b),
|
|
129
|
+
"sub" | "-" => Ok(a - b),
|
|
130
|
+
"mul" | "*" => Ok(a * b),
|
|
131
|
+
"div" | "/" => {
|
|
132
|
+
if b == 0.0 {
|
|
133
|
+
Err(Error::new(ruby.exception_zero_div_error(), "division by zero in Rust backend"))
|
|
134
|
+
} else {
|
|
135
|
+
Ok(a / b)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
_ => Err(Error::new(ruby.exception_arg_error(), format!("unknown operator: {}", op))),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn process_record(ruby: &Ruby, attributes: RHash) -> Result<RHash, Error> {
|
|
143
|
+
let result = ruby.hash_new();
|
|
144
|
+
attributes.foreach(|k: magnus::Value, v: magnus::Value| {
|
|
145
|
+
result.aset(k, v)?;
|
|
146
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
147
|
+
})?;
|
|
148
|
+
result.aset("rust_processed", true)?;
|
|
149
|
+
result.aset("rust_engine", "magnus-0.9")?;
|
|
150
|
+
Ok(result)
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
#[magnus::init]
|
|
123
154
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
124
155
|
let module = ruby.define_module("RustBackend")?;
|
|
156
|
+
module.define_singleton_method("hello", function!(hello, 1))?;
|
|
157
|
+
module.define_singleton_method("calculate", function!(calculate, 3))?;
|
|
158
|
+
module.define_singleton_method("process_record", function!(process_record, 1))?;
|
|
125
159
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)?;
|
|
130
|
-
|
|
131
|
-
module.define_singleton_method(
|
|
132
|
-
"aggregate_metrics",
|
|
133
|
-
magnus::function!(data_processor::aggregate_metrics, 2),
|
|
134
|
-
)?;
|
|
135
|
-
|
|
136
|
-
module.define_singleton_method(
|
|
137
|
-
"transform_record",
|
|
138
|
-
magnus::function!(data_processor::transform_record, 2),
|
|
139
|
-
)?;
|
|
160
|
+
// High performance data processing with dynamic Hashes & Rayon (GVL released)
|
|
161
|
+
module.define_singleton_method("process_batch", function!(data_processor::process_batch, 2))?;
|
|
162
|
+
module.define_singleton_method("aggregate_metrics", function!(data_processor::aggregate_metrics, 2))?;
|
|
140
163
|
|
|
141
164
|
Ok(())
|
|
142
165
|
}
|
|
@@ -145,123 +168,130 @@ module Railst
|
|
|
145
168
|
|
|
146
169
|
def self.gvl_rs_content
|
|
147
170
|
<<~RUST
|
|
148
|
-
use
|
|
149
|
-
use std::os::raw::c_void;
|
|
150
|
-
|
|
151
|
-
extern "C" fn nogvl_trampoline<F, R>(ptr: *mut c_void) -> *mut c_void
|
|
152
|
-
where
|
|
153
|
-
F: FnOnce() -> R,
|
|
154
|
-
{
|
|
155
|
-
let f = unsafe { Box::from_raw(ptr as *mut F) };
|
|
156
|
-
let result = f();
|
|
157
|
-
Box::into_raw(Box::new(result)) as *mut c_void
|
|
158
|
-
}
|
|
171
|
+
use std::ffi::c_void;
|
|
159
172
|
|
|
160
173
|
pub fn call_without_gvl<F, R>(f: F) -> R
|
|
161
174
|
where
|
|
162
175
|
F: FnOnce() -> R + Send,
|
|
163
176
|
R: Send,
|
|
164
177
|
{
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
struct State<F, R> {
|
|
179
|
+
f: Option<F>,
|
|
180
|
+
result: Option<R>,
|
|
181
|
+
}
|
|
167
182
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
unsafe extern "C" fn runner<F, R>(data: *mut c_void) -> *mut c_void
|
|
184
|
+
where
|
|
185
|
+
F: FnOnce() -> R + Send,
|
|
186
|
+
R: Send,
|
|
187
|
+
{
|
|
188
|
+
let state = &mut *(data as *mut State<F, R>);
|
|
189
|
+
if let Some(f) = state.f.take() {
|
|
190
|
+
state.result = Some(f());
|
|
191
|
+
}
|
|
192
|
+
std::ptr::null_mut()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let mut state = State {
|
|
196
|
+
f: Some(f),
|
|
197
|
+
result: None,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
unsafe {
|
|
201
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
202
|
+
Some(runner::<F, R>),
|
|
203
|
+
&mut state as *mut _ as *mut c_void,
|
|
172
204
|
None,
|
|
173
205
|
std::ptr::null_mut(),
|
|
174
|
-
)
|
|
175
|
-
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
176
208
|
|
|
177
|
-
|
|
178
|
-
*boxed_res
|
|
209
|
+
state.result.expect("GVL callback finished")
|
|
179
210
|
}
|
|
180
211
|
RUST
|
|
181
212
|
end
|
|
182
213
|
|
|
183
214
|
def self.dynamic_hash_rs_content
|
|
184
215
|
<<~RUST
|
|
185
|
-
use magnus::{
|
|
216
|
+
use magnus::{
|
|
217
|
+
prelude::*, rb_sys::AsRawValue, Error, Float, Integer, RArray, RHash, RString, Ruby, Symbol, Value,
|
|
218
|
+
};
|
|
186
219
|
use serde_json::{Map, Number, Value as JsonValue};
|
|
187
220
|
|
|
188
221
|
pub fn rval_to_json(val: Value) -> JsonValue {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
if let Ok(s) = String::try_convert(val) {
|
|
204
|
-
return JsonValue::String(s);
|
|
205
|
-
}
|
|
206
|
-
if let Ok(sym) = Symbol::try_convert(val) {
|
|
207
|
-
if let Ok(s) = sym.name() {
|
|
208
|
-
return JsonValue::String(s.to_string());
|
|
222
|
+
let raw = val.as_raw();
|
|
223
|
+
// CRuby special constants: Qnil = 8, Qtrue = 20, Qfalse = 0
|
|
224
|
+
if raw == 8 || val.is_nil() {
|
|
225
|
+
JsonValue::Null
|
|
226
|
+
} else if raw == 20 {
|
|
227
|
+
JsonValue::Bool(true)
|
|
228
|
+
} else if raw == 0 {
|
|
229
|
+
JsonValue::Bool(false)
|
|
230
|
+
} else if let Some(i) = Integer::from_value(val) {
|
|
231
|
+
if let Ok(num) = i.to_i64() {
|
|
232
|
+
JsonValue::Number(num.into())
|
|
233
|
+
} else {
|
|
234
|
+
JsonValue::Number(i.to_u64().unwrap_or(0).into())
|
|
209
235
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
236
|
+
} else if let Some(f) = Float::from_value(val) {
|
|
237
|
+
Number::from_f64(f.to_f64())
|
|
238
|
+
.map(JsonValue::Number)
|
|
239
|
+
.unwrap_or(JsonValue::Null)
|
|
240
|
+
} else if let Some(s) = RString::from_value(val) {
|
|
241
|
+
JsonValue::String(s.to_string().unwrap_or_default())
|
|
242
|
+
} else if let Some(sym) = Symbol::from_value(val) {
|
|
243
|
+
JsonValue::String(sym.name().unwrap_or_default().to_string())
|
|
244
|
+
} else if let Some(h) = RHash::from_value(val) {
|
|
212
245
|
let mut map = Map::new();
|
|
213
|
-
let _ =
|
|
214
|
-
let
|
|
215
|
-
s
|
|
216
|
-
} else if let
|
|
217
|
-
sym.name().
|
|
246
|
+
let _ = h.foreach(|k: Value, v: Value| {
|
|
247
|
+
let key = if let Some(s) = RString::from_value(k) {
|
|
248
|
+
s.to_string().unwrap_or_default()
|
|
249
|
+
} else if let Some(sym) = Symbol::from_value(k) {
|
|
250
|
+
sym.name().unwrap_or_default().to_string()
|
|
218
251
|
} else {
|
|
219
252
|
k.to_string()
|
|
220
253
|
};
|
|
221
|
-
map.insert(
|
|
222
|
-
Ok(
|
|
254
|
+
map.insert(key, rval_to_json(v));
|
|
255
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
223
256
|
});
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
return JsonValue::Array(vec);
|
|
257
|
+
JsonValue::Object(map)
|
|
258
|
+
} else if let Some(arr) = RArray::from_value(val) {
|
|
259
|
+
let vec: Vec<JsonValue> = arr.into_iter().map(rval_to_json).collect();
|
|
260
|
+
JsonValue::Array(vec)
|
|
261
|
+
} else {
|
|
262
|
+
JsonValue::String(val.to_string())
|
|
232
263
|
}
|
|
233
|
-
JsonValue::String(val.to_string())
|
|
234
264
|
}
|
|
235
265
|
|
|
236
266
|
pub fn json_to_rval(ruby: &Ruby, val: &JsonValue) -> Result<Value, Error> {
|
|
237
267
|
match val {
|
|
238
268
|
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
239
|
-
JsonValue::Bool(b) => Ok(ruby.into_value(*b)),
|
|
269
|
+
JsonValue::Bool(b) => Ok(ruby.into_value(*b).as_value()),
|
|
240
270
|
JsonValue::Number(n) => {
|
|
241
271
|
if let Some(i) = n.as_i64() {
|
|
242
|
-
Ok(ruby.into_value(i))
|
|
272
|
+
Ok(ruby.into_value(i).as_value())
|
|
243
273
|
} else if let Some(f) = n.as_f64() {
|
|
244
|
-
Ok(ruby.into_value(f))
|
|
274
|
+
Ok(ruby.into_value(f).as_value())
|
|
245
275
|
} else {
|
|
246
|
-
Ok(ruby.
|
|
276
|
+
Ok(ruby.qnil().as_value())
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
|
-
JsonValue::String(s) => Ok(ruby.
|
|
279
|
+
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
250
280
|
JsonValue::Array(arr) => {
|
|
251
|
-
let
|
|
281
|
+
let r_arr = ruby.ary_new_capa(arr.len());
|
|
252
282
|
for item in arr {
|
|
253
|
-
|
|
283
|
+
r_arr.push(json_to_rval(ruby, item)?)?;
|
|
254
284
|
}
|
|
255
|
-
Ok(
|
|
285
|
+
Ok(r_arr.as_value())
|
|
256
286
|
}
|
|
257
287
|
JsonValue::Object(map) => {
|
|
258
|
-
let
|
|
288
|
+
let r_hash = ruby.hash_new();
|
|
259
289
|
for (k, v) in map {
|
|
260
|
-
let
|
|
261
|
-
let
|
|
262
|
-
|
|
290
|
+
let rk = ruby.str_new(k);
|
|
291
|
+
let rv = json_to_rval(ruby, v)?;
|
|
292
|
+
r_hash.aset(rk, rv)?;
|
|
263
293
|
}
|
|
264
|
-
Ok(
|
|
294
|
+
Ok(r_hash.as_value())
|
|
265
295
|
}
|
|
266
296
|
}
|
|
267
297
|
}
|
|
@@ -276,23 +306,8 @@ module Railst
|
|
|
276
306
|
use rayon::prelude::*;
|
|
277
307
|
use serde_json::{json, Value as JsonValue};
|
|
278
308
|
|
|
279
|
-
pub fn transform_record(ruby: &Ruby, record: RHash, config: RHash) -> Result<Value, Error> {
|
|
280
|
-
let mut raw_record = rval_to_json(record.as_value());
|
|
281
|
-
let raw_config = rval_to_json(config.as_value());
|
|
282
|
-
|
|
283
|
-
let field = raw_config.get("compute_field").and_then(|v| v.as_str()).unwrap_or("value").to_string();
|
|
284
|
-
let mult = raw_config.get("multiplier").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
|
285
|
-
|
|
286
|
-
if let JsonValue::Object(ref mut map) = raw_record {
|
|
287
|
-
let current_val = map.get(&field).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
288
|
-
map.insert(format!("{}_computed", field), json!(current_val * mult));
|
|
289
|
-
map.insert("processed_by".to_string(), json!("rust_magnus"));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
json_to_rval(ruby, &raw_record)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
309
|
pub fn process_batch(ruby: &Ruby, records: RArray, config: RHash) -> Result<Value, Error> {
|
|
310
|
+
// 1. Convert dynamic Ruby objects to Rust JsonValues while holding GVL
|
|
296
311
|
let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
|
|
297
312
|
let raw_config = rval_to_json(config.as_value());
|
|
298
313
|
|
|
@@ -312,6 +327,7 @@ module Railst
|
|
|
312
327
|
.and_then(|v| v.as_f64())
|
|
313
328
|
.unwrap_or(0.0);
|
|
314
329
|
|
|
330
|
+
// 2. Release GVL and process in parallel across all CPU cores with Rayon
|
|
315
331
|
let processed_records = call_without_gvl(move || {
|
|
316
332
|
raw_records
|
|
317
333
|
.into_par_iter()
|
|
@@ -336,6 +352,7 @@ module Railst
|
|
|
336
352
|
.collect::<Vec<JsonValue>>()
|
|
337
353
|
});
|
|
338
354
|
|
|
355
|
+
// 3. Serialize back into native Ruby Hash / Array
|
|
339
356
|
let result_json = JsonValue::Array(processed_records);
|
|
340
357
|
json_to_rval(ruby, &result_json)
|
|
341
358
|
}
|
|
@@ -344,6 +361,7 @@ module Railst
|
|
|
344
361
|
let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
|
|
345
362
|
let target_field = metric_field.clone();
|
|
346
363
|
|
|
364
|
+
// Release GVL for parallel reduction across all cores
|
|
347
365
|
let (count, sum, min, max) = call_without_gvl(move || {
|
|
348
366
|
let values: Vec<f64> = raw_records
|
|
349
367
|
.into_par_iter()
|
|
@@ -383,6 +401,39 @@ module Railst
|
|
|
383
401
|
|
|
384
402
|
json_to_rval(ruby, &summary)
|
|
385
403
|
}
|
|
404
|
+
|
|
405
|
+
#[cfg(test)]
|
|
406
|
+
mod tests {
|
|
407
|
+
use super::*;
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn test_parallel_reduction_metrics() {
|
|
411
|
+
let records = vec![
|
|
412
|
+
json!({ "reading": 10.0 }),
|
|
413
|
+
json!({ "reading": 20.0 }),
|
|
414
|
+
json!({ "reading": 30.0 }),
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
let target_field = "reading".to_string();
|
|
418
|
+
let values: Vec<f64> = records
|
|
419
|
+
.into_par_iter()
|
|
420
|
+
.filter_map(|rec| {
|
|
421
|
+
if let JsonValue::Object(map) = rec {
|
|
422
|
+
map.get(&target_field).and_then(|v| v.as_f64())
|
|
423
|
+
} else {
|
|
424
|
+
None
|
|
425
|
+
}
|
|
426
|
+
})
|
|
427
|
+
.collect();
|
|
428
|
+
|
|
429
|
+
assert_eq!(values.len(), 3);
|
|
430
|
+
let sum: f64 = values.par_iter().sum();
|
|
431
|
+
assert_eq!(sum, 60.0);
|
|
432
|
+
let count = values.len();
|
|
433
|
+
let avg = sum / count as f64;
|
|
434
|
+
assert_eq!(avg, 20.0);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
386
437
|
RUST
|
|
387
438
|
end
|
|
388
439
|
|
data/lib/railst/version.rb
CHANGED