Skip to content

Commit edf3b33

Browse files
authored
Merge pull request #74 from conductor-oss/fix/wait-task-until-duration-parsing
Fix wait task until and duration parsing
2 parents 5be5a12 + f3c83ba commit edf3b33

6 files changed

Lines changed: 167 additions & 40 deletions

File tree

conductor-client/src/main/java/com/netflix/conductor/common/run/tasks/WaitTask.java

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313
package com.netflix.conductor.common.run.tasks;
1414

1515
import java.time.Duration;
16+
import java.time.LocalDate;
17+
import java.time.LocalDateTime;
18+
import java.time.ZoneId;
1619
import java.time.ZonedDateTime;
1720
import java.time.format.DateTimeFormatter;
1821
import java.time.format.DateTimeParseException;
22+
import java.util.regex.Matcher;
23+
import java.util.regex.Pattern;
1924

2025
import com.netflix.conductor.common.metadata.tasks.Task;
2126
import com.netflix.conductor.common.metadata.tasks.TaskType;
@@ -53,8 +58,19 @@ public class WaitTask extends TypedTask {
5358
public static final String DURATION_INPUT = "duration";
5459
public static final String UNTIL_INPUT = "until";
5560

56-
public static final DateTimeFormatter DATE_TIME_FORMATTER =
57-
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
61+
private static final Pattern DURATION_PATTERN = Pattern.compile(
62+
"\\s*(?:(\\d+)\\s*(?:days?|d))?"
63+
+ "\\s*(?:(\\d+)\\s*(?:hours?|hrs?|h))?"
64+
+ "\\s*(?:(\\d+)\\s*(?:minutes?|mins?|m))?"
65+
+ "\\s*(?:(\\d+)\\s*(?:seconds?|secs?|s))?"
66+
+ "\\s*",
67+
Pattern.CASE_INSENSITIVE);
68+
69+
private static final DateTimeFormatter[] DATE_TIME_FORMATTERS = {
70+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
71+
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z"),
72+
DateTimeFormatter.ofPattern("yyyy-MM-dd")
73+
};
5874

5975
/**
6076
* Represents the type of wait condition.
@@ -153,56 +169,61 @@ private Duration parseDuration(String durationStr) {
153169
if (durationStr == null || durationStr.isEmpty()) {
154170
return null;
155171
}
156-
durationStr = durationStr.trim().toLowerCase();
157-
158-
if (durationStr.length() < 2) {
159-
// Assume seconds if just a number
160-
try {
161-
return Duration.ofSeconds(Long.parseLong(durationStr));
162-
} catch (NumberFormatException e) {
163-
return null;
164-
}
172+
173+
Matcher m = DURATION_PATTERN.matcher(durationStr);
174+
if (!m.matches()) {
175+
return null;
165176
}
166177

167-
char unit = durationStr.charAt(durationStr.length() - 1);
168-
String valueStr = durationStr.substring(0, durationStr.length() - 1);
178+
int days = (m.start(1) == -1 ? 0 : Integer.parseInt(m.group(1)));
179+
int hours = (m.start(2) == -1 ? 0 : Integer.parseInt(m.group(2)));
180+
int mins = (m.start(3) == -1 ? 0 : Integer.parseInt(m.group(3)));
181+
int secs = (m.start(4) == -1 ? 0 : Integer.parseInt(m.group(4)));
169182

170-
try {
171-
long value = Long.parseLong(valueStr);
172-
switch (unit) {
173-
case 's':
174-
return Duration.ofSeconds(value);
175-
case 'm':
176-
return Duration.ofMinutes(value);
177-
case 'h':
178-
return Duration.ofHours(value);
179-
case 'd':
180-
return Duration.ofDays(value);
181-
default:
182-
// If the last char is a digit, assume the whole string is seconds
183-
if (Character.isDigit(unit)) {
184-
return Duration.ofSeconds(Long.parseLong(durationStr));
185-
}
186-
return null;
187-
}
188-
} catch (NumberFormatException e) {
183+
// If all components are zero and string is not blank, it's invalid
184+
if (days == 0 && hours == 0 && mins == 0 && secs == 0 && !durationStr.trim().isEmpty()) {
189185
return null;
190186
}
187+
188+
return Duration.ofSeconds((days * 86400L) + (hours * 60L + mins) * 60L + secs);
191189
}
192190

193191
private ZonedDateTime parseDateTime(String dateTimeStr) {
194192
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
195193
return null;
196194
}
195+
196+
// Try each pattern in order (matching backend DateTimeUtils behavior)
197+
// Pattern 0: "yyyy-MM-dd HH:mm" - no timezone, use system default
198+
// Pattern 1: "yyyy-MM-dd HH:mm z" - with timezone
199+
// Pattern 2: "yyyy-MM-dd" - date only, use system default timezone
200+
201+
// Try pattern with timezone first
197202
try {
198-
return ZonedDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER);
199-
} catch (DateTimeParseException e) {
200-
// Try ISO format as fallback
201-
try {
202-
return ZonedDateTime.parse(dateTimeStr);
203-
} catch (DateTimeParseException e2) {
204-
return null;
205-
}
203+
return ZonedDateTime.parse(dateTimeStr, DATE_TIME_FORMATTERS[1]);
204+
} catch (DateTimeParseException ignored) {
206205
}
206+
207+
// Try datetime without timezone
208+
try {
209+
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, DATE_TIME_FORMATTERS[0]);
210+
return localDateTime.atZone(ZoneId.systemDefault());
211+
} catch (DateTimeParseException ignored) {
212+
}
213+
214+
// Try date only
215+
try {
216+
LocalDate localDate = LocalDate.parse(dateTimeStr, DATE_TIME_FORMATTERS[2]);
217+
return localDate.atStartOfDay(ZoneId.systemDefault());
218+
} catch (DateTimeParseException ignored) {
219+
}
220+
221+
// Try ISO format as fallback
222+
try {
223+
return ZonedDateTime.parse(dateTimeStr);
224+
} catch (DateTimeParseException ignored) {
225+
}
226+
227+
return null;
207228
}
208229
}

conductor-client/src/test/java/com/netflix/conductor/common/run/tasks/TypedTaskTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,68 @@ void testIsWaitTask() throws IOException {
153153
assertFalse(WaitTask.isWaitTask(httpTask));
154154
assertFalse(WaitTask.isWaitTask(null));
155155
}
156+
157+
@Test
158+
void testWaitDurationComplex() throws IOException {
159+
Task task = loadTask("wait_duration_complex.json");
160+
161+
WaitTask wait = new WaitTask(task);
162+
163+
assertTrue(wait.isDurationBased());
164+
assertEquals("1d 2h 3m 4s", wait.getDurationString());
165+
166+
Duration expected = Duration.ofDays(1).plusHours(2).plusMinutes(3).plusSeconds(4);
167+
assertEquals(expected, wait.getDuration());
168+
}
169+
170+
@Test
171+
void testWaitDurationWithWords() throws IOException {
172+
Task task = loadTask("wait_duration_words.json");
173+
174+
WaitTask wait = new WaitTask(task);
175+
176+
assertTrue(wait.isDurationBased());
177+
assertEquals("2 days 5 hours 30 minutes", wait.getDurationString());
178+
179+
Duration expected = Duration.ofDays(2).plusHours(5).plusMinutes(30);
180+
assertEquals(expected, wait.getDuration());
181+
}
182+
183+
@Test
184+
void testWaitUntilNoTimezone() throws IOException {
185+
Task task = loadTask("wait_until_no_timezone.json");
186+
187+
WaitTask wait = new WaitTask(task);
188+
189+
assertTrue(wait.isUntilBased());
190+
assertEquals("2025-12-31 23:59", wait.getUntilString());
191+
192+
ZonedDateTime until = wait.getUntil();
193+
assertNotNull(until);
194+
assertEquals(2025, until.getYear());
195+
assertEquals(12, until.getMonthValue());
196+
assertEquals(31, until.getDayOfMonth());
197+
assertEquals(23, until.getHour());
198+
assertEquals(59, until.getMinute());
199+
}
200+
201+
@Test
202+
void testWaitUntilDateOnly() throws IOException {
203+
Task task = loadTask("wait_until_date_only.json");
204+
205+
WaitTask wait = new WaitTask(task);
206+
207+
assertTrue(wait.isUntilBased());
208+
assertEquals("2025-12-31", wait.getUntilString());
209+
210+
ZonedDateTime until = wait.getUntil();
211+
assertNotNull(until);
212+
assertEquals(2025, until.getYear());
213+
assertEquals(12, until.getMonthValue());
214+
assertEquals(31, until.getDayOfMonth());
215+
assertEquals(0, until.getHour());
216+
assertEquals(0, until.getMinute());
217+
}
156218
}
157219

158220
@Nested
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"taskType": "WAIT",
3+
"status": "IN_PROGRESS",
4+
"taskId": "wait-task-002",
5+
"referenceTaskName": "wait_for_complex_duration",
6+
"workflowInstanceId": "workflow-002",
7+
"inputData": {
8+
"duration": "1d 2h 3m 4s"
9+
},
10+
"outputData": {}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"taskType": "WAIT",
3+
"status": "IN_PROGRESS",
4+
"taskId": "wait-task-003",
5+
"referenceTaskName": "wait_for_duration_words",
6+
"workflowInstanceId": "workflow-003",
7+
"inputData": {
8+
"duration": "2 days 5 hours 30 minutes"
9+
},
10+
"outputData": {}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"taskType": "WAIT",
3+
"status": "IN_PROGRESS",
4+
"taskId": "wait-task-005",
5+
"referenceTaskName": "wait_until_date_only",
6+
"workflowInstanceId": "workflow-005",
7+
"inputData": {
8+
"until": "2025-12-31"
9+
},
10+
"outputData": {}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"taskType": "WAIT",
3+
"status": "IN_PROGRESS",
4+
"taskId": "wait-task-004",
5+
"referenceTaskName": "wait_until_no_tz",
6+
"workflowInstanceId": "workflow-004",
7+
"inputData": {
8+
"until": "2025-12-31 23:59"
9+
},
10+
"outputData": {}
11+
}

0 commit comments

Comments
 (0)