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

Caching Abstraction #211

Merged
merged 8 commits into from
Feb 6, 2016
Merged

Caching Abstraction #211

merged 8 commits into from
Feb 6, 2016

Conversation

loganlinn
Copy link
Contributor

Introduce a functional caching abstraction. The purpose is to provide extensible
API with abstractions that are composable. Includes implementations for basic
cache strategy (current) and an LRU cache (which wraps BasicCache).

A custom caching strategy can be injected via Reactor constructor.

All existing caching behavior remains the same (for now).

Tangential to #208

Introduce a functional caching abstraction. The purpose is to provide extensible
API with abstractions that are composable. Includes implementations for basic
cache strategy (current) and an LRU cache (which wraps BasicCache).

A custom caching strategy can be injected via Reactor constructor.

All existing caching behavior remains the same (for now).
We weren't using notion of monoatomically increasing tick in LRU cache because
an OrderedMap does not re-order exising items that are set(), so we depend on
remove()+set(). This means the value of OrderedMap isn't used and simplifies to
OrderedSet.

/*******************************************************************************
* interface PersistentCache {
* has(item)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are lookup, has, and hit all necessary? Is a get method insufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

lookup and has are simply analogous to get and has in an Immutable.Map.

The reason why there seems to be extra operations is because nuclear is designed to be used with a persistent data structure for the cache. For a caching abstraction to encapsulate something like LRU, the cache has to record when an item is read from cache. Since lookup returns the actual value in cache and we can't perform side-effects, another operation is required to update the cache to track LRU.

I meant to add a comment in the code, but this interface is modeled directly after the caching protocol in clojure.core.cache, which is also for immutable data caches

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. I figured the reason was to keep the cache construct immutable, IMO would be simpler for developers to allow mutability and go with get + put

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My initial approach had this, but changing from a immutable to mutable cache had negative implications (e.g. when reactor evaluates using previous state causing cache churn)

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, makes sense

@scjackson
Copy link
Contributor

@loganlinn I dont like the interface but do like the approach

If you call hit() on an item that's not actually in the LRUCache (edge case),
it's a no-op, so we can just return this.
@loganlinn
Copy link
Contributor Author

@scjackson Updated to support evicting a bunch of values from LRU when limit is reached

*/
miss(item, entry) {
if (this.lru.size >= this.limit) {
if (this.has(item)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why bother with this case?

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's reasonable that you successfully lookup something from the cache, but you have your own notion of whether it's stale and want to write a new value back. We do this currently with isDirtyCacheEntry

Copy link
Contributor

Choose a reason for hiding this comment

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

I was more wondering why worry about whether or not the cache has the value -- we could just evict LRU values and add the item. But I guess that might lead to having CACHE_LIMIT - 1 values in cache, so nvm.

const args = getDeps(keyPathOrGetter).map(dep => evaluate(reactorState, dep).result)
const evaluatedValue = getComputeFn(keyPathOrGetter).apply(null, args)
const cache = reactorState.get('cache')
var cacheEntry = cache.lookup(keyPathOrGetter)
Copy link
Contributor

Choose a reason for hiding this comment

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

do you think cacheLookup here should also handle the resolution of storeStates versus externalizing it?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the cache is a hit on a getter then subsequent evalutes will always think the value is a hit.

Maybe I am missing something here, but it seems like this is wrong in a lot of cases.

// store state 1
var result1 = reactor.evaluate(getter)
reactor.dispatch('action')
// store state 2
var results2 = reactor.evaluate(getter)
// it would seem that since the reactor cache has an entry for `getter` than it will always be a hit

Is there a test case to verify this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the cache is a hit on a getter then subsequent evalutes will always think the value is a hit.

Ah, good catch. This should be

const isCacheMiss = !cacheEntry || isDirtyCacheEntry(reactorState, cacheEntry)
if (isCacheMiss) {
  // ...
}
// ...

I'm surprised this isn't covered by a test case already. Will add 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.

do you think cacheLookup here should also handle the resolution of storeStates versus externalizing it?

The caches are generalized right now and don't have nuclear specific logic. I felt like this was still the right place for that logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jordangarcia
Copy link
Contributor

Design looks good. I like how we can build the LRU cache using the BasicCache.

As far as implementation, see my comment regarding false positive cache hits.

@scjackson
Copy link
Contributor

@loganlinn this LGTM. What do you want to do with this in terms of merging and my PR? Thoughts @jordangarcia?

loganlinn added a commit that referenced this pull request Feb 6, 2016
@loganlinn loganlinn merged commit b719efb into master Feb 6, 2016
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