do not query dao for channels closed a long time ago

This commit is contained in:
Carsten Otto
2021-12-30 15:38:47 +01:00
parent 462d3651da
commit 2d49014f4d
13 changed files with 94 additions and 41 deletions

View File

@@ -6,6 +6,7 @@ import de.cotto.lndmanagej.caching.CacheBuilder;
import de.cotto.lndmanagej.forwardinghistory.ForwardingEventsDao;
import de.cotto.lndmanagej.model.Channel;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.ClosedChannel;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.FeeReport;
import de.cotto.lndmanagej.model.ForwardingEvent;
@@ -13,19 +14,24 @@ import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Period;
@Component
public class FeeService {
private static final Period DEFAULT_MAX_AGE = Period.ofYears(Integer.MAX_VALUE);
private static final Duration DEFAULT_MAX_AGE = Duration.ofDays(365 * 1_000);
private final ForwardingEventsDao forwardingEventsDao;
private final ChannelService channelService;
private final OwnNodeService ownNodeService;
private final LoadingCache<CacheKey, FeeReport> cacheForOpenChannels;
private final LoadingCache<CacheKey, FeeReport> cacheForClosedChannels;
public FeeService(ForwardingEventsDao forwardingEventsDao, ChannelService channelService) {
public FeeService(
ForwardingEventsDao forwardingEventsDao,
ChannelService channelService,
OwnNodeService ownNodeService
) {
this.forwardingEventsDao = forwardingEventsDao;
this.channelService = channelService;
this.ownNodeService = ownNodeService;
cacheForOpenChannels = new CacheBuilder()
.withRefresh(Duration.ofSeconds(5))
.withExpiry(Duration.ofSeconds(10))
@@ -41,7 +47,7 @@ public class FeeService {
}
@Timed
public FeeReport getFeeReportForPeer(Pubkey pubkey, Period maxAge) {
public FeeReport getFeeReportForPeer(Pubkey pubkey, Duration maxAge) {
return channelService.getAllChannelsWith(pubkey).parallelStream()
.map(Channel::getId)
.map(channelId -> getFeeReportForChannel(channelId, maxAge))
@@ -53,12 +59,16 @@ public class FeeService {
}
@Timed
public FeeReport getFeeReportForChannel(ChannelId channelId, Period maxAge) {
public FeeReport getFeeReportForChannel(ChannelId channelId, Duration maxAge) {
CacheKey cacheKey = new CacheKey(channelId, maxAge);
if (channelService.isClosed(channelId)) {
return cacheForClosedChannels.get(cacheKey);
ClosedChannel closedChannel = channelService.getClosedChannel(channelId).orElse(null);
if (closedChannel == null) {
return cacheForOpenChannels.get(cacheKey);
}
return cacheForOpenChannels.get(cacheKey);
if (isClosedLongerThan(closedChannel, maxAge)) {
return FeeReport.EMPTY;
}
return cacheForClosedChannels.get(cacheKey);
}
private FeeReport getFeeReportForChannelWithoutCache(CacheKey cacheKey) {
@@ -68,19 +78,25 @@ public class FeeService {
);
}
private Coins getEarnedFeesForChannel(ChannelId channelId, Period maxAge) {
private Coins getEarnedFeesForChannel(ChannelId channelId, Duration maxAge) {
return forwardingEventsDao.getEventsWithOutgoingChannel(channelId, maxAge).parallelStream()
.map(ForwardingEvent::fees)
.reduce(Coins.NONE, Coins::add);
}
private Coins getSourcedFeesForChannel(ChannelId channelId, Period maxAge) {
private Coins getSourcedFeesForChannel(ChannelId channelId, Duration maxAge) {
return forwardingEventsDao.getEventsWithIncomingChannel(channelId, maxAge).parallelStream()
.map(ForwardingEvent::fees)
.reduce(Coins.NONE, Coins::add);
}
private boolean isClosedLongerThan(ClosedChannel closedChannel, Duration maxAge) {
int blocksSinceClose = ownNodeService.getBlockHeight() - closedChannel.getCloseHeight();
int daysClosedWithSafetyMargin = (int) (0.5 * blocksSinceClose * 10.0 / 60 / 24);
return maxAge.minus(Duration.ofDays(daysClosedWithSafetyMargin)).isNegative();
}
@SuppressWarnings("UnusedVariable")
private record CacheKey(ChannelId channelId, Period maxAge) {
private record CacheKey(ChannelId channelId, Duration maxAge) {
}
}

View File

@@ -3,14 +3,16 @@ package de.cotto.lndmanagej.service;
import de.cotto.lndmanagej.forwardinghistory.ForwardingEventsDao;
import de.cotto.lndmanagej.model.Coins;
import de.cotto.lndmanagej.model.FeeReport;
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 java.time.Period;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static de.cotto.lndmanagej.model.ChannelIdFixtures.CHANNEL_ID;
@@ -23,13 +25,17 @@ import static de.cotto.lndmanagej.model.LocalOpenChannelFixtures.LOCAL_OPEN_CHAN
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.WaitingCloseChannelFixtures.WAITING_CLOSE_CHANNEL_2;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FeeServiceTest {
private static final Period DEFAULT_MAX_DURATION = Period.ofYears(Integer.MAX_VALUE);
private static final Duration DEFAULT_MAX_DURATION = Duration.ofDays(365 * 1_000);
private static final int BLOCK_HEIGHT = 700_000;
@InjectMocks
private FeeService feeService;
@@ -40,6 +46,14 @@ class FeeServiceTest {
@Mock
private ChannelService channelService;
@Mock
private OwnNodeService ownNodeService;
@BeforeEach
void setUp() {
lenient().when(ownNodeService.getBlockHeight()).thenReturn(BLOCK_HEIGHT);
}
@Test
void getFeeReportForChannel() {
when(dao.getEventsWithOutgoingChannel(CHANNEL_ID, DEFAULT_MAX_DURATION))
@@ -60,7 +74,7 @@ class FeeServiceTest {
@Test
void getFeeReportForChannel_with_day_limit() {
Period maxAge = Period.ofDays(7);
Duration maxAge = Duration.ofDays(7);
assertThat(feeService.getFeeReportForChannel(CHANNEL_ID, maxAge))
.isEqualTo(new FeeReport(Coins.NONE, Coins.NONE));
verify(dao).getEventsWithIncomingChannel(CHANNEL_ID, maxAge);
@@ -76,7 +90,7 @@ class FeeServiceTest {
@Test
void getFeeReportForChannel_closed() {
when(channelService.isClosed(CHANNEL_ID)).thenReturn(true);
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL));
when(dao.getEventsWithOutgoingChannel(CHANNEL_ID, DEFAULT_MAX_DURATION))
.thenReturn(List.of(FORWARDING_EVENT));
assertThat(feeService.getFeeReportForChannel(CHANNEL_ID))
@@ -85,12 +99,32 @@ class FeeServiceTest {
@Test
void getFeeReportForChannel_closed_cached() {
when(channelService.isClosed(CHANNEL_ID)).thenReturn(true);
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL));
feeService.getFeeReportForChannel(CHANNEL_ID);
feeService.getFeeReportForChannel(CHANNEL_ID);
verify(dao, times(1)).getEventsWithIncomingChannel(CHANNEL_ID, DEFAULT_MAX_DURATION);
}
@Test
void getFeeReportForChannel_closed_with_max_age_just_after_channel_close_height() {
int blocks = BLOCK_HEIGHT - CLOSED_CHANNEL.getCloseHeight();
double daysWithTenMinutesPerBlock = blocks * 10.0 / 60 / 24;
Duration duration = Duration.ofDays((int) (daysWithTenMinutesPerBlock * 0.9));
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL));
assertThat(feeService.getFeeReportForChannel(CHANNEL_ID, duration)).isEqualTo(FeeReport.EMPTY);
verify(dao).getEventsWithIncomingChannel(any(), any());
}
@Test
void getFeeReportForChannel_closed_with_max_age_long_after_channel_close_height() {
int blocks = BLOCK_HEIGHT - CLOSED_CHANNEL.getCloseHeight();
double daysWithTenMinutesPerBlock = blocks * 10.0 / 60 / 24;
Duration duration = Duration.ofDays((int) (daysWithTenMinutesPerBlock * 0.49));
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL));
assertThat(feeService.getFeeReportForChannel(CHANNEL_ID, duration)).isEqualTo(FeeReport.EMPTY);
verify(dao, never()).getEventsWithIncomingChannel(any(), any());
}
@Test
void getFeeReportForChannel_no_forward() {
when(dao.getEventsWithOutgoingChannel(CHANNEL_ID, DEFAULT_MAX_DURATION)).thenReturn(List.of());

View File

@@ -3,7 +3,8 @@ package de.cotto.lndmanagej.forwardinghistory;
import de.cotto.lndmanagej.model.ChannelId;
import de.cotto.lndmanagej.model.ForwardingEvent;
import java.time.Period;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
@@ -12,7 +13,7 @@ public interface ForwardingEventsDao {
int getOffset();
List<ForwardingEvent> getEventsWithOutgoingChannel(ChannelId channelId, Period maxAge);
List<ForwardingEvent> getEventsWithOutgoingChannel(ChannelId channelId, Duration maxAge);
List<ForwardingEvent> getEventsWithIncomingChannel(ChannelId channelId, Period maxAge);
List<ForwardingEvent> getEventsWithIncomingChannel(ChannelId channelId, Duration maxAge);
}

View File

@@ -6,6 +6,7 @@ import de.cotto.lndmanagej.model.ForwardingEvent;
import org.springframework.stereotype.Component;
import javax.transaction.Transactional;
import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.time.temporal.ChronoUnit;
@@ -15,7 +16,6 @@ import java.util.List;
@Component
@Transactional
public class ForwardingEventsDaoImpl implements ForwardingEventsDao {
private static final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1_000;
private final ForwardingEventsRepository repository;
public ForwardingEventsDaoImpl(ForwardingEventsRepository repository) {
@@ -36,7 +36,7 @@ public class ForwardingEventsDaoImpl implements ForwardingEventsDao {
}
@Override
public List<ForwardingEvent> getEventsWithOutgoingChannel(ChannelId channelId, Period maxAge) {
public List<ForwardingEvent> getEventsWithOutgoingChannel(ChannelId channelId, Duration maxAge) {
return repository.findByChannelOutgoingAndTimestampGreaterThan(
channelId.getShortChannelId(),
getAfterEpochMilliSeconds(maxAge)
@@ -46,7 +46,7 @@ public class ForwardingEventsDaoImpl implements ForwardingEventsDao {
}
@Override
public List<ForwardingEvent> getEventsWithIncomingChannel(ChannelId channelId, Period maxAge) {
public List<ForwardingEvent> getEventsWithIncomingChannel(ChannelId channelId, Duration maxAge) {
return repository.findByChannelIncomingAndTimestampGreaterThan(
channelId.getShortChannelId(),
getAfterEpochMilliSeconds(maxAge)
@@ -55,7 +55,7 @@ public class ForwardingEventsDaoImpl implements ForwardingEventsDao {
.toList();
}
private long getAfterEpochMilliSeconds(Period maxAge) {
return Instant.now().toEpochMilli() - maxAge.get(ChronoUnit.DAYS) * MILLISECONDS_PER_DAY;
private long getAfterEpochMilliSeconds(Duration maxAge) {
return Instant.now().toEpochMilli() - maxAge.getSeconds() * 1_000;
}
}

View File

@@ -8,8 +8,8 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Duration;
import java.time.Instant;
import java.time.Period;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -29,7 +29,7 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ForwardingEventsDaoImplTest {
private static final Period MAX_AGE = Period.ofYears(Integer.MAX_VALUE);
private static final Duration MAX_AGE = Duration.ofDays(365 * 1_000);
@InjectMocks
private ForwardingEventsDaoImpl dao;
@@ -81,7 +81,7 @@ class ForwardingEventsDaoImplTest {
@Test
void getEventsWithOutgoingChannel_uses_max_age() {
Period maxAge = Period.ofDays(10);
Duration maxAge = Duration.ofDays(10);
long timestampAfter = Instant.now().minus(maxAge).getEpochSecond() * 1_000;
when(repository.findByChannelOutgoingAndTimestampGreaterThan(eq(CHANNEL_ID_2.getShortChannelId()), anyLong()))
.thenReturn(List.of(ForwardingEventJpaDto.createFromModel(FORWARDING_EVENT_2)));
@@ -111,7 +111,7 @@ class ForwardingEventsDaoImplTest {
@Test
void getEventsWithIncomingChannel_uses_max_age() {
Period maxAge = Period.ofDays(10);
Duration maxAge = Duration.ofDays(10);
long timestampAfter = Instant.now().minus(maxAge).getEpochSecond() * 1_000;
when(repository.findByChannelIncomingAndTimestampGreaterThan(eq(CHANNEL_ID_2.getShortChannelId()), anyLong()))
.thenReturn(List.of(ForwardingEventJpaDto.createFromModel(FORWARDING_EVENT)));

View File

@@ -76,7 +76,7 @@ class BreachForceClosedChannelTest {
@Test
void getCloseHeight() {
assertThat(FORCE_CLOSED_CHANNEL_BREACH.getCloseHeight()).isEqualTo(987_654);
assertThat(FORCE_CLOSED_CHANNEL_BREACH.getCloseHeight()).isEqualTo(600_000);
}
@Test

View File

@@ -102,7 +102,7 @@ class CoopClosedChannelTest {
@Test
void getCloseHeight() {
assertThat(CLOSED_CHANNEL.getCloseHeight()).isEqualTo(987_654);
assertThat(CLOSED_CHANNEL.getCloseHeight()).isEqualTo(600_000);
}
@Test

View File

@@ -89,7 +89,7 @@ class ForceClosedChannelTest {
@Test
void getCloseHeight() {
assertThat(FORCE_CLOSED_CHANNEL_REMOTE.getCloseHeight()).isEqualTo(987_654);
assertThat(FORCE_CLOSED_CHANNEL_REMOTE.getCloseHeight()).isEqualTo(600_000);
}
@Test

View File

@@ -8,7 +8,7 @@ import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY;
import static de.cotto.lndmanagej.model.PubkeyFixtures.PUBKEY_2;
public final class ClosedChannelFixtures {
public static final int CLOSE_HEIGHT = 987_654;
public static final int CLOSE_HEIGHT = 600_000;
private ClosedChannelFixtures() {
// do not instantiate

View File

@@ -109,7 +109,7 @@ class ChannelControllerIT {
.andExpect(jsonPath("$.totalSent", is("0")))
.andExpect(jsonPath("$.totalReceived", is("0")))
.andExpect(jsonPath("$.closeDetails.initiator", is("REMOTE")))
.andExpect(jsonPath("$.closeDetails.height", is(987_654)))
.andExpect(jsonPath("$.closeDetails.height", is(600_000)))
.andExpect(jsonPath("$.status.active", is(false)))
.andExpect(jsonPath("$.status.closed", is(true)))
.andExpect(jsonPath("$.status.openClosed", is("CLOSED")));
@@ -171,7 +171,7 @@ class ChannelControllerIT {
when(channelDetailsService.getDetails(CLOSED_CHANNEL)).thenReturn(CHANNEL_DETAILS_CLOSED);
mockMvc.perform(get(DETAILS_PREFIX))
.andExpect(jsonPath("$.closeDetails.initiator", is("REMOTE")))
.andExpect(jsonPath("$.closeDetails.height", is(987_654)))
.andExpect(jsonPath("$.closeDetails.height", is(600_000)))
.andExpect(jsonPath("$.status.openClosed", is("CLOSED")))
.andExpect(jsonPath("$.totalSent", is("0")))
.andExpect(jsonPath("$.totalReceived", is("0")))
@@ -224,7 +224,7 @@ class ChannelControllerIT {
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(CLOSED_CHANNEL));
mockMvc.perform(get(CHANNEL_PREFIX + "/close-details"))
.andExpect(jsonPath("$.initiator", is("REMOTE")))
.andExpect(jsonPath("$.height", is(987_654)))
.andExpect(jsonPath("$.height", is(600_000)))
.andExpect(jsonPath("$.force", is(false)))
.andExpect(jsonPath("$.breach", is(false)));
}
@@ -234,7 +234,7 @@ class ChannelControllerIT {
when(channelService.getClosedChannel(CHANNEL_ID)).thenReturn(Optional.of(FORCE_CLOSED_CHANNEL));
mockMvc.perform(get(CHANNEL_PREFIX + "/close-details"))
.andExpect(jsonPath("$.initiator", is("REMOTE")))
.andExpect(jsonPath("$.height", is(987_654)))
.andExpect(jsonPath("$.height", is(600_000)))
.andExpect(jsonPath("$.force", is(true)))
.andExpect(jsonPath("$.breach", is(false)));
}
@@ -245,7 +245,7 @@ class ChannelControllerIT {
mockMvc.perform(get(CHANNEL_PREFIX + "/close-details"))
.andExpect(jsonPath("$.breach", is(true)))
.andExpect(jsonPath("$.initiator", is("REMOTE")))
.andExpect(jsonPath("$.height", is(987_654)))
.andExpect(jsonPath("$.height", is(600_000)))
.andExpect(jsonPath("$.force", is(true)));
}

View File

@@ -14,7 +14,7 @@ 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 java.time.Period;
import java.time.Duration;
import java.util.List;
import java.util.Set;
@@ -149,7 +149,7 @@ class NodeControllerIT {
@Test
void getFeeReport_last_days() throws Exception {
when(feeService.getFeeReportForPeer(PUBKEY, Period.ofDays(123))).thenReturn(FEE_REPORT);
when(feeService.getFeeReportForPeer(PUBKEY, Duration.ofDays(123))).thenReturn(FEE_REPORT);
mockMvc.perform(get(NODE_PREFIX + "/fee-report/last-days/123"))
.andExpect(jsonPath("$.earned", is("1234")))
.andExpect(jsonPath("$.sourced", is("567")));

View File

@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Period;
import java.util.List;
import java.util.Set;
@@ -91,7 +92,7 @@ public class NodeController {
@Timed
@GetMapping("/fee-report/last-days/{lastDays}")
public FeeReportDto getFeeReport(@PathVariable Pubkey pubkey, @PathVariable int lastDays) {
return FeeReportDto.createFromModel(feeService.getFeeReportForPeer(pubkey, Period.ofDays(lastDays)));
return FeeReportDto.createFromModel(feeService.getFeeReportForPeer(pubkey, Duration.ofDays(lastDays)));
}
private List<ChannelId> toSortedList(Set<? extends Channel> channels) {

View File

@@ -17,6 +17,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Duration;
import java.time.Period;
import java.util.List;
import java.util.Set;
@@ -115,7 +116,7 @@ class NodeControllerTest {
@Test
void getFeeReportLastDays() {
when(feeService.getFeeReportForPeer(PUBKEY, Period.ofDays(123))).thenReturn(FEE_REPORT);
when(feeService.getFeeReportForPeer(PUBKEY, Duration.ofDays(123))).thenReturn(FEE_REPORT);
assertThat(nodeController.getFeeReport(PUBKEY, 123)).isEqualTo(FeeReportDto.createFromModel(FEE_REPORT));
}
}