mirror of https://github.com/hyperledger/besu
NC-1721: Filter timeout if not queried for 10 minutes (#66)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
3b5fcfb40a
commit
b4330969c1
@ -0,0 +1,28 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** Tracks new blocks being added to the blockchain. */ |
||||
class BlockFilter extends Filter { |
||||
|
||||
private final List<Hash> blockHashes = new ArrayList<>(); |
||||
|
||||
BlockFilter(final String id) { |
||||
super(id); |
||||
} |
||||
|
||||
void addBlockHash(final Hash hash) { |
||||
blockHashes.add(hash); |
||||
} |
||||
|
||||
List<Hash> blockHashes() { |
||||
return blockHashes; |
||||
} |
||||
|
||||
void clearBlockHashes() { |
||||
blockHashes.clear(); |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import java.time.Duration; |
||||
import java.time.Instant; |
||||
|
||||
import com.google.common.annotations.VisibleForTesting; |
||||
|
||||
abstract class Filter { |
||||
|
||||
private static final Duration DEFAULT_EXPIRE_DURATION = Duration.ofMinutes(10); |
||||
|
||||
private final String id; |
||||
private Instant expireTime; |
||||
|
||||
Filter(final String id) { |
||||
this.id = id; |
||||
resetExpireTime(); |
||||
} |
||||
|
||||
String getId() { |
||||
return id; |
||||
} |
||||
|
||||
void resetExpireTime() { |
||||
this.expireTime = Instant.now().plus(DEFAULT_EXPIRE_DURATION); |
||||
} |
||||
|
||||
boolean isExpired() { |
||||
return Instant.now().isAfter(expireTime); |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
void setExpireTime(final Instant expireTime) { |
||||
this.expireTime = expireTime; |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
Instant getExpireTime() { |
||||
return expireTime; |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
public class FilterRepository { |
||||
|
||||
private final Map<String, Filter> filters = new ConcurrentHashMap<>(); |
||||
|
||||
public FilterRepository() {} |
||||
|
||||
Collection<Filter> getFilters() { |
||||
return new ArrayList<>(filters.values()); |
||||
} |
||||
|
||||
<T extends Filter> Collection<T> getFiltersOfType(final Class<T> filterClass) { |
||||
return filters |
||||
.values() |
||||
.stream() |
||||
.flatMap(f -> getIfTypeMatches(f, filterClass).map(Stream::of).orElseGet(Stream::empty)) |
||||
.collect(Collectors.toList()); |
||||
} |
||||
|
||||
<T extends Filter> Optional<T> getFilter(final String filterId, final Class<T> filterClass) { |
||||
final Filter filter = filters.get(filterId); |
||||
return getIfTypeMatches(filter, filterClass); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T extends Filter> Optional<T> getIfTypeMatches( |
||||
final Filter filter, final Class<T> filterClass) { |
||||
if (filter == null) { |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
if (!filterClass.isAssignableFrom(filter.getClass())) { |
||||
return Optional.empty(); |
||||
} |
||||
|
||||
return Optional.of((T) filter); |
||||
} |
||||
|
||||
boolean exists(final String id) { |
||||
return filters.containsKey(id); |
||||
} |
||||
|
||||
void save(final Filter filter) { |
||||
if (filter == null) { |
||||
throw new IllegalArgumentException("Can't save null filter"); |
||||
} |
||||
|
||||
if (exists(filter.getId())) { |
||||
throw new IllegalArgumentException( |
||||
String.format("Filter with id %s already exists", filter.getId())); |
||||
} |
||||
|
||||
filters.put(filter.getId(), filter); |
||||
} |
||||
|
||||
void delete(final String id) { |
||||
filters.remove(id); |
||||
} |
||||
|
||||
void deleteAll() { |
||||
filters.clear(); |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
class FilterTimeoutMonitor { |
||||
|
||||
private final FilterRepository filterRepository; |
||||
|
||||
FilterTimeoutMonitor(final FilterRepository filterRepository) { |
||||
this.filterRepository = filterRepository; |
||||
} |
||||
|
||||
void checkFilters() { |
||||
filterRepository |
||||
.getFilters() |
||||
.forEach( |
||||
filter -> { |
||||
if (filter.isExpired()) { |
||||
filterRepository.delete(filter.getId()); |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; |
||||
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
class LogFilter extends Filter { |
||||
|
||||
private final BlockParameter fromBlock; |
||||
private final BlockParameter toBlock; |
||||
private final LogsQuery logsQuery; |
||||
|
||||
private final List<LogWithMetadata> logs = new ArrayList<>(); |
||||
|
||||
LogFilter( |
||||
final String id, |
||||
final BlockParameter fromBlock, |
||||
final BlockParameter toBlock, |
||||
final LogsQuery logsQuery) { |
||||
super(id); |
||||
this.fromBlock = fromBlock; |
||||
this.toBlock = toBlock; |
||||
this.logsQuery = logsQuery; |
||||
} |
||||
|
||||
public BlockParameter getFromBlock() { |
||||
return fromBlock; |
||||
} |
||||
|
||||
public BlockParameter getToBlock() { |
||||
return toBlock; |
||||
} |
||||
|
||||
public LogsQuery getLogsQuery() { |
||||
return logsQuery; |
||||
} |
||||
|
||||
void addLog(final List<LogWithMetadata> logs) { |
||||
this.logs.addAll(logs); |
||||
} |
||||
|
||||
List<LogWithMetadata> logs() { |
||||
return logs; |
||||
} |
||||
|
||||
void clearLogs() { |
||||
logs.clear(); |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** Tracks new pending transactions that have arrived in the transaction pool */ |
||||
class PendingTransactionFilter extends Filter { |
||||
|
||||
private final List<Hash> transactionHashes = new ArrayList<>(); |
||||
|
||||
PendingTransactionFilter(final String id) { |
||||
super(id); |
||||
} |
||||
|
||||
void addTransactionHash(final Hash hash) { |
||||
transactionHashes.add(hash); |
||||
} |
||||
|
||||
List<Hash> transactionHashes() { |
||||
return transactionHashes; |
||||
} |
||||
|
||||
void clearTransactionHashes() { |
||||
transactionHashes.clear(); |
||||
} |
||||
} |
@ -0,0 +1,191 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.catchThrowable; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Optional; |
||||
|
||||
import org.assertj.core.util.Lists; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class FilterRepositoryTest { |
||||
|
||||
private FilterRepository repository; |
||||
|
||||
@Before |
||||
public void before() { |
||||
repository = new FilterRepository(); |
||||
} |
||||
|
||||
@Test |
||||
public void getFiltersShouldReturnAllFilters() { |
||||
BlockFilter filter1 = new BlockFilter("foo"); |
||||
BlockFilter filter2 = new BlockFilter("bar"); |
||||
repository.save(filter1); |
||||
repository.save(filter2); |
||||
|
||||
Collection<Filter> filters = repository.getFilters(); |
||||
|
||||
assertThat(filters).containsExactlyInAnyOrderElementsOf(Lists.newArrayList(filter1, filter2)); |
||||
} |
||||
|
||||
@Test |
||||
public void getFiltersShouldReturnEmptyListWhenRepositoryIsEmpty() { |
||||
assertThat(repository.getFilters()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void saveShouldAddFilterToRepository() { |
||||
BlockFilter filter = new BlockFilter("id"); |
||||
repository.save(filter); |
||||
|
||||
BlockFilter retrievedFilter = repository.getFilter("id", BlockFilter.class).get(); |
||||
|
||||
assertThat(retrievedFilter).isEqualToComparingFieldByField(filter); |
||||
} |
||||
|
||||
@Test |
||||
public void saveNullFilterShouldFail() { |
||||
Throwable throwable = catchThrowable(() -> repository.save(null)); |
||||
|
||||
assertThat(throwable) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("Can't save null filter"); |
||||
} |
||||
|
||||
@Test |
||||
public void saveFilterWithSameIdShouldFail() { |
||||
BlockFilter filter = new BlockFilter("x"); |
||||
repository.save(filter); |
||||
|
||||
Throwable throwable = catchThrowable(() -> repository.save(filter)); |
||||
|
||||
assertThat(throwable) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("Filter with id x already exists"); |
||||
} |
||||
|
||||
@Test |
||||
public void getSingleFilterShouldReturnExistingFilterOfCorrectType() { |
||||
BlockFilter filter = new BlockFilter("id"); |
||||
repository.save(filter); |
||||
|
||||
Optional<BlockFilter> optional = repository.getFilter(filter.getId(), BlockFilter.class); |
||||
|
||||
assertThat(optional.isPresent()).isTrue(); |
||||
assertThat(optional.get()).isEqualToComparingFieldByField(filter); |
||||
} |
||||
|
||||
@Test |
||||
public void getSingleFilterShouldReturnEmptyForFilterOfIncorrectType() { |
||||
BlockFilter filter = new BlockFilter("id"); |
||||
repository.save(filter); |
||||
|
||||
Optional<PendingTransactionFilter> optional = |
||||
repository.getFilter(filter.getId(), PendingTransactionFilter.class); |
||||
|
||||
assertThat(optional.isPresent()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void getSingleFilterShouldReturnEmptyForAbsentId() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
repository.save(filter); |
||||
|
||||
Optional<BlockFilter> optional = repository.getFilter("bar", BlockFilter.class); |
||||
|
||||
assertThat(optional.isPresent()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void getSingleFilterShouldReturnEmptyForEmptyRepository() { |
||||
Optional<BlockFilter> optional = repository.getFilter("id", BlockFilter.class); |
||||
|
||||
assertThat(optional.isPresent()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void getFilterCollectionShouldReturnAllFiltersOfSpecificType() { |
||||
BlockFilter blockFilter1 = new BlockFilter("foo"); |
||||
BlockFilter blockFilter2 = new BlockFilter("biz"); |
||||
PendingTransactionFilter pendingTxFilter1 = new PendingTransactionFilter("bar"); |
||||
|
||||
Collection<BlockFilter> expectedFilters = Lists.newArrayList(blockFilter1, blockFilter2); |
||||
|
||||
repository.save(blockFilter1); |
||||
repository.save(blockFilter2); |
||||
repository.save(pendingTxFilter1); |
||||
|
||||
Collection<BlockFilter> blockFilters = repository.getFiltersOfType(BlockFilter.class); |
||||
|
||||
assertThat(blockFilters).containsExactlyInAnyOrderElementsOf(expectedFilters); |
||||
} |
||||
|
||||
@Test |
||||
public void getFilterCollectionShouldReturnEmptyForNoneMatchingTypes() { |
||||
PendingTransactionFilter filter = new PendingTransactionFilter("foo"); |
||||
repository.save(filter); |
||||
|
||||
Collection<BlockFilter> filters = repository.getFiltersOfType(BlockFilter.class); |
||||
|
||||
assertThat(filters).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void getFilterCollectionShouldReturnEmptyListForEmptyRepository() { |
||||
Collection<BlockFilter> filters = repository.getFiltersOfType(BlockFilter.class); |
||||
|
||||
assertThat(filters).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void existsShouldReturnTrueForExistingId() { |
||||
BlockFilter filter = new BlockFilter("id"); |
||||
repository.save(filter); |
||||
|
||||
assertThat(repository.exists("id")).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void existsShouldReturnFalseForAbsentId() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
repository.save(filter); |
||||
|
||||
assertThat(repository.exists("bar")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void existsShouldReturnFalseForEmptyRepository() { |
||||
assertThat(repository.exists("id")).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void deleteExistingFilterShouldDeleteSuccessfully() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
repository.save(filter); |
||||
repository.delete(filter.getId()); |
||||
|
||||
assertThat(repository.exists(filter.getId())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void deleteAbsentFilterDoesNothing() { |
||||
assertThat(repository.exists("foo")).isFalse(); |
||||
repository.delete("foo"); |
||||
} |
||||
|
||||
@Test |
||||
public void deleteAllShouldClearFilters() { |
||||
BlockFilter filter1 = new BlockFilter("foo"); |
||||
BlockFilter filter2 = new BlockFilter("biz"); |
||||
repository.save(filter1); |
||||
repository.save(filter2); |
||||
|
||||
repository.deleteAll(); |
||||
|
||||
assertThat(repository.exists(filter1.getId())).isFalse(); |
||||
assertThat(repository.exists(filter2.getId())).isFalse(); |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import java.time.Duration; |
||||
import java.time.Instant; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
public class FilterTest { |
||||
|
||||
@Test |
||||
public void filterJustCreatedShouldNotBeExpired() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
|
||||
assertThat(filter.isExpired()).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void isExpiredShouldReturnTrueForExpiredFilter() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
filter.setExpireTime(Instant.now().minusSeconds(1)); |
||||
|
||||
assertThat(filter.isExpired()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void resetExpireDateShouldIncrementExpireDate() { |
||||
BlockFilter filter = new BlockFilter("foo"); |
||||
filter.setExpireTime(Instant.now().minus(Duration.ofDays(1))); |
||||
filter.resetExpireTime(); |
||||
|
||||
assertThat(filter.getExpireTime()) |
||||
.isBeforeOrEqualTo(Instant.now().plus(Duration.ofMinutes(10))); |
||||
} |
||||
} |
@ -0,0 +1,64 @@ |
||||
package tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter; |
||||
|
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.spy; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import java.util.Collections; |
||||
|
||||
import com.google.common.collect.Lists; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class FilterTimeoutMonitorTest { |
||||
|
||||
@Mock private FilterRepository filterRepository; |
||||
|
||||
private FilterTimeoutMonitor timeoutMonitor; |
||||
|
||||
@Before |
||||
public void before() { |
||||
timeoutMonitor = new FilterTimeoutMonitor(filterRepository); |
||||
} |
||||
|
||||
@Test |
||||
public void expiredFilterShouldBeDeleted() { |
||||
Filter filter = spy(new BlockFilter("foo")); |
||||
when(filter.isExpired()).thenReturn(true); |
||||
when(filterRepository.getFilters()).thenReturn(Lists.newArrayList(filter)); |
||||
|
||||
timeoutMonitor.checkFilters(); |
||||
|
||||
verify(filterRepository).getFilters(); |
||||
verify(filterRepository).delete("foo"); |
||||
verifyNoMoreInteractions(filterRepository); |
||||
} |
||||
|
||||
@Test |
||||
public void nonExpiredFilterShouldNotBeDeleted() { |
||||
Filter filter = mock(Filter.class); |
||||
when(filter.isExpired()).thenReturn(false); |
||||
when(filterRepository.getFilters()).thenReturn(Lists.newArrayList(filter)); |
||||
|
||||
timeoutMonitor.checkFilters(); |
||||
|
||||
verify(filter).isExpired(); |
||||
verifyNoMoreInteractions(filter); |
||||
} |
||||
|
||||
@Test |
||||
public void checkEmptyFilterRepositoryDoesNothing() { |
||||
when(filterRepository.getFilters()).thenReturn(Collections.emptyList()); |
||||
|
||||
timeoutMonitor.checkFilters(); |
||||
|
||||
verify(filterRepository).getFilters(); |
||||
verifyNoMoreInteractions(filterRepository); |
||||
} |
||||
} |
Loading…
Reference in new issue