report online percentage in online report

This commit is contained in:
Carsten Otto
2021-12-25 20:03:36 +01:00
parent c3eead54bc
commit 6d9e33b18d
14 changed files with 194 additions and 14 deletions

View File

@@ -3,15 +3,19 @@ package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.Node;
import de.cotto.lndmanagej.model.OnlineReport;
import de.cotto.lndmanagej.model.OnlineStatus;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Component
public class OnlinePeersService {
private static final int DAYS_FOR_OFFLINE_PERCENTAGE = 7;
private final OnlinePeersDao dao;
public OnlinePeersService(OnlinePeersDao dao) {
@@ -20,11 +24,44 @@ public class OnlinePeersService {
public OnlineReport getOnlineReport(Node node) {
boolean online = node.online();
OnlineStatus mostRecentOnlineStatus = dao.getMostRecentOnlineStatus(node.pubkey()).orElse(null);
if (mostRecentOnlineStatus != null && mostRecentOnlineStatus.online() == online) {
return OnlineReport.createFromStatus(mostRecentOnlineStatus);
OnlineStatus mostRecentOnlineStatus = dao.getMostRecentOnlineStatus(node.pubkey())
.orElse(new OnlineStatus(online, now()));
int onlinePercentageLastWeek = getOnlinePercentageLastWeek(node.pubkey());
if (mostRecentOnlineStatus.online() == online) {
return OnlineReport.createFromStatus(mostRecentOnlineStatus, onlinePercentageLastWeek);
}
return new OnlineReport(online, now());
return new OnlineReport(online, now(), onlinePercentageLastWeek);
}
public int getOnlinePercentageLastWeek(Pubkey pubkey) {
Duration total = Duration.ZERO;
Duration online = Duration.ZERO;
ZonedDateTime intervalStart = ZonedDateTime.now(ZoneOffset.UTC);
ZonedDateTime cutoff = intervalStart.minusDays(DAYS_FOR_OFFLINE_PERCENTAGE);
boolean shouldContinue = true;
List<OnlineStatus> allForPeer = dao.getAllForPeer(pubkey);
for (int i = 0; i < allForPeer.size() && shouldContinue; i++) {
OnlineStatus onlineStatus = allForPeer.get(i);
ZonedDateTime intervalEnd = onlineStatus.since();
if (intervalEnd.isBefore(cutoff)) {
intervalEnd = cutoff;
shouldContinue = false;
}
Duration difference = Duration.between(intervalEnd, intervalStart);
total = total.plus(difference);
if (onlineStatus.online()) {
online = online.plus(difference);
}
intervalStart = intervalEnd;
}
if (total.isZero()) {
return 0;
}
return getRoundedPercentage(total, online);
}
private int getRoundedPercentage(Duration total, Duration offline) {
return (int) (offline.getSeconds() * 100.0 / total.getSeconds());
}
private ZonedDateTime now() {

View File

@@ -1,6 +1,7 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.OnlineReport;
import de.cotto.lndmanagej.model.OnlineStatus;
import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -10,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE;
@@ -23,6 +25,8 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OnlinePeersServiceTest {
private static final ZonedDateTime NOW = ZonedDateTime.now(ZoneOffset.UTC);
@InjectMocks
private OnlinePeersService onlinePeersService;
@@ -32,6 +36,7 @@ class OnlinePeersServiceTest {
@Test
void with_time_if_given_status_matches_last_known_status() {
when(dao.getMostRecentOnlineStatus(PUBKEY)).thenReturn(Optional.of(ONLINE_STATUS));
mockFor23PercentOffline();
assertThat(onlinePeersService.getOnlineReport(NODE_PEER)).isEqualTo(ONLINE_REPORT);
}
@@ -57,9 +62,92 @@ class OnlinePeersServiceTest {
assertVeryRecentSince(report);
}
@Test
void getOnlinePercentageLastWeek_no_data() {
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero();
}
@Test
void getOnlinePercentageLastWeek_always_online() {
ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, early)));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100);
}
@Test
void getOnlinePercentageLastWeek_always_offline() {
ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, early)));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero();
}
@Test
void getOnlinePercentageLastWeek_limited_data_offline() {
ZonedDateTime oneHourAgo = NOW.minusHours(1);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, oneHourAgo)));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero();
}
@Test
void getOnlinePercentageLastWeek_limited_data_online() {
ZonedDateTime oneHourAgo = NOW.minusHours(1);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, oneHourAgo)));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100);
}
@Test
void getOnlinePercentageLastWeek_limited_data_online_then_offline() {
ZonedDateTime twoHoursAgo = NOW.minusHours(2);
ZonedDateTime oneHourAgo = NOW.minusHours(1);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(
new OnlineStatus(true, oneHourAgo),
new OnlineStatus(false, twoHoursAgo)
));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(50);
}
@Test
void getOnlinePercentageLastWeek_limited_data_offline_then_online() {
ZonedDateTime twoHoursAgo = NOW.minusHours(2);
ZonedDateTime oneHourAgo = NOW.minusHours(1);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(
new OnlineStatus(false, oneHourAgo),
new OnlineStatus(true, twoHoursAgo)
));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(49);
}
@Test
void getOnlinePercentageLastWeek_cuts_off_old_data() {
ZonedDateTime twoYearsAgo = NOW.minusYears(2);
ZonedDateTime oneYearAgo = NOW.minusYears(1);
ZonedDateTime thirteenDaysAgo = NOW.minusDays(6);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(
new OnlineStatus(true, thirteenDaysAgo),
new OnlineStatus(false, oneYearAgo),
new OnlineStatus(true, twoYearsAgo)
));
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(85);
}
@Test
void getOnlinePercentageLastWeek_is_rounded() {
mockFor23PercentOffline();
assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(77);
}
private void assertVeryRecentSince(OnlineReport report) {
assertThat(report.since())
.isAfter(ZonedDateTime.now(ZoneOffset.UTC).minusSeconds(1))
.isAfter(NOW.minusSeconds(1))
.asString().hasSize(20);
}
private void mockFor23PercentOffline() {
ZonedDateTime longAgo = NOW.minusDays(14);
ZonedDateTime offlineSince = NOW.minusMinutes(2318);
when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(
new OnlineStatus(false, offlineSince),
new OnlineStatus(true, longAgo)
));
}
}

View File

@@ -2,8 +2,8 @@ package de.cotto.lndmanagej.model;
import java.time.ZonedDateTime;
public record OnlineReport(boolean online, ZonedDateTime since) {
public static OnlineReport createFromStatus(OnlineStatus onlineStatus) {
return new OnlineReport(onlineStatus.online(), onlineStatus.since());
public record OnlineReport(boolean online, ZonedDateTime since, int onlinePercentageLastWeek) {
public static OnlineReport createFromStatus(OnlineStatus onlineStatus, int onlinePercentageLastWeek) {
return new OnlineReport(onlineStatus.online(), onlineStatus.since(), onlinePercentageLastWeek);
}
}

View File

@@ -21,8 +21,13 @@ class OnlineReportTest {
assertThat(ONLINE_REPORT.since()).isEqualTo(TIMESTAMP);
}
@Test
void onlinePercentageLastWeek() {
assertThat(ONLINE_REPORT.onlinePercentageLastWeek()).isEqualTo(77);
}
@Test
void createFromOnlineStatus() {
assertThat(OnlineReport.createFromStatus(ONLINE_STATUS)).isEqualTo(ONLINE_REPORT);
assertThat(OnlineReport.createFromStatus(ONLINE_STATUS, 77)).isEqualTo(ONLINE_REPORT);
}
}

View File

@@ -7,6 +7,6 @@ import static java.time.ZoneOffset.UTC;
public class OnlineReportFixtures {
public static final ZonedDateTime TIMESTAMP = LocalDateTime.of(2021, 12, 23, 1, 2, 3).atZone(UTC);
public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP);
public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP);
public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP, 77);
public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP, 66);
}

View File

@@ -51,4 +51,22 @@ class OnlinePeersRepositoryIT {
assertThat(repository.findTopByPubkeyOrderByTimestampDesc(PUBKEY.toString()))
.map(OnlinePeerJpaDto::isOnline).contains(true);
}
@Test
void findByPubkeyOrderByTimestampDesc() {
repository.save(new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP));
repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.minusSeconds(1)));
repository.save(new OnlinePeerJpaDto(PUBKEY_2, false, TIMESTAMP));
assertThat(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).hasSize(2);
}
@Test
void findByPubkeyOrderByTimestampDesc_ordered_new_to_old() {
repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.minusSeconds(1)));
repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.plusSeconds(1)));
repository.save(new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP));
long timestamp = TIMESTAMP.toEpochSecond();
assertThat(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).map(OnlinePeerJpaDto::getTimestamp)
.containsExactlyInAnyOrder(timestamp + 1, timestamp, timestamp - 1);
}
}

View File

@@ -4,10 +4,13 @@ import de.cotto.lndmanagej.model.OnlineStatus;
import de.cotto.lndmanagej.model.Pubkey;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
public interface OnlinePeersDao {
void saveOnlineStatus(Pubkey pubkey, boolean online, ZonedDateTime timestamp);
Optional<OnlineStatus> getMostRecentOnlineStatus(Pubkey pubkey);
List<OnlineStatus> getAllForPeer(Pubkey pubkey);
}

View File

@@ -52,4 +52,8 @@ public class OnlinePeerJpaDto {
public String getPubkey() {
return pubkey;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import javax.transaction.Transactional;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
@Component
@@ -27,4 +28,11 @@ class OnlinePeersDaoImpl implements OnlinePeersDao {
public Optional<OnlineStatus> getMostRecentOnlineStatus(Pubkey pubkey) {
return repository.findTopByPubkeyOrderByTimestampDesc(pubkey.toString()).map(OnlinePeerJpaDto::toModel);
}
@Override
public List<OnlineStatus> getAllForPeer(Pubkey pubkey) {
return repository.findByPubkeyOrderByTimestampDesc(pubkey.toString()).stream()
.map(OnlinePeerJpaDto::toModel)
.toList();
}
}

View File

@@ -2,8 +2,11 @@ package de.cotto.lndmanagej.onlinepeers.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface OnlinePeersRepository extends JpaRepository<OnlinePeerJpaDto, Long> {
Optional<OnlinePeerJpaDto> findTopByPubkeyOrderByTimestampDesc(String pubkey);
List<OnlinePeerJpaDto> findByPubkeyOrderByTimestampDesc(String pubkey);
}

View File

@@ -8,6 +8,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -53,6 +54,14 @@ class OnlinePeersDaoImplTest {
assertThat(dao.getMostRecentOnlineStatus(PUBKEY)).contains(ONLINE_STATUS);
}
@Test
void getAllForPeer() {
OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP);
OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.plusSeconds(1));
when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(List.of(dto1, dto2));
assertThat(dao.getAllForPeer(PUBKEY)).containsExactly(dto1.toModel(), dto2.toModel());
}
private void verifySave(Pubkey pubkey, boolean expected) {
verify(repository).save(argThat(privateChannelJpaDto -> privateChannelJpaDto.isOnline() == expected));
verify(repository).save(argThat(dto -> pubkey.equals(Pubkey.create(Objects.requireNonNull(dto.getPubkey())))));

View File

@@ -101,6 +101,7 @@ class NodeControllerIT {
.andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000")))
.andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000")))
.andExpect(jsonPath("$.onlineReport.online", is(true)))
.andExpect(jsonPath("$.onlineReport.onlinePercentageLastWeek", is(77)))
.andExpect(jsonPath("$.onlineReport.since", is("2021-12-23T01:02:03Z")));
}

View File

@@ -4,9 +4,9 @@ import de.cotto.lndmanagej.model.OnlineReport;
import java.time.format.DateTimeFormatter;
public record OnlineReportDto(boolean online, String since) {
public record OnlineReportDto(boolean online, String since, int onlinePercentageLastWeek) {
public static OnlineReportDto createFromModel(OnlineReport onlineReport) {
String formattedDateTime = onlineReport.since().format(DateTimeFormatter.ISO_INSTANT);
return new OnlineReportDto(onlineReport.online(), formattedDateTime);
return new OnlineReportDto(onlineReport.online(), formattedDateTime, onlineReport.onlinePercentageLastWeek());
}
}

View File

@@ -9,7 +9,11 @@ class OnlineReportDtoTest {
@Test
void createFromModel() {
assertThat(OnlineReportDto.createFromModel(ONLINE_REPORT)).isEqualTo(
new OnlineReportDto(ONLINE_REPORT.online(), ONLINE_REPORT.since().toString())
new OnlineReportDto(
ONLINE_REPORT.online(),
ONLINE_REPORT.since().toString(),
ONLINE_REPORT.onlinePercentageLastWeek()
)
);
}