Skip to content

Commit 235ccf8

Browse files
authored
Ensure that we don't send massive error message over gRPC (#1108)
The gRPC servers are configured with a max size of 4MB, this patch adds more resiliency for potentially large error message that would bust the gRPC message size limit.
1 parent 1e02b3d commit 235ccf8

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

crates/arroyo-worker/src/lib.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use tonic::{Request, Response, Status};
4444
use tracing::{debug, error, info, warn};
4545

4646
use crate::job_controller::controller::WorkerJobController;
47-
use crate::utils::to_d2;
47+
use crate::utils::{MAX_TASK_ERROR_FIELD_BYTES, maybe_truncate, to_d2};
4848
use arroyo_datastream::logical::LogicalProgram;
4949
use arroyo_planner::physical::new_registry;
5050
use arroyo_rpc::config::config;
@@ -576,11 +576,14 @@ impl WorkerState {
576576
error: Some(rpc::TaskError {
577577
task_id,
578578
subtask_idx,
579-
error: error.message,
579+
error: maybe_truncate(error.message, MAX_TASK_ERROR_FIELD_BYTES),
580580
error_domain: rpc::ErrorDomain::from(error.domain) as i32,
581581
retry_hint: rpc::RetryHint::from(error.retry_hint) as i32,
582582
operator_id: error.operator_id.unwrap_or_default(),
583-
details: error.details.unwrap_or_default(),
583+
details: maybe_truncate(
584+
error.details.unwrap_or_default(),
585+
MAX_TASK_ERROR_FIELD_BYTES
586+
),
584587
}),
585588
};
586589
send_control_rpc!(
@@ -599,10 +602,10 @@ impl WorkerState {
599602
task_id,
600603
operator_id,
601604
subtask_idx,
602-
error: message,
605+
error: maybe_truncate(message, MAX_TASK_ERROR_FIELD_BYTES),
603606
error_domain: rpc::ErrorDomain::External as i32,
604607
retry_hint: rpc::RetryHint::NoRetry as i32,
605-
details,
608+
details: maybe_truncate(details, MAX_TASK_ERROR_FIELD_BYTES),
606609
}),
607610
};
608611
send_control_rpc!(

crates/arroyo-worker/src/utils.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use arroyo_operator::operator::Registry;
55
use arroyo_planner::physical::new_registry;
66
use std::fmt::Write;
77
use std::sync::Arc;
8+
use tracing::warn;
89

910
fn format_arrow_schema_fields(schema: &Schema) -> Vec<(String, String)> {
1011
schema
@@ -135,3 +136,55 @@ pub async fn to_d2(logical: &LogicalProgram) -> anyhow::Result<String> {
135136

136137
Ok(d2)
137138
}
139+
140+
pub(crate) const MAX_TASK_ERROR_FIELD_BYTES: usize = 64 * 1024;
141+
142+
pub(crate) fn maybe_truncate(mut value: String, max_size_bytes: usize) -> String {
143+
let original_bytes = value.len();
144+
if original_bytes <= max_size_bytes {
145+
return value;
146+
}
147+
148+
let suffix = format!(" [truncated; original_bytes={original_bytes}]");
149+
let mut end = max_size_bytes - suffix.len();
150+
while !value.is_char_boundary(end) {
151+
end -= 1;
152+
}
153+
154+
value.truncate(end);
155+
value.push_str(&suffix);
156+
157+
warn!(
158+
"Truncated oversized String from {} bytes to {} bytes: {}",
159+
original_bytes,
160+
value.len(),
161+
value
162+
);
163+
value
164+
}
165+
166+
#[cfg(test)]
167+
mod tests {
168+
use super::maybe_truncate;
169+
170+
#[test]
171+
fn maybe_truncate_preserves_value_at_limit() {
172+
let value = "a".repeat(64);
173+
174+
assert_eq!(maybe_truncate(value.clone(), value.len()), value);
175+
}
176+
177+
#[test]
178+
fn maybe_truncate_respects_limit() {
179+
let value = "a".repeat(100);
180+
let max_size_bytes = 64;
181+
182+
let truncated = maybe_truncate(value, max_size_bytes);
183+
184+
assert_eq!(truncated.len(), max_size_bytes);
185+
assert_eq!(
186+
truncated,
187+
format!("{} [truncated; original_bytes=100]", "a".repeat(32))
188+
);
189+
}
190+
}

0 commit comments

Comments
 (0)