mirror of
https://github.com/anthonyraymond/joal.git
synced 2024-09-20 07:16:26 +08:00
Start tracker implementation
This commit is contained in:
parent
6fd281f332
commit
7466a1f0ff
|
@ -0,0 +1,12 @@
|
|||
package org.araymond.joalcore.annotations.concurency;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Immutable {
|
||||
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package org.araymond.joalcore.annotations.ddd;
|
||||
|
||||
import org.araymond.joalcore.annotations.concurency.Immutable;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Immutable
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ValueObject {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
import org.araymond.joalcore.annotations.DomainService;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@DomainService
|
||||
public interface AnnounceBackoffService {
|
||||
Duration backoff(int consecutiveFails);
|
||||
|
||||
class DefaultBackoffService implements AnnounceBackoffService {
|
||||
private static final Duration minimumRetryDelay = Duration.ofSeconds(5);
|
||||
private static final Duration maximumRetryDelay = Duration.ofHours(1);
|
||||
|
||||
private final long backoffRatio;
|
||||
|
||||
public DefaultBackoffService() {
|
||||
this(250);
|
||||
}
|
||||
|
||||
public DefaultBackoffService(long backoffRatio) {
|
||||
if (backoffRatio < 0.0) {
|
||||
throw new IllegalArgumentException("backoffRatio must be greater than 0.0");
|
||||
}
|
||||
this.backoffRatio = backoffRatio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration backoff(int consecutiveFails) {
|
||||
try {
|
||||
long failSquare = (long) consecutiveFails * consecutiveFails;
|
||||
|
||||
var backoff = Duration.ofSeconds(failSquare)
|
||||
.multipliedBy(backoffRatio)
|
||||
.dividedBy(100);
|
||||
|
||||
return min(
|
||||
maximumRetryDelay,
|
||||
minimumRetryDelay.plus(backoff)
|
||||
);
|
||||
} catch (ArithmeticException ignore) {
|
||||
return maximumRetryDelay;
|
||||
}
|
||||
}
|
||||
|
||||
private Duration min(Duration d1, Duration d2) {
|
||||
return d1.compareTo(d2) < 0 ? d1 : d2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
public record AnnounceFailed() {
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AnnounceSucceed(Duration interval) {
|
||||
public AnnounceSucceed {
|
||||
Objects.requireNonNull("Duration required a non-null interval");
|
||||
}
|
||||
}
|
|
@ -1,4 +1,71 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
import org.araymond.joalcore.annotations.concurency.Immutable;
|
||||
import org.araymond.joalcore.annotations.ddd.DomainEntity;
|
||||
import org.araymond.joalcore.annotations.ddd.ValueObject;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@DomainEntity
|
||||
public class Tracker {
|
||||
private boolean announcing;
|
||||
private Instant nextAnnounceAt;
|
||||
private Counter consecutiveFails = new Counter();
|
||||
|
||||
public Tracker() {
|
||||
announcing = false;
|
||||
nextAnnounceAt = Instant.now();
|
||||
}
|
||||
|
||||
public boolean requireAnnounce(Instant at) {
|
||||
if (announcing) {
|
||||
return false;
|
||||
}
|
||||
if (nextAnnounceAt.isAfter(at)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void announce() {
|
||||
announcing = true;
|
||||
}
|
||||
|
||||
public void announceSucceed(AnnounceSucceed response) {
|
||||
announcing = false;
|
||||
consecutiveFails = new Counter();
|
||||
|
||||
nextAnnounceAt = Instant.now().plus(response.interval());
|
||||
}
|
||||
|
||||
public void announceFailed(AnnounceFailed response, AnnounceBackoffService backoff) {
|
||||
announcing = false;
|
||||
consecutiveFails = consecutiveFails.increment();
|
||||
|
||||
nextAnnounceAt = Instant.now().plus(backoff.backoff(consecutiveFails.count()));
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private static final class Counter {
|
||||
private final int count;
|
||||
|
||||
public Counter() {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
private Counter(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public Counter increment() {
|
||||
if (count == Integer.MAX_VALUE) {
|
||||
return new Counter(count);
|
||||
}
|
||||
return new Counter(count + 1);
|
||||
}
|
||||
|
||||
public int count() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AnnounceBackoffServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldIncrementDelayForEachCall() {
|
||||
var backoff = new AnnounceBackoffService.DefaultBackoffService(250);
|
||||
|
||||
var durations = IntStream.range(1, 10)
|
||||
.mapToObj(backoff::backoff)
|
||||
.toList();
|
||||
|
||||
List<Duration> sorted = new ArrayList<>(durations);
|
||||
sorted.sort(Duration::compareTo);
|
||||
|
||||
// if the original list is the same as the sorted one it means that the duration increase over time
|
||||
assertThat(durations).isEqualTo(sorted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotGoOverMaxRetryDelay() {
|
||||
var backoff = new AnnounceBackoffService.DefaultBackoffService(250);
|
||||
|
||||
assertThat(backoff.backoff(Integer.MAX_VALUE)).isEqualTo(Duration.ofHours(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotGoBelowMinRetryDelay() {
|
||||
var backoff = new AnnounceBackoffService.DefaultBackoffService(1);
|
||||
|
||||
assertThat(backoff.backoff(1)).isGreaterThanOrEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package org.araymond.joalcore.core.trackers.domain;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TrackerTest {
|
||||
|
||||
public Instant inThirtyMinutes() {
|
||||
return Instant.now().plus(Duration.ofMinutes(30));
|
||||
}
|
||||
public Instant now() {
|
||||
return Instant.now();
|
||||
}
|
||||
|
||||
private URL randomUrl() {
|
||||
try {
|
||||
var rand = new Random();
|
||||
return URI.create("http://a.%d.com/path".formatted(rand.nextInt())).toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Tracker randomTracker() {
|
||||
return new Tracker();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRequireAnnounceOnCreation() {
|
||||
var tracker = randomTracker();
|
||||
|
||||
assertThat(tracker.requireAnnounce(now())).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotRequireAnnounceAfterAnAnnounceWhileAResponseIsNotReceived() {
|
||||
var tracker = randomTracker();
|
||||
|
||||
tracker.announce();
|
||||
assertThat(tracker.requireAnnounce(now())).isFalse();
|
||||
assertThat(tracker.requireAnnounce(inThirtyMinutes())).isFalse();
|
||||
|
||||
tracker.announceSucceed(new AnnounceSucceed(Duration.ofMinutes(30)));
|
||||
assertThat(tracker.requireAnnounce(now())).isFalse();
|
||||
assertThat(tracker.requireAnnounce(inThirtyMinutes())).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldNotRequireAnnounceAfterAnAnnounceWhileAnErrorIsNotReceived() {
|
||||
var tracker = randomTracker();
|
||||
|
||||
tracker.announce();
|
||||
assertThat(tracker.requireAnnounce(now())).isFalse();
|
||||
assertThat(tracker.requireAnnounce(inThirtyMinutes())).isFalse();
|
||||
|
||||
tracker.announceFailed(new AnnounceFailed(), new AnnounceBackoffService.DefaultBackoffService());
|
||||
assertThat(tracker.requireAnnounce(now())).isFalse();
|
||||
assertThat(tracker.requireAnnounce(inThirtyMinutes())).isTrue();
|
||||
}
|
||||
|
||||
// TODO: add disable method
|
||||
// TODO: impossible to announce while disabled
|
||||
// TODO: requireAnnounce returns False for disabled
|
||||
// TODO: make it not possible to announce while still awaiting answer (unless STOPPED)
|
||||
// TODO: make it possible to announce COMPLETED AND STOPPED even when nextAnnounce is not reached
|
||||
// TODO: test that the backoff is reset after a successful announce
|
||||
|
||||
}
|
Loading…
Reference in a new issue