add node warnings functionality (at the moment: only online percentage)

This commit is contained in:
Carsten Otto
2021-12-27 20:18:05 +01:00
parent 5fa0851bd7
commit de40c32f63
21 changed files with 317 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.FeeReport;
import de.cotto.lndmanagej.model.Node;
import de.cotto.lndmanagej.model.NodeDetails;
import de.cotto.lndmanagej.model.NodeWarnings;
import de.cotto.lndmanagej.model.OnChainCosts;
import de.cotto.lndmanagej.model.OnlineReport;
import de.cotto.lndmanagej.model.Pubkey;
@@ -26,6 +27,7 @@ public class NodeDetailsService {
private final FeeService feeService;
private final RebalanceService rebalanceService;
private final OnlinePeersService onlinePeersService;
private final NodeWarningsService warningsService;
public NodeDetailsService(
ChannelService channelService,
@@ -34,7 +36,8 @@ public class NodeDetailsService {
BalanceService balanceService,
FeeService feeService,
RebalanceService rebalanceService,
OnlinePeersService onlinePeersService
OnlinePeersService onlinePeersService,
NodeWarningsService warningsService
) {
this.channelService = channelService;
this.nodeService = nodeService;
@@ -43,6 +46,7 @@ public class NodeDetailsService {
this.feeService = feeService;
this.rebalanceService = rebalanceService;
this.onlinePeersService = onlinePeersService;
this.warningsService = warningsService;
}
public NodeDetails getDetails(Pubkey pubkey) {
@@ -52,6 +56,7 @@ public class NodeDetailsService {
CompletableFuture<BalanceInformation> balanceInformation = getBalanceInformation(pubkey);
CompletableFuture<FeeReport> feeReport = getFeeReport(pubkey);
CompletableFuture<RebalanceReport> rebalanceReport = getRebalanceReport(pubkey);
CompletableFuture<NodeWarnings> nodeWarnings = getNodeWarnings(pubkey);
List<ChannelId> openChannelIds =
getSortedChannelIds(channelService.getOpenChannelsWith(pubkey));
List<ChannelId> closedChannelIds =
@@ -72,7 +77,8 @@ public class NodeDetailsService {
balanceInformation.get(),
onlineReport.get(),
feeReport.get(),
rebalanceReport.get()
rebalanceReport.get(),
nodeWarnings.get()
);
} catch (InterruptedException | ExecutionException exception) {
throw new IllegalStateException("Unable to compute node details for " + pubkey, exception);
@@ -95,6 +101,10 @@ public class NodeDetailsService {
return CompletableFuture.supplyAsync(() -> rebalanceService.getReportForPeer(pubkey));
}
private CompletableFuture<NodeWarnings> getNodeWarnings(Pubkey pubkey) {
return CompletableFuture.supplyAsync(() -> warningsService.getNodeWarnings(pubkey));
}
private CompletableFuture<BalanceInformation> getBalanceInformation(Pubkey pubkey) {
return CompletableFuture.supplyAsync(() -> balanceService.getBalanceInformationForPeer(pubkey));
}

View File

@@ -0,0 +1,40 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.NodeOnlinePercentageWarning;
import de.cotto.lndmanagej.model.NodeWarning;
import de.cotto.lndmanagej.model.NodeWarnings;
import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
@Component
public class NodeWarningsService {
private static final int THRESHOLD = 80;
private final OnlinePeersService onlinePeersService;
public NodeWarningsService(OnlinePeersService onlinePeersService) {
this.onlinePeersService = onlinePeersService;
}
public NodeWarnings getNodeWarnings(Pubkey pubkey) {
List<NodeWarning> warnings = Stream.of((Function<Pubkey, Optional<NodeWarning>>) this::getOnlineWarning)
.map(function -> function.apply(pubkey))
.flatMap(Optional::stream)
.toList();
return new NodeWarnings(warnings);
}
private Optional<NodeWarning> getOnlineWarning(Pubkey pubkey) {
int percentage = onlinePeersService.getOnlinePercentageLastWeek(pubkey);
if (percentage < THRESHOLD) {
return Optional.of(new NodeOnlinePercentageWarning(percentage));
}
return Optional.empty();
}
}

View File

@@ -2,6 +2,7 @@ package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.BalanceInformation;
import de.cotto.lndmanagej.model.FeeReport;
import de.cotto.lndmanagej.model.NodeWarnings;
import de.cotto.lndmanagej.model.OnChainCosts;
import de.cotto.lndmanagej.model.RebalanceReport;
import org.junit.jupiter.api.Test;
@@ -21,6 +22,7 @@ import static de.cotto.lndmanagej.model.NodeDetailsFixtures.NODE_DETAILS;
import static de.cotto.lndmanagej.model.NodeDetailsFixtures.NODE_DETAILS_EMPTY;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE;
import static de.cotto.lndmanagej.model.NodeFixtures.NODE_PEER;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static de.cotto.lndmanagej.model.OnChainCostsFixtures.ON_CHAIN_COSTS;
import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT;
import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT_OFFLINE;
@@ -56,6 +58,9 @@ class NodeDetailsServiceTest {
@Mock
private OnlinePeersService onlinePeersService;
@Mock
private NodeWarningsService nodeWarningsService;
@Test
void getDetails_no_channel() {
when(nodeService.getNode(PUBKEY)).thenReturn(NODE);
@@ -64,6 +69,7 @@ class NodeDetailsServiceTest {
when(feeService.getFeeReportForPeer(PUBKEY)).thenReturn(FeeReport.EMPTY);
when(rebalanceService.getReportForPeer(PUBKEY)).thenReturn(RebalanceReport.EMPTY);
when(onlinePeersService.getOnlineReport(NODE_PEER)).thenReturn(ONLINE_REPORT_OFFLINE);
when(nodeWarningsService.getNodeWarnings(PUBKEY)).thenReturn(NodeWarnings.NONE);
assertThat(nodeDetailsService.getDetails(PUBKEY)).isEqualTo(NODE_DETAILS_EMPTY);
}
@@ -79,6 +85,7 @@ class NodeDetailsServiceTest {
when(feeService.getFeeReportForPeer(PUBKEY)).thenReturn(FEE_REPORT);
when(rebalanceService.getReportForPeer(PUBKEY)).thenReturn(REBALANCE_REPORT);
when(onlinePeersService.getOnlineReport(NODE_PEER)).thenReturn(ONLINE_REPORT);
when(nodeWarningsService.getNodeWarnings(PUBKEY)).thenReturn(NODE_WARNINGS);
assertThat(nodeDetailsService.getDetails(PUBKEY)).isEqualTo(NODE_DETAILS);
}
}

View File

@@ -0,0 +1,40 @@
package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.model.NodeOnlinePercentageWarning;
import de.cotto.lndmanagej.model.NodeWarnings;
import org.junit.jupiter.api.BeforeEach;
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 static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class NodeWarningsServiceTest {
@InjectMocks
private NodeWarningsService nodeWarningsService;
@Mock
private OnlinePeersService onlinePeersService;
@BeforeEach
void setUp() {
when(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).thenReturn(80);
}
@Test
void getNodeWarnings_online_below_threshold() {
when(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).thenReturn(79);
assertThat(nodeWarningsService.getNodeWarnings(PUBKEY))
.isEqualTo(new NodeWarnings(new NodeOnlinePercentageWarning(79)));
}
@Test
void getNodeWarnings_ok() {
assertThat(nodeWarningsService.getNodeWarnings(PUBKEY)).isEqualTo(NodeWarnings.NONE);
}
}

View File

@@ -13,6 +13,7 @@ public record NodeDetails(
BalanceInformation balanceInformation,
OnlineReport onlineReport,
FeeReport feeReport,
RebalanceReport rebalanceReport
RebalanceReport rebalanceReport,
NodeWarnings nodeWarnings
) {
}

View File

@@ -0,0 +1,4 @@
package de.cotto.lndmanagej.model;
public record NodeOnlinePercentageWarning(int onlinePercentage) implements NodeWarning {
}

View File

@@ -0,0 +1,4 @@
package de.cotto.lndmanagej.model;
public interface NodeWarning {
}

View File

@@ -0,0 +1,12 @@
package de.cotto.lndmanagej.model;
import java.util.Arrays;
import java.util.List;
public record NodeWarnings(List<NodeWarning> warnings) {
public static final NodeWarnings NONE = new NodeWarnings();
public NodeWarnings(NodeWarning... nodeWarnings) {
this(Arrays.stream(nodeWarnings).toList());
}
}

View File

@@ -0,0 +1,13 @@
package de.cotto.lndmanagej.model;
import org.junit.jupiter.api.Test;
import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_PERCENTAGE_WARNING;
import static org.assertj.core.api.Assertions.assertThat;
class NodeOnlinePercentageWarningTest {
@Test
void onlinePercentage() {
assertThat(NODE_ONLINE_PERCENTAGE_WARNING.onlinePercentage()).isEqualTo(51);
}
}

View File

@@ -0,0 +1,21 @@
package de.cotto.lndmanagej.model;
import org.junit.jupiter.api.Test;
import java.util.List;
import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_PERCENTAGE_WARNING;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static org.assertj.core.api.Assertions.assertThat;
class NodeWarningsTest {
@Test
void warnings() {
assertThat(NODE_WARNINGS.warnings()).containsExactly(NODE_ONLINE_PERCENTAGE_WARNING);
}
@Test
void none() {
assertThat(NodeWarnings.NONE).isEqualTo(new NodeWarnings(List.of()));
}
}

View File

@@ -9,6 +9,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_3;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_4;
import static de.cotto.lndmanagej.model.FeeReportFixtures.FEE_REPORT;
import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static de.cotto.lndmanagej.model.OnChainCostsFixtures.ON_CHAIN_COSTS;
import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT;
import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT_OFFLINE;
@@ -27,7 +28,8 @@ public class NodeDetailsFixtures {
BALANCE_INFORMATION_2,
ONLINE_REPORT,
FEE_REPORT,
REBALANCE_REPORT
REBALANCE_REPORT,
NODE_WARNINGS
);
public static final NodeDetails NODE_DETAILS_EMPTY = new NodeDetails(
PUBKEY,
@@ -40,6 +42,7 @@ public class NodeDetailsFixtures {
BalanceInformation.EMPTY,
ONLINE_REPORT_OFFLINE,
FeeReport.EMPTY,
RebalanceReport.EMPTY
RebalanceReport.EMPTY,
NodeWarnings.NONE
);
}

View File

@@ -0,0 +1,6 @@
package de.cotto.lndmanagej.model;
public class NodeWarningFixtures {
public static final NodeOnlinePercentageWarning NODE_ONLINE_PERCENTAGE_WARNING =
new NodeOnlinePercentageWarning(51);
}

View File

@@ -0,0 +1,9 @@
package de.cotto.lndmanagej.model;
import static de.cotto.lndmanagej.model.NodeWarningFixtures.NODE_ONLINE_PERCENTAGE_WARNING;
public class NodeWarningsFixtures {
public static final NodeWarnings NODE_WARNINGS = new NodeWarnings(
NODE_ONLINE_PERCENTAGE_WARNING
);
}

View File

@@ -97,6 +97,7 @@ class NodeControllerIT {
.andExpect(jsonPath("$.balance.remoteAvailable", is("203")))
.andExpect(jsonPath("$.feeReport.earned", is("1234")))
.andExpect(jsonPath("$.feeReport.sourced", is("567")))
.andExpect(jsonPath("$.nodeWarnings[0].onlinePercentage", is(51)))
.andExpect(jsonPath("$.onChainCosts.openCosts", is("1000")))
.andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000")))
.andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000")))

View File

@@ -0,0 +1,47 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.model.ChannelIdResolver;
import de.cotto.lndmanagej.model.NodeWarnings;
import de.cotto.lndmanagej.service.NodeWarningsService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.core.Is.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@WebMvcTest(controllers = WarningsController.class)
class WarningsControllerIT {
private static final String NODE_PREFIX = "/api/node/" + PUBKEY;
@Autowired
private MockMvc mockMvc;
@MockBean
@SuppressWarnings("unused")
private ChannelIdResolver channelIdResolver;
@MockBean
private NodeWarningsService nodeWarningsService;
@Test
void getWarningsForNode() throws Exception {
when(nodeWarningsService.getNodeWarnings(PUBKEY)).thenReturn(NODE_WARNINGS);
mockMvc.perform(get(NODE_PREFIX + "/warnings"))
.andExpect(jsonPath("$.nodeWarnings[0].onlinePercentage", is(51)));
}
@Test
void getWarningsForNode_empty() throws Exception {
when(nodeWarningsService.getNodeWarnings(PUBKEY)).thenReturn(NodeWarnings.NONE);
mockMvc.perform(get(NODE_PREFIX + "/warnings"))
.andExpect(jsonPath("$.nodeWarnings", hasSize(0)));
}
}

View File

@@ -0,0 +1,30 @@
package de.cotto.lndmanagej.controller;
import com.codahale.metrics.annotation.Timed;
import de.cotto.lndmanagej.controller.dto.NodeWarningsDto;
import de.cotto.lndmanagej.controller.dto.ObjectMapperConfiguration;
import de.cotto.lndmanagej.model.Pubkey;
import de.cotto.lndmanagej.service.NodeWarningsService;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/")
@Import(ObjectMapperConfiguration.class)
public class WarningsController {
private final NodeWarningsService nodeWarningsService;
public WarningsController(NodeWarningsService nodeWarningsService) {
this.nodeWarningsService = nodeWarningsService;
}
@Timed
@GetMapping("/node/{pubkey}/warnings")
public NodeWarningsDto getWarningsForNode(@PathVariable Pubkey pubkey) {
return NodeWarningsDto.createFromModel(nodeWarningsService.getNodeWarnings(pubkey));
}
}

View File

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.NodeDetails;
import de.cotto.lndmanagej.model.NodeWarning;
import de.cotto.lndmanagej.model.Pubkey;
import java.util.List;
@@ -19,7 +20,8 @@ public record NodeDetailsDto(
BalanceInformationDto balance,
OnlineReportDto onlineReport,
FeeReportDto feeReport,
RebalanceReportDto rebalanceReport
RebalanceReportDto rebalanceReport,
List<NodeWarning> nodeWarnings
) {
public static NodeDetailsDto createFromModel(NodeDetails nodeDetails) {
return new NodeDetailsDto(
@@ -33,7 +35,8 @@ public record NodeDetailsDto(
BalanceInformationDto.createFromModel(nodeDetails.balanceInformation()),
OnlineReportDto.createFromModel(nodeDetails.onlineReport()),
FeeReportDto.createFromModel(nodeDetails.feeReport()),
RebalanceReportDto.createFromModel(nodeDetails.rebalanceReport())
RebalanceReportDto.createFromModel(nodeDetails.rebalanceReport()),
nodeDetails.nodeWarnings().warnings()
);
}
}

View File

@@ -0,0 +1,12 @@
package de.cotto.lndmanagej.controller.dto;
import de.cotto.lndmanagej.model.NodeWarning;
import de.cotto.lndmanagej.model.NodeWarnings;
import java.util.List;
public record NodeWarningsDto(List<NodeWarning> nodeWarnings) {
public static NodeWarningsDto createFromModel(NodeWarnings nodeWarnings) {
return new NodeWarningsDto(nodeWarnings.warnings());
}
}

View File

@@ -0,0 +1,30 @@
package de.cotto.lndmanagej.controller;
import de.cotto.lndmanagej.controller.dto.NodeWarningsDto;
import de.cotto.lndmanagej.service.NodeWarningsService;
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 static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class WarningsControllerTest {
@InjectMocks
private WarningsController warningsController;
@Mock
private NodeWarningsService nodeWarningsService;
@Test
void getWarningsForNode() {
when(nodeWarningsService.getNodeWarnings(PUBKEY)).thenReturn(NODE_WARNINGS);
assertThat(warningsController.getWarningsForNode(PUBKEY))
.isEqualTo(NodeWarningsDto.createFromModel(NODE_WARNINGS));
}
}

View File

@@ -12,6 +12,7 @@ import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID_4;
import static de.cotto.lndmanagej.model.FeeReportFixtures.FEE_REPORT;
import static de.cotto.lndmanagej.model.NodeDetailsFixtures.NODE_DETAILS;
import static de.cotto.lndmanagej.model.NodeFixtures.ALIAS;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static de.cotto.lndmanagej.model.OnChainCostsFixtures.ON_CHAIN_COSTS;
import static de.cotto.lndmanagej.model.OnlineReportFixtures.ONLINE_REPORT;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
@@ -32,7 +33,8 @@ class NodeDetailsDtoTest {
BalanceInformationDto.createFromModel(BALANCE_INFORMATION_2),
OnlineReportDto.createFromModel(ONLINE_REPORT),
FeeReportDto.createFromModel(FEE_REPORT),
RebalanceReportDto.createFromModel(REBALANCE_REPORT)
RebalanceReportDto.createFromModel(REBALANCE_REPORT),
NODE_WARNINGS.warnings()
);
assertThat(NodeDetailsDto.createFromModel(NODE_DETAILS)).isEqualTo(expected);
}

View File

@@ -0,0 +1,14 @@
package de.cotto.lndmanagej.controller.dto;
import org.junit.jupiter.api.Test;
import static de.cotto.lndmanagej.model.NodeWarningsFixtures.NODE_WARNINGS;
import static org.assertj.core.api.Assertions.assertThat;
class NodeWarningsDtoTest {
@Test
void createFromModel() {
assertThat(NodeWarningsDto.createFromModel(NODE_WARNINGS))
.isEqualTo(new NodeWarningsDto(NODE_WARNINGS.warnings()));
}
}