Skip to content

Commit fe353cd

Browse files
davejcamerondnagoda
authored andcommitted
Add batch mode for processing multiple inputs via JSONL
1 parent 6ab5b13 commit fe353cd

1 file changed

Lines changed: 174 additions & 17 deletions

File tree

‎src/main.rs‎

Lines changed: 174 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use wasmtime::Module;
33

44
use std::{
55
fs::File,
6-
io::{stdin, BufReader, Read},
6+
io::{stdin, BufRead, BufReader, Read},
77
path::PathBuf,
88
};
99

@@ -61,6 +61,14 @@ struct Opts {
6161
/// Path to graphql file containing Function input query; if omitted, defaults will be used to calculate limits.
6262
#[clap(short = 'q', long)]
6363
query_path: Option<PathBuf>,
64+
65+
/// Enable batch mode - read multiple JSON inputs (one per line) from stdin/file
66+
#[clap(short, long)]
67+
batch: bool,
68+
69+
/// In batch mode, continue processing on individual input errors (default: false)
70+
#[clap(long)]
71+
batch_continue_on_error: bool,
6472
}
6573

6674
impl Opts {
@@ -114,6 +122,31 @@ fn read_file_to_string(file_path: &PathBuf) -> Result<String> {
114122
fn main() -> Result<()> {
115123
let opts: Opts = Opts::parse();
116124

125+
// Create engine and module once (expensive operations - amortize across all inputs)
126+
let engine = function_runner::engine::new_engine()?;
127+
let module = Module::from_file(&engine, &opts.function)
128+
.map_err(|e| anyhow!("Couldn't load the Function {:?}: {}", &opts.function, e))?;
129+
130+
// Infer codec from the module based on imported modules
131+
let codec = if function_runner::engine::uses_msgpack_provider(&module) {
132+
Codec::Messagepack
133+
} else {
134+
Codec::Json
135+
};
136+
137+
if opts.batch {
138+
run_batch_mode(&opts, &engine, &module, codec)
139+
} else {
140+
run_single_mode(&opts, &engine, &module, codec)
141+
}
142+
}
143+
144+
fn run_single_mode(
145+
opts: &Opts,
146+
engine: &wasmtime::Engine,
147+
module: &Module,
148+
codec: Codec,
149+
) -> Result<()> {
117150
let mut input: Box<dyn Read + Sync + Send + 'static> = if let Some(ref input) = opts.input {
118151
Box::new(BufReader::new(File::open(input).map_err(|e| {
119152
anyhow!("Couldn't load input {:?}: {}", input, e)
@@ -130,20 +163,8 @@ fn main() -> Result<()> {
130163
input.read_to_end(&mut buffer)?;
131164

132165
let schema_string = opts.read_schema_to_string().transpose()?;
133-
134166
let query_string = opts.read_query_to_string().transpose()?;
135167

136-
let engine = function_runner::engine::new_engine()?;
137-
let module = Module::from_file(&engine, &opts.function)
138-
.map_err(|e| anyhow!("Couldn't load the Function {:?}: {}", &opts.function, e))?;
139-
140-
// Infer codec from the module based on imported modules
141-
let codec = if function_runner::engine::uses_msgpack_provider(&module) {
142-
Codec::Messagepack
143-
} else {
144-
Codec::Json
145-
};
146-
147168
let input = BytesContainer::new(BytesContainerType::Input, codec, buffer)?;
148169
let scale_factor = if let (Some(schema_string), Some(query_string), Some(json_value)) =
149170
(schema_string, query_string, input.json_value.clone())
@@ -156,19 +177,19 @@ fn main() -> Result<()> {
156177
&json_value,
157178
)?
158179
} else {
159-
DEFAULT_SCALE_FACTOR // Use default scale factor when schema or query is missing
180+
DEFAULT_SCALE_FACTOR
160181
};
161182

162183
let profile_opts = opts.profile_opts();
163184

164185
let function_run_result = run(FunctionRunParams {
165-
function_path: opts.function,
186+
function_path: opts.function.clone(),
166187
input,
167188
export: opts.export.as_ref(),
168189
profile_opts: profile_opts.as_ref(),
169190
scale_factor,
170-
module,
171-
engine,
191+
module: module.clone(),
192+
engine: engine.clone(),
172193
})?;
173194

174195
if opts.json {
@@ -187,3 +208,139 @@ fn main() -> Result<()> {
187208
anyhow::bail!("The Function execution failed. Review the logs for more information.")
188209
}
189210
}
211+
212+
fn run_batch_mode(
213+
opts: &Opts,
214+
engine: &wasmtime::Engine,
215+
module: &Module,
216+
codec: Codec,
217+
) -> Result<()> {
218+
let input_reader: Box<dyn BufRead> = if let Some(ref input) = opts.input {
219+
Box::new(BufReader::new(File::open(input).map_err(|e| {
220+
anyhow!("Couldn't load input {:?}: {}", input, e)
221+
})?))
222+
} else if !std::io::stdin().is_terminal() {
223+
Box::new(BufReader::new(stdin()))
224+
} else {
225+
return Err(anyhow!(
226+
"You must provide input via the --input flag or piped via stdin."
227+
));
228+
};
229+
230+
// Pre-calculate scale factor (constant across all inputs in batch)
231+
let schema_string = opts.read_schema_to_string().transpose()?;
232+
let query_string = opts.read_query_to_string().transpose()?;
233+
234+
// Disable profiling in batch mode for performance
235+
let profile_opts = None;
236+
237+
let mut line_num = 0;
238+
let mut success_count = 0;
239+
let mut error_count = 0;
240+
241+
for line_result in input_reader.lines() {
242+
line_num += 1;
243+
244+
let line = match line_result {
245+
Ok(l) => l,
246+
Err(e) => {
247+
error_count += 1;
248+
if opts.batch_continue_on_error {
249+
eprintln!("Error reading line {}: {}", line_num, e);
250+
println!(r#"{{"success":false,"error":"Error reading input: {}"}}"#, e);
251+
continue;
252+
} else {
253+
return Err(e.into());
254+
}
255+
}
256+
};
257+
258+
// Skip empty lines
259+
if line.trim().is_empty() {
260+
continue;
261+
}
262+
263+
// Parse input
264+
let input = match BytesContainer::new(
265+
BytesContainerType::Input,
266+
codec,
267+
line.into_bytes(),
268+
) {
269+
Ok(i) => i,
270+
Err(e) => {
271+
error_count += 1;
272+
if opts.batch_continue_on_error {
273+
eprintln!("Error parsing line {}: {}", line_num, e);
274+
println!(r#"{{"success":false,"error":"Invalid JSON input: {}"}}"#, e);
275+
continue;
276+
} else {
277+
return Err(e);
278+
}
279+
}
280+
};
281+
282+
// Calculate scale factor for this input
283+
let scale_factor = if let (Some(ref schema_string), Some(ref query_string), Some(ref json_value)) =
284+
(&schema_string, &query_string, &input.json_value)
285+
{
286+
match BluejaySchemaAnalyzer::analyze_schema_definition(
287+
schema_string,
288+
opts.schema_path.as_ref().and_then(|p| p.to_str()),
289+
query_string,
290+
opts.query_path.as_ref().and_then(|p| p.to_str()),
291+
json_value,
292+
) {
293+
Ok(sf) => sf,
294+
Err(e) => {
295+
error_count += 1;
296+
if opts.batch_continue_on_error {
297+
eprintln!("Error analyzing schema for line {}: {}", line_num, e);
298+
println!(r#"{{"success":false,"error":"Schema analysis failed: {}"}}"#, e);
299+
continue;
300+
} else {
301+
return Err(e);
302+
}
303+
}
304+
}
305+
} else {
306+
DEFAULT_SCALE_FACTOR
307+
};
308+
309+
// Run function (reusing engine/module!)
310+
let result = run(FunctionRunParams {
311+
function_path: opts.function.clone(),
312+
input,
313+
export: opts.export.as_ref(),
314+
profile_opts,
315+
scale_factor,
316+
module: module.clone(),
317+
engine: engine.clone(),
318+
});
319+
320+
// Output result immediately (streaming JSONL - compact format for line-by-line parsing)
321+
match result {
322+
Ok(function_result) => {
323+
success_count += 1;
324+
// Use compact JSON (not pretty-printed) for JSONL format
325+
let compact_json = serde_json::to_string(&function_result)
326+
.unwrap_or_else(|error| error.to_string());
327+
println!("{}", compact_json);
328+
}
329+
Err(e) => {
330+
error_count += 1;
331+
if opts.batch_continue_on_error {
332+
eprintln!("Error executing line {}: {}", line_num, e);
333+
println!(r#"{{"success":false,"error":"Execution failed: {}"}}"#, e);
334+
} else {
335+
return Err(e);
336+
}
337+
}
338+
}
339+
}
340+
341+
// Log summary to stderr (so it doesn't interfere with JSONL output on stdout)
342+
eprintln!("Batch complete: {} inputs processed, {} successful, {} errors",
343+
line_num, success_count, error_count);
344+
345+
Ok(())
346+
}

0 commit comments

Comments
 (0)