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

[Off-chain] feat: in-memory query cache(s) #994

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

bryanchriswhite
Copy link
Contributor

@bryanchriswhite bryanchriswhite commented Dec 11, 2024

Summary

Adds the QueryCache[T any] and HistoricalQueryCache[T any] interfaces, InMemoryCache[T any] implementation, configurations, and options functions.

---
title: Legend
---

classDiagram-v2

class GenericInterface__T__any {
    <<interface>>
    GenericMethod() T
}

class Implemenetation {
    ExportedField FieldType
    unexportedField FieldType
}

Implemenetation --|> GenericInterface__T__any: implements

%% class Embedder__T__any {
%%     <<interface>>
%%     GenericInterface[T]
%% }

%% Embedder__T__any ..|> GenericInterface__T__any: embeds
Loading
---
title: Query Caches
---

classDiagram-v2


class QueryCache__T__any {
    <<interface>>
    Get(key string) (value T, err error)
    Set(key string, value T) (err error)
    Delete(key string)
    Clear()
}

class HistoricalQueryCache__T__any {
    <<interface>>
    GetLatestVersion(key string) (value T, err error)
    GetVersion(key string, version int64) (value T, err error)
    SetVersion(key string, value T, version int64) (err error)
}

class InMemoryCache__T__any:::cacheImpl
InMemoryCache__T__any --|> QueryCache__T__any
InMemoryCache__T__any --|> HistoricalQueryCache__T__any
Loading

Issue

Type of change

Select one or more from the following:

Testing

  • Documentation: make docusaurus_start; only needed if you make doc changes
  • Unit Tests: make go_develop_and_test
  • LocalNet E2E Tests: make test_e2e
  • DevNet E2E Tests: Add the devnet-test-e2e label to the PR.

Sanity Checklist

  • I have tested my changes using the available tooling
  • I have commented my code
  • I have performed a self-review of my own code; both comments & source code
  • I create and reference any new tickets, if applicable
  • I have left TODOs throughout the codebase, if applicable

@bryanchriswhite bryanchriswhite added the off-chain Off-chain business logic label Dec 11, 2024
@bryanchriswhite bryanchriswhite self-assigned this Dec 11, 2024
@bryanchriswhite bryanchriswhite linked an issue Dec 11, 2024 that may be closed by this pull request
4 tasks
@bryanchriswhite bryanchriswhite marked this pull request as ready for review December 12, 2024 11:20
* pokt/main:
  [Relayminer, Bug] fix: sessiontree logger never initialized (#993)
  fix: E2E tests - RPC URL path (#1008)
  Updated cheat sheat docs with an example after installation (#1004)
  fix: Nil session tree logger (#1007)
@bryanchriswhite bryanchriswhite changed the base branch from fix/sessiontree to main December 13, 2024 14:24
Copy link
Member

@Olshansk Olshansk left a comment

Choose a reason for hiding this comment

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

@bryanchriswhite I did a superficial review but did not dive into the validation of the business logic line-by-line.

Is there any section where you'd want another pair of 👀 ?

pkg/client/interface.go Outdated Show resolved Hide resolved
pkg/client/query/cache/config.go Outdated Show resolved Hide resolved
pkg/client/query/cache/config.go Outdated Show resolved Hide resolved
pkg/client/query/cache/config.go Outdated Show resolved Hide resolved
pkg/client/query/cache/config.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory_test.go Show resolved Hide resolved
c.itemsMu.Lock()
defer c.itemsMu.Unlock()

delete(c.items, key)
Copy link
Member

Choose a reason for hiding this comment

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

What if we delete the (key,value) pair for latestHeight?

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 16, 2024

Choose a reason for hiding this comment

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

Could you elaborate? I'm not quite following.

Copy link
Member

Choose a reason for hiding this comment

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

Yea, this comment doesn't make sense if you don't know where I was looking or what I was thinking...

If we delete the key for the latest height, do we need to also make an appropriate call to c.latestHeight.Store(??)?

If this is indeed the case, please add a unit test for it.

If not, would just appreciate a quick (no need for anything big) explanation.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 18, 2024

Choose a reason for hiding this comment

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

Gotcha, good question. I don't think we need to update c.latestHeight, regardless of which key is being deleted. For Gets, this should be completely transparent to the caller; for Sets, it means that if you call Delete, followed by Set (as opposed to SetAtHeight), then the height you're setting will be the last c.latestHeight. I think this behavior is correct.

If not, would just appreciate a quick (no need for anything big) explanation.

I did see 👆 but had to think it through for myself anyways:

flowchart LR

subgraph hc2[Historical Cache]
    lh2["latestHeight: 2"]
    subgraph k2_2h["key2 history"]
        k2_2h1["height: 1; value 100"]
    end
    subgraph k2_1h["key1 history"]
        k2_1h1["height: 1; value 10"]
        k2_1h2["height: 2; value 20"]
    end
end

subgraph hc3[Historical Cache]
    lh3["latestHeight: 2"]
    subgraph k3_2h["key2 history"]
        k3_2h1["height: 1; value 100"]
    end
end


subgraph hc4[Historical Cache]
    lh4["latestHeight: 2"]
    subgraph k4_1h["key1 history"]
        k4_1h2["height: 2; value 30"]
    end
end

hc2 --"Delete(key1)"--> hc3
hc3 --"Set(key1, 30)"--> hc4

lh2 --> k2_1h2
lh4 --> k4_1h2
Loading

Copy link
Member

Choose a reason for hiding this comment

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

Given the explanation and diagram, I don't fully understand the need for latestHeight at all. In fact, I think we should delete it now if the behaviour you described is expected.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 20, 2024

Choose a reason for hiding this comment

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

I failed to reconcile with this thread but after discussing with @red-0ne, we decided that it should be an error to call Set() on a HistoricalQueryCache. I think this indicates that HistoricalQueryCache SHOULD NOT embed QueryCache after all. Subsequently, latestVersion is no longer necessary and can be removed.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 20, 2024

Choose a reason for hiding this comment

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

// QueryCache is a key/value store style interface for a cache of a single type.
// It is intended to be used to cache query responses (or derivatives thereof),
// where each key uniquely indexes the most recent query response.
type QueryCache[T any] interface {
	Get(key string) (T, error)
	Set(key string, value T) error
	Delete(key string)
	Clear()
}

// HistoricalQueryCache extends QueryCache to support getting and setting values
// at multiple heights for a given key.
type HistoricalQueryCache[T any] interface {
	// GetLatestVersion retrieves the historical value with the highest version number.
	GetLatestVersion(key string) (T, error)
	// GetAsOfVersion retrieves the nearest value <= the specified version number.
	GetAsOfVersion(key string, version int64) (T, error)
	// SetAsOfVersion adds or updates a value at a specific version number.
	SetAsOfVersion(key string, value T, version int64) error
}

(class diagrams also updated)

pkg/client/query/cache/memory_test.go Outdated Show resolved Hide resolved
Copy link
Member

@Olshansk Olshansk left a comment

Choose a reason for hiding this comment

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

Love the iteration. Thanks @bryanchriswhite!

Left a few new comments, a few replies to older threads, but it should be g2g after the next round 🙌

pkg/client/query/cache/config.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
}

// GetAtHeight retrieves the value from the cache with the given key, at the given
// height. If a value is not found for that height, the value at the nearest previous
Copy link
Member

Choose a reason for hiding this comment

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

When I see a function like GetAtHeight, my immediate thought is that it'll return the value at the height provided (if available) or nil otherwise.

For height=5, it's the difference between select * from table where height=5 and select * from table where height <= 5.

There could be business logic that depends on state transitions AT A SPECIFIC HEIGHT, so I'm just afraid that it's not explicit enough.

pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Show resolved Hide resolved
c.itemsMu.Lock()
defer c.itemsMu.Unlock()

delete(c.items, key)
Copy link
Member

Choose a reason for hiding this comment

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

Yea, this comment doesn't make sense if you don't know where I was looking or what I was thinking...

If we delete the key for the latest height, do we need to also make an appropriate call to c.latestHeight.Store(??)?

If this is indeed the case, please add a unit test for it.

If not, would just appreciate a quick (no need for anything big) explanation.

pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
Copy link
Contributor

@red-0ne red-0ne left a comment

Choose a reason for hiding this comment

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

Great caching design.

Left some comments but did not tend to pkg/client/query/cache/memory_test.go yet. Which I'll do right after this one.

pkg/client/query/cache/config.go Show resolved Hide resolved
pkg/client/query/cache/memory.go Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
pkg/client/query/cache/memory.go Outdated Show resolved Hide resolved
Copy link
Contributor

@red-0ne red-0ne left a comment

Choose a reason for hiding this comment

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

Asking for a small change, but otherwise LGTM.

Not approving yet as I want review the tests too.

pkg/client/query/cache/memory.go Show resolved Hide resolved
pkg/client/query/cache/memory.go Show resolved Hide resolved
// SetAtHeight adds or updates a value at a specific height
SetAtHeight(key string, value T, height int64) error
// GetAsOfVersion retrieves the nearest value <= the specified version number.
GetAsOfVersion(key string, version int64) (T, error)
Copy link
Member

Choose a reason for hiding this comment

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

Can we go with one of the following instead:

  1. GetVersioned
  2. GetAtVersion
  3. GetHistorical

Don't care which one but AsOf in a function name just doesn't feel right.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 20, 2024

Choose a reason for hiding this comment

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

In that case, GetVersion and SetVersion would be my preference. Any objections @red-0ne @Olshansk?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also adding GetLatestVersion.

pkg/client/query/cache/config.go Show resolved Hide resolved
// values. If 0, no historical pruning is performed. It only applies when
// historical is true.
pruneOlderThan int64
// maxVersionAge is the max difference between the latest known version and
Copy link
Member

Choose a reason for hiding this comment

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

this sentence is hard to grok.

Any chance you could improve it with an example?

items map[string]any
type inMemoryCache[T any] struct {
config queryCacheConfig
latestVersion atomic.Int64
Copy link
Member

Choose a reason for hiding this comment

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

Per my other comment, can you #PUC why we need this and how its used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🫡 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
off-chain Off-chain business logic
Projects
Status: 👀 In review
Development

Successfully merging this pull request may close these issues.

[Off-Chain] ModuleParamsClient & Historical Params
3 participants