Skip to content

Commit

Permalink
merge: #9418
Browse files Browse the repository at this point in the history
9418: feat(engine): support iso8601 <start> for timer cycle r=saig0 a=lzgabel

## Description
Support ISO8601 `<start>` for time cycle.

Time cycle with start time and duration `R[n]/<start>/<duration>` (see [ISO 8601 Repeating Intervals](https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals))
```xml
 <bpmn:timerEventDefinition>
    <bpmn:timeCycle>R/2019-10-02T08:09:40+02:00[Europe/Berlin]/P1D</bpmn:timeCycle>
  </bpmn:timerEventDefinition>
```
<!-- Please explain the changes you made here. -->

## Related issues
<!-- Which issues are closed by this PR or are related -->

closes #3038 



Co-authored-by: lzgabel <lz19960321lz@gmail.com>
  • Loading branch information
zeebe-bors-camunda[bot] and lzgabel committed Jun 27, 2022
2 parents 84de60c + 63c8d56 commit cc2e514
Show file tree
Hide file tree
Showing 4 changed files with 402 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/** Combines {@link java.time.Period}, and {@link java.time.Duration} */
public class Interval implements TemporalAmount {
Expand All @@ -36,10 +37,17 @@ public class Interval implements TemporalAmount {
private final List<TemporalUnit> units;
private final Period period;
private final Duration duration;
private final Optional<ZonedDateTime> start;

public Interval(final Period period, final Duration duration) {
this(Optional.empty(), period, duration);
}

public Interval(
final Optional<ZonedDateTime> start, final Period period, final Duration duration) {
this.period = period;
this.duration = duration;
this.start = start;
units = new ArrayList<>();

units.addAll(period.getUnits());
Expand All @@ -62,14 +70,35 @@ public Duration getDuration() {
return duration;
}

public Optional<ZonedDateTime> getStart() {
return start;
}

public long toEpochMilli(final long fromEpochMilli) {
if (!isCalendarBased()) {
return fromEpochMilli + getDuration().toMillis();
if (!start.isPresent()) {
if (!isCalendarBased()) {
return fromEpochMilli + getDuration().toMillis();
}

return ZonedDateTime.ofInstant(Instant.ofEpochMilli(fromEpochMilli), ZoneId.systemDefault())
.plus(this)
.toInstant()
.toEpochMilli();
}

final Instant start = Instant.ofEpochMilli(fromEpochMilli);
final ZonedDateTime zoneAwareStart = ZonedDateTime.ofInstant(start, ZoneId.systemDefault());
return zoneAwareStart.plus(this).toInstant().toEpochMilli();
return start.get().toInstant().toEpochMilli();
}

/**
* Creates a new interval with the specified start instant.
*
* @param start the start instant for the new interval
* @return a new interval from this interval and the specified start
*/
public Interval withStart(final Instant start) {
final ZoneId zoneId = getStart().map(ZonedDateTime::getZone).orElse(ZoneId.systemDefault());
return new Interval(
Optional.of(ZonedDateTime.ofInstant(start, zoneId).plus(this)), getPeriod(), getDuration());
}

/**
Expand Down Expand Up @@ -122,7 +151,8 @@ public boolean equals(final Object o) {

final Interval interval = (Interval) o;
return Objects.equals(getPeriod(), interval.getPeriod())
&& Objects.equals(getDuration(), interval.getDuration());
&& Objects.equals(getDuration(), interval.getDuration())
&& Objects.equals(getStart(), interval.getStart());
}

@Override
Expand All @@ -143,31 +173,38 @@ private boolean isCalendarBased() {
}

/**
* Only supports a subset of ISO8601, combining both period and duration.
* Only supports a subset of ISO8601, combining start, period and duration.
*
* @param text ISO8601 conforming interval expression
* @return parsed interval
*/
public static Interval parse(final String text) {
String sign = "";
int startOffset = 0;
final int index = text.lastIndexOf("/");
Optional<ZonedDateTime> start = Optional.empty();
if (index > 0) {
start = Optional.ofNullable(ZonedDateTime.parse(text.substring(0, index)));
}

if (text.startsWith("-")) {
final String intervalExp = text.substring(index + 1);
if (intervalExp.startsWith("-")) {
startOffset = 1;
sign = "-";
} else if (text.startsWith("+")) {
} else if (intervalExp.startsWith("+")) {
startOffset = 1;
}

final int durationOffset = text.indexOf('T');
final int durationOffset = intervalExp.indexOf('T');
if (durationOffset == -1) {
return new Interval(Period.parse(text), Duration.ZERO);
return new Interval(start, Period.parse(intervalExp), Duration.ZERO);
} else if (durationOffset == startOffset + 1) {
return new Interval(Period.ZERO, Duration.parse(text));
return new Interval(start, Period.ZERO, Duration.parse(intervalExp));
}

return new Interval(
Period.parse(text.substring(0, durationOffset)),
Duration.parse(sign + "P" + text.substring(durationOffset)));
start,
Period.parse(intervalExp.substring(0, durationOffset)),
Duration.parse(sign + "P" + intervalExp.substring(durationOffset)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.Optional;
import org.junit.Test;

public class RepeatingIntervalTest {
Expand Down Expand Up @@ -127,4 +130,72 @@ public void shouldFailToParseIfRepetitionCountDoesNotStartWithR() {
public void shouldFailToParseEmptyString() {
assertThatThrownBy(() -> Interval.parse("")).isInstanceOf(DateTimeParseException.class);
}

@Test
public void shouldParseWithSpecifiedStartTime() {
// given
final String text = "R/2022-05-20T08:09:40+02:00[Europe/Berlin]/PT10S";
final RepeatingInterval expected =
new RepeatingInterval(
-1,
new Interval(
Optional.ofNullable(
ZonedDateTime.parse("2022-05-20T08:09:40+02:00[Europe/Berlin]")),
Period.ZERO,
Duration.ofSeconds(10)));

// when
final RepeatingInterval parsed = RepeatingInterval.parse(text);

// then
assertThat(parsed).isEqualTo(expected);
}

@Test
public void shouldParseWithSpecialUTCStartTime() {
// given
final String text = "R/2022-05-20T08:09:40Z/PT10S";
final RepeatingInterval expected =
new RepeatingInterval(
-1,
new Interval(
Optional.ofNullable(ZonedDateTime.parse("2022-05-20T08:09:40Z")),
Period.ZERO,
Duration.ofSeconds(10)));

// when
final RepeatingInterval parsed = RepeatingInterval.parse(text);

// then
assertThat(parsed).isEqualTo(expected);
}

@Test
public void shouldParseWithEmptyStartTime() {
// given
final String text = "R//PT10S";
final RepeatingInterval expected =
new RepeatingInterval(-1, new Interval(Period.ZERO, Duration.ofSeconds(10)));

// when
final RepeatingInterval parsed = RepeatingInterval.parse(text);

// then
assertThat(parsed).isEqualTo(expected);
}

@Test
public void shouldCalculateDueDate() {
// given
final Interval interval = new Interval(Period.ZERO, Duration.ofSeconds(10));
final long dueDate = interval.toEpochMilli(System.currentTimeMillis());
final long expected = dueDate + 10_000L;

// when
final long newDueDate =
interval.withStart(Instant.ofEpochMilli(dueDate)).toEpochMilli(System.currentTimeMillis());

// then
assertThat(newDueDate).isEqualTo(expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import io.camunda.zeebe.protocol.record.intent.TimerIntent;
import io.camunda.zeebe.util.Either;
import io.camunda.zeebe.util.buffer.BufferUtil;
import java.time.Instant;
import java.util.function.Consumer;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
Expand Down Expand Up @@ -161,8 +162,13 @@ private void rescheduleTimer(
repetitions--;
}

final Interval interval = timer.map(Timer::getInterval).get();
final Timer repeatingInterval = new RepeatingInterval(repetitions, interval);
// Use the timer's last due date instead of the current time to avoid a time shift.
final Interval refreshedInterval =
timer
.map(Timer::getInterval)
.map(interval -> interval.withStart(Instant.ofEpochMilli(record.getDueDate())))
.get();
final Timer repeatingInterval = new RepeatingInterval(repetitions, refreshedInterval);
catchEventBehavior.subscribeToTimerEvent(
record.getElementInstanceKey(),
record.getProcessInstanceKey(),
Expand Down

0 comments on commit cc2e514

Please sign in to comment.