periodically log channel balances

This commit is contained in:
Carsten Otto
2021-11-23 11:43:15 +01:00
parent 874e8fffa6
commit 765efd4de2
18 changed files with 345 additions and 9 deletions

View File

@@ -3,9 +3,11 @@ plugins {
}
dependencies {
implementation project(':web')
runtimeOnly project(':web')
runtimeOnly project(':statistics')
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'com.h2database:h2'
testImplementation project(':backend')
testImplementation project(':grpc-adapter')
}

View File

@@ -1,8 +1,8 @@
package de.cotto.lndmanagej;
import de.cotto.lndmanagej.controller.LegacyController;
import de.cotto.lndmanagej.grpc.GrpcRouterService;
import de.cotto.lndmanagej.grpc.GrpcService;
import de.cotto.lndmanagej.service.ChannelService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class ApplicationContextIT {
@Autowired
private LegacyController legacyController;
private ChannelService channelService;
@MockBean
@SuppressWarnings("unused")
@@ -25,6 +25,6 @@ class ApplicationContextIT {
@Test
void contextStarts() {
assertThat(legacyController).isNotNull();
assertThat(channelService).isNotNull();
}
}

View File

@@ -36,10 +36,10 @@ class BalanceServiceTest {
@Test
void getBalanceInformation_for_pubkey() {
BalanceInformation expected = new BalanceInformation(
Coins.ofSatoshis(3_000),
Coins.ofSatoshis(300),
Coins.ofSatoshis(346),
Coins.ofSatoshis(30)
Coins.ofSatoshis(4_000),
Coins.ofSatoshis(400),
Coins.ofSatoshis(446),
Coins.ofSatoshis(40)
);
when(channelService.getOpenChannelsWith(PUBKEY)).thenReturn(Set.of(LOCAL_OPEN_CHANNEL, LOCAL_OPEN_CHANNEL_2));
when(grpcChannels.getChannel(CHANNEL_ID)).thenReturn(Optional.of(LOCAL_OPEN_CHANNEL_MORE_BALANCE));

View File

@@ -67,7 +67,7 @@ public class LocalOpenChannelFixtures {
CAPACITY,
PUBKEY,
PUBKEY_2,
BALANCE_INFORMATION,
BALANCE_INFORMATION_2,
REMOTE,
false
);

View File

@@ -6,5 +6,6 @@ include 'grpc-adapter'
include 'grpc-client'
include 'metrics'
include 'model'
include 'statistics'
include 'transactions'
include 'web'

10
statistics/build.gradle Normal file
View File

@@ -0,0 +1,10 @@
plugins {
id 'lnd-manageJ.java-library-conventions'
}
dependencies {
implementation project(':backend')
implementation project(':model')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testFixturesApi testFixtures(project(':model'))
}

View File

@@ -0,0 +1,13 @@
package de.cotto.lndmanagej.statistics;
import de.cotto.lndmanagej.model.BalanceInformation;
import de.cotto.lndmanagej.model.ChannelId;
import java.time.LocalDateTime;
public record Statistics(
LocalDateTime timestamp,
ChannelId channelId,
BalanceInformation balanceInformation
) {
}

View File

@@ -0,0 +1,5 @@
package de.cotto.lndmanagej.statistics;
public interface StatisticsDao {
void saveStatistics(Statistics statistics);
}

View File

@@ -0,0 +1,34 @@
package de.cotto.lndmanagej.statistics;
import de.cotto.lndmanagej.model.BalanceInformation;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.LocalOpenChannel;
import de.cotto.lndmanagej.service.ChannelService;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
public class StatisticsService {
private final ChannelService channelService;
private final StatisticsDao statisticsDao;
public StatisticsService(ChannelService channelService, StatisticsDao statisticsDao) {
this.channelService = channelService;
this.statisticsDao = statisticsDao;
}
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
public void storeBalances() {
LocalDateTime timestamp = LocalDateTime.now(ZoneOffset.UTC);
channelService.getOpenChannels().forEach(channel -> storeBalance(channel, timestamp));
}
private void storeBalance(LocalOpenChannel channel, LocalDateTime timestamp) {
ChannelId channelId = channel.getId();
BalanceInformation balanceInformation = channel.getBalanceInformation();
Statistics statistics = new Statistics(timestamp, channelId, balanceInformation);
statisticsDao.saveStatistics(statistics);
}
}

View File

@@ -0,0 +1,22 @@
package de.cotto.lndmanagej.statistics.persistence;
import de.cotto.lndmanagej.statistics.Statistics;
import de.cotto.lndmanagej.statistics.StatisticsDao;
import org.springframework.stereotype.Component;
import javax.transaction.Transactional;
@Component
@Transactional
public class StatisticsDaoImpl implements StatisticsDao {
private final StatisticsRepository statisticsRepository;
public StatisticsDaoImpl(StatisticsRepository statisticsRepository) {
this.statisticsRepository = statisticsRepository;
}
@Override
public void saveStatistics(Statistics statistics) {
statisticsRepository.save(StatisticsJpaDto.fromModel(statistics));
}
}

View File

@@ -0,0 +1,6 @@
package de.cotto.lndmanagej.statistics.persistence;
import java.io.Serializable;
public record StatisticsId(Long channelId, long timestamp) implements Serializable {
}

View File

@@ -0,0 +1,74 @@
package de.cotto.lndmanagej.statistics.persistence;
import com.google.common.annotations.VisibleForTesting;
import de.cotto.lndmanagej.statistics.Statistics;
import javax.annotation.Nullable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.time.ZoneOffset;
import java.util.Objects;
@Entity
@IdClass(StatisticsId.class)
@Table(name = "statistics")
class StatisticsJpaDto {
@Id
private long timestamp;
@Id
@Nullable
private Long channelId;
private long localBalance;
private long localReserved;
private long remoteBalance;
private long remoteReserved;
StatisticsJpaDto() {
// for JPA
}
protected static StatisticsJpaDto fromModel(Statistics statistics) {
StatisticsJpaDto dto = new StatisticsJpaDto();
dto.timestamp = statistics.timestamp().toEpochSecond(ZoneOffset.UTC);
dto.channelId = statistics.channelId().getShortChannelId();
dto.localBalance = statistics.balanceInformation().localBalance().satoshis();
dto.localReserved = statistics.balanceInformation().localReserve().satoshis();
dto.remoteBalance = statistics.balanceInformation().remoteBalance().satoshis();
dto.remoteReserved = statistics.balanceInformation().remoteReserve().satoshis();
return dto;
}
@VisibleForTesting
protected long getTimestamp() {
return timestamp;
}
@VisibleForTesting
protected long getChannelId() {
return Objects.requireNonNull(channelId);
}
@VisibleForTesting
protected long getLocalBalance() {
return localBalance;
}
@VisibleForTesting
protected long getLocalReserved() {
return localReserved;
}
@VisibleForTesting
protected long getRemoteBalance() {
return remoteBalance;
}
@VisibleForTesting
protected long getRemoteReserved() {
return remoteReserved;
}
}

View File

@@ -0,0 +1,6 @@
package de.cotto.lndmanagej.statistics.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StatisticsRepository extends JpaRepository<StatisticsJpaDto, String> {
}

View File

@@ -0,0 +1,56 @@
package de.cotto.lndmanagej.statistics;
import de.cotto.lndmanagej.model.LocalOpenChannel;
import de.cotto.lndmanagej.service.ChannelService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Set;
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL;
import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHANNEL_MORE_BALANCE_2;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StatisticsServiceTest {
@InjectMocks
private StatisticsService statisticsService;
@Mock
private ChannelService channelService;
@Mock
private StatisticsDao statisticsDao;
@Test
void storeBalances() {
when(channelService.getOpenChannels()).thenReturn(Set.of(
LOCAL_OPEN_CHANNEL,
LOCAL_OPEN_CHANNEL_MORE_BALANCE_2
));
statisticsService.storeBalances();
verify(statisticsDao).saveStatistics(argThat(withBalanceInformation(LOCAL_OPEN_CHANNEL_MORE_BALANCE_2)));
verify(statisticsDao).saveStatistics(argThat(withChannelId(LOCAL_OPEN_CHANNEL_MORE_BALANCE_2)));
verify(statisticsDao).saveStatistics(argThat(withBalanceInformation(LOCAL_OPEN_CHANNEL)));
verify(statisticsDao).saveStatistics(argThat(withChannelId(LOCAL_OPEN_CHANNEL)));
verify(statisticsDao, times(2)).saveStatistics(any());
verifyNoMoreInteractions(statisticsDao);
}
private ArgumentMatcher<Statistics> withChannelId(LocalOpenChannel channel) {
return statistics -> statistics.channelId().equals(channel.getId());
}
private ArgumentMatcher<Statistics> withBalanceInformation(LocalOpenChannel channel) {
return statistics -> statistics.balanceInformation().equals(channel.getBalanceInformation());
}
}

View File

@@ -0,0 +1,47 @@
package de.cotto.lndmanagej.statistics.persistence;
import de.cotto.lndmanagej.statistics.StatisticsFixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.ZoneOffset;
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.BALANCE_INFORMATION;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class StatisticsDaoImplTest {
@InjectMocks
private StatisticsDaoImpl statisticsDaoImpl;
@Mock
private StatisticsRepository statisticsRepository;
@Test
void saveStatistics() {
statisticsDaoImpl.saveStatistics(StatisticsFixtures.STATISTICS);
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getTimestamp() == StatisticsFixtures.TIMESTAMP.toEpochSecond(ZoneOffset.UTC)
));
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getChannelId() == CHANNEL_ID.getShortChannelId()));
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getLocalBalance() == BALANCE_INFORMATION.localBalance().satoshis()
));
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getLocalReserved() == BALANCE_INFORMATION.localReserve().satoshis()
));
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getRemoteBalance() == BALANCE_INFORMATION.remoteBalance().satoshis()
));
verify(statisticsRepository).save(argThat(jpaDto ->
jpaDto.getRemoteReserved() == BALANCE_INFORMATION.remoteReserve().satoshis()
));
}
}

View File

@@ -0,0 +1,23 @@
package de.cotto.lndmanagej.statistics.persistence;
import org.junit.jupiter.api.Test;
import java.time.ZoneOffset;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.statistics.StatisticsFixtures.TIMESTAMP;
import static org.assertj.core.api.Assertions.assertThat;
class StatisticsIdTest {
private static final long CHANNEL_ID_LONG = CHANNEL_ID.getShortChannelId();
private static final long TIMESTAMP_LONG = TIMESTAMP.toEpochSecond(ZoneOffset.UTC);
private final StatisticsId statisticsId = new StatisticsId(CHANNEL_ID_LONG, TIMESTAMP_LONG);
@Test
void test_types() {
assertThat(statisticsId.channelId()).isEqualTo(CHANNEL_ID_LONG);
assertThat(statisticsId.timestamp()).isEqualTo(TIMESTAMP_LONG);
}
}

View File

@@ -0,0 +1,25 @@
package de.cotto.lndmanagej.statistics.persistence;
import org.junit.jupiter.api.Test;
import java.time.ZoneOffset;
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.BALANCE_INFORMATION;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
import static de.cotto.lndmanagej.statistics.StatisticsFixtures.STATISTICS;
import static de.cotto.lndmanagej.statistics.StatisticsFixtures.TIMESTAMP;
import static org.assertj.core.api.Assertions.assertThat;
class StatisticsJpaDtoTest {
@Test
@SuppressWarnings("PMD.JUnitTestContainsTooManyAsserts")
void fromModel() {
StatisticsJpaDto jpaDto = StatisticsJpaDto.fromModel(STATISTICS);
assertThat(jpaDto.getTimestamp()).isEqualTo(TIMESTAMP.toEpochSecond(ZoneOffset.UTC));
assertThat(jpaDto.getChannelId()).isEqualTo(CHANNEL_ID.getShortChannelId());
assertThat(jpaDto.getLocalBalance()).isEqualTo(BALANCE_INFORMATION.localBalance().satoshis());
assertThat(jpaDto.getLocalReserved()).isEqualTo(BALANCE_INFORMATION.localReserve().satoshis());
assertThat(jpaDto.getRemoteBalance()).isEqualTo(BALANCE_INFORMATION.remoteBalance().satoshis());
assertThat(jpaDto.getRemoteReserved()).isEqualTo(BALANCE_INFORMATION.remoteReserve().satoshis());
}
}

View File

@@ -0,0 +1,12 @@
package de.cotto.lndmanagej.statistics;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import static de.cotto.lndmanagej.model.BalanceInformationFixtures.BALANCE_INFORMATION;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
public class StatisticsFixtures {
public static final LocalDateTime TIMESTAMP = LocalDateTime.now(ZoneOffset.UTC);
public static final Statistics STATISTICS = new Statistics(TIMESTAMP, CHANNEL_ID, BALANCE_INFORMATION);
}