-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6fd281f
commit 7466a1f
Showing
8 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
src/main/java/org/araymond/joalcore/annotations/concurency/Immutable.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
|
||
} |
3 changes: 3 additions & 0 deletions
3
src/main/java/org/araymond/joalcore/annotations/ddd/ValueObject.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/main/java/org/araymond/joalcore/core/trackers/domain/AnnounceBackoffService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
src/main/java/org/araymond/joalcore/core/trackers/domain/AnnounceFailed.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package org.araymond.joalcore.core.trackers.domain; | ||
|
||
public record AnnounceFailed() { | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/org/araymond/joalcore/core/trackers/domain/AnnounceSucceed.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
src/main/java/org/araymond/joalcore/core/trackers/domain/Tracker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/test/java/org/araymond/joalcore/core/trackers/domain/AnnounceBackoffServiceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
|
||
} |
78 changes: 78 additions & 0 deletions
78
src/test/java/org/araymond/joalcore/core/trackers/domain/TrackerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} |