Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MX-14354: snapshotless observers support #388

Conversation

bogdan-rosianu
Copy link
Contributor

@bogdan-rosianu bogdan-rosianu commented Jun 30, 2023

Added support for snapshotless observers. When configuring snapshotless observers, they will be used for real-time requests (if available).
Example:

  • the endpoint /address/erd1.. will call snapshotless observers, being a real-time request
  • the endpoint /hyperblock/by-nonce/.. will call regular observers, being a historical request
    if no snapshotless observer is active, the requests that were meant for them will be sent to the regular observers

@bogdan-rosianu bogdan-rosianu changed the base branch from master to rc/v1.6.0 July 5, 2023 13:18
@bogdan-rosianu bogdan-rosianu changed the base branch from rc/v1.6.0 to master July 5, 2023 13:18
@bogdan-rosianu bogdan-rosianu changed the base branch from master to rc/v1.6.0 July 5, 2023 13:28
@bogdan-rosianu bogdan-rosianu changed the title [WIP] snapshotless observers support MX-14354: snapshotless observers support Sep 19, 2023
@bogdan-rosianu bogdan-rosianu marked this pull request as ready for review September 19, 2023 06:47
observer/holder/nodesHolder.go Outdated Show resolved Hide resolved
observer/baseNodeProvider.go Outdated Show resolved Hide resolved
observer/baseNodeProvider.go Outdated Show resolved Hide resolved
observer/holder/nodesHolder.go Outdated Show resolved Hide resolved
observer/holder/nodesHolder.go Show resolved Hide resolved
observer/holder/nodesHolder.go Outdated Show resolved Hide resolved
process/scQueryProcessor.go Outdated Show resolved Hide resolved
observer/holder/nodesHolder_test.go Outdated Show resolved Hide resolved
syncedNodesCache cacheType = "syncedNodes"
outOfSyncNodesCache cacheType = "outOfSyncNodes"
syncedFallbackCache cacheType = "syncedFallbackNodes"
outOfSyncFallbackNodes cacheType = "outOfSyncFallbackNodes"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
outOfSyncFallbackNodes cacheType = "outOfSyncFallbackNodes"
outOfSyncFallbackCache cacheType = "outOfSyncFallbackCache"

for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. renamed

func (nh *nodesHolder) getObservers(cache cacheType, shardID uint32) []*data.NodeData {
cacheKey := getCacheKey(cache, shardID)
nh.mut.RLock()
cachedValues, exists := nh.cache[getCacheKey(cache, shardID)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cachedValues, exists := nh.cache[getCacheKey(cache, shardID)]
cachedValues, exists := nh.cache[cacheKey]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. fixed

Comment on lines 128 to 141
if cache == syncedFallbackCache && isSynced && isFallback {
return true
}
if cache == outOfSyncFallbackNodes && !isSynced && isFallback {
return true
}
if cache == syncedNodesCache && isSynced && !isFallback {
return true
}
if cache == outOfSyncNodesCache && !isSynced && !isFallback {
return true
}

return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if cache == syncedFallbackCache && isSynced && isFallback {
return true
}
if cache == outOfSyncFallbackNodes && !isSynced && isFallback {
return true
}
if cache == syncedNodesCache && isSynced && !isFallback {
return true
}
if cache == outOfSyncNodesCache && !isSynced && !isFallback {
return true
}
return false
return cache == syncedFallbackCache && isSynced && isFallback ||
cache == outOfSyncFallbackNodes && !isSynced && isFallback ||
cache == syncedNodesCache && isSynced && !isFallback ||
cache == outOfSyncNodesCache && !isSynced && !isFallback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it reduces the code size, I do not like this approach is it is more cryptic in terms of understanding the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep it as it is since it's easier to read and can be helpful for debugging (you can trace on which if it returned true

// nodes not cached, compute the list and update the cache
recomputedList := make([]*data.NodeData, 0)
nh.mut.Lock()
for _, node := range nh.allNodes[shardID] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we consider having here one extra check, just in case the list is already recomputed in between RUnlock and Lock?

cachedValues, exists := nh.cache[getCacheKey(cache, shardID)]
if exists {
	return cachedValues
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure. good idea. applied the double-checked locking pattern

"github.com/stretchr/testify/require"
)

func TestNodesHolder_ConstructorAndGetters(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Parallel() on all tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. added

func TestNodesHolder_ConstructorAndGetters(t *testing.T) {
nh, err := NewNodesHolder([]*data.NodeData{}, []*data.NodeData{}, data.AvailabilityAll)
require.Equal(t, errEmptyNodesList, err)
require.True(t, check.IfNil(nh))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.True(t, check.IfNil(nh))
require.Nil(t, nh)

+new test for IsInterfaceNil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


nh, err = NewNodesHolder(syncedNodes, fallbackNodes, data.AvailabilityAll)
require.NoError(t, err)
require.False(t, nh.IsInterfaceNil())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.False(t, nh.IsInterfaceNil())
require.NotNil(t, nh)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"github.com/stretchr/testify/require"
)

func TestNodesHolder_ConstructorAndGetters(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps add some calls to Count() here for 100% coverage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been called on the concurrency test, but added here as well

@@ -35,11 +35,11 @@ func NewCircularQueueNodesProvider(observers []*data.NodeData, configurationFile
}

// GetNodesByShardId will return a slice of observers for the given shard
func (cqnp *circularQueueNodesProvider) GetNodesByShardId(shardId uint32) ([]*data.NodeData, error) {
func (cqnp *circularQueueNodesProvider) GetNodesByShardId(shardId uint32, dataAvailability data.ObserverDataAvailabilityType) ([]*data.NodeData, error) {
cqnp.mutNodes.Lock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RLock ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since I also perform write operations, I would leave it as it is

@@ -51,11 +51,11 @@ func (cqnp *circularQueueNodesProvider) GetNodesByShardId(shardId uint32) ([]*da
}

// GetAllNodes will return a slice containing all observers
func (cqnp *circularQueueNodesProvider) GetAllNodes() ([]*data.NodeData, error) {
func (cqnp *circularQueueNodesProvider) GetAllNodes(dataAvailability data.ObserverDataAvailabilityType) ([]*data.NodeData, error) {
cqnp.mutNodes.Lock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RLock ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since I also perform write operations, I would leave it as it is

@@ -111,6 +111,15 @@ type AccountQueryOptions struct {
HintEpoch core.OptionalUint32
}

// AreHistoricalCoordinatesSet returns true if historical block coordinates are set
func (a AccountQueryOptions) AreHistoricalCoordinatesSet() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

if hasBackup {
return []*data.NodeData{backupNode}, nil
}

return nil, ErrShardNotAvailable
}

func (bnp *baseNodeProvider) getSyncedNodesUnprotected() ([]*data.NodeData, error) {
func filterNodesInShard(nodes []*data.NodeData, shardID uint32) []*data.NodeData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we do not need this if the bnp.regularNodes.GetXXXNodes() methods will return the nodes as maps: map[uint32][]*data.NodeData, where the key uint32 is the shard ID

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

}

func copyNodes(nodes []*data.NodeData) []*data.NodeData {
sliceCopy := make([]*data.NodeData, 0, len(nodes))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could have used copy method. However, if we are going to refactor this implementation to hold the nodes as maps, we will still require the iteration on the keys of the map. We can then use copy for the slices

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

log = logger.GetOrCreate("observer/holder")
)

type nodesHolder struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed, we can greatly simplify this struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

nh.allNodes = make(map[uint32][]*data.NodeData)
nh.cache = make(map[string][]*data.NodeData)
for _, node := range nodesWithSyncStatus {
shouldSkipNode := nh.availability == data.AvailabilityRecent && !node.IsSnapshotless ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please split this in 2. You have 2 && operators along with one ||

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

mapToReturn := make(map[uint32][]*data.NodeData)
// in the first step, consider all the nodes to be active
for _, node := range regularNodes {
node.IsSynced = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this ok? You change a property of the provided slice of nodes
Why not use a clone method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. created a slice clone function and used it

"github.com/stretchr/testify/require"
)

func TestNodesHolder_ConstructorAndGetters(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's stop reinventing new ways of writing tests, especially for complex components. So, let's have a proper test for the constructor and for all setters & getters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a new test for the constructor. For getters, I preferred to keep the same test (split in 2: before and after update) because one getter's response is considered 'correct' if the other getter's response is also correct


require.Equal(t, []*data.NodeData{}, nh.GetOutOfSyncFallbackNodes(0))
require.Equal(t, []*data.NodeData{fallbackNodes[1]}, nh.GetOutOfSyncFallbackNodes(1))
require.Equal(t, []*data.NodeData{}, nh.GetOutOfSyncFallbackNodes(core.MetachainShardId))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so much for "a test should test one thing"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

@@ -507,10 +299,18 @@ func initAllNodesSlice(nodesOnShards map[uint32][]*data.NodeData) ([]*data.NodeD
}

node := nodesOnShards[shardID][i]
if node.IsFallback {
fallbackNodes = append(fallbackNodes, node)
if node.IsSnapshotless {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty ugly if & else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't find a way to refactor it

@@ -452,3 +463,11 @@ func (ap *AccountProcessor) IsDataTrieMigrated(address string, options common.Ac

return nil, ErrSendingRequest
}

func getAvailabilityBasedOnAccountQueryOptions(options common.AccountQueryOptions) data.ObserverDataAvailabilityType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create a stateless component for these If's because the ifs are higher level decision-making code
The component could also have functions that will be used in nodesHolder component L58
L157
L182

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

sstanculeanu
sstanculeanu previously approved these changes Sep 22, 2023
iulianpascalau
iulianpascalau previously approved these changes Sep 25, 2023
func cloneNodesSlice(input []*data.NodeData) []*data.NodeData {
clonedSlice := make([]*data.NodeData, len(input))
for idx, node := range input {
clonedSlice[idx] = &data.NodeData{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works but it is not future changes proof
Could have been:

clonedNodeData := *node
clonedSlice[idx] = &clonedNodeData 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. done

sstanculeanu
sstanculeanu previously approved these changes Sep 25, 2023
iulianpascalau
iulianpascalau previously approved these changes Sep 25, 2023
@bogdan-rosianu bogdan-rosianu changed the base branch from rc/v1.6.0 to feat/snapshotless-observer-support September 26, 2023 10:52
@bogdan-rosianu bogdan-rosianu dismissed stale reviews from iulianpascalau and sstanculeanu September 26, 2023 10:52

The base branch was changed.

@bogdan-rosianu bogdan-rosianu merged commit 8f0c5d9 into feat/snapshotless-observer-support Sep 26, 2023
7 checks passed
@iulianpascalau iulianpascalau deleted the MX-14354-snapshotless-observers branch September 26, 2023 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants