Skip to content

Commit 283e4e4

Browse files
Extend the APIs for block requests [ECR-3255] (#953)
Add `ExonumClient#findNonEmptyBlocks` to find a certain number of the most recent non-empty blocks (from the last block and up to the genesis block). Also change `ExonumClient#getBlocks` and `ExonumClient#getLastBlocks` to return as many blocks as requested (regardless of the limitations of the underlying transport). Both now return blocks from a certain closed range, with empty blocks filtering no longer extending that range.
1 parent 183e42d commit 283e4e4

18 files changed

Lines changed: 1049 additions & 162 deletions

exonum-light-client/CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1515

1616
## Unreleased
1717

18+
### Added
19+
* `ExonumClient#findNonEmptyBlocks` to find a certain number of the most recent non-empty
20+
blocks (from the last block and up to the genesis block). (#953)
21+
22+
### Changed
23+
- `ExonumClient#getBlocks` accepts a closed range of block heights _[from; to]_
24+
and returns only the blocks which heights fall into that range. The size of
25+
the range _(to - from + 1)_ is no longer limited. If needed, the client
26+
performs multiple requests to the node. (#953)
27+
- `ExonumClient#getLastBlocks` returns only the blocks from the closed range
28+
_[max(0, blockchainHeight - count + 1), blockchainHeight]_. The size of
29+
the range _(min(count, blockchainHeight + 1))_ is no longer limited. (#953)
30+
1831
## 0.2.0 - 2019-05-27
1932

2033
Second release of Exonum Java Light Client which brings
21-
system API and blockhain explorer API endpoints support.
34+
system API and blockchain explorer API endpoints support.
2235

2336
### Versions Support
2437
- Exonum version, 0.11.0

exonum-light-client/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323
<suppressions>
2424
<!-- Allow `aBlock` name -->
2525
<suppress files="BlockTest\.java" checks="MethodName"/>
26+
<suppress files="Blocks\.java" checks="MethodName"/>
2627
</suppressions>

exonum-light-client/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@
229229
<argLine>
230230
${jacoco.args}
231231
</argLine>
232+
<!-- Set up JUnit 5 configuration parameters.
233+
See also: https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven-config-params -->
234+
<properties>
235+
<configurationParameters>
236+
# Enable parallel execution, with concurrent mode by default
237+
junit.jupiter.execution.parallel.enabled = true
238+
junit.jupiter.execution.parallel.mode.default = concurrent
239+
240+
# Use 1.5 times the number of CPUs
241+
junit.jupiter.execution.parallel.config.dynamic.factor = 1.5
242+
</configurationParameters>
243+
</properties>
232244
</configuration>
233245
</plugin>
234246

@@ -284,6 +296,19 @@
284296
</execution>
285297
</executions>
286298
</plugin>
299+
300+
<plugin>
301+
<groupId>org.pitest</groupId>
302+
<artifactId>pitest-maven</artifactId>
303+
<version>1.4.8</version>
304+
<dependencies>
305+
<dependency>
306+
<groupId>org.pitest</groupId>
307+
<artifactId>pitest-junit5-plugin</artifactId>
308+
<version>0.8</version>
309+
</dependency>
310+
</dependencies>
311+
</plugin>
287312
</plugins>
288313
</build>
289314

@@ -314,6 +339,7 @@
314339
<exclude>com/exonum/client/response/Block.class</exclude>
315340
<exclude>com/exonum/client/response/Block*BlockBuilder.class</exclude>
316341
<exclude>com/exonum/client/response/BlockResponse.class</exclude>
342+
<exclude>com/exonum/client/response/BlocksRange.class</exclude>
317343
<exclude>com/exonum/client/response/BlocksResponse.class</exclude>
318344
<exclude>com/exonum/client/response/HealthCheckInfo.class</exclude>
319345
<exclude>com/exonum/client/response/TransactionResponse.class</exclude>

exonum-light-client/src/main/java/com/exonum/client/ExonumApi.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@
1616

1717
package com.exonum.client;
1818

19-
public final class ExonumApi {
19+
import com.exonum.binding.common.serialization.json.JsonSerializer;
20+
import com.google.gson.FieldNamingPolicy;
21+
import com.google.gson.Gson;
22+
23+
final class ExonumApi {
24+
/**
25+
* The Gson instance configured to (de)serialize Exonum responses.
26+
*/
27+
static final Gson JSON = JsonSerializer.builder()
28+
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
29+
.create();
30+
2031
/**
2132
* The maximum allowed blocks count per the request.
2233
*/
23-
public static final int MAX_BLOCKS_PER_REQUEST = 1000;
34+
static final int MAX_BLOCKS_PER_REQUEST = 1000;
35+
2436

2537
private ExonumApi() {
2638
}

exonum-light-client/src/main/java/com/exonum/client/ExonumClient.java

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
import com.exonum.client.request.BlockTimeOption;
2626
import com.exonum.client.response.Block;
2727
import com.exonum.client.response.BlockResponse;
28-
import com.exonum.client.response.BlocksResponse;
28+
import com.exonum.client.response.BlocksRange;
2929
import com.exonum.client.response.HealthCheckInfo;
3030
import com.exonum.client.response.TransactionResponse;
3131
import java.net.MalformedURLException;
3232
import java.net.URL;
33+
import java.util.List;
3334
import java.util.Optional;
3435
import okhttp3.OkHttpClient;
3536

@@ -38,9 +39,10 @@
3839
* Provides a convenient way for interaction with Exonum framework APIs.
3940
* All the methods of the interface work in a blocking way
4041
* i.e. invoke underlying request immediately, and block until the response can be processed
41-
* or an error occurs.
42+
* or an error occurs. In case the thread is interrupted, the blocked methods will complete
43+
* exceptionally.
4244
*
43-
* <p><i>Implementations of that interface are required to be thread-safe</i>.
45+
* <p><em>Implementations of this interface are required to be thread-safe</em>.
4446
**/
4547
public interface ExonumClient {
4648

@@ -107,45 +109,68 @@ public interface ExonumClient {
107109
BlockResponse getBlockByHeight(long height);
108110

109111
/**
110-
* Returns blockchain blocks information for the requested range. The blocks are returned
111-
* in reverse order, starting from the {@code heightMax}.
112-
* @param count Number of blocks to return.
113-
* It should be in range [1, {@linkplain ExonumApi#MAX_BLOCKS_PER_REQUEST}]
112+
* Returns blockchain blocks in the requested <em>closed</em> range. The blocks are returned
113+
* in ascending order by their height.
114+
*
115+
* @param fromHeight the height of the first block to include. Must be non-negative
116+
* @param toHeight the height of the last block to include. Must be greater than
117+
* or equal to {@code fromHeight} and less than or equal to the blockchain height.
118+
* If the {@code toHeight} is greater than actual blockchain height then
119+
* the actual height will be used (such error-prone behaviour will be fixed
120+
* in Exonum 0.12)
114121
* @param blockFilter controls whether to skip blocks with no transactions
115-
* @param heightMax maximum height of the returned blocks.
116-
* If the {@code heightMax} is greater than actual blockchain height then
117-
* the actual height will be used
118-
* @param timeOption controls whether to include the block commit time.
119-
* See {@linkplain Block#getCommitTime()}.
120-
* The time value corresponds to the average time of submission of precommits by the
121-
* validators for every returned block
122-
* @return blocks information response
122+
* @param timeOption controls whether to include
123+
* the {@linkplain Block#getCommitTime() block commit time}
124+
* @return blocks in the requested range
123125
* @throws RuntimeException if the client is unable to complete a request
124126
* (e.g., in case of connectivity problems)
125-
* @throws IllegalArgumentException if count is out of range
126-
* [1, {@linkplain ExonumApi#MAX_BLOCKS_PER_REQUEST}]
127+
* @throws IllegalArgumentException if {@code fromHeight} or {@code toHeight} are not valid
127128
*/
128-
BlocksResponse getBlocks(int count, BlockFilteringOption blockFilter, long heightMax,
129+
List<Block> getBlocks(long fromHeight, long toHeight, BlockFilteringOption blockFilter,
129130
BlockTimeOption timeOption);
130131

131132
/**
132-
* Returns blockchain blocks information starting from the last block in the blockchain.
133-
* @param count Number of blocks to return.
134-
* It should be in range [1, {@linkplain ExonumApi#MAX_BLOCKS_PER_REQUEST}]
135-
* @param blockFilter controls whether to skip blocks with no transactions
136-
* @param timeOption controls whether to include the block commit time.
137-
* See {@linkplain Block#getCommitTime()}.
138-
* The time value corresponds to the average time of submission of precommits by the
139-
* validators for every returned block
133+
* Returns the range of the most recent blockchain blocks in ascending order by their height.
134+
* More precisely, returns the blocks in the closed range
135+
* {@code [max(0, blockchainHeight - size + 1), blockchainHeight]} of size
136+
* {@code max(blockchainHeight + 1, size)}.
137+
*
138+
* @param size the size of the range. If it exceeds the number of blocks in the blockchain,
139+
* this method will return all blocks ({@code blockchainHeight + 1})
140+
* @param blockFilter controls whether to skip blocks with no transactions. If filtering
141+
* is applied, the actual number of blocks may be smaller than {@code size};
142+
* but the range of blocks will not be extended beyond {@code blockchainHeight - size + 1}.
143+
* If a certain <em>number</em> of non-empty blocks is needed (not a certain
144+
* <em>range</em>), use {@link #findNonEmptyBlocks(int, BlockTimeOption)}
145+
* @param timeOption controls whether to include
146+
* the {@linkplain Block#getCommitTime() block commit time}
140147
* @return blocks information response
141148
* @throws RuntimeException if the client is unable to complete a request
142149
* (e.g., in case of connectivity problems)
143-
* @throws IllegalArgumentException if count is out of range
144-
* [1, {@linkplain ExonumApi#MAX_BLOCKS_PER_REQUEST}]
150+
* @throws IllegalArgumentException if size is non-positive
151+
* @see #findNonEmptyBlocks(int, BlockTimeOption)
145152
*/
146-
BlocksResponse getLastBlocks(int count, BlockFilteringOption blockFilter,
153+
BlocksRange getLastBlocks(int size, BlockFilteringOption blockFilter,
147154
BlockTimeOption timeOption);
148155

156+
/**
157+
* Returns up to the given number of the most recent non-empty blocks in ascending order
158+
* by their height. The search range is not limited, i.e., spans the whole blockchain.
159+
*
160+
* @param numBlocks the maximum number of blocks to return. Must be positive. If the number
161+
* of non-empty blocks in the blockchain is less than {@code numBlocks}, all such blocks
162+
* will be returned
163+
* @param timeOption controls whether to include
164+
* the {@linkplain Block#getCommitTime() block commit time}
165+
* @return a list of the most recent non-empty blocks
166+
* @throws RuntimeException if the client is unable to complete a request
167+
* (e.g., in case of connectivity problems)
168+
* @throws IllegalArgumentException if numBlocks is non-positive
169+
* @see #getLastBlocks(int, BlockFilteringOption, BlockTimeOption)
170+
* @see BlockFilteringOption#SKIP_EMPTY
171+
*/
172+
List<Block> findNonEmptyBlocks(int numBlocks, BlockTimeOption timeOption);
173+
149174
/**
150175
* Returns the last block in the blockchain.
151176
* @throws RuntimeException if the client is unable to complete a request

exonum-light-client/src/main/java/com/exonum/client/ExonumHttpClient.java

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package com.exonum.client;
1919

2020
import static com.exonum.client.ExonumApi.MAX_BLOCKS_PER_REQUEST;
21+
import static com.exonum.client.ExonumIterables.indexOf;
2122
import static com.exonum.client.ExonumUrls.BLOCK;
2223
import static com.exonum.client.ExonumUrls.BLOCKS;
2324
import static com.exonum.client.ExonumUrls.HEALTH_CHECK;
@@ -30,6 +31,8 @@
3031
import static com.exonum.client.request.BlockTimeOption.NO_COMMIT_TIME;
3132
import static com.google.common.base.Preconditions.checkArgument;
3233
import static com.google.common.base.Preconditions.checkNotNull;
34+
import static java.lang.Math.max;
35+
import static java.lang.Math.min;
3336
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
3437

3538
import com.exonum.binding.common.hash.HashCode;
@@ -38,12 +41,17 @@
3841
import com.exonum.client.request.BlockTimeOption;
3942
import com.exonum.client.response.Block;
4043
import com.exonum.client.response.BlockResponse;
44+
import com.exonum.client.response.BlocksRange;
4145
import com.exonum.client.response.BlocksResponse;
4246
import com.exonum.client.response.HealthCheckInfo;
4347
import com.exonum.client.response.TransactionResponse;
48+
import com.google.common.collect.ImmutableList;
49+
import com.google.common.collect.Lists;
4450
import java.io.IOException;
4551
import java.net.URL;
52+
import java.util.ArrayList;
4653
import java.util.HashMap;
54+
import java.util.List;
4755
import java.util.Map;
4856
import java.util.Optional;
4957
import java.util.function.Function;
@@ -60,6 +68,7 @@
6068
*/
6169
class ExonumHttpClient implements ExonumClient {
6270
private static final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json; charset=utf-8");
71+
private static final int GENESIS_BLOCK_HEIGHT = 0;
6372

6473
private final OkHttpClient httpClient;
6574
private final URL exonumHost;
@@ -141,19 +150,100 @@ public BlockResponse getBlockByHeight(long height) {
141150
}
142151

143152
@Override
144-
public BlocksResponse getBlocks(int count, BlockFilteringOption blockFilter, long heightMax,
153+
public List<Block> getBlocks(long fromHeight, long toHeight, BlockFilteringOption blockFilter,
145154
BlockTimeOption timeOption) {
146-
checkArgument(0 < count,
147-
"Requested number of blocks should be positive number but was %s", count);
148-
return doGetBlocks(count, blockFilter, heightMax, timeOption);
155+
checkArgument(0 <= fromHeight, "First block height (%s) must be non-negative", fromHeight);
156+
checkArgument(fromHeight <= toHeight,
157+
"First block height (%s) should be less than or equal to the last block height (%s)",
158+
fromHeight, toHeight);
159+
160+
// 'maximum' as when skipping empty the actual might be way smaller
161+
int maxSize = Math.toIntExact(toHeight - fromHeight + 1);
162+
List<Block> blocks = new ArrayList<>(maxSize);
163+
for (long rangeLast = toHeight; rangeLast >= fromHeight; ) {
164+
int remainingBlocks = Math.toIntExact(rangeLast - fromHeight + 1);
165+
int numBlocks = min(remainingBlocks, MAX_BLOCKS_PER_REQUEST);
166+
BlocksResponse blocksResponse = doGetBlocks(numBlocks, blockFilter, rangeLast, timeOption);
167+
168+
blocks.addAll(blocksResponse.getBlocks());
169+
170+
rangeLast = blocksResponse.getBlocksRangeStart() - 1;
171+
}
172+
173+
return postProcessResponseBlocks(fromHeight, toHeight, blocks)
174+
.getBlocks();
149175
}
150176

151177
@Override
152-
public BlocksResponse getLastBlocks(int count, BlockFilteringOption blockFilter,
178+
public BlocksRange getLastBlocks(int size, BlockFilteringOption blockFilter,
153179
BlockTimeOption timeOption) {
154-
checkArgument(0 < count,
155-
"Requested number of blocks should be positive number but was %s", count);
156-
return doGetBlocks(count, blockFilter, null, timeOption);
180+
checkArgument(0 < size,
181+
"Requested blocks range size should be positive but was %s", size);
182+
183+
List<Block> blocks = new ArrayList<>(size);
184+
// The first request does not specify the maximum height to get the top blocks
185+
long blockchainHeight = Long.MIN_VALUE;
186+
Long nextHeight = null;
187+
int remainingBlocks = size;
188+
while (remainingBlocks > 0
189+
&& (nextHeight == null || nextHeight >= GENESIS_BLOCK_HEIGHT)) {
190+
int numBlocks = min(remainingBlocks, MAX_BLOCKS_PER_REQUEST);
191+
BlocksResponse blocksResponse = doGetBlocks(numBlocks, blockFilter, nextHeight, timeOption);
192+
193+
blocks.addAll(blocksResponse.getBlocks());
194+
195+
nextHeight = blocksResponse.getBlocksRangeStart() - 1;
196+
blockchainHeight = max(blockchainHeight, blocksResponse.getBlocksRangeEnd() - 1);
197+
remainingBlocks = Math.toIntExact(size - (blockchainHeight - nextHeight));
198+
}
199+
200+
long fromHeight = max(blockchainHeight - size + 1, GENESIS_BLOCK_HEIGHT);
201+
long toHeight = blockchainHeight;
202+
return postProcessResponseBlocks(fromHeight, toHeight, blocks);
203+
}
204+
205+
/**
206+
* Post-processes the blocks, coming from
207+
* {@link #doGetBlocks(int, BlockFilteringOption, Long, BlockTimeOption)}:
208+
* 1. Turns them in ascending order by height.
209+
* 2. Keeps only blocks that fall in range [fromHeight; toHeight].
210+
*/
211+
private static BlocksRange postProcessResponseBlocks(long fromHeight, long toHeight,
212+
List<Block> blocks) {
213+
// Turn the blocks in ascending order
214+
blocks = Lists.reverse(blocks);
215+
216+
// Filter the possible blocks that are out of the requested range
217+
// No Stream#dropWhile in Java 8 :(
218+
int firstInRange = indexOf(blocks, b -> b.getHeight() >= fromHeight)
219+
.orElse(blocks.size());
220+
blocks = blocks.subList(firstInRange, blocks.size());
221+
222+
// Do not bother trimming — BlocksRange copies the list
223+
return new BlocksRange(fromHeight, toHeight, blocks);
224+
}
225+
226+
@Override
227+
public List<Block> findNonEmptyBlocks(int numBlocks, BlockTimeOption timeOption) {
228+
checkArgument(0 < numBlocks,
229+
"Requested number of blocks should be positive but was %s", numBlocks);
230+
231+
List<Block> blocks = new ArrayList<>(numBlocks);
232+
Long nextHeight = null;
233+
int remainingBlocks = numBlocks;
234+
while (remainingBlocks > 0
235+
&& (nextHeight == null || nextHeight >= GENESIS_BLOCK_HEIGHT)) {
236+
int numRequested = min(remainingBlocks, MAX_BLOCKS_PER_REQUEST);
237+
BlocksResponse blocksResponse = doGetBlocks(numRequested, SKIP_EMPTY, nextHeight, timeOption);
238+
239+
blocks.addAll(blocksResponse.getBlocks());
240+
241+
nextHeight = blocksResponse.getBlocksRangeStart() - 1;
242+
remainingBlocks -= blocksResponse.getBlocks().size();
243+
}
244+
245+
List<Block> ascBlocks = Lists.reverse(blocks);
246+
return ImmutableList.copyOf(ascBlocks);
157247
}
158248

159249
@Override

0 commit comments

Comments
 (0)