-
Notifications
You must be signed in to change notification settings - Fork 12
fix(model): do not cap <IntRangeExpr> expansion at the list-form limit #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,16 @@ pub struct EffectiveLimits { | |
| /// raises the job-template limit to 200. | ||
| pub max_env_template_param_count: usize, | ||
| pub max_filename_len: usize, | ||
| /// Maximum elements in a task parameter's *list*-form range — | ||
| /// `<IntRangeList>` (§3.4.1.1), `<FloatRangeList>` (§3.4.1.2) and | ||
| /// `<StringRangeList>` (§3.4.1.3). | ||
| /// | ||
| /// Do not apply this to an `<IntRangeExpr>` expansion. §3.4.1.1.1 | ||
| /// constrains that form only by "no two ranges may overlap", and its stated | ||
| /// purpose is expressing frame ranges succinctly — capping the expansion | ||
| /// rejects the form's primary use case and pre-empts the host service's own | ||
| /// task-count limits, which it may raise per account. A host that wants to | ||
| /// bound fan-out has `CallerLimits::max_task_count`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doc comment asserts 1. It is opt-in and defaults to off. 2. It does not gate the O(num_chunks) containment scan. // step_param_space.rs:859
if (0..self.num_chunks).any(|i| self.chunk_range_expr(i) == *r) {
This is reachable from If the §3.4.1.1.1 reading is right that the spec does not cap the expansion, then these two containment scans need to become index-arithmetic lookups (locate the chunk containing |
||
| pub max_task_param_range_len: usize, | ||
| pub max_task_param_string_len: usize, | ||
| pub max_job_param_string_len: usize, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -856,3 +856,31 @@ fn chunks_parameter_name() { | |
| let tasks: Vec<_> = iter.collect(); | ||
| assert_eq!(tasks.len(), 2); | ||
| } | ||
|
|
||
| // ══════════════════════════════════════════════════════════════ | ||
| // Range length — §3.4 caps the list form only | ||
| // ══════════════════════════════════════════════════════════════ | ||
|
|
||
| /// §3.4.1.1.1 `<IntRangeExpr>` states no element cap, so a CHUNK[INT] range | ||
| /// expression may expand past `max_task_param_range_len`. | ||
| #[test] | ||
| fn range_expression_expansion_is_not_capped() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is vacuous with respect to the change: it would pass unmodified on the base commit. The IntRange::Expression(expr) => {
if !expr.raw().contains("{{") {
if let Err(e) = expr.raw().parse::<openjd_expr::RangeExpr>() {Only the That makes this a no-op regression guard. To actually pin the CHUNK[INT] behavior the cap removal affects, the test needs to reach |
||
| for range in ["1-1025", "1-5000", "1-100000:2"] { | ||
| decode_ok(&chunk_job(&format!( | ||
| r#"{{"name": "foo", "type": "CHUNK[INT]", "range": "{range}", "chunks": {{"defaultTaskCount": 10, "rangeConstraint": "CONTIGUOUS"}}}}"# | ||
| ))); | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
// step_param_space.rs:359-364
} else {
// Step > 1: each value is isolated (has gaps between them).
let count = sr.len();
for idx in 0..count { // <-- O(values), not O(sub-ranges)
let val = sr.get(idx).expect("index within sub-range bounds");This runs at node-construction time — Under the old 1024 cap this loop was bounded. With the cap removed, Note this test only reaches The counting for stepped sub-ranges is closed-form (each value is its own interval, so it contributes |
||
|
|
||
| /// §3.4.1.1 item 4 caps the list form at 1024 elements. That cap stays. | ||
| #[test] | ||
| fn range_list_is_still_capped() { | ||
| let values: Vec<String> = (0..1025).map(|v| v.to_string()).collect(); | ||
| check_err( | ||
| &chunk_job(&format!( | ||
| r#"{{"name": "foo", "type": "CHUNK[INT]", "range": [{}], "chunks": {{"defaultTaskCount": 10, "rangeConstraint": "CONTIGUOUS"}}}}"#, | ||
| values.join(",") | ||
| )), | ||
| &["range exceeds 1024 elements"], | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -739,3 +739,57 @@ fn combination_expr_leading_star_rejected() { | |
| "Leading star in combination should be rejected" | ||
| ); | ||
| } | ||
|
|
||
| // ══════════════════════════════════════════════════════════════ | ||
| // Task parameter range length — §3.4 caps the list forms only | ||
| // ══════════════════════════════════════════════════════════════ | ||
|
|
||
| /// §3.4.1.1.1 `<IntRangeExpr>` states no element cap, so the expansion of a | ||
| /// range expression must not be bounded by `max_task_param_range_len`. The | ||
| /// form exists to express frame ranges, which routinely exceed 1024 values. | ||
| #[test] | ||
| fn int_range_expression_expansion_is_not_capped() { | ||
| for range in ["1-1024", "1-1025", "1-5000", "1-100000:2"] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR removes the cap at two sites, but the new tests only cover one of them.
Two consequences:
Suggest adding at least one test that goes through Separately: that existing test is now named |
||
| decode_ok(&job_with_param_space(&format!( | ||
| r#"{{"taskParameterDefinitions": [{{"name": "Frame", "type": "INT", "range": "{range}"}}]}}"# | ||
| ))); | ||
| } | ||
| } | ||
|
|
||
| /// A malformed range expression is still rejected — dropping the length cap | ||
| /// must not drop grammar validation. | ||
| #[test] | ||
| fn int_range_expression_grammar_is_still_validated() { | ||
| check_err( | ||
| &job_with_param_space( | ||
| r#"{"taskParameterDefinitions": [{"name": "Frame", "type": "INT", "range": "1-10,5-15"}]}"#, | ||
| ), | ||
| &["range expression error"], | ||
| ); | ||
| } | ||
|
|
||
| /// §3.4.1.1 item 4 caps `<IntRangeList>` at 1024 elements. That cap stays. | ||
| #[test] | ||
| fn int_range_list_is_still_capped() { | ||
| let values: Vec<String> = (0..1025).map(|v| v.to_string()).collect(); | ||
| check_err( | ||
| &job_with_param_space(&format!( | ||
| r#"{{"taskParameterDefinitions": [{{"name": "Frame", "type": "INT", "range": [{}]}}]}}"#, | ||
| values.join(",") | ||
| )), | ||
| &["range exceeds 1024 elements"], | ||
| ); | ||
| } | ||
|
|
||
| /// §3.4.1.3 caps `<StringRangeList>` too. | ||
| #[test] | ||
| fn string_range_list_is_still_capped() { | ||
| let values: Vec<String> = (0..1025).map(|v| format!("\"v{v}\"")).collect(); | ||
| check_err( | ||
| &job_with_param_space(&format!( | ||
| r#"{{"taskParameterDefinitions": [{{"name": "S", "type": "STRING", "range": [{}]}}]}}"#, | ||
| values.join(",") | ||
| )), | ||
| &["range exceeds 1024 elements"], | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping the cap here removes the only bound on an
IntRangeExprexpansion, and there is at least one path that then materializes the full expansion into memory.make_chunk_nodeinstep_param_space.rseagerly collects when adaptive chunking is selected:Adaptive chunking is selected whenever a
CHUNK[INT]parameter setschunks.targetRuntimeSeconds > 0(new_inner, ~line 1307), andAdaptiveChunkNode.valuesis aVec<i64>. So a template withnow allocates ~8 GB before any limit is consulted.
RangeExprvalues are bounded only byMAX_RANGE_VALUE_MAGNITUDE(2^62), so1-4611686018427387903is also accepted by the parser.The stated backstop does not cover this:
CallerLimits::max_task_countisOption<u64>and defaults toNone, so in the default configuration there is no bound at all.create_job(mod.rs:115) runs afterinstantiate_stepfor all steps, and it builds the iterator withnew_with_chunk_override(ps, Some(1))— which deliberately skipsadaptive_info. So the count check never exercises the allocating path, and cannot pre-empt a consumer that later iterates the space for real.The lazy
RangeExprNode/ContiguousChunkNodepaths are genuinely index-based and fine (thetest_truly_lazy_trillion_element_spacetest covers those). The adaptive path is the outlier.Suggest either bounding the eager collect in
make_chunk_node(return aModelErrorinstead of allocating), or keeping a separate expansion limit that is decoupled from the spec-derivedmax_task_param_range_lenso the §3.4.1.1.1 argument in this PR still holds.