From cefa2c5bda2ef1fee3e5e4107f2e13de10aa8d93 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 3 Oct 2024 21:00:17 +0400 Subject: [PATCH 1/9] Fix the RecoveringSidecarRetriever case when there are more than one block for the same slot in DB (#159) --- .../db/DataColumnSidecarCoreDB.java | 9 +++ .../retriever/RecoveringSidecarRetriever.java | 2 +- .../RecoveringSidecarRetrieverTest.java | 55 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/db/DataColumnSidecarCoreDB.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/db/DataColumnSidecarCoreDB.java index 3eb65552956..8b3dfcca11a 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/db/DataColumnSidecarCoreDB.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/db/DataColumnSidecarCoreDB.java @@ -18,6 +18,7 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; interface DataColumnSidecarCoreDB { @@ -28,6 +29,14 @@ interface DataColumnSidecarCoreDB { SafeFuture> getColumnIdentifiers(UInt64 slot); + default SafeFuture> getColumnIdentifiers( + SlotAndBlockRoot blockId) { + return getColumnIdentifiers(blockId.getSlot()) + .thenApply( + ids -> + ids.stream().filter(id -> id.blockRoot().equals(blockId.getBlockRoot())).toList()); + } + // update SafeFuture addSidecar(DataColumnSidecar sidecar); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetriever.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetriever.java index 141a72c4a92..79ad483747d 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetriever.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetriever.java @@ -140,7 +140,7 @@ private RecoveryEntry createNewRecovery(final BeaconBlock block) { recoveryEntry.block.getSlot(), recoveryEntry.block.getRoot()); sidecarDB - .getColumnIdentifiers(block.getSlot()) + .getColumnIdentifiers(block.getSlotAndBlockRoot()) .thenCompose( dataColumnIdentifiers -> SafeFuture.collectAll( diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetrieverTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetrieverTest.java index 4ef535820e0..f0b4a7c6b65 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetrieverTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/RecoveringSidecarRetrieverTest.java @@ -43,7 +43,7 @@ import tech.pegasys.teku.statetransition.datacolumns.db.DataColumnSidecarDB; import tech.pegasys.teku.statetransition.datacolumns.db.DataColumnSidecarDbAccessor; -@SuppressWarnings("FutureReturnValueIgnored") +@SuppressWarnings({"FutureReturnValueIgnored", "JavaCase"}) public class RecoveringSidecarRetrieverTest { final StubAsyncRunner stubAsyncRunner = new StubAsyncRunner(); @@ -133,6 +133,59 @@ void sanityTest() throws Exception { assertThat(delegateRetriever.requests).allMatch(r -> r.promise().isDone()); } + @Test + void testMoreThanOneBlockWithBlobsOnSameSlot() throws Exception { + int blobCount = 1; + int columnsInDbCount = 13; + + DataColumnSidecarRetrieverStub delegateRetriever = new DataColumnSidecarRetrieverStub(); + RecoveringSidecarRetriever recoverRetrievr = + new RecoveringSidecarRetriever( + delegateRetriever, + kzg, + miscHelpers, + schemaDefinitions, + blockResolver, + dbAccessor, + stubAsyncRunner, + Duration.ofSeconds(1), + 128); + List blobs_10_0 = + Stream.generate(dataStructureUtil::randomValidBlob).limit(blobCount).toList(); + BeaconBlock block_10_0 = blockResolver.addBlock(10, blobCount); + List sidecars_10_0 = + miscHelpers.constructDataColumnSidecars(createSigned(block_10_0), blobs_10_0, kzg); + sidecars_10_0.forEach(db::addSidecar); + + List blobs_10_1 = + Stream.generate(dataStructureUtil::randomValidBlob).limit(blobCount).toList(); + BeaconBlock block_10_1 = blockResolver.addBlock(10, blobCount); + List sidecars_10_1 = + miscHelpers.constructDataColumnSidecars(createSigned(block_10_1), blobs_10_1, kzg); + sidecars_10_1.stream().limit(columnsInDbCount).forEach(db::addSidecar); + + DataColumnSlotAndIdentifier id0 = createId(block_10_1, 100); + SafeFuture res0 = recoverRetrievr.retrieve(id0); + + assertThat(delegateRetriever.requests).hasSize(1); + + recoverRetrievr.maybeInitiateRecovery(id0, res0); + assertThat(delegateRetriever.requests).hasSize(1 + columnCount - columnsInDbCount); + + delegateRetriever.requests.stream() + .skip(50) + .limit(columnCount / 2 - columnsInDbCount) + .forEach( + req -> { + req.promise().complete(sidecars_10_1.get(req.columnId().columnIndex().intValue())); + }); + + stubAsyncRunner.executeQueuedActions(); + + assertThat(res0).isCompletedWithValue(sidecars_10_1.get(100)); + assertThat(delegateRetriever.requests).allMatch(r -> r.promise().isDone()); + } + @Test void cancellingRequestShouldStopRecovery() throws Exception { int blobCount = 3; From 06c784670cf072f764f1625ea17516388f8ded99 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 10 Oct 2024 07:30:58 +0300 Subject: [PATCH 2/9] Optimize SimpleSidecarRetriever (#161) --- .../retriever/SimpleSidecarRetriever.java | 19 +++++++--- .../retriever/SimpleSidecarRetrieverTest.java | 35 ++++++++++++++++-- .../DasPeerCustodyCountSupplierStub.java | 36 +++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java index c7d91384b3b..1d7c8d9f8ba 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java @@ -17,12 +17,14 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -32,6 +34,8 @@ import org.apache.tuweni.units.bigints.UInt256; import tech.pegasys.teku.infrastructure.async.AsyncRunner; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.collections.cache.Cache; +import tech.pegasys.teku.infrastructure.collections.cache.LRUCache; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.SpecMilestone; @@ -177,7 +181,7 @@ private synchronized void nextRound() { long activeRequestCount = pendingRequests.values().stream().filter(r -> r.activeRpcRequest != null).count(); LOG.info( - "[nyota] SimpleSidecarRetriever.nextRound: completed: {}, errored: {}, total pending: {}, active pending: {}, new pending: {}, number of custody peers: {}", + "[nyota] SimpleSidecarRetriever.nextRound: completed: {}, errored: {}, total pending: {}, active pending: {}, new active: {}, number of custody peers: {}", retrieveCounter, errorCounter, pendingRequests.size(), @@ -269,14 +273,21 @@ public int getPeerRequestCount(UInt256 peerId) { private class ConnectedPeer { final UInt256 nodeId; + final Cache> custodyIndexesCache = LRUCache.create(2); public ConnectedPeer(UInt256 nodeId) { this.nodeId = nodeId; } - private List getNodeCustodyIndexes(SpecVersion specVersion) { - return MiscHelpersEip7594.required(specVersion.miscHelpers()) - .computeCustodyColumnIndexes(nodeId, custodyCountSupplier.getCustodyCountForPeer(nodeId)); + private Set calcNodeCustodyIndexes(SpecVersion specVersion) { + return new HashSet<>( + MiscHelpersEip7594.required(specVersion.miscHelpers()) + .computeCustodyColumnIndexes( + nodeId, custodyCountSupplier.getCustodyCountForPeer(nodeId))); + } + + private Set getNodeCustodyIndexes(SpecVersion specVersion) { + return custodyIndexesCache.get(specVersion, this::calcNodeCustodyIndexes); } public boolean isCustodyFor(DataColumnSlotAndIdentifier columnId) { diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java index 785a641d499..bc49818e2cc 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java @@ -23,6 +23,7 @@ import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.async.StubAsyncRunner; @@ -58,8 +59,8 @@ public class SimpleSidecarRetrieverTest { final int columnCount = config.getNumberOfColumns(); final KZG kzg = KZG.getInstance(false); - final DasPeerCustodyCountSupplier custodyCountSupplier = - DasPeerCustodyCountSupplier.createStub(config.getCustodyRequirement()); + final DasPeerCustodyCountSupplierStub custodyCountSupplier = + new DasPeerCustodyCountSupplierStub(config.getCustodyRequirement()); final Duration retrieverRound = Duration.ofSeconds(1); final SimpleSidecarRetriever simpleSidecarRetriever = @@ -232,4 +233,34 @@ void cancellingRequestShouldRemoveItFromPending() { advanceTimeGradually(retrieverRound); assertThat(custodyPeer.getRequests()).hasSize(2); } + + @Test + void performanceTest() { + List testNodes = + craftNodeIds() + .map( + nodeId -> + new TestPeer(stubAsyncRunner, nodeId, Duration.ofMillis(100)) + .currentRequestLimit(1000)) + .limit(128) + .peek(node -> custodyCountSupplier.addCustomCount(node.getNodeId(), columnCount)) + .peek(testPeerManager::connectPeer) + .toList(); + + List columnIds = + IntStream.range(0, Integer.MAX_VALUE) + .mapToObj(UInt64::valueOf) + .flatMap( + slot -> + IntStream.range(0, columnCount) + .mapToObj(UInt64::valueOf) + .map(colIdx -> new DataColumnSlotAndIdentifier(slot, Bytes32.ZERO, colIdx))) + .limit(100_000) + .toList(); + + List> sidecarFutures = + columnIds.stream().map(simpleSidecarRetriever::retrieve).toList(); + + Assertions.assertTimeout(Duration.ofSeconds(10), () -> advanceTimeGradually(retrieverRound)); + } } diff --git a/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java new file mode 100644 index 00000000000..921e71763d8 --- /dev/null +++ b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java @@ -0,0 +1,36 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.retriever; + +import java.util.HashMap; +import java.util.Map; +import org.apache.tuweni.units.bigints.UInt256; + +public class DasPeerCustodyCountSupplierStub implements DasPeerCustodyCountSupplier { + private final int defaultCount; + private final Map customCounts = new HashMap<>(); + + public DasPeerCustodyCountSupplierStub(int defaultCount) { + this.defaultCount = defaultCount; + } + + @Override + public int getCustodyCountForPeer(UInt256 nodeId) { + return customCounts.getOrDefault(nodeId, defaultCount); + } + + public void addCustomCount(UInt256 nodeId, int count) { + customCounts.put(nodeId, count); + } +} From df5bae9c888a09fe79ddddd2ed83a79a297f2739 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Mon, 14 Oct 2024 18:19:17 +0300 Subject: [PATCH 3/9] Use AsyncStream for data column req/resp interfaces (#162) * AsyncStream: * Refactor and rename some AsyncStream classes * Add AsyncStreamPublisher and its implementation * Add a generic AsyncSteram.consume()method * Add a publisher test * Add columns by range interface BatchDataColumnsByRangeReqResp * Convert BatchDataColumnsByRootReqResp to return AsyncStream instead of SafeFuture * Implement the above in DataColumnPeerManagerImpl * Adopt AsyncStream usage in DataColumnReqRespBatchingImpl --- .../BatchDataColumnsByRangeReqResp.java | 28 +++++ ...ava => BatchDataColumnsByRootReqResp.java} | 6 +- .../DataColumnReqRespBatchingImpl.java | 75 +++++++----- ...a => AbstractDelegatingStreamHandler.java} | 6 +- .../async/stream/AsyncIterator.java | 17 ++- .../async/stream/AsyncIteratorCollector.java | 51 ++++---- .../async/stream/AsyncQueue.java | 23 ++++ .../async/stream/AsyncStream.java | 12 +- ...eamReduce.java => AsyncStreamConsume.java} | 9 +- ...rCallback.java => AsyncStreamHandler.java} | 2 +- .../async/stream/AsyncStreamPublisher.java | 16 +++ ...educe.java => BaseAsyncStreamConsume.java} | 7 +- .../stream/BufferingStreamPublisher.java | 109 ++++++++++++++++++ ...lback.java => FilteringStreamHandler.java} | 4 +- ...allback.java => FlattenStreamHandler.java} | 8 +- .../async/stream/FutureAsyncIteratorImpl.java | 2 +- .../async/stream/LimitedAsyncQueue.java | 62 ++++++++++ ...torCallback.java => MapStreamHandler.java} | 4 +- .../async/stream/OperationAsyncIterator.java | 11 +- ...rCallback.java => SliceStreamHandler.java} | 17 ++- .../async/stream/SyncToAsyncIteratorImpl.java | 4 +- .../stream/AsyncStreamPublisherTest.java | 102 ++++++++++++++++ .../eth2/peers/DataColumnPeerManagerImpl.java | 47 +++++--- 23 files changed, 508 insertions(+), 114 deletions(-) create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java rename ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/{BatchDataColumnReqResp.java => BatchDataColumnsByRootReqResp.java} (85%) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{AbstractDelegatingIteratorCallback.java => AbstractDelegatingStreamHandler.java} (78%) create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{AsyncStreamReduce.java => AsyncStreamConsume.java} (87%) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{AsyncIteratorCallback.java => AsyncStreamHandler.java} (97%) create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisher.java rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{BaseAsyncStreamReduce.java => BaseAsyncStreamConsume.java} (80%) create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BufferingStreamPublisher.java rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{FilteringIteratorCallback.java => FilteringStreamHandler.java} (84%) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{FlattenIteratorCallback.java => FlattenStreamHandler.java} (86%) create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{MapIteratorCallback.java => MapStreamHandler.java} (85%) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{SliceIteratorCallback.java => SliceStreamHandler.java} (69%) create mode 100644 infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java new file mode 100644 index 00000000000..cba0addf104 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRangeReqResp.java @@ -0,0 +1,28 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.retriever; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; + +public interface BatchDataColumnsByRangeReqResp { + + AsyncStream requestDataColumnSidecarsByRange( + UInt256 nodeId, UInt64 startSlot, int slotCount, List columnIndexes); + + int getCurrentRequestLimit(UInt256 nodeId); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java similarity index 85% rename from ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnReqResp.java rename to ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java index fa023170fe4..d537ec850cb 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnReqResp.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/BatchDataColumnsByRootReqResp.java @@ -15,13 +15,13 @@ import java.util.List; import org.apache.tuweni.units.bigints.UInt256; -import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; -public interface BatchDataColumnReqResp { +public interface BatchDataColumnsByRootReqResp { - SafeFuture> requestDataColumnSidecar( + AsyncStream requestDataColumnSidecarsByRoot( UInt256 nodeId, List columnIdentifiers); int getCurrentRequestLimit(UInt256 nodeId); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java index 34d56901155..36fd69da371 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java @@ -18,19 +18,23 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.units.bigints.UInt256; import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStreamHandler; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; public class DataColumnReqRespBatchingImpl implements DataColumnReqResp { private static final Logger LOG = LogManager.getLogger("das-nyota"); - private final BatchDataColumnReqResp batchRpc; + private final BatchDataColumnsByRootReqResp batchRpc; - public DataColumnReqRespBatchingImpl(BatchDataColumnReqResp batchRpc) { + public DataColumnReqRespBatchingImpl(BatchDataColumnsByRootReqResp batchRpc) { this.batchRpc = batchRpc; } @@ -68,42 +72,59 @@ private void flushForNode(UInt256 nodeId, List nodeRequests) { nodeRequests.size(), "0x..." + nodeId.toHexString().substring(58), nodeRequests.hashCode()); - SafeFuture> response = - SafeFuture.of( - () -> - batchRpc.requestDataColumnSidecar( - nodeId, nodeRequests.stream().map(e -> e.columnIdentifier).toList())); + AsyncStream response = + batchRpc.requestDataColumnSidecarsByRoot( + nodeId, nodeRequests.stream().map(e -> e.columnIdentifier).toList()); - response.finish( - resp -> { - LOG.info( - "[nyota] Response batch of {} from {}, hash={}", - resp.size(), - "0x..." + nodeId.toHexString().substring(58), - nodeRequests.hashCode()); - Map byIds = new HashMap<>(); - for (DataColumnSidecar sidecar : resp) { - byIds.put(DataColumnIdentifier.createFromSidecar(sidecar), sidecar); - } - for (RequestEntry nodeRequest : nodeRequests) { - DataColumnSidecar maybeResponse = byIds.get(nodeRequest.columnIdentifier); - if (maybeResponse != null) { - nodeRequest.promise().complete(maybeResponse); + response.consume( + new AsyncStreamHandler<>() { + private final AtomicInteger count = new AtomicInteger(); + private final Map requestsNyColumnId = + nodeRequests.stream() + .collect(Collectors.toMap(RequestEntry::columnIdentifier, req -> req)); + + @Override + public SafeFuture onNext(DataColumnSidecar dataColumnSidecar) { + DataColumnIdentifier colId = DataColumnIdentifier.createFromSidecar(dataColumnSidecar); + RequestEntry request = requestsNyColumnId.get(colId); + if (request == null) { + return SafeFuture.failedFuture( + new IllegalArgumentException( + "Responded data column was not requested: " + colId)); } else { - nodeRequest.promise().completeExceptionally(new DasColumnNotAvailableException()); + request.promise().complete(dataColumnSidecar); + count.incrementAndGet(); + return TRUE_FUTURE; } } - }, - err -> + + @Override + public void onComplete() { + LOG.info( + "[nyota] Response batch of {} from {}, hash={}", + count, + "0x..." + nodeId.toHexString().substring(58), + nodeRequests.hashCode()); + nodeRequests.stream() + .filter(req -> !req.promise().isDone()) + .forEach( + req -> + req.promise().completeExceptionally(new DasColumnNotAvailableException())); + } + + @Override + public void onError(Throwable err) { nodeRequests.forEach( e -> { LOG.info( "[nyota] Error batch from {}, hash={}, err: {}", - nodeId.mod(65536).toHexString(), + "0x..." + nodeId.toHexString().substring(58), nodeRequests.hashCode(), e.toString()); e.promise().completeExceptionally(err); - })); + }); + } + }); } @Override diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingStreamHandler.java similarity index 78% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingStreamHandler.java index 8322e09bbb0..42f90b6e131 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AbstractDelegatingStreamHandler.java @@ -13,11 +13,11 @@ package tech.pegasys.teku.infrastructure.async.stream; -abstract class AbstractDelegatingIteratorCallback implements AsyncIteratorCallback { +abstract class AbstractDelegatingStreamHandler implements AsyncStreamHandler { - protected final AsyncIteratorCallback delegate; + protected final AsyncStreamHandler delegate; - protected AbstractDelegatingIteratorCallback(AsyncIteratorCallback delegate) { + protected AbstractDelegatingStreamHandler(AsyncStreamHandler delegate) { this.delegate = delegate; } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java index d71a89e8b69..2034e92e346 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java @@ -15,12 +15,10 @@ import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collector; -import tech.pegasys.teku.infrastructure.async.SafeFuture; abstract class AsyncIterator implements AsyncStream { - abstract void iterate(AsyncIteratorCallback callback); + abstract void iterate(AsyncStreamHandler callback); @Override public AsyncIterator flatMap(Function> toStreamMapper) { @@ -29,30 +27,29 @@ public AsyncIterator flatMap(Function> toStreamMapper) return OperationAsyncIterator.create( this, sourceCallback -> - new MapIteratorCallback<>( - new FlattenIteratorCallback<>(sourceCallback), toIteratorMapper)); + new MapStreamHandler<>(new FlattenStreamHandler<>(sourceCallback), toIteratorMapper)); } @Override public AsyncIterator map(Function mapper) { return OperationAsyncIterator.create( - this, sourceCallback -> new MapIteratorCallback<>(sourceCallback, mapper)); + this, sourceCallback -> new MapStreamHandler<>(sourceCallback, mapper)); } @Override public AsyncIterator filter(Predicate filter) { return OperationAsyncIterator.create( - this, sourceCallback -> new FilteringIteratorCallback<>(sourceCallback, filter)); + this, sourceCallback -> new FilteringStreamHandler<>(sourceCallback, filter)); } @Override - public SafeFuture collect(Collector collector) { - return new AsyncIteratorCollector<>(this).collect(collector); + public void consume(AsyncStreamHandler consumer) { + iterate(consumer); } @Override public AsyncStream slice(BaseSlicer slicer) { return OperationAsyncIterator.create( - this, sourceCallback -> new SliceIteratorCallback<>(sourceCallback, slicer)); + this, sourceCallback -> new SliceStreamHandler<>(sourceCallback, slicer)); } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCollector.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCollector.java index f0fbb2c9127..c7db0435bf6 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCollector.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCollector.java @@ -16,36 +16,35 @@ import java.util.stream.Collector; import tech.pegasys.teku.infrastructure.async.SafeFuture; -class AsyncIteratorCollector { +class AsyncIteratorCollector implements AsyncStreamHandler { + private final A accumulator; + private final Collector collector; - private final AsyncIterator iterator; + private final SafeFuture promise = new SafeFuture<>(); - public AsyncIteratorCollector(AsyncIterator iterator) { - this.iterator = iterator; + public AsyncIteratorCollector(Collector collector) { + this.collector = collector; + this.accumulator = collector.supplier().get(); } - public SafeFuture collect(Collector collector) { - SafeFuture promise = new SafeFuture<>(); - A accumulator = collector.supplier().get(); - iterator.iterate( - new AsyncIteratorCallback() { - @Override - public SafeFuture onNext(T t) { - collector.accumulator().accept(accumulator, t); - return TRUE_FUTURE; - } - - @Override - public void onComplete() { - R result = collector.finisher().apply(accumulator); - promise.complete(result); - } - - @Override - public void onError(Throwable t) { - promise.completeExceptionally(t); - } - }); + @Override + public SafeFuture onNext(T t) { + collector.accumulator().accept(accumulator, t); + return TRUE_FUTURE; + } + + @Override + public void onComplete() { + R result = collector.finisher().apply(accumulator); + promise.complete(result); + } + + @Override + public void onError(Throwable t) { + promise.completeExceptionally(t); + } + + public SafeFuture getPromise() { return promise; } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java new file mode 100644 index 00000000000..290668e2111 --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import tech.pegasys.teku.infrastructure.async.SafeFuture; + +interface AsyncQueue { + + void put(T item); + + SafeFuture take(); +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java index f37564c259c..9d93e3ac6c1 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java @@ -19,12 +19,18 @@ import java.util.stream.Stream; /** Similar to {@link java.util.stream.Stream} but may perform async operations */ -public interface AsyncStream extends AsyncStreamTransform, AsyncStreamReduce { +public interface AsyncStream extends AsyncStreamTransform, AsyncStreamConsume { static AsyncStream empty() { return of(); } + static AsyncStream exceptional(Throwable error) { + AsyncStreamPublisher ret = createPublisher(1); + ret.onError(error); + return ret; + } + @SafeVarargs static AsyncStream of(T... elements) { return create(List.of(elements).iterator()); @@ -41,4 +47,8 @@ static AsyncStream create(Iterator iterator) { static AsyncStream create(CompletionStage future) { return new FutureAsyncIteratorImpl<>(future); } + + static AsyncStreamPublisher createPublisher(int maxBufferSize) { + return new BufferingStreamPublisher<>(maxBufferSize); + } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamReduce.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java similarity index 87% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamReduce.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java index 01af5c09844..2f0464bf37b 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamReduce.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java @@ -23,7 +23,14 @@ import java.util.stream.Collectors; import tech.pegasys.teku.infrastructure.async.SafeFuture; -public interface AsyncStreamReduce extends BaseAsyncStreamReduce, AsyncStreamTransform { +public interface AsyncStreamConsume extends BaseAsyncStreamConsume, AsyncStreamTransform { + + default SafeFuture collect(Collector collector) { + AsyncIteratorCollector asyncIteratorCollector = + new AsyncIteratorCollector<>(collector); + consume(asyncIteratorCollector); + return asyncIteratorCollector.getPromise(); + } default SafeFuture> findFirst() { return this.limit(1) diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamHandler.java similarity index 97% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamHandler.java index 04e4e9cb2a2..25a3903673f 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamHandler.java @@ -19,7 +19,7 @@ * Internal {@link AsyncStream} implementation interface which is analogous to {@link * java.util.concurrent.Flow.Subscriber} in RX world */ -interface AsyncIteratorCallback { +public interface AsyncStreamHandler { SafeFuture TRUE_FUTURE = SafeFuture.completedFuture(true); SafeFuture FALSE_FUTURE = SafeFuture.completedFuture(false); diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisher.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisher.java new file mode 100644 index 00000000000..1497cf8a2dc --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisher.java @@ -0,0 +1,16 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +public interface AsyncStreamPublisher extends AsyncStreamHandler, AsyncStream {} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamReduce.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java similarity index 80% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamReduce.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java index ffb7b1b1678..48a103474b3 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamReduce.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java @@ -13,14 +13,11 @@ package tech.pegasys.teku.infrastructure.async.stream; -import java.util.stream.Collector; -import tech.pegasys.teku.infrastructure.async.SafeFuture; - /** * Contains fundamental terminal (reduce or collect) stream methods All other terminal methods are * expressed my means of those methods */ -public interface BaseAsyncStreamReduce { +public interface BaseAsyncStreamConsume { - SafeFuture collect(Collector collector); + void consume(AsyncStreamHandler consumer); } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BufferingStreamPublisher.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BufferingStreamPublisher.java new file mode 100644 index 00000000000..93336b0484f --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BufferingStreamPublisher.java @@ -0,0 +1,109 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import tech.pegasys.teku.infrastructure.async.SafeFuture; + +class BufferingStreamPublisher extends AsyncIterator implements AsyncStreamPublisher { + + private final AsyncQueue> eventQueue; + + private boolean isDone = false; + + BufferingStreamPublisher(int maxBufferSize) { + this.eventQueue = new LimitedAsyncQueue<>(maxBufferSize); + } + + sealed interface Event { + boolean isTerminal(); + } + + record ItemEvent(T item, SafeFuture nextReturn) implements Event { + @Override + public boolean isTerminal() { + return false; + } + } + + record CompleteEvent() implements Event { + @Override + public boolean isTerminal() { + return true; + } + } + + record ErrorEvent(Throwable error) implements Event { + @Override + public boolean isTerminal() { + return true; + } + } + + private synchronized void putNext(Event event) { + if (isDone) { + throw new IllegalStateException("Stream has been done already"); + } + isDone = event.isTerminal(); + eventQueue.put(event); + } + + private SafeFuture> takeNext() { + return eventQueue.take(); + } + + @Override + void iterate(AsyncStreamHandler delegate) { + SafeFuture.asyncDoWhile( + () -> + takeNext() + .thenCompose( + event -> + switch (event) { + case ItemEvent item -> { + delegate.onNext(item.item()).propagateTo(item.nextReturn()); + yield item.nextReturn(); + } + case CompleteEvent ignored -> { + delegate.onComplete(); + yield FALSE_FUTURE; + } + case ErrorEvent errorEvent -> { + delegate.onError(errorEvent.error()); + yield FALSE_FUTURE; + } + })) + .finish(delegate::onError); + } + + @Override + public SafeFuture onNext(T t) { + SafeFuture ret = new SafeFuture<>(); + try { + putNext(new ItemEvent<>(t, ret)); + } catch (Exception e) { + ret.completeExceptionally(e); + } + return ret; + } + + @Override + public void onComplete() { + putNext(new CompleteEvent<>()); + } + + @Override + public synchronized void onError(Throwable t) { + putNext(new ErrorEvent<>(t)); + } +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringStreamHandler.java similarity index 84% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringStreamHandler.java index da5e8bfbb1b..dd6877f328e 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FilteringStreamHandler.java @@ -16,11 +16,11 @@ import java.util.function.Predicate; import tech.pegasys.teku.infrastructure.async.SafeFuture; -class FilteringIteratorCallback extends AbstractDelegatingIteratorCallback { +class FilteringStreamHandler extends AbstractDelegatingStreamHandler { private final Predicate filter; - protected FilteringIteratorCallback(AsyncIteratorCallback delegate, Predicate filter) { + protected FilteringStreamHandler(AsyncStreamHandler delegate, Predicate filter) { super(delegate); this.filter = filter; } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java similarity index 86% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java index 1e80338285e..2aea4ed4f52 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java @@ -15,10 +15,10 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; -class FlattenIteratorCallback, T> - extends AbstractDelegatingIteratorCallback { +class FlattenStreamHandler, T> + extends AbstractDelegatingStreamHandler { - protected FlattenIteratorCallback(AsyncIteratorCallback delegate) { + protected FlattenStreamHandler(AsyncStreamHandler delegate) { super(delegate); } @@ -26,7 +26,7 @@ protected FlattenIteratorCallback(AsyncIteratorCallback delegate) { public SafeFuture onNext(TCol asyncIterator) { SafeFuture ret = new SafeFuture<>(); asyncIterator.iterate( - new AsyncIteratorCallback() { + new AsyncStreamHandler() { @Override public SafeFuture onNext(T t) { SafeFuture proceedFuture = delegate.onNext(t); diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FutureAsyncIteratorImpl.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FutureAsyncIteratorImpl.java index 1cee6ef7b29..c893e8c5360 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FutureAsyncIteratorImpl.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FutureAsyncIteratorImpl.java @@ -25,7 +25,7 @@ class FutureAsyncIteratorImpl extends AsyncIterator { } @Override - public void iterate(AsyncIteratorCallback callback) { + public void iterate(AsyncStreamHandler callback) { future.finish( succ -> callback.onNext(succ).finish(__ -> callback.onComplete(), callback::onError), callback::onError); diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java new file mode 100644 index 00000000000..6dedfe7bc63 --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java @@ -0,0 +1,62 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import tech.pegasys.teku.infrastructure.async.SafeFuture; + +class LimitedAsyncQueue implements AsyncQueue { + + private final int maxSize; + + private final Queue items = new ArrayDeque<>(); + private final Queue> takers = new ArrayDeque<>(); + + public LimitedAsyncQueue(int maxSize) { + this.maxSize = maxSize; + } + + // Adds an item to the queue + @Override + public synchronized void put(T item) { + if (!takers.isEmpty()) { + // If there are pending takers, complete one with the item + CompletableFuture taker = takers.poll(); + taker.complete(item); + } else { + // Otherwise, add the item to the items queue + if (items.size() >= maxSize) { + throw new IllegalStateException("Buffer size overflow: " + maxSize); + } + items.offer(item); + } + } + + // Returns a CompletableFuture that will be completed when an item is available + @Override + public synchronized SafeFuture take() { + if (!items.isEmpty()) { + // If items are available, return a completed future + T item = items.poll(); + return SafeFuture.completedFuture(item); + } else { + // If no items, create a new CompletableFuture and add it to takers + SafeFuture future = new SafeFuture<>(); + takers.offer(future); + return future; + } + } +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapStreamHandler.java similarity index 85% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapStreamHandler.java index afcc4e07ee1..3a88c771d5a 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/MapStreamHandler.java @@ -16,11 +16,11 @@ import java.util.function.Function; import tech.pegasys.teku.infrastructure.async.SafeFuture; -class MapIteratorCallback extends AbstractDelegatingIteratorCallback { +class MapStreamHandler extends AbstractDelegatingStreamHandler { private final Function mapper; - protected MapIteratorCallback(AsyncIteratorCallback delegate, Function mapper) { + protected MapStreamHandler(AsyncStreamHandler delegate, Function mapper) { super(delegate); this.mapper = mapper; } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java index 85f27af6ce0..2ff5488c189 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java @@ -20,11 +20,10 @@ abstract class OperationAsyncIterator extends AsyncIterator { public static AsyncIterator create( AsyncIterator srcIterator, - Function, AsyncIteratorCallback> callbackMapper) { + Function, AsyncStreamHandler> callbackMapper) { return new OperationAsyncIterator<>(srcIterator) { @Override - protected AsyncIteratorCallback createDelegateCallback( - AsyncIteratorCallback sourceCallback) { + protected AsyncStreamHandler createDelegateCallback(AsyncStreamHandler sourceCallback) { return callbackMapper.apply(sourceCallback); } }; @@ -34,11 +33,11 @@ public OperationAsyncIterator(AsyncIterator delegateIterator) { this.delegateIterator = delegateIterator; } - protected abstract AsyncIteratorCallback createDelegateCallback( - AsyncIteratorCallback sourceCallback); + protected abstract AsyncStreamHandler createDelegateCallback( + AsyncStreamHandler sourceCallback); @Override - public void iterate(AsyncIteratorCallback callback) { + public void iterate(AsyncStreamHandler callback) { delegateIterator.iterate(createDelegateCallback(callback)); } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceIteratorCallback.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java similarity index 69% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceIteratorCallback.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java index cc8eae393ca..e2be0f0773b 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceIteratorCallback.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java @@ -15,12 +15,12 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; -class SliceIteratorCallback extends AbstractDelegatingIteratorCallback { +class SliceStreamHandler extends AbstractDelegatingStreamHandler { private final BaseAsyncStreamTransform.BaseSlicer slicer; - protected SliceIteratorCallback( - AsyncIteratorCallback delegate, BaseAsyncStreamTransform.BaseSlicer slicer) { + protected SliceStreamHandler( + AsyncStreamHandler delegate, BaseAsyncStreamTransform.BaseSlicer slicer) { super(delegate); this.slicer = slicer; } @@ -30,8 +30,15 @@ public SafeFuture onNext(T t) { BaseAsyncStreamTransform.SliceResult sliceResult = slicer.slice(t); return switch (sliceResult) { case CONTINUE -> delegate.onNext(t); - case SKIP_AND_STOP -> FALSE_FUTURE; - case INCLUDE_AND_STOP -> delegate.onNext(t).thenApply(__ -> false); + case SKIP_AND_STOP -> { + delegate.onComplete(); + yield FALSE_FUTURE; + } + case INCLUDE_AND_STOP -> { + SafeFuture ret = delegate.onNext(t).thenApply(__ -> false); + delegate.onComplete(); + yield ret; + } }; } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SyncToAsyncIteratorImpl.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SyncToAsyncIteratorImpl.java index f12bac9aa2e..b139b1586f5 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SyncToAsyncIteratorImpl.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SyncToAsyncIteratorImpl.java @@ -19,14 +19,14 @@ class SyncToAsyncIteratorImpl extends AsyncIterator { private final Iterator iterator; - private AsyncIteratorCallback callback; + private AsyncStreamHandler callback; SyncToAsyncIteratorImpl(Iterator iterator) { this.iterator = iterator; } @Override - public void iterate(AsyncIteratorCallback callback) { + public void iterate(AsyncStreamHandler callback) { synchronized (this) { if (this.callback != null) { throw new IllegalStateException("This one-shot iterator has been used already"); diff --git a/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java b/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java new file mode 100644 index 00000000000..8913f0c4f16 --- /dev/null +++ b/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java @@ -0,0 +1,102 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.infrastructure.async.SafeFuture; + +@SuppressWarnings("FutureReturnValueIgnored") +public class AsyncStreamPublisherTest { + + AsyncStreamPublisher publisher = AsyncStream.createPublisher(Integer.MAX_VALUE); + + @Test + void sanityTest() { + ArrayList collector = new ArrayList<>(); + + SafeFuture> listPromise = + publisher + .flatMap( + i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) + .filter(i -> i % 2 == 0) + .map(i -> i * 10) + .limit(10) + .collect(collector); + + assertThat(collector).isEmpty(); + + { + SafeFuture f = publisher.onNext(0); + assertThat(f).isCompletedWithValue(true); + } + assertThat(collector).containsExactly(0, 20, 40); + + { + SafeFuture f = publisher.onNext(1); + assertThat(f).isCompletedWithValue(true); + } + assertThat(collector).containsExactly(0, 20, 40, 100, 120, 140); + + { + SafeFuture f = publisher.onNext(2); + assertThat(f).isCompletedWithValue(true); + } + assertThat(collector).containsExactly(0, 20, 40, 100, 120, 140, 200, 220, 240); + assertThat(listPromise).isNotDone(); + + { + SafeFuture f = publisher.onNext(3); + assertThat(f).isCompletedWithValue(false); + } + // limit(10) kicks in + assertThat(collector).containsExactly(0, 20, 40, 100, 120, 140, 200, 220, 240, 300); + assertThat(listPromise) + .isCompletedWithValue(List.of(0, 20, 40, 100, 120, 140, 200, 220, 240, 300)); + } + + @Test + void completeShouldCompleteStream() { + SafeFuture> listPromise = + publisher + .flatMap( + i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) + .filter(i -> i % 2 == 0) + .map(i -> i * 10) + .toList(); + + publisher.onNext(0); + publisher.onComplete(); + assertThat(listPromise).isCompletedWithValue(List.of(0, 20, 40)); + } + + @Test + void errorShouldCompleteStream() { + SafeFuture> listPromise = + publisher + .flatMap( + i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) + .filter(i -> i % 2 == 0) + .map(i -> i * 10) + .toList(); + + publisher.onNext(0); + publisher.onError(new RuntimeException("test")); + assertThat(listPromise).isCompletedExceptionally(); + } +} diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java index d9aef8cb66e..2ee6ea4c86e 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java @@ -13,22 +13,27 @@ package tech.pegasys.teku.networking.eth2.peers; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.tuweni.units.bigints.UInt256; -import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStreamPublisher; import tech.pegasys.teku.infrastructure.subscribers.Subscribers; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.networking.p2p.peer.PeerConnectedSubscriber; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; -import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnReqResp; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRangeReqResp; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRootReqResp; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnPeerManager; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnReqResp; public class DataColumnPeerManagerImpl - implements DataColumnPeerManager, PeerConnectedSubscriber, BatchDataColumnReqResp { + implements DataColumnPeerManager, + PeerConnectedSubscriber, + BatchDataColumnsByRootReqResp, + BatchDataColumnsByRangeReqResp { private final Subscribers listeners = Subscribers.create(true); private Map connectedPeers = new ConcurrentHashMap<>(); @@ -62,22 +67,34 @@ public void banNode(UInt256 node) { } @Override - public SafeFuture> requestDataColumnSidecar( + public AsyncStream requestDataColumnSidecarsByRoot( UInt256 nodeId, List columnIdentifiers) { Eth2Peer eth2Peer = connectedPeers.get(nodeId); + AsyncStreamPublisher ret = AsyncStream.createPublisher(Integer.MAX_VALUE); if (eth2Peer == null) { - return SafeFuture.failedFuture(new DataColumnReqResp.DasPeerDisconnectedException()); + ret.onError(new DataColumnReqResp.DasPeerDisconnectedException()); } else { - List responseCollector = new ArrayList<>(); - return eth2Peer - .requestDataColumnSidecarsByRoot( - columnIdentifiers, - sidecar -> { - responseCollector.add(sidecar); - return SafeFuture.COMPLETE; - }) - .thenApply(__ -> responseCollector); + eth2Peer + .requestDataColumnSidecarsByRoot(columnIdentifiers, ret::onNext) + .finish(__ -> ret.onComplete(), ret::onError); } + return ret; + } + + @Override + public AsyncStream requestDataColumnSidecarsByRange( + UInt256 nodeId, UInt64 startSlot, int slotCount, List columnIndexes) { + Eth2Peer eth2Peer = connectedPeers.get(nodeId); + AsyncStreamPublisher ret = AsyncStream.createPublisher(Integer.MAX_VALUE); + if (eth2Peer == null) { + ret.onError(new DataColumnReqResp.DasPeerDisconnectedException()); + } else { + eth2Peer + .requestDataColumnSidecarsByRange( + startSlot, UInt64.valueOf(slotCount), columnIndexes, ret::onNext) + .finish(__ -> ret.onComplete(), ret::onError); + } + return ret; } @Override From 99d2c9863689227a560cb3f3cb961a6650d9a167 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Mon, 14 Oct 2024 18:19:52 +0300 Subject: [PATCH 4/9] Fix SimpleSidecarRetriever cache (#165) * Fix SimpleSidecarRetriever cache * Add regression test --- .../retriever/SimpleSidecarRetriever.java | 15 +++--- .../retriever/SimpleSidecarRetrieverTest.java | 50 ++++++++++++++++--- .../DasPeerCustodyCountSupplierStub.java | 2 +- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java index 1d7c8d9f8ba..4f282c81ef5 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java @@ -273,21 +273,24 @@ public int getPeerRequestCount(UInt256 peerId) { private class ConnectedPeer { final UInt256 nodeId; - final Cache> custodyIndexesCache = LRUCache.create(2); + final Cache> custodyIndexesCache = LRUCache.create(2); + + private record CacheKey(SpecVersion specVersion, int custodyCount) {} public ConnectedPeer(UInt256 nodeId) { this.nodeId = nodeId; } - private Set calcNodeCustodyIndexes(SpecVersion specVersion) { + private Set calcNodeCustodyIndexes(CacheKey cacheKey) { return new HashSet<>( - MiscHelpersEip7594.required(specVersion.miscHelpers()) - .computeCustodyColumnIndexes( - nodeId, custodyCountSupplier.getCustodyCountForPeer(nodeId))); + MiscHelpersEip7594.required(cacheKey.specVersion().miscHelpers()) + .computeCustodyColumnIndexes(nodeId, cacheKey.custodyCount())); } private Set getNodeCustodyIndexes(SpecVersion specVersion) { - return custodyIndexesCache.get(specVersion, this::calcNodeCustodyIndexes); + return custodyIndexesCache.get( + new CacheKey(specVersion, custodyCountSupplier.getCustodyCountForPeer(nodeId)), + this::calcNodeCustodyIndexes); } public boolean isCustodyFor(DataColumnSlotAndIdentifier columnId) { diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java index bc49818e2cc..059513102e1 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetrieverTest.java @@ -73,10 +73,10 @@ public class SimpleSidecarRetrieverTest { stubAsyncRunner, retrieverRound); - UInt64 columnId = UInt64.valueOf(1); + UInt64 columnIndex = UInt64.valueOf(1); - Iterator custodyNodeIds = craftNodeIdsCustodyOf(columnId).iterator(); - Iterator nonCustodyNodeIds = craftNodeIdsNotCustodyOf(columnId).iterator(); + Iterator custodyNodeIds = craftNodeIdsCustodyOf(columnIndex).iterator(); + Iterator nonCustodyNodeIds = craftNodeIdsNotCustodyOf(columnIndex).iterator(); private final DataStructureUtil dataStructureUtil = new DataStructureUtil(0, spec); final CanonicalBlockResolverStub blockResolver = new CanonicalBlockResolverStub(spec); @@ -131,9 +131,9 @@ void sanityTest() { BeaconBlock block = blockResolver.addBlock(10, 1); List sidecars = miscHelpers.constructDataColumnSidecars(createSigned(block), blobs, kzg); - DataColumnSidecar sidecar0 = sidecars.get(columnId.intValue()); + DataColumnSidecar sidecar0 = sidecars.get(columnIndex.intValue()); - DataColumnSlotAndIdentifier id0 = createId(block, columnId.intValue()); + DataColumnSlotAndIdentifier id0 = createId(block, columnIndex.intValue()); testPeerManager.connectPeer(custodyPeerMissingData); testPeerManager.connectPeer(nonCustodyPeer); @@ -182,7 +182,7 @@ void selectingBestPeerShouldRespectPeerMetrics() { allPeers.forEach(testPeerManager::connectPeer); DataColumnSlotAndIdentifier id0 = - new DataColumnSlotAndIdentifier(UInt64.ONE, Bytes32.ZERO, columnId); + new DataColumnSlotAndIdentifier(UInt64.ONE, Bytes32.ZERO, columnIndex); SafeFuture resp0 = simpleSidecarRetriever.retrieve(id0); advanceTimeGradually(retrieverRound); @@ -217,7 +217,7 @@ void cancellingRequestShouldRemoveItFromPending() { testPeerManager.connectPeer(custodyPeer); DataColumnSlotAndIdentifier id0 = - new DataColumnSlotAndIdentifier(UInt64.ONE, Bytes32.ZERO, columnId); + new DataColumnSlotAndIdentifier(UInt64.ONE, Bytes32.ZERO, columnIndex); SafeFuture resp0_0 = simpleSidecarRetriever.retrieve(id0); advanceTimeGradually(retrieverRound); @@ -243,7 +243,7 @@ void performanceTest() { new TestPeer(stubAsyncRunner, nodeId, Duration.ofMillis(100)) .currentRequestLimit(1000)) .limit(128) - .peek(node -> custodyCountSupplier.addCustomCount(node.getNodeId(), columnCount)) + .peek(node -> custodyCountSupplier.setCustomCount(node.getNodeId(), columnCount)) .peek(testPeerManager::connectPeer) .toList(); @@ -263,4 +263,38 @@ void performanceTest() { Assertions.assertTimeout(Duration.ofSeconds(10), () -> advanceTimeGradually(retrieverRound)); } + + @Test + @SuppressWarnings("FutureReturnValueIgnored") + void shouldTrackCustodyCountChangesForPeers() { + Duration responseLatency = Duration.ofDays(1); // complete responses manually + TestPeer peer = + new TestPeer(stubAsyncRunner, custodyNodeIds.next(), responseLatency) + .currentRequestLimit(1000); + testPeerManager.connectPeer(peer); + + List colIds = + IntStream.range(0, columnCount) + .mapToObj(UInt64::valueOf) + .map(colIdx -> new DataColumnSlotAndIdentifier(UInt64.ONE, Bytes32.ZERO, colIdx)) + .toList(); + + colIds.forEach(simpleSidecarRetriever::retrieve); + + int peerCustodyCount = custodyCountSupplier.getCustodyCountForPeer(peer.getNodeId()); + + advanceTimeGradually(retrieverRound); + assertThat(peer.getRequests()).hasSize(peerCustodyCount); + + int newPeerCustodyCount = peerCustodyCount + 1; + custodyCountSupplier.setCustomCount(peer.getNodeId(), newPeerCustodyCount); + + advanceTimeGradually(retrieverRound); + assertThat(peer.getRequests()).hasSize(newPeerCustodyCount); + + custodyCountSupplier.setCustomCount(peer.getNodeId(), columnCount); + + advanceTimeGradually(retrieverRound); + assertThat(peer.getRequests()).hasSize(columnCount); + } } diff --git a/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java index 921e71763d8..5eb43b12fd1 100644 --- a/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java +++ b/ethereum/statetransition/src/testFixtures/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DasPeerCustodyCountSupplierStub.java @@ -30,7 +30,7 @@ public int getCustodyCountForPeer(UInt256 nodeId) { return customCounts.getOrDefault(nodeId, defaultCount); } - public void addCustomCount(UInt256 nodeId, int count) { + public void setCustomCount(UInt256 nodeId, int count) { customCounts.put(nodeId, count); } } From 7645ed03ce17ea87e4e43ea95ee324b5c3127418 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Tue, 15 Oct 2024 19:48:19 +0300 Subject: [PATCH 5/9] Fix missing Gossip subnet subscription on startup (#167) * isOptimisticHead flag should have a single source for GossipForkManager * Add unit test --- .../networking/eth2/ActiveEth2P2PNetwork.java | 3 ++- .../eth2/gossip/forks/GossipForkManager.java | 26 +++++++++++-------- .../gossip/forks/GossipForkManagerTest.java | 13 ++++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java index 3f9f91d5719..c108c3d8754 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/ActiveEth2P2PNetwork.java @@ -182,6 +182,8 @@ private synchronized void stopGossip() { @Override public void onSyncStateChanged(final boolean isInSync, final boolean isOptimistic) { + gossipForkManager.onOptimisticHeadChanged(isOptimistic); + if (state.get() != State.RUNNING) { return; } @@ -190,7 +192,6 @@ public void onSyncStateChanged(final boolean isInSync, final boolean isOptimisti } else { stopGossip(); } - gossipForkManager.onOptimisticHeadChanged(isOptimistic); } @VisibleForTesting diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java index 917e214edc8..99229b78a62 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManager.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; @@ -56,7 +57,7 @@ public class GossipForkManager { private static final Logger LOG = LogManager.getLogger(); private static final int EPOCHS_PRIOR_TO_FORK_TO_ACTIVATE = 2; private final Spec spec; - private final RecentChainData recentChainData; + private final Bytes32 genesisValidatorsRoot; private final NavigableMap forksByActivationEpoch; private final Set activeSubscriptions = new HashSet<>(); private final IntSet currentAttestationSubnets = new IntOpenHashSet(); @@ -64,13 +65,16 @@ public class GossipForkManager { private final IntSet currentDataColumnSidecarSubnets = new IntOpenHashSet(); private Optional currentEpoch = Optional.empty(); + private boolean isHeadOptimistic; private GossipForkManager( final Spec spec, - final RecentChainData recentChainData, + final Bytes32 genesisValidatorsRoot, + final boolean isHeadOptimistic, final NavigableMap forksByActivationEpoch) { this.spec = spec; - this.recentChainData = recentChainData; + this.genesisValidatorsRoot = genesisValidatorsRoot; + this.isHeadOptimistic = isHeadOptimistic; this.forksByActivationEpoch = forksByActivationEpoch; } @@ -142,14 +146,12 @@ public synchronized void stopGossip() { } public synchronized void onOptimisticHeadChanged(final boolean isHeadOptimistic) { + this.isHeadOptimistic = isHeadOptimistic; if (isHeadOptimistic) { activeSubscriptions.forEach(GossipForkSubscriptions::stopGossipForOptimisticSync); } else { activeSubscriptions.forEach( - subscriptions -> - subscriptions.startGossip( - recentChainData.getGenesisData().orElseThrow().getGenesisValidatorsRoot(), - false)); + subscriptions -> subscriptions.startGossip(genesisValidatorsRoot, false)); } } @@ -302,9 +304,7 @@ private boolean isActive(final GossipForkSubscriptions subscriptions) { private void startSubscriptions(final GossipForkSubscriptions subscription) { if (activeSubscriptions.add(subscription)) { - subscription.startGossip( - recentChainData.getGenesisData().orElseThrow().getGenesisValidatorsRoot(), - recentChainData.isChainHeadOptimistic()); + subscription.startGossip(genesisValidatorsRoot, isHeadOptimistic); currentAttestationSubnets.forEach(subscription::subscribeToAttestationSubnetId); currentSyncCommitteeSubnets.forEach(subscription::subscribeToSyncCommitteeSubnet); currentDataColumnSidecarSubnets.forEach(subscription::subscribeToDataColumnSidecarSubnet); @@ -356,7 +356,11 @@ public GossipForkManager build() { checkNotNull(spec, "Must supply spec"); checkNotNull(recentChainData, "Must supply recentChainData"); checkState(!forksByActivationEpoch.isEmpty(), "Must specify at least one fork"); - return new GossipForkManager(spec, recentChainData, forksByActivationEpoch); + return new GossipForkManager( + spec, + recentChainData.getGenesisData().orElseThrow().getGenesisValidatorsRoot(), + recentChainData.isChainHeadOptimistic(), + forksByActivationEpoch); } } } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManagerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManagerTest.java index 90ce3580cf3..05596025a86 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManagerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/gossip/forks/GossipForkManagerTest.java @@ -504,6 +504,19 @@ void shouldStartSubscriptionsInOptimisticSyncMode() { verify(subscriptions).startGossip(GENESIS_VALIDATORS_ROOT, true); } + @Test + void shouldStartSubscriptionsInNonOptimisticSyncModeWhenSyncStateChangedBeforeStart() { + when(recentChainData.isChainHeadOptimistic()).thenReturn(true); + + final GossipForkSubscriptions subscriptions = forkAtEpoch(0); + final GossipForkManager manager = managerForForks(subscriptions); + + manager.onOptimisticHeadChanged(false); + manager.configureGossipForEpoch(UInt64.ZERO); + + verify(subscriptions).startGossip(GENESIS_VALIDATORS_ROOT, false); + } + private GossipForkSubscriptions forkAtEpoch(final long epoch) { final GossipForkSubscriptions subscriptions = mock(GossipForkSubscriptions.class, "subscriptionsForEpoch" + epoch); From 6a670610e7c3b25291037c5c163f54606b063606 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 17 Oct 2024 17:44:56 +0300 Subject: [PATCH 6/9] Refactor AsyncStream (#169) * Refactor AsyncStream implementation classes: make it more canonical. * Leave just a single fundamental transform operation: transform() * Add AsyncStream.peek(AsyncStreamVisitor) * Add some test cases --- .../async/stream/AsyncIterator.java | 30 +---- .../async/stream/AsyncQueue.java | 5 + .../async/stream/AsyncStream.java | 91 +++++++++++++- ...treamConsume.java => AsyncStreamBase.java} | 8 +- .../async/stream/AsyncStreamConsume.java | 88 ------------- .../async/stream/AsyncStreamSlicer.java | 73 +++++++++++ .../async/stream/AsyncStreamTransform.java | 113 ----------------- .../async/stream/AsyncStreamTransformer.java | 19 +++ .../async/stream/AsyncStreamVisitor.java | 23 ++++ .../async/stream/CircularBuf.java | 45 +++++++ .../async/stream/FlattenStreamHandler.java | 4 +- .../async/stream/LimitedAsyncQueue.java | 26 ++-- .../async/stream/SliceStreamHandler.java | 7 +- ...rator.java => TransformAsyncIterator.java} | 25 +--- ...aseAsyncStreamTransform.java => Util.java} | 25 +--- .../async/stream/VisitorHandler.java | 43 +++++++ .../stream/AsyncStreamPublisherTest.java | 119 ++++++++++++++---- 17 files changed, 429 insertions(+), 315 deletions(-) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{BaseAsyncStreamConsume.java => AsyncStreamBase.java} (78%) delete mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamSlicer.java delete mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransform.java create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransformer.java create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamVisitor.java create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/CircularBuf.java rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{OperationAsyncIterator.java => TransformAsyncIterator.java} (52%) rename infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/{BaseAsyncStreamTransform.java => Util.java} (57%) create mode 100644 infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/VisitorHandler.java diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java index 2034e92e346..207ee5aa987 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncIterator.java @@ -13,43 +13,17 @@ package tech.pegasys.teku.infrastructure.async.stream; -import java.util.function.Function; -import java.util.function.Predicate; - abstract class AsyncIterator implements AsyncStream { abstract void iterate(AsyncStreamHandler callback); @Override - public AsyncIterator flatMap(Function> toStreamMapper) { - Function> toIteratorMapper = - toStreamMapper.andThen(stream -> (AsyncIterator) stream); - return OperationAsyncIterator.create( - this, - sourceCallback -> - new MapStreamHandler<>(new FlattenStreamHandler<>(sourceCallback), toIteratorMapper)); - } - - @Override - public AsyncIterator map(Function mapper) { - return OperationAsyncIterator.create( - this, sourceCallback -> new MapStreamHandler<>(sourceCallback, mapper)); - } - - @Override - public AsyncIterator filter(Predicate filter) { - return OperationAsyncIterator.create( - this, sourceCallback -> new FilteringStreamHandler<>(sourceCallback, filter)); + public AsyncIterator transform(AsyncStreamTransformer transformer) { + return new TransformAsyncIterator<>(this, transformer); } @Override public void consume(AsyncStreamHandler consumer) { iterate(consumer); } - - @Override - public AsyncStream slice(BaseSlicer slicer) { - return OperationAsyncIterator.create( - this, sourceCallback -> new SliceStreamHandler<>(sourceCallback, slicer)); - } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java index 290668e2111..86ff1892918 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncQueue.java @@ -15,6 +15,11 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; +/** + * FIFO queue analogous to {@link java.util.concurrent.BlockingQueue} which returns {@link + * SafeFuture} element promise from {@link #take()} instead of blocking when no elements available + * in the queue. + */ interface AsyncQueue { void put(T item); diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java index 9d93e3ac6c1..dee8681eba8 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStream.java @@ -13,13 +13,23 @@ package tech.pegasys.teku.infrastructure.async.stream; +import static tech.pegasys.teku.infrastructure.async.stream.Util.noCallBinaryOperator; + +import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; import java.util.stream.Stream; +import tech.pegasys.teku.infrastructure.async.SafeFuture; /** Similar to {@link java.util.stream.Stream} but may perform async operations */ -public interface AsyncStream extends AsyncStreamTransform, AsyncStreamConsume { +public interface AsyncStream extends AsyncStreamBase { static AsyncStream empty() { return of(); @@ -51,4 +61,83 @@ static AsyncStream create(CompletionStage future) { static AsyncStreamPublisher createPublisher(int maxBufferSize) { return new BufferingStreamPublisher<>(maxBufferSize); } + + // transformation + + default AsyncStream flatMap(Function> toStreamMapper) { + return map(toStreamMapper).transform(FlattenStreamHandler::new); + } + + default AsyncStream map(Function mapper) { + return transform(sourceCallback -> new MapStreamHandler<>(sourceCallback, mapper)); + } + + default AsyncStream filter(Predicate filter) { + return transform(sourceCallback -> new FilteringStreamHandler<>(sourceCallback, filter)); + } + + default AsyncStream peek(AsyncStreamVisitor visitor) { + return transform(src -> new VisitorHandler<>(src, visitor)); + } + + default AsyncStream mapAsync(Function> mapper) { + return flatMap(e -> AsyncStream.create(mapper.apply(e))); + } + + // slicing + + default AsyncStream slice(AsyncStreamSlicer slicer) { + return transform(sourceCallback -> new SliceStreamHandler<>(sourceCallback, slicer)); + } + + default AsyncStream limit(long count) { + return slice(AsyncStreamSlicer.limit(count)); + } + + default AsyncStream takeWhile(Predicate whileCondition) { + return slice(AsyncStreamSlicer.takeWhile(whileCondition)); + } + + default AsyncStream takeUntil(Predicate untilCondition, boolean includeLast) { + AsyncStreamSlicer whileSlicer = AsyncStreamSlicer.takeWhile(untilCondition.negate()); + AsyncStreamSlicer untilSlicer = + includeLast ? whileSlicer.then(AsyncStreamSlicer.limit(1)) : whileSlicer; + return slice(untilSlicer); + } + + // consuming + + default SafeFuture collect(Collector collector) { + AsyncIteratorCollector asyncIteratorCollector = + new AsyncIteratorCollector<>(collector); + consume(asyncIteratorCollector); + return asyncIteratorCollector.getPromise(); + } + + default SafeFuture> findFirst() { + return this.limit(1) + .toList() + .thenApply(l -> l.isEmpty() ? Optional.empty() : Optional.of(l.getFirst())); + } + + default SafeFuture forEach(Consumer consumer) { + return collect(Collector.of(() -> null, (a, t) -> consumer.accept(t), noCallBinaryOperator())); + } + + default > SafeFuture collect(C targetCollection) { + return collect(Collectors.toCollection(() -> targetCollection)); + } + + default SafeFuture> toList() { + return collect(Collectors.toUnmodifiableList()); + } + + default SafeFuture> findLast() { + return collectLast(1) + .thenApply(l -> l.isEmpty() ? Optional.empty() : Optional.of(l.getFirst())); + } + + default SafeFuture> collectLast(int count) { + return collect(CircularBuf.createCollector(count)); + } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamBase.java similarity index 78% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamBase.java index 48a103474b3..0f2a172aaff 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamConsume.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamBase.java @@ -13,11 +13,9 @@ package tech.pegasys.teku.infrastructure.async.stream; -/** - * Contains fundamental terminal (reduce or collect) stream methods All other terminal methods are - * expressed my means of those methods - */ -public interface BaseAsyncStreamConsume { +public interface AsyncStreamBase { + + AsyncStream transform(AsyncStreamTransformer transformer); void consume(AsyncStreamHandler consumer); } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java deleted file mode 100644 index 2f0464bf37b..00000000000 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamConsume.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2024 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package tech.pegasys.teku.infrastructure.async.stream; - -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.function.BinaryOperator; -import java.util.function.Consumer; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import tech.pegasys.teku.infrastructure.async.SafeFuture; - -public interface AsyncStreamConsume extends BaseAsyncStreamConsume, AsyncStreamTransform { - - default SafeFuture collect(Collector collector) { - AsyncIteratorCollector asyncIteratorCollector = - new AsyncIteratorCollector<>(collector); - consume(asyncIteratorCollector); - return asyncIteratorCollector.getPromise(); - } - - default SafeFuture> findFirst() { - return this.limit(1) - .toList() - .thenApply(l -> l.isEmpty() ? Optional.empty() : Optional.of(l.getFirst())); - } - - default SafeFuture forEach(Consumer consumer) { - return collect(Collector.of(() -> null, (a, t) -> consumer.accept(t), noCallBinaryOperator())); - } - - default > SafeFuture collect(C targetCollection) { - return collect(Collectors.toCollection(() -> targetCollection)); - } - - default SafeFuture> toList() { - return collect(Collectors.toUnmodifiableList()); - } - - default SafeFuture> findLast() { - return collectLast(1) - .thenApply(l -> l.isEmpty() ? Optional.empty() : Optional.of(l.getFirst())); - } - - default SafeFuture> collectLast(int count) { - class CircularBuf { - final ArrayDeque buf; - final int maxSize; - - public CircularBuf(int maxSize) { - buf = new ArrayDeque<>(maxSize); - this.maxSize = maxSize; - } - - public void add(C t) { - if (buf.size() == maxSize) { - buf.removeFirst(); - } - buf.add(t); - } - } - return collect( - Collector.of( - () -> new CircularBuf(count), - CircularBuf::add, - noCallBinaryOperator(), - buf -> buf.buf.stream().toList())); - } - - private static BinaryOperator noCallBinaryOperator() { - return (c, c2) -> { - throw new UnsupportedOperationException("Shouldn't be called"); - }; - } -} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamSlicer.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamSlicer.java new file mode 100644 index 00000000000..b29249d3bd1 --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamSlicer.java @@ -0,0 +1,73 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import static tech.pegasys.teku.infrastructure.async.stream.AsyncStreamSlicer.SliceResult.CONTINUE; +import static tech.pegasys.teku.infrastructure.async.stream.AsyncStreamSlicer.SliceResult.INCLUDE_AND_STOP; +import static tech.pegasys.teku.infrastructure.async.stream.AsyncStreamSlicer.SliceResult.SKIP_AND_STOP; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +public interface AsyncStreamSlicer { + + enum SliceResult { + CONTINUE, + INCLUDE_AND_STOP, + SKIP_AND_STOP + } + + SliceResult slice(T element); + + static AsyncStreamSlicer limit(long count) { + return new AsyncStreamSlicer<>() { + private final AtomicLong remainCount = new AtomicLong(count); + + @Override + public SliceResult slice(T element) { + return remainCount.decrementAndGet() > 0 ? CONTINUE : INCLUDE_AND_STOP; + } + }; + } + + static AsyncStreamSlicer takeWhile(Predicate condition) { + return t -> condition.test(t) ? CONTINUE : SKIP_AND_STOP; + } + + default AsyncStreamSlicer then(AsyncStreamSlicer nextSlicer) { + return new AsyncStreamSlicer<>() { + private boolean thisSlicerCompleted = false; + + @Override + public SliceResult slice(T element) { + if (thisSlicerCompleted) { + return nextSlicer.slice(element); + } else { + SliceResult result = AsyncStreamSlicer.this.slice(element); + return switch (result) { + case CONTINUE -> result; + case SKIP_AND_STOP -> { + thisSlicerCompleted = true; + yield nextSlicer.slice(element); + } + case INCLUDE_AND_STOP -> { + thisSlicerCompleted = true; + yield CONTINUE; + } + }; + } + } + }; + } +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransform.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransform.java deleted file mode 100644 index 90046be35e4..00000000000 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransform.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2024 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package tech.pegasys.teku.infrastructure.async.stream; - -import static tech.pegasys.teku.infrastructure.async.stream.BaseAsyncStreamTransform.SliceResult.CONTINUE; -import static tech.pegasys.teku.infrastructure.async.stream.BaseAsyncStreamTransform.SliceResult.INCLUDE_AND_STOP; -import static tech.pegasys.teku.infrastructure.async.stream.BaseAsyncStreamTransform.SliceResult.SKIP_AND_STOP; - -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import tech.pegasys.teku.infrastructure.async.SafeFuture; - -public interface AsyncStreamTransform extends BaseAsyncStreamTransform { - - interface Slicer extends BaseSlicer { - - static Slicer limit(long count) { - return new Slicer<>() { - private final AtomicLong remainCount = new AtomicLong(count); - - @Override - public SliceResult slice(T element) { - return remainCount.decrementAndGet() > 0 ? CONTINUE : INCLUDE_AND_STOP; - } - }; - } - - static Slicer takeWhile(Predicate condition) { - return t -> condition.test(t) ? CONTINUE : SKIP_AND_STOP; - } - - default Slicer then(Slicer nextSlicer) { - return new Slicer() { - private boolean thisSlicerCompleted = false; - - @Override - public SliceResult slice(T element) { - if (thisSlicerCompleted) { - return nextSlicer.slice(element); - } else { - SliceResult result = Slicer.this.slice(element); - return switch (result) { - case CONTINUE -> result; - case SKIP_AND_STOP -> { - thisSlicerCompleted = true; - yield nextSlicer.slice(element); - } - case INCLUDE_AND_STOP -> { - thisSlicerCompleted = true; - yield CONTINUE; - } - }; - } - } - }; - } - } - - // transformation - - // Suboptimal reference implementation - // To be overridden in implementation with a faster variant - default AsyncStream map(Function mapper) { - return flatMap(t -> AsyncStream.of(mapper.apply(t))); - } - - // Suboptimal reference implementation - // To be overridden in implementation with a faster variant - default AsyncStream filter(Predicate filter) { - return flatMap(t -> filter.test(t) ? AsyncStream.of(t) : AsyncStream.empty()); - } - - default AsyncStream peek(Consumer visitor) { - return map( - t -> { - visitor.accept(t); - return t; - }); - } - - default AsyncStream mapAsync(Function> mapper) { - return flatMap(e -> AsyncStream.create(mapper.apply(e))); - } - - // slicing - - default AsyncStream limit(long count) { - return slice(Slicer.limit(count)); - } - - default AsyncStream takeWhile(Predicate whileCondition) { - return slice(Slicer.takeWhile(whileCondition)); - } - - default AsyncStream takeUntil(Predicate untilCondition, boolean includeLast) { - Slicer whileSlicer = Slicer.takeWhile(untilCondition.negate()); - Slicer untilSlicer = includeLast ? whileSlicer.then(Slicer.limit(1)) : whileSlicer; - return slice(untilSlicer); - } -} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransformer.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransformer.java new file mode 100644 index 00000000000..1b0f747e1a6 --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamTransformer.java @@ -0,0 +1,19 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +public interface AsyncStreamTransformer { + + AsyncStreamHandler process(AsyncStreamHandler downstreamHandler); +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamVisitor.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamVisitor.java new file mode 100644 index 00000000000..621a5252249 --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamVisitor.java @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +public interface AsyncStreamVisitor { + + void onNext(T t); + + void onComplete(); + + void onError(Throwable t); +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/CircularBuf.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/CircularBuf.java new file mode 100644 index 00000000000..9d71a1d1b2c --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/CircularBuf.java @@ -0,0 +1,45 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import static tech.pegasys.teku.infrastructure.async.stream.Util.noCallBinaryOperator; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.stream.Collector; + +class CircularBuf { + final ArrayDeque buf; + final int maxSize; + + public CircularBuf(int maxSize) { + buf = new ArrayDeque<>(maxSize); + this.maxSize = maxSize; + } + + public void add(C t) { + if (buf.size() == maxSize) { + buf.removeFirst(); + } + buf.add(t); + } + + public static Collector> createCollector(int count) { + return Collector., List>of( + () -> new CircularBuf(count), + CircularBuf::add, + noCallBinaryOperator(), + buf -> buf.buf.stream().toList()); + } +} diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java index 2aea4ed4f52..d3d31662f6e 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/FlattenStreamHandler.java @@ -15,7 +15,7 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; -class FlattenStreamHandler, T> +class FlattenStreamHandler, T> extends AbstractDelegatingStreamHandler { protected FlattenStreamHandler(AsyncStreamHandler delegate) { @@ -25,7 +25,7 @@ protected FlattenStreamHandler(AsyncStreamHandler delegate) { @Override public SafeFuture onNext(TCol asyncIterator) { SafeFuture ret = new SafeFuture<>(); - asyncIterator.iterate( + asyncIterator.consume( new AsyncStreamHandler() { @Override public SafeFuture onNext(T t) { diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java index 6dedfe7bc63..5f0499f0f38 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/LimitedAsyncQueue.java @@ -31,17 +31,23 @@ public LimitedAsyncQueue(int maxSize) { // Adds an item to the queue @Override - public synchronized void put(T item) { - if (!takers.isEmpty()) { - // If there are pending takers, complete one with the item - CompletableFuture taker = takers.poll(); - taker.complete(item); - } else { - // Otherwise, add the item to the items queue - if (items.size() >= maxSize) { - throw new IllegalStateException("Buffer size overflow: " + maxSize); + public void put(T item) { + final CompletableFuture maybeTaker; + synchronized (this) { + if (!takers.isEmpty()) { + // If there are pending takers, complete one with the item + maybeTaker = takers.poll(); + } else { + // Otherwise, add the item to the items queue + if (items.size() >= maxSize) { + throw new IllegalStateException("Buffer size overflow: " + maxSize); + } + items.offer(item); + maybeTaker = null; } - items.offer(item); + } + if (maybeTaker != null) { + maybeTaker.complete(item); } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java index e2be0f0773b..50bbf38253c 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/SliceStreamHandler.java @@ -17,17 +17,16 @@ class SliceStreamHandler extends AbstractDelegatingStreamHandler { - private final BaseAsyncStreamTransform.BaseSlicer slicer; + private final AsyncStreamSlicer slicer; - protected SliceStreamHandler( - AsyncStreamHandler delegate, BaseAsyncStreamTransform.BaseSlicer slicer) { + protected SliceStreamHandler(AsyncStreamHandler delegate, AsyncStreamSlicer slicer) { super(delegate); this.slicer = slicer; } @Override public SafeFuture onNext(T t) { - BaseAsyncStreamTransform.SliceResult sliceResult = slicer.slice(t); + AsyncStreamSlicer.SliceResult sliceResult = slicer.slice(t); return switch (sliceResult) { case CONTINUE -> delegate.onNext(t); case SKIP_AND_STOP -> { diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/TransformAsyncIterator.java similarity index 52% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/TransformAsyncIterator.java index 2ff5488c189..d1d4ae75f0a 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/OperationAsyncIterator.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/TransformAsyncIterator.java @@ -13,31 +13,18 @@ package tech.pegasys.teku.infrastructure.async.stream; -import java.util.function.Function; - -abstract class OperationAsyncIterator extends AsyncIterator { +class TransformAsyncIterator extends AsyncIterator { private final AsyncIterator delegateIterator; + private final AsyncStreamTransformer streamTransformer; - public static AsyncIterator create( - AsyncIterator srcIterator, - Function, AsyncStreamHandler> callbackMapper) { - return new OperationAsyncIterator<>(srcIterator) { - @Override - protected AsyncStreamHandler createDelegateCallback(AsyncStreamHandler sourceCallback) { - return callbackMapper.apply(sourceCallback); - } - }; - } - - public OperationAsyncIterator(AsyncIterator delegateIterator) { + public TransformAsyncIterator( + AsyncIterator delegateIterator, AsyncStreamTransformer streamTransformer) { this.delegateIterator = delegateIterator; + this.streamTransformer = streamTransformer; } - protected abstract AsyncStreamHandler createDelegateCallback( - AsyncStreamHandler sourceCallback); - @Override public void iterate(AsyncStreamHandler callback) { - delegateIterator.iterate(createDelegateCallback(callback)); + delegateIterator.iterate(streamTransformer.process(callback)); } } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamTransform.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/Util.java similarity index 57% rename from infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamTransform.java rename to infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/Util.java index 2b2da464fdd..d747448b1ee 100644 --- a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/BaseAsyncStreamTransform.java +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/Util.java @@ -13,25 +13,12 @@ package tech.pegasys.teku.infrastructure.async.stream; -import java.util.function.Function; +import java.util.function.BinaryOperator; -/** - * Contains fundamental transformation stream methods All other transformations are expressed my - * means of those methods - */ -public interface BaseAsyncStreamTransform { - - enum SliceResult { - CONTINUE, - INCLUDE_AND_STOP, - SKIP_AND_STOP - } - - interface BaseSlicer { - SliceResult slice(T element); +class Util { + static BinaryOperator noCallBinaryOperator() { + return (c, c2) -> { + throw new UnsupportedOperationException("Shouldn't be called"); + }; } - - AsyncStream flatMap(Function> toStreamMapper); - - AsyncStream slice(BaseSlicer slicer); } diff --git a/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/VisitorHandler.java b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/VisitorHandler.java new file mode 100644 index 00000000000..68d7af1b08c --- /dev/null +++ b/infrastructure/async/src/main/java/tech/pegasys/teku/infrastructure/async/stream/VisitorHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.infrastructure.async.stream; + +import tech.pegasys.teku.infrastructure.async.SafeFuture; + +class VisitorHandler extends AbstractDelegatingStreamHandler { + private final AsyncStreamVisitor visitor; + + public VisitorHandler(AsyncStreamHandler delegate, AsyncStreamVisitor visitor) { + super(delegate); + this.visitor = visitor; + } + + @Override + public void onComplete() { + visitor.onComplete(); + delegate.onComplete(); + } + + @Override + public void onError(Throwable t) { + visitor.onError(t); + delegate.onError(t); + } + + @Override + public SafeFuture onNext(T t) { + visitor.onNext(t); + return delegate.onNext(t); + } +} diff --git a/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java b/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java index 8913f0c4f16..d723fb8a8bd 100644 --- a/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java +++ b/infrastructure/async/src/test/java/tech/pegasys/teku/infrastructure/async/stream/AsyncStreamPublisherTest.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -25,19 +27,19 @@ public class AsyncStreamPublisherTest { AsyncStreamPublisher publisher = AsyncStream.createPublisher(Integer.MAX_VALUE); + AsyncStream stream = + publisher + .flatMap(i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) + .filter(i -> i % 2 == 0) + .map(i -> i * 10) + .limit(10); + + // List expectedValues = List.of(0, 20, 40, 100, 120, 140, 200, 220, 240, 300); @Test void sanityTest() { - ArrayList collector = new ArrayList<>(); - - SafeFuture> listPromise = - publisher - .flatMap( - i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) - .filter(i -> i % 2 == 0) - .map(i -> i * 10) - .limit(10) - .collect(collector); + List collector = new ArrayList<>(); + SafeFuture> listPromise = stream.collect(collector); assertThat(collector).isEmpty(); @@ -72,14 +74,7 @@ void sanityTest() { @Test void completeShouldCompleteStream() { - SafeFuture> listPromise = - publisher - .flatMap( - i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) - .filter(i -> i % 2 == 0) - .map(i -> i * 10) - .toList(); - + SafeFuture> listPromise = stream.toList(); publisher.onNext(0); publisher.onComplete(); assertThat(listPromise).isCompletedWithValue(List.of(0, 20, 40)); @@ -87,16 +82,88 @@ void completeShouldCompleteStream() { @Test void errorShouldCompleteStream() { - SafeFuture> listPromise = - publisher - .flatMap( - i -> AsyncStream.create(IntStream.range(i * 10, i * 10 + 5).boxed().iterator())) - .filter(i -> i % 2 == 0) - .map(i -> i * 10) - .toList(); - + SafeFuture> listPromise = stream.toList(); publisher.onNext(0); publisher.onError(new RuntimeException("test")); assertThat(listPromise).isCompletedExceptionally(); } + + @Test + void publishingAllPriorToConsumeShouldWork() { + publisher.onNext(0); + publisher.onNext(1); + publisher.onComplete(); + + assertThat(stream.toList()).isCompletedWithValue(List.of(0, 20, 40, 100, 120, 140)); + } + + @Test + void publishingPartiallyPriorToConsumeShouldWork() { + publisher.onNext(0); + SafeFuture> list = stream.toList(); + publisher.onNext(1); + publisher.onComplete(); + + assertThat(list).isCompletedWithValue(List.of(0, 20, 40, 100, 120, 140)); + } + + @Test + void issuingErrorPriorToConsumeShouldWork() { + publisher.onNext(0); + publisher.onError(new RuntimeException("test")); + + assertThat(stream.toList()).isCompletedExceptionally(); + } + + @Test + void shouldIgnoreAnyItemsAfterOnComplete() { + publisher.onNext(0); + publisher.onComplete(); + publisher.onNext(1); + + assertThat(stream.toList()).isCompletedWithValue(List.of(0, 20, 40)); + } + + @Test + void sanityThreadSafetyTest() throws Exception { + AsyncStreamPublisher myPublisher = AsyncStream.createPublisher(Integer.MAX_VALUE); + AsyncStream stream = + myPublisher + .map(i -> i * 10) + .flatMap(i -> AsyncStream.of(i / 10, 777777)) + .filter(i -> i != 777777); + + int threadCount = 16; + CountDownLatch startLatch = new CountDownLatch(threadCount); + CountDownLatch finishLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + int finalI = i; + new Thread( + () -> { + startLatch.countDown(); + try { + startLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + for (int j = 0; j < 1000; j++) { + myPublisher.onNext(finalI * 1000 + j); + } + finishLatch.countDown(); + }) + .start(); + } + SafeFuture> listPromise = stream.toList(); + + boolean rc = finishLatch.await(5, TimeUnit.SECONDS); + assertThat(rc).isTrue(); + + myPublisher.onComplete(); + + List list = listPromise.get(5, TimeUnit.SECONDS); + + assertThat(list) + .containsExactlyInAnyOrderElementsOf( + IntStream.range(0, threadCount * 1000).boxed().toList()); + } } From 8f49253b9cd37078d4f667df2e59452597c6fdad Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 17 Oct 2024 17:53:35 +0300 Subject: [PATCH 7/9] Add non-verbose DAS Gossip logger (#168) Add a dedicated DAS Gossip logging class which doesn't produce too much noise in logs by batching logging events once per second --- .../datacolumns/DasPreSampler.java | 2 +- .../DataColumnSidecarManagerImpl.java | 7 +- .../log/gossip/DasGossipBatchLogger.java | 236 ++++++++++++++++++ .../log/gossip/DasGossipLogger.java | 37 +++ .../datacolumns/log/gossip/GossipLogger.java | 24 ++ .../log/gossip/SubnetGossipLogger.java | 21 ++ .../datacolumns/util/StringifyUtil.java | 10 +- .../eth2/Eth2P2PNetworkBuilder.java | 10 +- .../DataColumnSidecarGossipManager.java | 26 +- .../GossipForkSubscriptionsEip7594.java | 8 +- .../eth2/Eth2P2PNetworkFactory.java | 4 +- .../beaconchain/BeaconChainController.java | 8 +- 12 files changed, 365 insertions(+), 28 deletions(-) create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipBatchLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/GossipLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/SubnetGossipLogger.java diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DasPreSampler.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DasPreSampler.java index 8dcc2f21130..b7390edfd31 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DasPreSampler.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DasPreSampler.java @@ -44,7 +44,7 @@ public void onNewPreImportBlocks(Collection blocks) { "DasPreSampler: requesting pre-sample for {} (of {} received) blocks: {}", blocksToSample.size(), blocks.size(), - StringifyUtil.toIntRangeString( + StringifyUtil.toIntRangeStringWithSize( blocksToSample.stream().map(block -> block.getSlot().intValue()).toList())); blocksToSample.forEach(this::onNewPreImportBlock); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarManagerImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarManagerImpl.java index 128ad4bd3d6..bc6cf7e44fd 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarManagerImpl.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/DataColumnSidecarManagerImpl.java @@ -18,6 +18,7 @@ import tech.pegasys.teku.infrastructure.subscribers.Subscribers; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; @@ -26,9 +27,12 @@ public class DataColumnSidecarManagerImpl implements DataColumnSidecarManager { private final DataColumnSidecarGossipValidator validator; private final Subscribers validDataColumnSidecarsSubscribers = Subscribers.create(true); + private final DasGossipLogger dasGossipLogger; - public DataColumnSidecarManagerImpl(DataColumnSidecarGossipValidator validator) { + public DataColumnSidecarManagerImpl( + DataColumnSidecarGossipValidator validator, DasGossipLogger dasGossipLogger) { this.validator = validator; + this.dasGossipLogger = dasGossipLogger; } @Override @@ -38,6 +42,7 @@ public SafeFuture onDataColumnSidecarGossip( .validate(dataColumnSidecar) .thenPeek( res -> { + dasGossipLogger.onReceive(dataColumnSidecar, res); if (res.isAccept()) { validDataColumnSidecarsSubscribers.forEach( listener -> listener.onNewValidSidecar(dataColumnSidecar)); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipBatchLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipBatchLogger.java new file mode 100644 index 00000000000..ffc8d6b647b --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipBatchLogger.java @@ -0,0 +1,236 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.gossip; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.logging.LogFormatter; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; +import tech.pegasys.teku.statetransition.datacolumns.util.StringifyUtil; +import tech.pegasys.teku.statetransition.validation.InternalValidationResult; +import tech.pegasys.teku.statetransition.validation.ValidationResultCode; + +public class DasGossipBatchLogger implements DasGossipLogger { + private static final Logger LOG = LogManager.getLogger(DasGossipLogger.class); + private final TimeProvider timeProvider; + + private List events = new ArrayList<>(); + + public DasGossipBatchLogger(AsyncRunner asyncRunner, TimeProvider timeProvider) { + this.timeProvider = timeProvider; + asyncRunner.runWithFixedDelay( + this::logBatchedEvents, + Duration.ofSeconds(1), + err -> LOG.info("DasGossipBatchLogger error: {}", err.toString())); + } + + interface Event { + + long time(); + } + + interface ColumnEvent extends Event { + DataColumnSidecar sidecar(); + } + + record ReceiveEvent( + long time, DataColumnSidecar sidecar, InternalValidationResult validationResult) + implements ColumnEvent {} + + record PublishEvent(long time, DataColumnSidecar sidecar, Optional result) + implements ColumnEvent {} + + record SubscribeEvent(long time, int subnetId) implements Event {} + + record UnsubscribeEvent(long time, int subnetId) implements Event {} + + private void logBatchedEvents() { + final List eventsLoc; + synchronized (this) { + if (events.isEmpty()) { + return; + } + eventsLoc = events; + events = new ArrayList<>(); + } + + groupByBlock(ReceiveEvent.class, eventsLoc).forEach(this::logReceiveEvents); + groupByBlock(PublishEvent.class, eventsLoc).forEach(this::logPublishEvents); + logSubscriptionEvents(eventsLoc); + } + + private void logReceiveEvents(SlotAndBlockRoot blockId, List events) { + Map> eventsByValidateCode = + events.stream().collect(Collectors.groupingBy(e -> e.validationResult().code())); + eventsByValidateCode.forEach( + (validationCode, codeEvents) -> { + Level level = validationCode == ValidationResultCode.REJECT ? Level.INFO : Level.DEBUG; + LOG.log( + level, + "Received {} data columns (validation result: {}) by gossip {} for block {}: {}", + codeEvents.size(), + validationCode, + msAgoString(codeEvents), + blockIdString(blockId), + columnIndexesString(codeEvents)); + }); + } + + private void logPublishEvents(SlotAndBlockRoot blockId, List events) { + Map>, List> eventsByError = + events.stream() + .collect( + Collectors.groupingBy( + e -> e.result().map(thr -> ExceptionUtils.getRootCause(thr).getClass()))); + eventsByError.forEach( + (maybeErrorClass, errEvents) -> { + Optional someError = errEvents.getFirst().result(); + someError.ifPresentOrElse( + error -> { + LOG.info( + "Error publishing {} data columns ({}) by gossip {} for block {}: {}", + errEvents.size(), + columnIndexesString(errEvents), + msAgoString(errEvents), + blockIdString(blockId), + error); + }, + () -> { + LOG.debug( + "Published {} data columns by gossip {} for block {}: {}", + errEvents.size(), + msAgoString(errEvents), + blockIdString(blockId), + columnIndexesString(errEvents)); + }); + }); + } + + private void logSubscriptionEvents(List events) { + List subscribedSubnets = new ArrayList<>(); + List unsubscribedSubnets = new ArrayList<>(); + events.forEach( + e -> { + switch (e) { + case SubscribeEvent event -> subscribedSubnets.add(event.subnetId()); + case UnsubscribeEvent event -> unsubscribedSubnets.add(event.subnetId()); + default -> {} + } + }); + + if (!(subscribedSubnets.isEmpty() && unsubscribedSubnets.isEmpty())) { + String subscribeString = + subscribedSubnets.isEmpty() + ? "" + : "subscribed: " + StringifyUtil.toIntRangeStringWithSize(subscribedSubnets); + String unsubscribeString = + unsubscribedSubnets.isEmpty() + ? "" + : "unsubscribed: " + StringifyUtil.toIntRangeStringWithSize(unsubscribedSubnets); + String maybeDelim = subscribedSubnets.isEmpty() || unsubscribedSubnets.isEmpty() ? "" : ", "; + LOG.info( + "Data column gossip subnets subscriptions changed: " + + subscribeString + + maybeDelim + + unsubscribeString); + } + } + + private String columnIndexesString(List events) { + List columnIndexes = + events.stream().map(e -> e.sidecar().getIndex().intValue()).toList(); + return StringifyUtil.toIntRangeString(columnIndexes); + } + + private static String blockIdString(SlotAndBlockRoot blockId) { + return "#" + + blockId.getSlot() + + " (0x" + + LogFormatter.formatAbbreviatedHashRoot(blockId.getBlockRoot()) + + ")"; + } + + private String msAgoString(List events) { + long curTime = timeProvider.getTimeInMillis().longValue(); + long firstMillisAgo = curTime - events.getFirst().time(); + long lastMillisAgo = curTime - events.getLast().time(); + return (lastMillisAgo == firstMillisAgo + ? lastMillisAgo + "ms" + : lastMillisAgo + "ms-" + firstMillisAgo + "ms") + + " ago"; + } + + private boolean needToLogEvent(boolean isSevereEvent) { + return LOG.isDebugEnabled() || (isSevereEvent && LOG.isInfoEnabled()); + } + + @Override + public synchronized void onReceive( + DataColumnSidecar sidecar, InternalValidationResult validationResult) { + if (needToLogEvent(validationResult.isReject())) { + events.add( + new ReceiveEvent(timeProvider.getTimeInMillis().longValue(), sidecar, validationResult)); + } + } + + @Override + public synchronized void onPublish(DataColumnSidecar sidecar, Optional result) { + if (needToLogEvent(result.isPresent())) { + events.add(new PublishEvent(timeProvider.getTimeInMillis().longValue(), sidecar, result)); + } + } + + @Override + public void onDataColumnSubnetSubscribe(int subnetId) { + if (needToLogEvent(false)) { + events.add(new SubscribeEvent(timeProvider.getTimeInMillis().longValue(), subnetId)); + } + } + + @Override + public void onDataColumnSubnetUnsubscribe(int subnetId) { + if (needToLogEvent(false)) { + events.add(new UnsubscribeEvent(timeProvider.getTimeInMillis().longValue(), subnetId)); + } + } + + private static + SortedMap> groupByBlock( + Class eventClass, List allEvents) { + SortedMap> eventsByBlock = new TreeMap<>(); + for (Event event : allEvents) { + if (eventClass.isAssignableFrom(event.getClass())) { + @SuppressWarnings("unchecked") + TEvent e = (TEvent) event; + DataColumnSidecar sidecar = e.sidecar(); + SlotAndBlockRoot blockId = new SlotAndBlockRoot(sidecar.getSlot(), sidecar.getBlockRoot()); + eventsByBlock.computeIfAbsent(blockId, __ -> new ArrayList<>()).add(e); + } + } + return eventsByBlock; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipLogger.java new file mode 100644 index 00000000000..66e7f787f41 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/DasGossipLogger.java @@ -0,0 +1,37 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.gossip; + +import java.util.Optional; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.statetransition.validation.InternalValidationResult; + +public interface DasGossipLogger extends SubnetGossipLogger { + + DasGossipLogger NOOP = + new DasGossipLogger() { + @Override + public void onReceive( + DataColumnSidecar sidecar, InternalValidationResult validationResult) {} + + @Override + public void onPublish(DataColumnSidecar sidecar, Optional result) {} + + @Override + public void onDataColumnSubnetSubscribe(int subnetId) {} + + @Override + public void onDataColumnSubnetUnsubscribe(int subnetId) {} + }; +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/GossipLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/GossipLogger.java new file mode 100644 index 00000000000..c1346155e2d --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/GossipLogger.java @@ -0,0 +1,24 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.gossip; + +import java.util.Optional; +import tech.pegasys.teku.statetransition.validation.InternalValidationResult; + +public interface GossipLogger { + + void onReceive(TMessage message, InternalValidationResult validationResult); + + void onPublish(TMessage message, Optional result); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/SubnetGossipLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/SubnetGossipLogger.java new file mode 100644 index 00000000000..f12a34ab6d4 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/gossip/SubnetGossipLogger.java @@ -0,0 +1,21 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.gossip; + +public interface SubnetGossipLogger extends GossipLogger { + + void onDataColumnSubnetSubscribe(int subnetId); + + void onDataColumnSubnetUnsubscribe(int subnetId); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java index 01a5fbb85c4..19e14fbc373 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java @@ -64,13 +64,13 @@ public static String columnIndexesToString(Collection indexes, int maxC } } + public static String toIntRangeStringWithSize(Collection ints) { + return "(size: " + ints.size() + ") " + toIntRangeString(ints); + } + public static String toIntRangeString(Collection ints) { List ranges = reduceToIntRanges(ints); - return "(size: " - + ints.size() - + ") [" - + ranges.stream().map(Objects::toString).collect(Collectors.joining(",")) - + "]"; + return "[" + ranges.stream().map(Objects::toString).collect(Collectors.joining(",")) + "]"; } private record IntRange(int first, int last) { diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java index 7498c59c014..432f1049b31 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java @@ -88,6 +88,7 @@ import tech.pegasys.teku.spec.datastructures.util.ForkAndSpecMilestone; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.store.KeyValueStore; @@ -134,6 +135,7 @@ public class Eth2P2PNetworkBuilder { protected StatusMessageFactory statusMessageFactory; protected KZG kzg; protected boolean recordMessageArrival; + private DasGossipLogger dasGossipLogger; protected Eth2P2PNetworkBuilder() {} @@ -331,7 +333,8 @@ private GossipForkSubscriptions createSubscriptions( gossipedSignedContributionAndProofProcessor, gossipedSyncCommitteeMessageProcessor, gossipedSignedBlsToExecutionChangeProcessor, - dataColumnSidecarOperationProcessor); + dataColumnSidecarOperationProcessor, + dasGossipLogger); }; } @@ -652,4 +655,9 @@ public Eth2P2PNetworkBuilder recordMessageArrival(final boolean recordMessageArr this.recordMessageArrival = recordMessageArrival; return this; } + + public Eth2P2PNetworkBuilder gossipDasLogger(DasGossipLogger dasGossipLogger) { + this.dasGossipLogger = dasGossipLogger; + return this; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java index 69ab4a58369..289717a9ace 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/DataColumnSidecarGossipManager.java @@ -13,44 +13,38 @@ package tech.pegasys.teku.networking.eth2.gossip; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import java.util.Optional; import tech.pegasys.teku.networking.eth2.gossip.subnets.DataColumnSidecarSubnetSubscriptions; import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; public class DataColumnSidecarGossipManager implements GossipManager { - private static final Logger LOG = LogManager.getLogger(); - - private final GossipFailureLogger gossipFailureLogger = - new GossipFailureLogger("data column sidecar"); private final DataColumnSidecarSubnetSubscriptions subnetSubscriptions; + private final DasGossipLogger dasGossipLogger; public DataColumnSidecarGossipManager( - final DataColumnSidecarSubnetSubscriptions dataColumnSidecarSubnetSubscriptions) { + final DataColumnSidecarSubnetSubscriptions dataColumnSidecarSubnetSubscriptions, + DasGossipLogger dasGossipLogger) { subnetSubscriptions = dataColumnSidecarSubnetSubscriptions; + this.dasGossipLogger = dasGossipLogger; } public void publish(final DataColumnSidecar dataColumnSidecar) { subnetSubscriptions .gossip(dataColumnSidecar) .finish( - __ -> - LOG.debug( - "Successfully published data column sidecar for slot {}", - dataColumnSidecar::toLogString), - error -> - gossipFailureLogger.logWithSuppression( - error, dataColumnSidecar.getSlot(), dataColumnSidecar.toLogString())); + __ -> dasGossipLogger.onPublish(dataColumnSidecar, Optional.empty()), + error -> dasGossipLogger.onPublish(dataColumnSidecar, Optional.of(error))); } public void subscribeToSubnetId(final int subnetId) { - LOG.trace("Subscribing to subnet ID {}", subnetId); subnetSubscriptions.subscribeToSubnetId(subnetId); + dasGossipLogger.onDataColumnSubnetSubscribe(subnetId); } public void unsubscribeFromSubnetId(final int subnetId) { - LOG.trace("Unsubscribing to subnet ID {}", subnetId); subnetSubscriptions.unsubscribeFromSubnetId(subnetId); + dasGossipLogger.onDataColumnSubnetUnsubscribe(subnetId); } @Override diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsEip7594.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsEip7594.java index 1182cde1c48..cd44bbb0a53 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsEip7594.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/forks/versions/GossipForkSubscriptionsEip7594.java @@ -32,12 +32,14 @@ import tech.pegasys.teku.spec.datastructures.operations.versions.altair.ValidatableSyncCommitteeMessage; import tech.pegasys.teku.spec.datastructures.state.Fork; import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.storage.client.RecentChainData; public class GossipForkSubscriptionsEip7594 extends GossipForkSubscriptionsCapella { private final OperationProcessor dataColumnSidecarOperationProcessor; private DataColumnSidecarGossipManager dataColumnSidecarGossipManager; + public DasGossipLogger dasGossipLogger; public GossipForkSubscriptionsEip7594( final Fork fork, @@ -59,7 +61,8 @@ public GossipForkSubscriptionsEip7594( syncCommitteeMessageOperationProcessor, final OperationProcessor signedBlsToExecutionChangeOperationProcessor, - final OperationProcessor dataColumnSidecarOperationProcessor) { + final OperationProcessor dataColumnSidecarOperationProcessor, + DasGossipLogger dasGossipLogger) { super( fork, spec, @@ -78,6 +81,7 @@ public GossipForkSubscriptionsEip7594( syncCommitteeMessageOperationProcessor, signedBlsToExecutionChangeOperationProcessor); this.dataColumnSidecarOperationProcessor = dataColumnSidecarOperationProcessor; + this.dasGossipLogger = dasGossipLogger; } @Override @@ -99,7 +103,7 @@ void addDataColumnSidecarGossipManager(final ForkInfo forkInfo) { forkInfo); dataColumnSidecarGossipManager = - new DataColumnSidecarGossipManager(dataColumnSidecarSubnetSubscriptions); + new DataColumnSidecarGossipManager(dataColumnSidecarSubnetSubscriptions, dasGossipLogger); addGossipManager(dataColumnSidecarGossipManager); } diff --git a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java index f6d0bf116c8..12a0d4707db 100644 --- a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java +++ b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java @@ -106,6 +106,7 @@ import tech.pegasys.teku.statetransition.BeaconChainUtil; import tech.pegasys.teku.statetransition.block.VerifiedBlockOperationsListener; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.storage.api.StorageQueryChannel; import tech.pegasys.teku.storage.api.StubStorageQueryChannel; import tech.pegasys.teku.storage.client.CombinedChainDataClient; @@ -478,7 +479,8 @@ private GossipForkSubscriptions createSubscriptions( signedContributionAndProofProcessor, syncCommitteeMessageProcessor, signedBlsToExecutionChangeProcessor, - dataColumnSidecarOperationProcessor); + dataColumnSidecarOperationProcessor, + DasGossipLogger.NOOP); }; } diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index e4a93c32fe3..7c6beae79c7 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -160,6 +160,8 @@ import tech.pegasys.teku.statetransition.datacolumns.UpdatableDataColumnSidecarCustody; import tech.pegasys.teku.statetransition.datacolumns.db.DataColumnSidecarDB; import tech.pegasys.teku.statetransition.datacolumns.db.DataColumnSidecarDbAccessor; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipBatchLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; import tech.pegasys.teku.statetransition.datacolumns.retriever.DasPeerCustodyCountSupplier; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnPeerSearcher; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnReqResp; @@ -309,6 +311,7 @@ public class BeaconChainController extends Service implements BeaconChainControl protected volatile ForkChoiceStateProvider forkChoiceStateProvider; protected volatile ExecutionLayerChannel executionLayer; protected volatile GossipValidationHelper gossipValidationHelper; + protected volatile DasGossipLogger dasGossipLogger; protected volatile KZG kzg; protected volatile BlobSidecarManager blobSidecarManager; protected volatile DataColumnSidecarManager dataColumnSidecarManager; @@ -373,6 +376,7 @@ public BeaconChainController( "future_items_size", "Current number of items held for future slots, labelled by type", "type"); + this.dasGossipLogger = new DasGossipBatchLogger(operationPoolAsyncRunner, timeProvider); } @Override @@ -653,7 +657,8 @@ protected void initDataColumnSidecarManager() { MiscHelpersEip7594.required(spec.forMilestone(SpecMilestone.EIP7594).miscHelpers()), kzg, metricsSystem); - dataColumnSidecarManager = new DataColumnSidecarManagerImpl(dataColumnSidecarGossipValidator); + dataColumnSidecarManager = + new DataColumnSidecarManagerImpl(dataColumnSidecarGossipValidator, dasGossipLogger); eventChannels.subscribe( DataColumnSidecarGossipChannel.class, dataColumnSidecarManager::onDataColumnSidecarPublish); @@ -1363,6 +1368,7 @@ protected void initP2PNetwork() { .gossipedSignedContributionAndProofProcessor(syncCommitteeContributionPool::addRemote) .gossipedSyncCommitteeMessageProcessor(syncCommitteeMessagePool::addRemote) .gossipedSignedBlsToExecutionChangeProcessor(blsToExecutionChangePool::addRemote) + .gossipDasLogger(dasGossipLogger) .processedAttestationSubscriptionProvider( attestationManager::subscribeToAttestationsToSend) .metricsSystem(metricsSystem) From 40e2769c2ca8261452662180cf1716242cac1ae1 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Thu, 17 Oct 2024 20:10:54 +0300 Subject: [PATCH 8/9] Add non-verbose DAS Req/Resp logger (#170) --- .../log/rpc/AbstractDasResponseLogger.java | 131 ++++++++++++++++++ .../log/rpc/AbstractResponseLogger.java | 107 ++++++++++++++ .../log/rpc/DasByRangeResponseLogger.java | 70 ++++++++++ .../log/rpc/DasByRootResponseLogger.java | 84 +++++++++++ .../datacolumns/log/rpc/DasReqRespLogger.java | 49 +++++++ .../log/rpc/DasReqRespLoggerImpl.java | 75 ++++++++++ ...LoggingBatchDataColumnsByRangeReqResp.java | 52 +++++++ .../LoggingBatchDataColumnsByRootReqResp.java | 49 +++++++ .../datacolumns/log/rpc/LoggingPeerId.java | 43 ++++++ .../log/rpc/NoopReqRespMethodLogger.java | 43 ++++++ .../log/rpc/ReqRespMethodLogger.java | 21 +++ .../log/rpc/ReqRespResponseLogger.java | 44 ++++++ .../datacolumns/util/StringifyUtil.java | 11 +- .../datacolumns/util/StringifyUtilTest.java | 2 +- .../eth2/Eth2P2PNetworkBuilder.java | 10 +- .../eth2/peers/Eth2PeerManager.java | 13 +- .../rpc/beaconchain/BeaconChainMethods.java | 20 ++- ...taColumnSidecarsByRangeMessageHandler.java | 53 +++---- ...ataColumnSidecarsByRootMessageHandler.java | 54 ++++---- .../methods/LoggingResponseCallback.java | 54 ++++++++ .../eth2/peers/Eth2PeerManagerTest.java | 4 +- .../beaconchain/BeaconChainMethodsTest.java | 4 +- .../rpc/core/AbstractRequestHandlerTest.java | 4 +- .../eth2/Eth2P2PNetworkFactory.java | 4 +- .../beaconchain/BeaconChainController.java | 11 +- 25 files changed, 923 insertions(+), 89 deletions(-) create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java create mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java create mode 100644 networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java new file mode 100644 index 00000000000..9fa04779c6e --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractDasResponseLogger.java @@ -0,0 +1,131 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.logging.LogFormatter; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.util.StringifyUtil; + +abstract class AbstractDasResponseLogger + extends AbstractResponseLogger { + private static final Logger LOG = LogManager.getLogger(DasReqRespLogger.class); + + protected static final UInt64 UNKNOWN_SLOT = UInt64.MAX_VALUE; + + private final int columnCount = 128; + private final int maxResponseLongStringLength = 512; + + public AbstractDasResponseLogger( + TimeProvider timeProvider, Direction direction, LoggingPeerId peerId, TRequest request) { + super(timeProvider, direction, peerId, request, DataColumnSlotAndIdentifier::fromDataColumn); + } + + protected abstract int requestedMaxCount(); + + @Override + protected Logger getLogger() { + return LOG; + } + + protected String responseString( + List responses, Optional result) { + final String responsesString; + if (responses.isEmpty()) { + responsesString = ""; + } else if (responses.size() == requestedMaxCount()) { + responsesString = ""; + } else { + responsesString = columnIdsToString(responses); + } + + if (result.isEmpty()) { + return responsesString; + } else if (responses.isEmpty()) { + return "error: " + result.get(); + } else { + return responsesString + ", error: " + result.get(); + } + } + + protected String columnIdsToString(List responses) { + String longString = columnIdsToStringLong(responses); + if (longString.length() <= maxResponseLongStringLength) { + return longString; + } else { + return columnIdsToStringShorter(responses); + } + } + + protected String columnIdsToStringLong(List responses) { + return responses.size() + + " columns: " + + mapGroupingByBlock( + responses, + (blockId, columns) -> + blockIdString(blockId) + " colIdxs: " + blockResponsesToString(columns)) + .collect(Collectors.joining(", ")); + } + + protected String columnIdsToStringShorter(List responses) { + + return mapGroupingByBlock( + responses, (blockId, columns) -> blockIdString(blockId) + ": " + columns.size()) + .collect(Collectors.joining(", ")); + } + + protected Stream mapGroupingByBlock( + List responses, + BiFunction, R> mapper) { + SortedMap> responsesByBlock = + new TreeMap<>( + responses.stream() + .collect(Collectors.groupingBy(AbstractDasResponseLogger::blockIdFromColumnId))); + return responsesByBlock.entrySet().stream() + .map(entry -> mapper.apply(entry.getKey(), entry.getValue())); + } + + protected String blockResponsesToString(List responses) { + return StringifyUtil.columnIndexesToString( + responses.stream().map(it -> it.columnIndex().intValue()).toList(), columnCount); + } + + private static String blockIdString(SlotAndBlockRoot blockId) { + if (blockId.getSlot().equals(UNKNOWN_SLOT)) { + return blockId.getBlockRoot().toHexString(); + } else { + return "#" + + blockId.getSlot() + + " (0x" + + LogFormatter.formatAbbreviatedHashRoot(blockId.getBlockRoot()) + + ")"; + } + } + + private static SlotAndBlockRoot blockIdFromColumnId(DataColumnSlotAndIdentifier columnId) { + return new SlotAndBlockRoot(columnId.slot(), columnId.blockRoot()); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java new file mode 100644 index 00000000000..0cbcfecae96 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/AbstractResponseLogger.java @@ -0,0 +1,107 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; +import org.apache.logging.log4j.Logger; +import tech.pegasys.teku.infrastructure.time.TimeProvider; + +abstract class AbstractResponseLogger + implements ReqRespResponseLogger { + + enum Direction { + INBOUND, + OUTBOUND; + + @Override + public String toString() { + return name().toLowerCase(Locale.US); + } + } + + protected record Timestamped(long time, T value) {} + + protected final TimeProvider timeProvider; + protected final Direction direction; + protected final LoggingPeerId peerId; + protected final TRequest request; + private final Function responseSummarizer; + protected final long requestTime; + + private final List> responseSummaries = new ArrayList<>(); + private volatile boolean done = false; + + public AbstractResponseLogger( + TimeProvider timeProvider, + Direction direction, + LoggingPeerId peerId, + TRequest request, + Function responseSummarizer) { + this.timeProvider = timeProvider; + this.direction = direction; + this.peerId = peerId; + this.request = request; + this.responseSummarizer = responseSummarizer; + this.requestTime = timeProvider.getTimeInMillis().longValue(); + } + + protected abstract Logger getLogger(); + + protected abstract void responseComplete( + List> responseSummaries, Optional result); + + @Override + public synchronized void onNextItem(TResponse responseItem) { + if (getLogger().isDebugEnabled()) { + TResponseSummary responseSummary = responseSummarizer.apply(responseItem); + if (done) { + getLogger().debug("ERROR: Extra onNextItem: " + responseSummary); + return; + } + responseSummaries.add( + new Timestamped<>(timeProvider.getTimeInMillis().longValue(), responseSummary)); + } + } + + @Override + public void onComplete() { + if (getLogger().isDebugEnabled()) { + if (done) { + getLogger().debug("ERROR: Extra onComplete"); + return; + } + finalize(Optional.empty()); + } + } + + @Override + public void onError(Throwable error) { + if (getLogger().isDebugEnabled()) { + if (done) { + getLogger().debug("ERROR: Extra onError: " + error); + return; + } + finalize(Optional.ofNullable(error)); + } + } + + private void finalize(Optional result) { + done = true; + responseComplete(responseSummaries, result); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java new file mode 100644 index 00000000000..c27519a35e5 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRangeResponseLogger.java @@ -0,0 +1,70 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Optional; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.util.StringifyUtil; + +class DasByRangeResponseLogger extends AbstractDasResponseLogger { + public DasByRangeResponseLogger( + TimeProvider timeProvider, + Direction direction, + LoggingPeerId peerId, + DasReqRespLogger.ByRangeRequest request) { + super(timeProvider, direction, peerId, request); + } + + @Override + protected void responseComplete( + List> responseSummaries, + Optional result) { + + List responseSummariesUnboxed = + responseSummaries.stream().map(Timestamped::value).toList(); + long curTime = timeProvider.getTimeInMillis().longValue(); + + getLogger() + .debug( + "ReqResp {} {}, columns: {}/{} in {} ms{}, peer {}: request: {}, response: {}", + direction, + "data_column_sidecars_by_range", + responseSummaries.size(), + requestedMaxCount(), + curTime - requestTime, + result.isEmpty() ? "" : " with ERROR", + peerId, + requestToString(), + responseString(responseSummariesUnboxed, result)); + } + + @Override + protected int requestedMaxCount() { + return request.slotCount() * request.columnIndexes().size(); + } + + private String requestToString() { + return "[startSlot = " + + request.startSlot() + + ", count = " + + request.slotCount() + + ", columns = " + + StringifyUtil.toIntRangeString( + request.columnIndexes().stream().map(UInt64::intValue).toList()) + + "]"; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java new file mode 100644 index 00000000000..8cee592d955 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasByRootResponseLogger.java @@ -0,0 +1,84 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; +import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; + +class DasByRootResponseLogger extends AbstractDasResponseLogger> { + + public DasByRootResponseLogger( + TimeProvider timeProvider, + Direction direction, + LoggingPeerId peerId, + List dataColumnIdentifiers) { + super(timeProvider, direction, peerId, dataColumnIdentifiers); + } + + @Override + protected void responseComplete( + List> responseSummaries, + Optional result) { + + List responseSummariesUnboxed = + responseSummaries.stream().map(Timestamped::value).toList(); + long curTime = timeProvider.getTimeInMillis().longValue(); + + getLogger() + .debug( + "ReqResp {} {}, columns: {}/{} in {} ms{}, peer {}: request: {}, response: {}", + direction, + "data_column_sidecars_by_root", + responseSummaries.size(), + requestedMaxCount(), + curTime - requestTime, + result.isEmpty() ? "" : " with ERROR", + peerId, + requestToString(responseSummariesUnboxed), + responseString(responseSummariesUnboxed, result)); + } + + @Override + protected int requestedMaxCount() { + return request.size(); + } + + protected String requestToString(List responses) { + Map blockRootToSlot = + responses.stream() + .collect( + Collectors.toMap( + DataColumnSlotAndIdentifier::blockRoot, + DataColumnSlotAndIdentifier::slot, + (s1, s2) -> s1)); + List idsWithMaybeSlot = + request.stream() + .map( + it -> + new DataColumnSlotAndIdentifier( + blockRootToSlot.getOrDefault(it.getBlockRoot(), UNKNOWN_SLOT), + it.getBlockRoot(), + it.getIndex())) + .toList(); + + return columnIdsToString(idsWithMaybeSlot); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java new file mode 100644 index 00000000000..52ce3f7a5f1 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLogger.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; + +public interface DasReqRespLogger { + + record ByRangeRequest(UInt64 startSlot, int slotCount, List columnIndexes) {} + + static DasReqRespLogger create(TimeProvider timeProvider) { + return new DasReqRespLoggerImpl(timeProvider); + } + + DasReqRespLogger NOOP = + new DasReqRespLogger() { + @Override + public ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger() { + return new NoopReqRespMethodLogger<>(); + } + + @Override + public ReqRespMethodLogger + getDataColumnSidecarsByRangeLogger() { + return new NoopReqRespMethodLogger<>(); + } + }; + + ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger(); + + ReqRespMethodLogger getDataColumnSidecarsByRangeLogger(); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java new file mode 100644 index 00000000000..12f4ca444a9 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/DasReqRespLoggerImpl.java @@ -0,0 +1,75 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; + +class DasReqRespLoggerImpl implements DasReqRespLogger { + + private final TimeProvider timeProvider; + + private final ReqRespMethodLogger, DataColumnSidecar> + byRootMethodLogger = + new ReqRespMethodLogger<>() { + @Override + public ReqRespResponseLogger onInboundRequest( + LoggingPeerId fromPeer, List request) { + return new DasByRootResponseLogger( + timeProvider, AbstractResponseLogger.Direction.INBOUND, fromPeer, request); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + LoggingPeerId toPeer, List request) { + return new DasByRootResponseLogger( + timeProvider, AbstractResponseLogger.Direction.OUTBOUND, toPeer, request); + } + }; + + private final ReqRespMethodLogger byRangeMethodLogger = + new ReqRespMethodLogger<>() { + @Override + public ReqRespResponseLogger onInboundRequest( + LoggingPeerId fromPeer, ByRangeRequest request) { + return new DasByRangeResponseLogger( + timeProvider, AbstractResponseLogger.Direction.INBOUND, fromPeer, request); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + LoggingPeerId toPeer, ByRangeRequest request) { + return new DasByRangeResponseLogger( + timeProvider, AbstractResponseLogger.Direction.OUTBOUND, toPeer, request); + } + }; + + public DasReqRespLoggerImpl(TimeProvider timeProvider) { + this.timeProvider = timeProvider; + } + + @Override + public ReqRespMethodLogger, DataColumnSidecar> + getDataColumnSidecarsByRootLogger() { + return byRootMethodLogger; + } + + @Override + public ReqRespMethodLogger + getDataColumnSidecarsByRangeLogger() { + return byRangeMethodLogger; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java new file mode 100644 index 00000000000..b737ee88886 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRangeReqResp.java @@ -0,0 +1,52 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRangeReqResp; + +public class LoggingBatchDataColumnsByRangeReqResp implements BatchDataColumnsByRangeReqResp { + + private final BatchDataColumnsByRangeReqResp delegate; + private final DasReqRespLogger logger; + + public LoggingBatchDataColumnsByRangeReqResp( + BatchDataColumnsByRangeReqResp delegate, DasReqRespLogger logger) { + this.delegate = delegate; + this.logger = logger; + } + + @Override + public AsyncStream requestDataColumnSidecarsByRange( + UInt256 nodeId, UInt64 startSlot, int slotCount, List columnIndexes) { + ReqRespResponseLogger responseLogger = + logger + .getDataColumnSidecarsByRangeLogger() + .onOutboundRequest( + LoggingPeerId.fromNodeId(nodeId), + new DasReqRespLogger.ByRangeRequest(startSlot, slotCount, columnIndexes)); + return delegate + .requestDataColumnSidecarsByRange(nodeId, startSlot, slotCount, columnIndexes) + .peek(responseLogger.asAsyncStreamVisitor()); + } + + @Override + public int getCurrentRequestLimit(UInt256 nodeId) { + return delegate.getCurrentRequestLimit(nodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java new file mode 100644 index 00000000000..16607eead5d --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingBatchDataColumnsByRootReqResp.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.List; +import org.apache.tuweni.units.bigints.UInt256; +import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; +import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; +import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRootReqResp; + +public class LoggingBatchDataColumnsByRootReqResp implements BatchDataColumnsByRootReqResp { + private final BatchDataColumnsByRootReqResp delegate; + private final DasReqRespLogger logger; + + public LoggingBatchDataColumnsByRootReqResp( + BatchDataColumnsByRootReqResp delegate, DasReqRespLogger logger) { + this.delegate = delegate; + this.logger = logger; + } + + @Override + public AsyncStream requestDataColumnSidecarsByRoot( + UInt256 nodeId, List columnIdentifiers) { + ReqRespResponseLogger responseLogger = + logger + .getDataColumnSidecarsByRootLogger() + .onOutboundRequest(LoggingPeerId.fromNodeId(nodeId), columnIdentifiers); + return delegate + .requestDataColumnSidecarsByRoot(nodeId, columnIdentifiers) + .peek(responseLogger.asAsyncStreamVisitor()); + } + + @Override + public int getCurrentRequestLimit(UInt256 nodeId) { + return delegate.getCurrentRequestLimit(nodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java new file mode 100644 index 00000000000..57730d8c70c --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/LoggingPeerId.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import java.util.Optional; +import org.apache.tuweni.units.bigints.UInt256; + +public class LoggingPeerId { + public static LoggingPeerId fromNodeId(UInt256 nodeId) { + return new LoggingPeerId(nodeId, Optional.empty()); + } + + public static LoggingPeerId fromPeerAndNodeId(String base58PeerId, UInt256 nodeId) { + return new LoggingPeerId(nodeId, Optional.of(base58PeerId)); + } + + private final UInt256 nodeId; + private final Optional base58PeerId; + + public LoggingPeerId(UInt256 nodeId, Optional base58PeerId) { + this.nodeId = nodeId; + this.base58PeerId = base58PeerId; + } + + @Override + public String toString() { + String sNodeId = nodeId.toHexString(); + String sShortNodeId = + sNodeId.substring(0, 10) + "..." + sNodeId.substring(sNodeId.length() - 8); + return base58PeerId.map(s -> s + " (nodeId = " + sShortNodeId + ")").orElse(sShortNodeId); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java new file mode 100644 index 00000000000..882f31be225 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/NoopReqRespMethodLogger.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +class NoopReqRespMethodLogger + implements ReqRespMethodLogger { + + @Override + public ReqRespResponseLogger onInboundRequest( + LoggingPeerId fromPeer, TRequest request) { + return noopResponseLogger(); + } + + @Override + public ReqRespResponseLogger onOutboundRequest( + LoggingPeerId toPeer, TRequest request) { + return noopResponseLogger(); + } + + static ReqRespResponseLogger noopResponseLogger() { + return new ReqRespResponseLogger<>() { + @Override + public void onNextItem(TResponse s) {} + + @Override + public void onComplete() {} + + @Override + public void onError(Throwable error) {} + }; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java new file mode 100644 index 00000000000..f52221d16e2 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespMethodLogger.java @@ -0,0 +1,21 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +public interface ReqRespMethodLogger { + + ReqRespResponseLogger onInboundRequest(LoggingPeerId fromPeer, TRequest request); + + ReqRespResponseLogger onOutboundRequest(LoggingPeerId toPeer, TRequest request); +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java new file mode 100644 index 00000000000..a7a457f2906 --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/log/rpc/ReqRespResponseLogger.java @@ -0,0 +1,44 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.statetransition.datacolumns.log.rpc; + +import tech.pegasys.teku.infrastructure.async.stream.AsyncStreamVisitor; + +public interface ReqRespResponseLogger { + + void onNextItem(TResponse s); + + void onComplete(); + + void onError(Throwable error); + + default AsyncStreamVisitor asAsyncStreamVisitor() { + return new AsyncStreamVisitor<>() { + @Override + public void onNext(TResponse tResponse) { + ReqRespResponseLogger.this.onNextItem(tResponse); + } + + @Override + public void onComplete() { + ReqRespResponseLogger.this.onComplete(); + } + + @Override + public void onError(Throwable t) { + ReqRespResponseLogger.this.onError(t); + } + }; + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java index 19e14fbc373..b8404d580b1 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtil.java @@ -48,18 +48,9 @@ public static String columnIndexesToString(Collection indexes, int maxC + ranges.stream().map(Objects::toString).collect(Collectors.joining(",")) + "]"; } else { - List sortedIndexes = indexes.stream().sorted().toList(); BitSet bitSet = new BitSet(maxColumns); indexes.forEach(bitSet::set); - return lenStr - + "[" - + sortAndJoin(sortedIndexes.subList(0, 4)) - + ",...(" - + (indexes.size() - 8) - + " more)..., " - + sortAndJoin(sortedIndexes.subList(sortedIndexes.size() - 4, sortedIndexes.size())) - + "], bitset: " - + Bytes.of(bitSet.toByteArray()); + return lenStr + "[bitmap: " + Bytes.of(bitSet.toByteArray()) + "]"; } } } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtilTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtilTest.java index 498c939ff1c..a84e8c2f31e 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtilTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/datacolumns/util/StringifyUtilTest.java @@ -51,7 +51,7 @@ public String toString() { new TestCase(range(0, 128).limit(64), "[0..63]"), new TestCase( range(0, 128).filter(i -> i % 3 != 0), - "[1,2,4,5,...(77 more)..., 122,124,125,127], bitset: 0xb66ddbb66ddbb66ddbb66ddbb66ddbb6"), + "[bitmap: 0xb66ddbb66ddbb66ddbb66ddbb66ddbb6]"), new TestCase( range(10, 100).filter(i -> !Set.of(14, 16, 18, 98).contains(i)), "[10..13,15,17,19..97,99]")); diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java index 432f1049b31..d26b34ac25f 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkBuilder.java @@ -89,6 +89,7 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsSupplier; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.store.KeyValueStore; @@ -136,6 +137,7 @@ public class Eth2P2PNetworkBuilder { protected KZG kzg; protected boolean recordMessageArrival; private DasGossipLogger dasGossipLogger; + private DasReqRespLogger dasReqRespLogger; protected Eth2P2PNetworkBuilder() {} @@ -185,7 +187,8 @@ public Eth2P2PNetwork build() { spec, kzg, discoveryNodeIdExtractor, - dasTotalCustodySubnetCount); + dasTotalCustodySubnetCount, + dasReqRespLogger); final Collection> eth2RpcMethods = eth2PeerManager.getBeaconChainMethods().all(); rpcMethods.addAll(eth2RpcMethods); @@ -660,4 +663,9 @@ public Eth2P2PNetworkBuilder gossipDasLogger(DasGossipLogger dasGossipLogger) { this.dasGossipLogger = dasGossipLogger; return this; } + + public Eth2P2PNetworkBuilder reqRespDasLogger(final DasReqRespLogger dasReqRespLogger) { + this.dasReqRespLogger = dasReqRespLogger; + return this; + } } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java index 819729b30a9..62cda71a8cb 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManager.java @@ -47,6 +47,7 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.metadata.MetadataMessageSchema; import tech.pegasys.teku.spec.datastructures.state.Checkpoint; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -83,7 +84,8 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { final RpcEncoding rpcEncoding, final Duration eth2RpcPingInterval, final int eth2RpcOutstandingPingThreshold, - final Duration eth2StatusUpdateInterval) { + final Duration eth2StatusUpdateInterval, + final DasReqRespLogger dasLogger) { this.asyncRunner = asyncRunner; this.recentChainData = recentChainData; this.eth2PeerFactory = eth2PeerFactory; @@ -99,7 +101,8 @@ public class Eth2PeerManager implements PeerLookup, PeerHandler { metricsSystem, statusMessageFactory, metadataMessagesFactory, - rpcEncoding); + rpcEncoding, + dasLogger); this.eth2RpcPingInterval = eth2RpcPingInterval; this.eth2RpcOutstandingPingThreshold = eth2RpcOutstandingPingThreshold; this.eth2StatusUpdateInterval = eth2StatusUpdateInterval; @@ -125,7 +128,8 @@ public static Eth2PeerManager create( final Spec spec, final KZG kzg, final DiscoveryNodeIdExtractor discoveryNodeIdExtractor, - final Optional custodySubnetCount) { + final Optional custodySubnetCount, + final DasReqRespLogger dasLogger) { final MetadataMessagesFactory metadataMessagesFactory = new MetadataMessagesFactory(); @@ -159,7 +163,8 @@ public static Eth2PeerManager create( rpcEncoding, eth2RpcPingInterval, eth2RpcOutstandingPingThreshold, - eth2StatusUpdateInterval); + eth2StatusUpdateInterval, + dasLogger); } public MetadataMessage getMetadataMessage() { diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java index 0ff48dd3b90..05a2670c584 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethods.java @@ -68,6 +68,7 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsDeneb; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsEip7594; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -136,7 +137,8 @@ public static BeaconChainMethods create( final MetricsSystem metricsSystem, final StatusMessageFactory statusMessageFactory, final MetadataMessagesFactory metadataMessagesFactory, - final RpcEncoding rpcEncoding) { + final RpcEncoding rpcEncoding, + final DasReqRespLogger dasLogger) { return new BeaconChainMethods( createStatus(spec, asyncRunner, statusMessageFactory, peerLookup, rpcEncoding), createGoodBye(spec, asyncRunner, metricsSystem, peerLookup, rpcEncoding), @@ -174,7 +176,8 @@ public static BeaconChainMethods create( dataColumnSidecarCustody, peerLookup, rpcEncoding, - recentChainData), + recentChainData, + dasLogger), createDataColumnsSidecarsByRange( spec, metricsSystem, @@ -182,7 +185,8 @@ public static BeaconChainMethods create( combinedChainDataClient, peerLookup, rpcEncoding, - recentChainData), + recentChainData, + dasLogger), createMetadata(spec, asyncRunner, metadataMessagesFactory, peerLookup, rpcEncoding), createPing(spec, asyncRunner, metadataMessagesFactory, peerLookup, rpcEncoding)); } @@ -391,7 +395,8 @@ private static Eth2RpcMethod createGoodBye( final DataColumnSidecarByRootCustody dataColumnSidecarCustody, final PeerLookup peerLookup, final RpcEncoding rpcEncoding, - final RecentChainData recentChainData) { + final RecentChainData recentChainData, + final DasReqRespLogger dasLogger) { if (!spec.isMilestoneSupported(SpecMilestone.EIP7594)) { return Optional.empty(); } @@ -402,7 +407,7 @@ private static Eth2RpcMethod createGoodBye( final DataColumnSidecarsByRootMessageHandler dataColumnSidecarsByRootMessageHandler = new DataColumnSidecarsByRootMessageHandler( - spec, metricsSystem, combinedChainDataClient, dataColumnSidecarCustody); + spec, metricsSystem, combinedChainDataClient, dataColumnSidecarCustody, dasLogger); final DataColumnSidecarsByRootRequestMessageSchema dataColumnSidecarsByRootRequestMessageSchema = SchemaDefinitionsEip7594.required( @@ -431,7 +436,8 @@ private static Eth2RpcMethod createGoodBye( final CombinedChainDataClient combinedChainDataClient, final PeerLookup peerLookup, final RpcEncoding rpcEncoding, - final RecentChainData recentChainData) { + final RecentChainData recentChainData, + final DasReqRespLogger dasLogger) { if (!spec.isMilestoneSupported(SpecMilestone.EIP7594)) { return Optional.empty(); @@ -449,7 +455,7 @@ private static Eth2RpcMethod createGoodBye( final DataColumnSidecarsByRangeMessageHandler dataColumnSidecarsByRangeMessageHandler = new DataColumnSidecarsByRangeMessageHandler( - spec, getSpecConfigEip7594(spec), metricsSystem, combinedChainDataClient); + spec, getSpecConfigEip7594(spec), metricsSystem, combinedChainDataClient, dasLogger); return Optional.of( new SingleProtocolEth2RpcMethod<>( diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java index a69ca8e5893..e56ce2e35a0 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRangeMessageHandler.java @@ -46,6 +46,9 @@ import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRangeRequestMessage; import tech.pegasys.teku.spec.datastructures.util.DataColumnSlotAndIdentifier; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.LoggingPeerId; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; /** @@ -58,19 +61,20 @@ public class DataColumnSidecarsByRangeMessageHandler DataColumnSidecarsByRangeRequestMessage, DataColumnSidecar> { private static final Logger LOG = LogManager.getLogger(); - private static final Logger LOG_DAS = LogManager.getLogger("das-nyota"); private final Spec spec; private final SpecConfigEip7594 specConfigEip7594; private final CombinedChainDataClient combinedChainDataClient; private final LabelledMetric requestCounter; private final Counter totalDataColumnSidecarsRequestedCounter; + private final DasReqRespLogger dasLogger; public DataColumnSidecarsByRangeMessageHandler( final Spec spec, final SpecConfigEip7594 specConfigEip7594, final MetricsSystem metricsSystem, - final CombinedChainDataClient combinedChainDataClient) { + final CombinedChainDataClient combinedChainDataClient, + final DasReqRespLogger dasLogger) { this.spec = spec; this.specConfigEip7594 = specConfigEip7594; this.combinedChainDataClient = combinedChainDataClient; @@ -85,6 +89,7 @@ public DataColumnSidecarsByRangeMessageHandler( TekuMetricCategory.NETWORK, "rpc_data_column_sidecars_by_range_requested_sidecars_total", "Total number of data column sidecars requested in accepted blob sidecars by range requests from peers"); + this.dasLogger = dasLogger; } @Override @@ -92,29 +97,27 @@ public void onIncomingMessage( final String protocolId, final Eth2Peer peer, final DataColumnSidecarsByRangeRequestMessage message, - final ResponseCallback callback) { + final ResponseCallback responseCallback) { final UInt64 startSlot = message.getStartSlot(); final UInt64 endSlot = message.getMaxSlot(); final List columns = message.getColumns(); - LOG.trace( - "Peer {} requested {} slots with columns {} of data column sidecars starting at slot {}.", - peer.getId(), - message.getCount(), - columns, - startSlot); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRangeMessageHandler: REQUEST {} slots with columns {} of data column sidecars starting at slot {} from {}", - message.getCount(), - columns, - startSlot, - peer.getId()); + ReqRespResponseLogger responseLogger = + dasLogger + .getDataColumnSidecarsByRangeLogger() + .onInboundRequest( + LoggingPeerId.fromPeerAndNodeId( + peer.getId().toBase58(), peer.getDiscoveryNodeId().orElseThrow()), + new DasReqRespLogger.ByRangeRequest( + message.getStartSlot(), message.getCount().intValue(), message.getColumns())); + LoggingResponseCallback responseCallbackWithLogging = + new LoggingResponseCallback<>(responseCallback, responseLogger); final int requestedCount = message.getMaximumResponseChunks(); if (requestedCount > specConfigEip7594.getMaxRequestDataColumnSidecars()) { requestCounter.labels("count_too_big").inc(); - callback.completeWithErrorResponse( + responseCallbackWithLogging.completeWithErrorResponse( new RpcException( INVALID_REQUEST_CODE, String.format( @@ -124,7 +127,7 @@ public void onIncomingMessage( } final Optional dataColumnSidecarsRequestApproval = - peer.approveDataColumnSidecarsRequest(callback, requestedCount); + peer.approveDataColumnSidecarsRequest(responseCallbackWithLogging, requestedCount); if (!peer.approveRequest() || dataColumnSidecarsRequestApproval.isEmpty()) { requestCounter.labels("rate_limited").inc(); @@ -171,7 +174,7 @@ public void onIncomingMessage( final RequestState initialState = new RequestState( - callback, + responseCallbackWithLogging, specConfigEip7594.getMaxRequestDataColumnSidecars(), startSlot, endSlot, @@ -190,21 +193,11 @@ public void onIncomingMessage( peer.adjustDataColumnSidecarsRequest( dataColumnSidecarsRequestApproval.get(), sentDataColumnSidecars); } - LOG.trace( - "Sent {} data column sidecars to peer {}.", sentDataColumnSidecars, peer.getId()); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRangeMessageHandler: RESPONSE sent {} sidecars to {}", - sentDataColumnSidecars, - peer.getId()); - callback.completeSuccessfully(); + responseCallbackWithLogging.completeSuccessfully(); }, error -> { peer.adjustDataColumnSidecarsRequest(dataColumnSidecarsRequestApproval.get(), 0); - handleProcessingRequestError(error, callback); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRangeMessageHandler: ERROR to {}: {}", - peer.getId(), - error.toString()); + handleProcessingRequestError(error, responseCallbackWithLogging); }); ; } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java index 72ca89be812..158fb2211ee 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/DataColumnSidecarsByRootMessageHandler.java @@ -19,7 +19,6 @@ import java.nio.channels.ClosedChannelException; import java.util.List; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -41,6 +40,9 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnSidecarsByRootRequestMessage; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.LoggingPeerId; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; /** @@ -53,8 +55,6 @@ public class DataColumnSidecarsByRootMessageHandler DataColumnSidecarsByRootRequestMessage, DataColumnSidecar> { private static final Logger LOG = LogManager.getLogger(); - private static final Logger LOG_DAS = LogManager.getLogger("das-nyota"); - private static final AtomicLong REQUEST_ID_COUNTER = new AtomicLong(); private final Spec spec; private final CombinedChainDataClient combinedChainDataClient; @@ -62,12 +62,14 @@ public class DataColumnSidecarsByRootMessageHandler private final LabelledMetric requestCounter; private final Counter totalDataColumnSidecarsRequestedCounter; + private final DasReqRespLogger dasLogger; public DataColumnSidecarsByRootMessageHandler( final Spec spec, final MetricsSystem metricsSystem, final CombinedChainDataClient combinedChainDataClient, - final DataColumnSidecarByRootCustody dataColumnSidecarCustody) { + final DataColumnSidecarByRootCustody dataColumnSidecarCustody, + final DasReqRespLogger dasLogger) { this.spec = spec; this.combinedChainDataClient = combinedChainDataClient; requestCounter = @@ -82,6 +84,7 @@ public DataColumnSidecarsByRootMessageHandler( "rpc_data_column_sidecars_by_root_requested_blob_sidecars_total", "Total number of data column sidecars requested in accepted data column sidecars by root requests from peers"); this.dataColumnSidecarCustody = dataColumnSidecarCustody; + this.dasLogger = dasLogger; } private SafeFuture validateAndSendMaybeRespond( @@ -102,23 +105,21 @@ public void onIncomingMessage( final String protocolId, final Eth2Peer peer, final DataColumnSidecarsByRootRequestMessage message, - final ResponseCallback callback) { + final ResponseCallback responseCallback) { - long requestId = REQUEST_ID_COUNTER.getAndIncrement(); + ReqRespResponseLogger responseLogger = + dasLogger + .getDataColumnSidecarsByRootLogger() + .onInboundRequest( + LoggingPeerId.fromPeerAndNodeId( + peer.getId().toBase58(), peer.getDiscoveryNodeId().orElseThrow()), + message.asList()); - LOG.debug( - "Peer {} requested {} data column sidecars with identifiers: {}", - peer.getId(), - message.size(), - message); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRootMessageHandler: REQUEST(#{}) {} data column sidecars from {}", - requestId, - message.size(), - peer.getId()); + LoggingResponseCallback responseCallbackWithLogging = + new LoggingResponseCallback<>(responseCallback, responseLogger); final Optional dataColumnSidecarsRequestApproval = - peer.approveDataColumnSidecarsRequest(callback, message.size()); + peer.approveDataColumnSidecarsRequest(responseCallbackWithLogging, message.size()); if (!peer.approveRequest() || dataColumnSidecarsRequestApproval.isEmpty()) { requestCounter.labels("rate_limited").inc(); @@ -138,7 +139,10 @@ public void onIncomingMessage( .thenCompose( maybeSidecar -> validateAndSendMaybeRespond( - identifier, maybeSidecar, finalizedEpoch, callback))); + identifier, + maybeSidecar, + finalizedEpoch, + responseCallbackWithLogging))); SafeFuture> listOfResponses = SafeFuture.collectAll(responseStream); @@ -150,22 +154,12 @@ public void onIncomingMessage( peer.adjustDataColumnSidecarsRequest( dataColumnSidecarsRequestApproval.get(), sentDataColumnSidecarsCount); } - callback.completeSuccessfully(); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRootMessageHandler: RESPOND(#{}) {} data column sidecars to {}", - requestId, - sentDataColumnSidecarsCount, - peer.getId()); + responseCallbackWithLogging.completeSuccessfully(); }) .finish( err -> { peer.adjustDataColumnSidecarsRequest(dataColumnSidecarsRequestApproval.get(), 0); - handleError(callback, err); - LOG_DAS.info( - "[nyota] DataColumnSidecarsByRootMessageHandler: ERROR(#{}) to {}: {}", - requestId, - peer.getId(), - err.toString()); + handleError(responseCallbackWithLogging, err); }); } diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java new file mode 100644 index 00000000000..7ba5984b843 --- /dev/null +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/methods/LoggingResponseCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.networking.eth2.rpc.beaconchain.methods; + +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.networking.eth2.rpc.core.ResponseCallback; +import tech.pegasys.teku.networking.eth2.rpc.core.RpcException; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.ReqRespResponseLogger; + +public record LoggingResponseCallback( + ResponseCallback callback, ReqRespResponseLogger logger) implements ResponseCallback { + + @Override + public SafeFuture respond(T data) { + logger.onNextItem(data); + return callback.respond(data); + } + + @Override + public void respondAndCompleteSuccessfully(T data) { + logger.onNextItem(data); + logger.onComplete(); + callback.respondAndCompleteSuccessfully(data); + } + + @Override + public void completeSuccessfully() { + logger.onComplete(); + callback.completeSuccessfully(); + } + + @Override + public void completeWithErrorResponse(RpcException error) { + logger.onError(error); + callback.completeWithErrorResponse(error); + } + + @Override + public void completeWithUnexpectedError(Throwable error) { + logger.onError(error); + callback.completeWithUnexpectedError(error); + } +} diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java index fd657239ce3..6a8bf2db13a 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/peers/Eth2PeerManagerTest.java @@ -46,6 +46,7 @@ import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -79,7 +80,8 @@ public class Eth2PeerManagerTest { rpcEncoding, Eth2P2PNetworkBuilder.DEFAULT_ETH2_RPC_PING_INTERVAL, Eth2P2PNetworkBuilder.DEFAULT_ETH2_RPC_OUTSTANDING_PING_THRESHOLD, - Eth2P2PNetworkBuilder.DEFAULT_ETH2_STATUS_UPDATE_INTERVAL); + Eth2P2PNetworkBuilder.DEFAULT_ETH2_STATUS_UPDATE_INTERVAL, + DasReqRespLogger.NOOP); @Test public void subscribeConnect_singleListener() { diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java index 3402e99b63e..0b9936f8660 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/beaconchain/BeaconChainMethodsTest.java @@ -36,6 +36,7 @@ import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.StatusMessage; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -156,6 +157,7 @@ private BeaconChainMethods getMethods(final Spec spec) { metricsSystem, statusMessageFactory, metadataMessagesFactory, - RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxChunkSize())); + RpcEncoding.createSszSnappyEncoding(spec.getNetworkingConfig().getMaxChunkSize()), + DasReqRespLogger.NOOP); } } diff --git a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java index b04b7004d23..9984a6d0329 100644 --- a/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java +++ b/networking/eth2/src/test/java/tech/pegasys/teku/networking/eth2/rpc/core/AbstractRequestHandlerTest.java @@ -38,6 +38,7 @@ import tech.pegasys.teku.spec.TestSpecFactory; import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.client.CombinedChainDataClient; import tech.pegasys.teku.storage.client.RecentChainData; @@ -70,7 +71,8 @@ public void setup() { new NoOpMetricsSystem(), new StatusMessageFactory(recentChainData), new MetadataMessagesFactory(), - getRpcEncoding()); + getRpcEncoding(), + DasReqRespLogger.NOOP); reqHandler = createRequestHandler(beaconChainMethods); diff --git a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java index 12a0d4707db..4345c9b37a8 100644 --- a/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java +++ b/networking/eth2/src/testFixtures/java/tech/pegasys/teku/networking/eth2/Eth2P2PNetworkFactory.java @@ -107,6 +107,7 @@ import tech.pegasys.teku.statetransition.block.VerifiedBlockOperationsListener; import tech.pegasys.teku.statetransition.datacolumns.DataColumnSidecarByRootCustody; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; import tech.pegasys.teku.storage.api.StorageQueryChannel; import tech.pegasys.teku.storage.api.StubStorageQueryChannel; import tech.pegasys.teku.storage.client.CombinedChainDataClient; @@ -251,7 +252,8 @@ protected Eth2P2PNetwork buildNetwork(final P2PConfig config) { spec, KZG.NOOP, (__) -> Optional.empty(), - dasTotalCustodySubnetCount); + dasTotalCustodySubnetCount, + DasReqRespLogger.NOOP); List> rpcMethods = eth2PeerManager.getBeaconChainMethods().all().stream() diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index 7c6beae79c7..1251e491141 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -162,6 +162,9 @@ import tech.pegasys.teku.statetransition.datacolumns.db.DataColumnSidecarDbAccessor; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipBatchLogger; import tech.pegasys.teku.statetransition.datacolumns.log.gossip.DasGossipLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.DasReqRespLogger; +import tech.pegasys.teku.statetransition.datacolumns.log.rpc.LoggingBatchDataColumnsByRootReqResp; +import tech.pegasys.teku.statetransition.datacolumns.retriever.BatchDataColumnsByRootReqResp; import tech.pegasys.teku.statetransition.datacolumns.retriever.DasPeerCustodyCountSupplier; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnPeerSearcher; import tech.pegasys.teku.statetransition.datacolumns.retriever.DataColumnReqResp; @@ -312,10 +315,10 @@ public class BeaconChainController extends Service implements BeaconChainControl protected volatile ExecutionLayerChannel executionLayer; protected volatile GossipValidationHelper gossipValidationHelper; protected volatile DasGossipLogger dasGossipLogger; + protected volatile DasReqRespLogger dasReqRespLogger; protected volatile KZG kzg; protected volatile BlobSidecarManager blobSidecarManager; protected volatile DataColumnSidecarManager dataColumnSidecarManager; - // protected volatile DataColumnSidecarCustody dataColumnSidecarCustody; protected volatile LateInitDataColumnSidecarCustody dataColumnSidecarCustody = new LateInitDataColumnSidecarCustody(); protected volatile DasCustodySync dasCustodySync; @@ -377,6 +380,7 @@ public BeaconChainController( "Current number of items held for future slots, labelled by type", "type"); this.dasGossipLogger = new DasGossipBatchLogger(operationPoolAsyncRunner, timeProvider); + this.dasReqRespLogger = DasReqRespLogger.create(timeProvider); } @Override @@ -746,7 +750,9 @@ protected void initDasCustody() { DataColumnPeerManagerImpl dasPeerManager = new DataColumnPeerManagerImpl(); p2pNetwork.subscribeConnect(dasPeerManager); - DataColumnReqResp dasRpc = new DataColumnReqRespBatchingImpl(dasPeerManager); + BatchDataColumnsByRootReqResp loggingByRootReqResp = + new LoggingBatchDataColumnsByRootReqResp(dasPeerManager, dasReqRespLogger); + DataColumnReqResp dasRpc = new DataColumnReqRespBatchingImpl(loggingByRootReqResp); MetadataDasPeerCustodyTracker peerCustodyTracker = new MetadataDasPeerCustodyTracker(); p2pNetwork.subscribeConnect(peerCustodyTracker); @@ -1369,6 +1375,7 @@ protected void initP2PNetwork() { .gossipedSyncCommitteeMessageProcessor(syncCommitteeMessagePool::addRemote) .gossipedSignedBlsToExecutionChangeProcessor(blsToExecutionChangePool::addRemote) .gossipDasLogger(dasGossipLogger) + .reqRespDasLogger(dasReqRespLogger) .processedAttestationSubscriptionProvider( attestationManager::subscribeToAttestationsToSend) .metricsSystem(metricsSystem) From 874ba2f08f80248629199cac6ed8ef67f4a2127c Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Mon, 21 Oct 2024 18:53:24 +0300 Subject: [PATCH 9/9] Misc logging cleans up (#172) * Remove excessive logging * Fix minor issue with adding a new connected peer in DataColumnPeerManagerImpl * Reduce log exceptions noise in case of invalid ENR Eth2 field encoding * A bit more verbose logging for PeerManager --- .../DataColumnReqRespBatchingImpl.java | 24 +------ .../retriever/DataColumnReqRespImpl.java | 71 ------------------- .../retriever/SimpleSidecarRetriever.java | 2 +- .../eth2/peers/DataColumnPeerManagerImpl.java | 2 +- .../discovery/discv5/NodeRecordConverter.java | 2 +- .../p2p/libp2p/MultiaddrPeerAddress.java | 5 ++ 6 files changed, 9 insertions(+), 97 deletions(-) delete mode 100644 ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespImpl.java diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java index 36fd69da371..a36f0e05d29 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespBatchingImpl.java @@ -20,8 +20,6 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.apache.tuweni.units.bigints.UInt256; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.async.stream.AsyncStream; @@ -30,8 +28,6 @@ import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; public class DataColumnReqRespBatchingImpl implements DataColumnReqResp { - private static final Logger LOG = LogManager.getLogger("das-nyota"); - private final BatchDataColumnsByRootReqResp batchRpc; public DataColumnReqRespBatchingImpl(BatchDataColumnsByRootReqResp batchRpc) { @@ -67,11 +63,6 @@ public void flush() { } private void flushForNode(UInt256 nodeId, List nodeRequests) { - LOG.info( - "[nyota] Requesting batch of {} from {}, hash={}", - nodeRequests.size(), - "0x..." + nodeId.toHexString().substring(58), - nodeRequests.hashCode()); AsyncStream response = batchRpc.requestDataColumnSidecarsByRoot( nodeId, nodeRequests.stream().map(e -> e.columnIdentifier).toList()); @@ -100,11 +91,6 @@ public SafeFuture onNext(DataColumnSidecar dataColumnSidecar) { @Override public void onComplete() { - LOG.info( - "[nyota] Response batch of {} from {}, hash={}", - count, - "0x..." + nodeId.toHexString().substring(58), - nodeRequests.hashCode()); nodeRequests.stream() .filter(req -> !req.promise().isDone()) .forEach( @@ -114,15 +100,7 @@ public void onComplete() { @Override public void onError(Throwable err) { - nodeRequests.forEach( - e -> { - LOG.info( - "[nyota] Error batch from {}, hash={}, err: {}", - "0x..." + nodeId.toHexString().substring(58), - nodeRequests.hashCode(), - e.toString()); - e.promise().completeExceptionally(err); - }); + nodeRequests.forEach(e -> e.promise().completeExceptionally(err)); } }); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespImpl.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespImpl.java deleted file mode 100644 index f944f480633..00000000000 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/DataColumnReqRespImpl.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Consensys Software Inc., 2024 - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package tech.pegasys.teku.statetransition.datacolumns.retriever; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.tuweni.units.bigints.UInt256; -import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.spec.datastructures.blobs.versions.eip7594.DataColumnSidecar; -import tech.pegasys.teku.spec.datastructures.networking.libp2p.rpc.DataColumnIdentifier; - -public class DataColumnReqRespImpl implements DataColumnReqResp { - private static final Logger LOG = LogManager.getLogger("das-nyota"); - - // TODO: suppress until real ban is commented out - @SuppressWarnings("unused") - private final DataColumnPeerManager peerManager; - - private final DataColumnReqResp reqResp; - - public DataColumnReqRespImpl(DataColumnPeerManager peerManager, DataColumnReqResp reqResp) { - this.peerManager = peerManager; - this.reqResp = reqResp; - } - - @Override - public SafeFuture requestDataColumnSidecar( - UInt256 nodeId, DataColumnIdentifier columnIdentifier) { - return reqResp - .requestDataColumnSidecar(nodeId, columnIdentifier) - .whenException( - ex -> { - if (ex instanceof DataColumnReqRespException) { - LOG.debug( - "Error requesting data column sidecar {} from {}: {}", - columnIdentifier, - nodeId, - ex); - } else { - LOG.warn( - "Error requesting data column sidecar {} from {}: {}", - columnIdentifier, - nodeId, - ex); - // TODO: uncomment in production - // peerManager.banNode(nodeId); - } - }); - } - - @Override - public int getCurrentRequestLimit(UInt256 nodeId) { - return reqResp.getCurrentRequestLimit(nodeId); - } - - @Override - public void flush() { - reqResp.flush(); - } -} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java index 4f282c81ef5..b3c84f5d1cd 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/datacolumns/retriever/SimpleSidecarRetriever.java @@ -79,7 +79,7 @@ public SimpleSidecarRetriever( this.custodyCountSupplier = custodyCountSupplier; this.asyncRunner = asyncRunner; this.roundPeriod = roundPeriod; - this.reqResp = new DataColumnReqRespImpl(peerManager, reqResp); + this.reqResp = reqResp; peerManager.addPeerListener(this); this.maxRequestCount = SpecConfigEip7594.required(spec.forMilestone(SpecMilestone.EIP7594).getConfig()) diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java index 2ee6ea4c86e..53fcfb609ce 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/peers/DataColumnPeerManagerImpl.java @@ -45,8 +45,8 @@ public void onConnected(Eth2Peer peer) { private void peerConnected(Eth2Peer peer) { UInt256 nodeId = peer.getDiscoveryNodeId().orElseThrow(); - listeners.forEach(l -> l.peerConnected(nodeId)); connectedPeers.put(nodeId, peer); + listeners.forEach(l -> l.peerConnected(nodeId)); peer.subscribeDisconnect((__, ___) -> peerDisconnected(peer)); } diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java index f2522c13c38..ce342dd3e1b 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/discovery/discv5/NodeRecordConverter.java @@ -88,7 +88,7 @@ private static Optional parseField( try { return Optional.ofNullable((Bytes) nodeRecord.get(fieldName)).map(parse); } catch (final Exception e) { - LOG.debug("Failed to parse ENR field {}", fieldName, e); + LOG.debug("Failed to parse ENR field {}: {}", fieldName, e); return Optional.empty(); } } diff --git a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrPeerAddress.java b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrPeerAddress.java index 5adfeb6e341..9d3e1cc3dfa 100644 --- a/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrPeerAddress.java +++ b/networking/p2p/src/main/java/tech/pegasys/teku/networking/p2p/libp2p/MultiaddrPeerAddress.java @@ -57,6 +57,11 @@ public Multiaddr getMultiaddr() { return multiaddr; } + @Override + public String toString() { + return getId() + " (" + getMultiaddr() + ")"; + } + @Override public boolean equals(final Object o) { if (this == o) {