Skip to content

Commit e4d36b0

Browse files
authored
Reload improvements (#1135)
Various Reload endpoint enhancements. /reload now serves GET requests to provide status of current and last reload operation. The POST request only service one reload operation at a time. Fixed potential memory leaks by correctly utilizing ExecutorService. Moved SignerLoader processing of "config files to keystore files to ArtifactSigners" to virtual threads with batch handling that ease up CPU and memory requirements. Introduced volatile immutable maps with write-on-copy semantics for read-heavy ArtifactSigners in DefaultArtifactSigner and SignerLoader.
1 parent 38ecba6 commit e4d36b0

File tree

40 files changed

+2947
-428
lines changed

40 files changed

+2947
-428
lines changed

CHANGELOG.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,39 @@
22

33
## Upcoming Releases
44
### Breaking Changes
5-
- `--swagger-ui-enabled` cli option has been removed. The Specs UI can be accessed at https://consensys.github.io/web3signer/.
5+
#### `/reload` Endpoint Response Format Changed
6+
- Now returns HTTP `202 Accepted` (previously `200 OK`) with JSON response body
7+
- Returns `409 Conflict` with error message if reload already in progress
8+
- **Migration:** Update automation to expect `202` status code instead of `200`
9+
10+
#### Removed Swagger UI CLI Option
11+
- `--swagger-ui-enabled` option removed
12+
- Access OpenAPI specs at https://consensys.github.io/web3signer/
13+
14+
### Features Added
15+
- **Enhanced `/reload` endpoint with status monitoring:**
16+
- New `GET /reload` endpoint to check reload operation status
17+
- Reports detailed status: `idle`, `running`, `completed`, `completed_with_errors`, `failed`
18+
- Exposes error counts when individual signer configurations fail to load
19+
- Distinguishes between complete success and partial success (some signers failed)
20+
- Provides timestamps and error messages for last reload operation
21+
- Virtual thread-based signer loading for improved performance
22+
- New file system signer loading configuration:
23+
- `--signer-load-timeout` (default: 60 sec) - Timeout per file during parallel processing
24+
- `--signer-load-batch-size` (default: 500) - Files processed per batch in parallel mode
25+
- `--signer-load-sequential-threshold` (default: 100) - Minimum files to trigger parallel processing
26+
- `--signer-load-parallel` (default: true) - Enable/disable parallel processing
27+
- New `/reload` endpoint configuration:
28+
- `--reload-timeout` (default: 30 min) - Maximum time for entire reload operation
29+
- Improved reload concurrency control prevents multiple simultaneous reloads
630

7-
### Features Added
31+
### Bugs Fixed
32+
- Fix memory leak during reload API endpoint. Issue [#1073][issue_1073] via PR [#1135][PR_1135].
33+
- Fix race condition in reload flag management when executor initialization fails synchronously
834

35+
[issue_1073]: https://github.com/Consensys/web3signer/issues/1073
36+
[PR_1135]: https://github.com/Consensys/web3signer/pull/1135
937

10-
### Bugs Fixed
1138

1239
---
1340
## 25.11.0

acceptance-tests/src/test/java/tech/pegasys/web3signer/dsl/signer/Signer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ public Response callReload() {
264264
return given().baseUri(getUrl()).post(RELOAD_ENDPOINT);
265265
}
266266

267+
public Response getReloadStatus() {
268+
return given().baseUri(getUrl()).get(RELOAD_ENDPOINT);
269+
}
270+
267271
public Response healthcheck() {
268272
return given().baseUri(getUrl()).get(HEALTHCHECK_ENDPOINT);
269273
}

acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/commitboost/CommitBoostGetPubKeysAcceptanceTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
package tech.pegasys.web3signer.tests.commitboost;
1414

1515
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.hamcrest.Matchers.empty;
17+
import static org.hamcrest.Matchers.everyItem;
1618
import static org.hamcrest.Matchers.hasSize;
19+
import static org.hamcrest.Matchers.not;
1720

1821
import tech.pegasys.teku.bls.BLSKeyPair;
1922
import tech.pegasys.web3signer.KeystoreUtil;
@@ -27,6 +30,8 @@
2730
import java.io.UncheckedIOException;
2831
import java.nio.file.Files;
2932
import java.nio.file.Path;
33+
import java.time.Duration;
34+
import java.util.Comparator;
3035
import java.util.HashMap;
3136
import java.util.List;
3237
import java.util.Map;
@@ -35,6 +40,7 @@
3540
import io.restassured.http.ContentType;
3641
import io.restassured.response.Response;
3742
import org.apache.commons.lang3.tuple.Pair;
43+
import org.awaitility.Awaitility;
3844
import org.junit.jupiter.api.BeforeEach;
3945
import org.junit.jupiter.api.Test;
4046
import org.junit.jupiter.api.io.TempDir;
@@ -126,6 +132,41 @@ void listCommitBoostPublicKeys() {
126132
}
127133
}
128134

135+
@Test
136+
void reloadReturnsEmptyProxyKeysAfterPhysicalKeystoreDeletion() throws Exception {
137+
// call commit boost get pub keys, the proxy_bls and proxy_ecdsa should not be empty
138+
final Response initResponse = signer.callCommitBoostGetPubKeys();
139+
initResponse
140+
.then()
141+
.log()
142+
.body()
143+
.statusCode(200)
144+
.contentType(ContentType.JSON)
145+
.body("keys", hasSize(2))
146+
.body("keys.proxy_bls", everyItem(not(empty())))
147+
.body("keys.proxy_ecdsa", everyItem(not(empty())));
148+
149+
// delete everything under commitBoostKeystoresPath and /reload
150+
deleteDirectoryContents(commitBoostKeystoresPath);
151+
signer.callReload().then().statusCode(202);
152+
153+
// Wait until proxy keys are empty (reload completed)
154+
Awaitility.await()
155+
.atMost(Duration.ofSeconds(5))
156+
.pollInterval(Duration.ofMillis(100)) // Check every 100ms
157+
.untilAsserted(
158+
() -> {
159+
final Response response = signer.callCommitBoostGetPubKeys();
160+
response
161+
.then()
162+
.statusCode(200)
163+
.contentType(ContentType.JSON)
164+
.body("keys", hasSize(2))
165+
.body("keys.proxy_bls", everyItem(empty()))
166+
.body("keys.proxy_ecdsa", everyItem(empty()));
167+
});
168+
}
169+
129170
private List<String> getProxyECPubKeys(final String consensusKeyHex) {
130171
// return compressed secp256k1 public keys in hex format
131172
return proxySECPKeysMap.get(consensusKeyHex).stream()
@@ -239,4 +280,22 @@ static List<ECKeyPair> randomECKeyPairs(final int count) {
239280
static List<BLSKeyPair> randomBLSKeyPairs(final int count) {
240281
return Stream.generate(() -> BLSKeyPair.random(SECURE_RANDOM)).limit(count).toList();
241282
}
283+
284+
private void deleteDirectoryContents(Path directory) throws IOException {
285+
if (Files.exists(directory)) {
286+
try (Stream<Path> paths = Files.walk(directory)) {
287+
paths
288+
.filter(path -> !path.equals(directory)) // Keep the directory itself
289+
.sorted(Comparator.reverseOrder()) // Delete children before parents
290+
.forEach(
291+
path -> {
292+
try {
293+
Files.delete(path);
294+
} catch (IOException e) {
295+
throw new UncheckedIOException(e);
296+
}
297+
});
298+
}
299+
}
300+
}
242301
}

acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/eth1rpc/Eth1RpcReloadKeysTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void additionalPublicKeyAreReportedAfterReloadUsingEth1RPC() throws IOExc
7171
assertThat(accounts).isNotEmpty();
7272

7373
final String[] additionalKeys = createSecpKeys(prvKeys[1]);
74-
signer.callReload().then().statusCode(200);
74+
signer.callReload().then().statusCode(202);
7575

7676
// reload is async ...
7777
Awaitility.await()

acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/keymanager/ListKeysAcceptanceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void additionalPublicKeyAreReportedAfterReload() throws URISyntaxExceptio
5757
validateApiResponse(callListKeys(), "data.validating_pubkey", hasItem(firstPubKey));
5858

5959
final String secondPubKey = createKeystoreYamlFile(BLS_PRIVATE_KEY_2);
60-
signer.callReload().then().statusCode(200);
60+
signer.callReload().then().statusCode(202);
6161

6262
// reload is async
6363
Awaitility.await()

acceptance-tests/src/test/java/tech/pegasys/web3signer/tests/publickeys/KeyIdentifiersAcceptanceTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public void additionalPublicKeyAreReportedAfterReload(final KeyType keyType) {
107107
validateApiResponse(response, contains(keys));
108108

109109
final String[] additionalKeys = createKeys(keyType, true, prvKeys[1]);
110-
signer.callReload().then().statusCode(200);
110+
signer.callReload().then().statusCode(202);
111111

112112
// reload is async ...
113113
Awaitility.await()
@@ -131,7 +131,7 @@ public void healthCheckReportsKeysLoadedAfterReloadInEth2Mode() {
131131
assertThat(getHealthcheckStatusValue(jsonBody)).isEqualTo("UP");
132132

133133
final String[] additionalKeys = createKeys(keyType, true, prvKeys[1]);
134-
signer.callReload().then().statusCode(200);
134+
signer.callReload().then().statusCode(202);
135135

136136
// reload is async ...
137137
Awaitility.await()
@@ -160,7 +160,7 @@ public void publicKeysAreRemovedAfterReloadDefault(final KeyType keyType) {
160160
assertThat(testDirectory.resolve(keys[1] + ".yaml").toFile().delete()).isTrue();
161161

162162
// reload API call
163-
signer.callReload().then().statusCode(200);
163+
signer.callReload().then().statusCode(202);
164164

165165
// reload is async ... assert that the key is removed
166166
Awaitility.await()

0 commit comments

Comments
 (0)