Skip to content

Commit

Permalink
Add dynamic service configuration
Browse files Browse the repository at this point in the history
Signed-off-by: Bilyana Gospodinova <[email protected]>
  • Loading branch information
bilyana-gospodinova committed Nov 5, 2024
1 parent dda0c80 commit 7b1d58d
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ReadableKVState<?, ?>> tokenReadableServiceStates = new HashMap<>();
private final Map<String, ReadableKVState<?, ?>> contractReadableServiceStates = new HashMap<>();
private final Map<String, ReadableKVState<?, ?>> fileReadableServiceStates = new HashMap<>();

private final Map<String, ReadableStates> readableStates = new ConcurrentHashMap<>();
// Key is Service, value is Map of state name to state datasource
private final Map<String, Map<String, Object>> 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<String, ?> 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<String, Object> 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<String, ?> getWritableStates(final Map<String, ReadableKVState<?, ?>> readableStates) {
final Map<String, Object> data = new HashMap<>();
readableStates.forEach(((s, readableKVState) ->
data.put(s, new MapWritableKVState<>(readableKVState.getStateKey(), readableKVState))));
return data;
final Map<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <K> The key type
* @param <V> The value type
*/
public class MapReadableKVState<K, V> extends ReadableKVStateBase<K, V> {
/** Represents the backing storage for this state */
private final Map<K, V> 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<K, V> backingStore) {
super(stateKey);
this.backingStore = Objects.requireNonNull(backingStore);
}

@Override
protected V readFromDataSource(@Nonnull K key) {
return backingStore.get(key);
}

@Nonnull
@Override
protected Iterator<K> 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);
}
}
Loading

0 comments on commit 7b1d58d

Please sign in to comment.