From 7b1d58d7afb27db9e06fe764b5d5d1e5e9f5012d Mon Sep 17 00:00:00 2001 From: Bilyana Gospodinova Date: Tue, 5 Nov 2024 16:01:56 +0200 Subject: [PATCH] Add dynamic service configuration Signed-off-by: Bilyana Gospodinova --- .../mirror/web3/state/MirrorNodeState.java | 113 ++++++++++-------- .../web3/state/utils/MapReadableKVState.java | 81 +++++++++++++ .../web3/state/MirrorNodeStateTest.java | 90 +++++++++++--- .../state/utils/MapReadableKVStateTest.java | 109 +++++++++++++++++ 4 files changed, 323 insertions(+), 70 deletions(-) create mode 100644 hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/utils/MapReadableKVState.java create mode 100644 hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/utils/MapReadableKVStateTest.java diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/MirrorNodeState.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/MirrorNodeState.java index e3cdce4089..85d1971c00 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/MirrorNodeState.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/MirrorNodeState.java @@ -16,91 +16,98 @@ package com.hedera.mirror.web3.state; +import static java.util.Objects.requireNonNull; + +import com.hedera.mirror.web3.state.utils.MapReadableKVState; import com.hedera.mirror.web3.state.utils.MapReadableStates; import com.hedera.mirror.web3.state.utils.MapWritableKVState; import com.hedera.mirror.web3.state.utils.MapWritableStates; -import com.hedera.node.app.service.contract.ContractService; -import com.hedera.node.app.service.file.FileService; -import com.hedera.node.app.service.token.TokenService; import com.swirlds.state.State; -import com.swirlds.state.spi.ReadableKVState; +import com.swirlds.state.spi.EmptyReadableStates; +import com.swirlds.state.spi.EmptyWritableStates; import com.swirlds.state.spi.ReadableStates; import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; import jakarta.annotation.Nonnull; import jakarta.inject.Named; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +@SuppressWarnings({"rawtypes", "unchecked"}) @Named public class MirrorNodeState implements State { - private final Map> tokenReadableServiceStates = new HashMap<>(); - private final Map> contractReadableServiceStates = new HashMap<>(); - private final Map> fileReadableServiceStates = new HashMap<>(); private final Map readableStates = new ConcurrentHashMap<>(); + // Key is Service, value is Map of state name to state datasource + private final Map> states = new ConcurrentHashMap<>(); - public MirrorNodeState( - final AccountReadableKVState accountReadableKVState, - final AirdropsReadableKVState airdropsReadableKVState, - final AliasesReadableKVState aliasesReadableKVState, - final ContractBytecodeReadableKVState contractBytecodeReadableKVState, - final ContractStorageReadableKVState contractStorageReadableKVState, - final FileReadableKVState fileReadableKVState, - final NftReadableKVState nftReadableKVState, - final TokenReadableKVState tokenReadableKVState, - final TokenRelationshipReadableKVState tokenRelationshipReadableKVState) { - - tokenReadableServiceStates.put("ACCOUNTS", accountReadableKVState); - tokenReadableServiceStates.put("PENDING_AIRDROPS", airdropsReadableKVState); - tokenReadableServiceStates.put("ALIASES", aliasesReadableKVState); - tokenReadableServiceStates.put("NFTS", nftReadableKVState); - tokenReadableServiceStates.put("TOKENS", tokenReadableKVState); - tokenReadableServiceStates.put("TOKEN_RELS", tokenRelationshipReadableKVState); + public MirrorNodeState addService(@NonNull final String serviceName, @NonNull final Map dataSources) { + final var serviceStates = this.states.computeIfAbsent(serviceName, k -> new ConcurrentHashMap<>()); + dataSources.forEach((k, b) -> { + if (!serviceStates.containsKey(k)) { + serviceStates.put(k, b); + } + }); - contractReadableServiceStates.put("BYTECODE", contractBytecodeReadableKVState); - contractReadableServiceStates.put("STORAGE", contractStorageReadableKVState); + // Purge any readable states whose state definitions are now stale, + // since they don't include the new data sources we just added + readableStates.remove(serviceName); + return this; + } - fileReadableServiceStates.put("FILES", fileReadableKVState); + /** + * Removes the state with the given key for the service with the given name. + * + * @param serviceName the name of the service + * @param stateKey the key of the state + */ + public void removeServiceState(@NonNull final String serviceName, @NonNull final String stateKey) { + requireNonNull(serviceName); + requireNonNull(stateKey); + this.states.computeIfPresent(serviceName, (k, v) -> { + v.remove(stateKey); + readableStates.remove(serviceName); // Remove the service so that its states will be repopulated. + return v; + }); } @Nonnull @Override public ReadableStates getReadableStates(@Nonnull String serviceName) { return readableStates.computeIfAbsent(serviceName, s -> { - switch (s) { - case TokenService.NAME -> { - return new MapReadableStates(tokenReadableServiceStates); - } - case ContractService.NAME -> { - return new MapReadableStates(contractReadableServiceStates); - } - case FileService.NAME -> { - return new MapReadableStates(fileReadableServiceStates); - } - default -> { - return new MapReadableStates(Collections.emptyMap()); + final var serviceStates = this.states.get(s); + if (serviceStates == null) { + return new EmptyReadableStates(); + } + final Map states = new ConcurrentHashMap<>(); + for (final var entry : serviceStates.entrySet()) { + final var stateName = entry.getKey(); + final var state = entry.getValue(); + if (state instanceof Map map) { + states.put(stateName, new MapReadableKVState(stateName, map)); } } + return new MapReadableStates(states); }); } @Nonnull @Override public WritableStates getWritableStates(@Nonnull String serviceName) { - return switch (serviceName) { - case TokenService.NAME -> new MapWritableStates(getWritableStates(tokenReadableServiceStates)); - case ContractService.NAME -> new MapWritableStates(getWritableStates(contractReadableServiceStates)); - case FileService.NAME -> new MapWritableStates(getWritableStates(fileReadableServiceStates)); - default -> new MapWritableStates(Collections.emptyMap()); - }; - } + final var serviceStates = states.get(serviceName); + if (serviceStates == null) { + return new EmptyWritableStates(); + } - private Map getWritableStates(final Map> readableStates) { - final Map data = new HashMap<>(); - readableStates.forEach(((s, readableKVState) -> - data.put(s, new MapWritableKVState<>(readableKVState.getStateKey(), readableKVState)))); - return data; + final Map data = new ConcurrentHashMap<>(); + for (final var entry : serviceStates.entrySet()) { + final var stateName = entry.getKey(); + final var state = entry.getValue(); + if (state instanceof Map) { + final var readableState = getReadableStates(serviceName).get(stateName); + data.put(stateName, new MapWritableKVState<>(stateName, readableState)); + } + } + return new MapWritableStates(data); } } diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/utils/MapReadableKVState.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/utils/MapReadableKVState.java new file mode 100644 index 0000000000..3d37e99211 --- /dev/null +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/utils/MapReadableKVState.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * + * 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 com.hedera.mirror.web3.state.utils; + +import com.swirlds.state.spi.ReadableKVState; +import com.swirlds.state.spi.ReadableKVStateBase; +import jakarta.annotation.Nonnull; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** + * A simple implementation of {@link ReadableKVState} backed by a + * {@link Map}. Test code has the option of creating an instance disregarding the backing map, or by + * supplying the backing map to use. This latter option is useful if you want to use Mockito to spy + * on it, or if you want to pre-populate it, or use Mockito to make the map throw an exception in + * some strange case, or in some other way work with the backing map directly. + * + * @param The key type + * @param The value type + */ +public class MapReadableKVState extends ReadableKVStateBase { + /** Represents the backing storage for this state */ + private final Map backingStore; + + /** + * Create an instance using the given map as the backing store. This is useful when you want to + * pre-populate the map, or if you want to use Mockito to mock it or cause it to throw + * exceptions when certain keys are accessed, etc. + * + * @param stateKey The state key for this state + * @param backingStore The backing store to use + */ + public MapReadableKVState(@Nonnull final String stateKey, @Nonnull final Map backingStore) { + super(stateKey); + this.backingStore = Objects.requireNonNull(backingStore); + } + + @Override + protected V readFromDataSource(@Nonnull K key) { + return backingStore.get(key); + } + + @Nonnull + @Override + protected Iterator iterateFromDataSource() { + return backingStore.keySet().iterator(); + } + + @Override + public long size() { + return backingStore.size(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MapReadableKVState that = (MapReadableKVState) o; + return Objects.equals(getStateKey(), that.getStateKey()) && Objects.equals(backingStore, that.backingStore); + } + + @Override + public int hashCode() { + return Objects.hash(getStateKey(), backingStore); + } +} diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateTest.java index 6bcd187535..67686501d1 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/MirrorNodeStateTest.java @@ -19,13 +19,17 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.when; +import com.hedera.mirror.web3.state.utils.MapReadableKVState; import com.hedera.mirror.web3.state.utils.MapReadableStates; import com.hedera.mirror.web3.state.utils.MapWritableKVState; import com.hedera.mirror.web3.state.utils.MapWritableStates; import com.hedera.node.app.service.contract.ContractService; import com.hedera.node.app.service.file.FileService; import com.hedera.node.app.service.token.TokenService; +import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -65,10 +69,54 @@ class MirrorNodeStateTest { @Mock private TokenRelationshipReadableKVState tokenRelationshipReadableKVState; + @BeforeEach + void setup() { + final Map fileStateData = new HashMap<>(Map.of("FILES", Map.of("FILES", fileReadableKVState))); + final Map contractStateData = new HashMap<>(Map.of( + "BYTECODE", Map.of("BYTECODE", contractBytecodeReadableKVState), + "STORAGE", Map.of("STORAGE", contractStorageReadableKVState))); + final Map tokenStateData = new HashMap<>(Map.of( + "ACCOUNTS", Map.of("ACCOUNTS", accountReadableKVState), + "PENDING_AIRDROPS", Map.of("PENDING_AIRDROPS", airdropsReadableKVState), + "ALIASES", Map.of("ALIASES", aliasesReadableKVState), + "NFTS", Map.of("NFTS", nftReadableKVState), + "TOKENS", Map.of("TOKENS", tokenReadableKVState), + "TOKEN_RELS", Map.of("TOKEN_RELS", tokenRelationshipReadableKVState))); + + // Add service using the mock data source + mirrorNodeState = mirrorNodeState + .addService(FileService.NAME, fileStateData) + .addService(ContractService.NAME, contractStateData) + .addService(TokenService.NAME, tokenStateData); + } + + @Test + void testAddService() { + assertThat(mirrorNodeState.getReadableStates("NEW").contains("FILES")).isFalse(); + final var newState = + mirrorNodeState.addService("NEW", new HashMap<>(Map.of("FILES", Map.of("FILES", fileReadableKVState)))); + assertThat(newState.getReadableStates("NEW").contains("FILES")).isTrue(); + } + + @Test + void testRemoveService() { + final var testStates = new HashMap<>(Map.of( + "BYTECODE", Map.of("BYTECODE", contractBytecodeReadableKVState), + "STORAGE", Map.of("STORAGE", contractStorageReadableKVState))); + final var newState = mirrorNodeState.addService("NEW", testStates); + assertThat(newState.getReadableStates("NEW").contains("BYTECODE")).isTrue(); + assertThat(newState.getReadableStates("NEW").contains("STORAGE")).isTrue(); + newState.removeServiceState("NEW", "BYTECODE"); + assertThat(newState.getReadableStates("NEW").contains("BYTECODE")).isFalse(); + assertThat(newState.getReadableStates("NEW").contains("STORAGE")).isTrue(); + } + @Test void testGetReadableStatesForFileService() { final var readableStates = mirrorNodeState.getReadableStates(FileService.NAME); - assertThat(readableStates).isEqualTo(new MapReadableStates(Map.of("FILES", fileReadableKVState))); + assertThat(readableStates) + .isEqualTo(new MapReadableStates(new ConcurrentHashMap<>( + Map.of("FILES", new MapReadableKVState("FILES", Map.of("FILES", fileReadableKVState)))))); } @Test @@ -76,7 +124,10 @@ void testGetReadableStatesForContractService() { final var readableStates = mirrorNodeState.getReadableStates(ContractService.NAME); assertThat(readableStates) .isEqualTo(new MapReadableStates(Map.of( - "BYTECODE", contractBytecodeReadableKVState, "STORAGE", contractStorageReadableKVState))); + "BYTECODE", + new MapReadableKVState("BYTECODE", Map.of("BYTECODE", contractBytecodeReadableKVState)), + "STORAGE", + new MapReadableKVState("STORAGE", Map.of("STORAGE", contractStorageReadableKVState))))); } @Test @@ -85,17 +136,17 @@ void testGetReadableStatesForTokenService() { assertThat(readableStates) .isEqualTo(new MapReadableStates(Map.of( "ACCOUNTS", - accountReadableKVState, + new MapReadableKVState("ACCOUNTS", Map.of("ACCOUNTS", accountReadableKVState)), "PENDING_AIRDROPS", - airdropsReadableKVState, + new MapReadableKVState("PENDING_AIRDROPS", Map.of("PENDING_AIRDROPS", airdropsReadableKVState)), "ALIASES", - aliasesReadableKVState, + new MapReadableKVState("ALIASES", Map.of("ALIASES", aliasesReadableKVState)), "NFTS", - nftReadableKVState, + new MapReadableKVState("NFTS", Map.of("NFTS", nftReadableKVState)), "TOKENS", - tokenReadableKVState, + new MapReadableKVState("TOKENS", Map.of("TOKENS", tokenReadableKVState)), "TOKEN_RELS", - tokenRelationshipReadableKVState))); + new MapReadableKVState("TOKEN_RELS", Map.of("TOKEN_RELS", tokenRelationshipReadableKVState))))); } @Test @@ -108,9 +159,11 @@ void testGetWritableStatesForFileService() { when(fileReadableKVState.getStateKey()).thenReturn("FILES"); final var writableStates = mirrorNodeState.getWritableStates(FileService.NAME); + final var readableStates = mirrorNodeState.getReadableStates(FileService.NAME); assertThat(writableStates) .isEqualTo(new MapWritableStates(Map.of( - "FILES", new MapWritableKVState<>(fileReadableKVState.getStateKey(), fileReadableKVState)))); + "FILES", + new MapWritableKVState<>(fileReadableKVState.getStateKey(), readableStates.get("FILES"))))); } @Test @@ -119,14 +172,15 @@ void testGetWritableStatesForContractService() { when(contractStorageReadableKVState.getStateKey()).thenReturn("STORAGE"); final var writableStates = mirrorNodeState.getWritableStates(ContractService.NAME); + final var readableStates = mirrorNodeState.getReadableStates(ContractService.NAME); assertThat(writableStates) .isEqualTo(new MapWritableStates(Map.of( "BYTECODE", new MapWritableKVState<>( - contractBytecodeReadableKVState.getStateKey(), contractBytecodeReadableKVState), + contractBytecodeReadableKVState.getStateKey(), readableStates.get("BYTECODE")), "STORAGE", new MapWritableKVState<>( - contractStorageReadableKVState.getStateKey(), contractStorageReadableKVState)))); + contractStorageReadableKVState.getStateKey(), readableStates.get("STORAGE"))))); } @Test @@ -139,21 +193,23 @@ void testGetWritableStatesForTokenService() { when(tokenRelationshipReadableKVState.getStateKey()).thenReturn("TOKEN_RELS"); final var writableStates = mirrorNodeState.getWritableStates(TokenService.NAME); + final var readableStates = mirrorNodeState.getReadableStates(TokenService.NAME); assertThat(writableStates) .isEqualTo(new MapWritableStates(Map.of( "ACCOUNTS", - new MapWritableKVState<>(accountReadableKVState.getStateKey(), accountReadableKVState), + new MapWritableKVState<>(accountReadableKVState.getStateKey(), readableStates.get("ACCOUNTS")), "PENDING_AIRDROPS", - new MapWritableKVState<>(airdropsReadableKVState.getStateKey(), airdropsReadableKVState), + new MapWritableKVState<>( + airdropsReadableKVState.getStateKey(), readableStates.get("PENDING_AIRDROPS")), "ALIASES", - new MapWritableKVState<>(aliasesReadableKVState.getStateKey(), aliasesReadableKVState), + new MapWritableKVState<>(aliasesReadableKVState.getStateKey(), readableStates.get("ALIASES")), "NFTS", - new MapWritableKVState<>(nftReadableKVState.getStateKey(), nftReadableKVState), + new MapWritableKVState<>(nftReadableKVState.getStateKey(), readableStates.get("NFTS")), "TOKENS", - new MapWritableKVState<>(tokenReadableKVState.getStateKey(), tokenReadableKVState), + new MapWritableKVState<>(tokenReadableKVState.getStateKey(), readableStates.get("TOKENS")), "TOKEN_RELS", new MapWritableKVState<>( - tokenRelationshipReadableKVState.getStateKey(), tokenRelationshipReadableKVState)))); + tokenRelationshipReadableKVState.getStateKey(), readableStates.get("TOKEN_RELS"))))); } @Test diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/utils/MapReadableKVStateTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/utils/MapReadableKVStateTest.java new file mode 100644 index 0000000000..fdc51f25cd --- /dev/null +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/utils/MapReadableKVStateTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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 com.hedera.mirror.web3.state.utils; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.state.token.Account; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MapReadableKVStateTest { + + private MapReadableKVState mapReadableKVState; + + private Map accountMap; + + @Mock + private AccountID accountID; + + @Mock + private Account account; + + @BeforeEach + void setup() { + accountMap = Map.of(accountID, account); + mapReadableKVState = new MapReadableKVState<>("ACCOUNTS", accountMap); + } + + @Test + void testReadFromDataSource() { + assertThat(mapReadableKVState.readFromDataSource(accountID)).isEqualTo(account); + } + + @Test + void testReadFromDataSourceNotExisting() { + assertThat(mapReadableKVState.readFromDataSource( + AccountID.newBuilder().accountNum(1L).build())) + .isNull(); + } + + @Test + void testIterateFromDataSource() { + assertThat(mapReadableKVState.iterateFromDataSource().hasNext()).isTrue(); + assertThat(mapReadableKVState.iterateFromDataSource().next()).isEqualTo(accountID); + } + + @Test + void testSize() { + assertThat(mapReadableKVState.size()).isEqualTo(1L); + final var accountID1 = AccountID.newBuilder().accountNum(1L).build(); + final var accountID2 = AccountID.newBuilder().accountNum(2L).build(); + final var mapReadableKVStateBigger = new MapReadableKVState<>( + "ACCOUNTS", + Map.of( + accountID1, + Account.newBuilder().accountId(accountID1).build(), + accountID2, + Account.newBuilder().accountId(accountID2).build())); + assertThat(mapReadableKVStateBigger.size()).isEqualTo(2L); + } + + @Test + void testEqualsSameInstance() { + assertThat(mapReadableKVState).isEqualTo(mapReadableKVState); + } + + @Test + void testEqualsDifferentType() { + assertThat(mapReadableKVState).isNotEqualTo("someString"); + } + + @Test + void testEqualsSameValues() { + MapReadableKVState other = new MapReadableKVState<>("ACCOUNTS", accountMap); + assertThat(mapReadableKVState).isEqualTo(other); + } + + @Test + void testEqualsDifferentValues() { + MapReadableKVState other = new MapReadableKVState<>("ALIASES", accountMap); + assertThat(mapReadableKVState).isNotEqualTo(other); + } + + @Test + void testHashCode() { + MapReadableKVState other = new MapReadableKVState<>("ACCOUNTS", accountMap); + assertThat(mapReadableKVState).hasSameHashCodeAs(other); + } +}