Skip to content

Commit 9c3c01a

Browse files
refactor: Improve SessionContext::parse_duration API (#20816)
## Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123. --> - Closes #20815. ## Rationale for this change This is follow-up PR for #20371. Similarly to `SessionContext::parse_capacity_limit` API, `SessionContext::parse_duration` API also needs to have following improvements: 1. Validation for empty or blank duration values: ``` SET datafusion.runtime.list_files_cache_ttl = ' ' Current: DataFusion error: Error during planning: Failed to parse number from duration ' ' New: DataFusion error: Error during planning: Duration should not be empty or blank for 'datafusion.runtime.list_files_cache_ttl' ``` 2. Exposing config name in error messages, 3. Comprehensive test coverage for invalid durations, 4. Updating `datafusion.runtime.list_files_cache_ttl` documentation for other allowed settings. ## What changes are included in this PR? Explained in the first section. ## Are these changes tested? Yes, being extended existing test cases. ## Are there any user-facing changes? Yes, config name has been added to error message when the validation is failed.
1 parent 2c871b2 commit 9c3c01a

File tree

3 files changed

+142
-13
lines changed

3 files changed

+142
-13
lines changed

datafusion/core/src/execution/context/mod.rs

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,7 @@ impl SessionContext {
11841184
builder.with_object_list_cache_limit(limit)
11851185
}
11861186
"list_files_cache_ttl" => {
1187-
let duration = Self::parse_duration(value)?;
1187+
let duration = Self::parse_duration(variable, value)?;
11881188
builder.with_object_list_cache_ttl(Some(duration))
11891189
}
11901190
_ => return plan_err!("Unknown runtime configuration: {variable}"),
@@ -1323,36 +1323,66 @@ impl SessionContext {
13231323
}
13241324
}
13251325

1326-
fn parse_duration(duration: &str) -> Result<Duration> {
1326+
fn parse_duration(config_name: &str, duration: &str) -> Result<Duration> {
1327+
if duration.trim().is_empty() {
1328+
return Err(plan_datafusion_err!(
1329+
"Duration should not be empty or blank for '{config_name}'"
1330+
));
1331+
}
1332+
13271333
let mut minutes = None;
13281334
let mut seconds = None;
13291335

13301336
for duration in duration.split_inclusive(&['m', 's']) {
13311337
let (number, unit) = duration.split_at(duration.len() - 1);
13321338
let number: u64 = number.parse().map_err(|_| {
1333-
plan_datafusion_err!("Failed to parse number from duration '{duration}'")
1339+
plan_datafusion_err!("Failed to parse number from duration '{duration}' for '{config_name}'")
13341340
})?;
13351341

13361342
match unit {
13371343
"m" if minutes.is_none() && seconds.is_none() => minutes = Some(number),
13381344
"s" if seconds.is_none() => seconds = Some(number),
1339-
_ => plan_err!(
1340-
"Invalid duration, unit must be either 'm' (minutes), or 's' (seconds), and be in the correct order"
1345+
other => plan_err!(
1346+
"Invalid duration unit: '{other}'. The unit must be either 'm' (minutes), or 's' (seconds), and be in the correct order for '{config_name}'"
13411347
)?,
13421348
}
13431349
}
13441350

1345-
let duration = Duration::from_secs(
1346-
minutes.unwrap_or_default() * 60 + seconds.unwrap_or_default(),
1347-
);
1351+
let secs = Self::check_overflow(config_name, minutes, 60, seconds)?;
1352+
let duration = Duration::from_secs(secs);
13481353

13491354
if duration.is_zero() {
1350-
return plan_err!("Duration must be greater than 0 seconds");
1355+
return plan_err!(
1356+
"Duration must be greater than 0 seconds for '{config_name}'"
1357+
);
13511358
}
13521359

13531360
Ok(duration)
13541361
}
13551362

1363+
fn check_overflow(
1364+
config_name: &str,
1365+
mins: Option<u64>,
1366+
multiplier: u64,
1367+
secs: Option<u64>,
1368+
) -> Result<u64> {
1369+
let first_part_of_secs = mins.unwrap_or_default().checked_mul(multiplier);
1370+
if first_part_of_secs.is_none() {
1371+
plan_err!(
1372+
"Duration has overflowed allowed maximum limit due to 'mins * {multiplier}' when setting '{config_name}'"
1373+
)?
1374+
}
1375+
let second_part_of_secs = first_part_of_secs
1376+
.unwrap()
1377+
.checked_add(secs.unwrap_or_default());
1378+
if second_part_of_secs.is_none() {
1379+
plan_err!(
1380+
"Duration has overflowed allowed maximum limit due to 'mins * {multiplier} + secs' when setting '{config_name}'"
1381+
)?
1382+
}
1383+
Ok(second_part_of_secs.unwrap())
1384+
}
1385+
13561386
async fn create_custom_table(
13571387
&self,
13581388
cmd: &CreateExternalTable,
@@ -2803,21 +2833,90 @@ mod tests {
28032833

28042834
#[test]
28052835
fn test_parse_duration() {
2836+
const LIST_FILES_CACHE_TTL: &str = "datafusion.runtime.list_files_cache_ttl";
2837+
28062838
// Valid durations
28072839
for (duration, want) in [
28082840
("1s", Duration::from_secs(1)),
28092841
("1m", Duration::from_secs(60)),
28102842
("1m0s", Duration::from_secs(60)),
28112843
("1m1s", Duration::from_secs(61)),
28122844
] {
2813-
let have = SessionContext::parse_duration(duration).unwrap();
2845+
let have =
2846+
SessionContext::parse_duration(LIST_FILES_CACHE_TTL, duration).unwrap();
28142847
assert_eq!(want, have);
28152848
}
28162849

28172850
// Invalid durations
2818-
for duration in ["0s", "0m", "1s0m", "1s1m"] {
2819-
let have = SessionContext::parse_duration(duration);
2851+
for duration in [
2852+
"0s", "0m", "1s0m", "1s1m", "XYZ", "1h", "XYZm2s", "", " ", "-1m", "1m 1s",
2853+
"1m1s ", " 1m1s",
2854+
] {
2855+
let have = SessionContext::parse_duration(LIST_FILES_CACHE_TTL, duration);
28202856
assert!(have.is_err());
2857+
assert!(
2858+
have.unwrap_err()
2859+
.message()
2860+
.to_string()
2861+
.contains(LIST_FILES_CACHE_TTL)
2862+
);
2863+
}
2864+
}
2865+
2866+
#[test]
2867+
fn test_parse_duration_with_overflow_check() {
2868+
const LIST_FILES_CACHE_TTL: &str = "datafusion.runtime.list_files_cache_ttl";
2869+
2870+
// Valid durations which are close to max allowed limit
2871+
for (duration, want) in [
2872+
(
2873+
"18446744073709551615s",
2874+
Duration::from_secs(18446744073709551615),
2875+
),
2876+
(
2877+
"307445734561825860m",
2878+
Duration::from_secs(307445734561825860 * 60),
2879+
),
2880+
(
2881+
"307445734561825860m10s",
2882+
Duration::from_secs(307445734561825860 * 60 + 10),
2883+
),
2884+
(
2885+
"1m18446744073709551555s",
2886+
Duration::from_secs(60 + 18446744073709551555),
2887+
),
2888+
] {
2889+
let have =
2890+
SessionContext::parse_duration(LIST_FILES_CACHE_TTL, duration).unwrap();
2891+
assert_eq!(want, have);
2892+
}
2893+
2894+
// Invalid durations which overflow max allowed limit
2895+
for (duration, error_message_prefix) in [
2896+
(
2897+
"18446744073709551616s",
2898+
"Failed to parse number from duration",
2899+
),
2900+
(
2901+
"307445734561825861m",
2902+
"Duration has overflowed allowed maximum limit due to",
2903+
),
2904+
(
2905+
"307445734561825860m60s",
2906+
"Duration has overflowed allowed maximum limit due to",
2907+
),
2908+
(
2909+
"1m18446744073709551556s",
2910+
"Duration has overflowed allowed maximum limit due to",
2911+
),
2912+
] {
2913+
let have = SessionContext::parse_duration(LIST_FILES_CACHE_TTL, duration);
2914+
assert!(have.is_err());
2915+
let error_message = have.unwrap_err().message().to_string();
2916+
assert!(
2917+
error_message.contains(error_message_prefix)
2918+
&& error_message.contains(LIST_FILES_CACHE_TTL)
2919+
);
28212920
}
28222921
}
28232922

datafusion/sqllogictest/test_files/set_variable.slt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,33 @@ datafusion.runtime.temp_directory
450450

451451
statement error DataFusion error: Error during planning: Unsupported value Null
452452
SET datafusion.runtime.memory_limit = NULL
453+
454+
statement error DataFusion error: Error during planning: Unsupported value Null
455+
SET datafusion.runtime.list_files_cache_ttl = NULL
456+
457+
statement error DataFusion error: Error during planning: Duration should not be empty or blank for 'datafusion.runtime.list_files_cache_ttl'
458+
SET datafusion.runtime.list_files_cache_ttl = ' '
459+
460+
statement ok
461+
SET datafusion.runtime.list_files_cache_ttl = '18446744073709551615s'
462+
463+
statement error DataFusion error: Error during planning: Failed to parse number from duration '18446744073709551616s' for 'datafusion.runtime.list_files_cache_ttl'
464+
SET datafusion.runtime.list_files_cache_ttl = '18446744073709551616s'
465+
466+
statement ok
467+
SET datafusion.runtime.list_files_cache_ttl = '307445734561825860m'
468+
469+
statement ok
470+
SET datafusion.runtime.list_files_cache_ttl = '307445734561825860m10s'
471+
472+
statement error DataFusion error: Error during planning: Duration has overflowed allowed maximum limit due to 'mins \* 60' when setting 'datafusion\.runtime\.list_files_cache_ttl'
473+
SET datafusion.runtime.list_files_cache_ttl = '307445734561825861m'
474+
475+
statement error DataFusion error: Error during planning: Duration has overflowed allowed maximum limit due to 'mins \* 60 \+ secs' when setting 'datafusion\.runtime\.list_files_cache_ttl'
476+
SET datafusion.runtime.list_files_cache_ttl = '307445734561825860m60s'
477+
478+
statement ok
479+
SET datafusion.runtime.list_files_cache_ttl = '1m18446744073709551555s'
480+
481+
statement error DataFusion error: Error during planning: Duration has overflowed allowed maximum limit due to 'mins \* 60 \+ secs' when setting 'datafusion\.runtime\.list_files_cache_ttl'
482+
SET datafusion.runtime.list_files_cache_ttl = '1m18446744073709551556s'

docs/source/library-user-guide/upgrading/52.0.0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ those changes until the `ListingTableProvider` instance is dropped and recreated
5151
You can configure the maximum cache size and cache entry expiration time via configuration options:
5252

5353
- `datafusion.runtime.list_files_cache_limit` - Limits the size of the cache in bytes
54-
- `datafusion.runtime.list_files_cache_ttl` - Limits the TTL (time-to-live) of an entry in seconds
54+
- `datafusion.runtime.list_files_cache_ttl` - Limits the TTL (time-to-live) of an entry in minutes and/or seconds
5555

5656
Detailed configuration information can be found in the [DataFusion Runtime
5757
Configuration](https://datafusion.apache.org/user-guide/configs.html#runtime-configuration-settings) user's guide.

0 commit comments

Comments
 (0)