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

✨ Refactor Cache to be an interface #41

Merged
merged 1 commit into from
Dec 9, 2019

Conversation

eschlenz
Copy link
Contributor

@eschlenz eschlenz commented Dec 6, 2019

…tions

In reference to this issue: #40

What is the problem you are trying to solve?

Per issue #40, reading from the cache is not as fast as it could be in certain scenarios. There is logic that both reads from, and writes to, the disk cache even when the cached data is readily available from memory. This drastically slows down what should otherwise be a fast fetch of data.

The reason for the disk read and write in this scenario ^ makes sense. You are making sure the disk LRU is updated to reflect the recent access to a cached entry.

I think, ideally, such updates would be non-blocking (performed asynchronously). I actually started down this path. The contract requirement to return a Result, however, halted my progress. If a caller issues a cache put expecting a Result, the only two options are Result.Success or Result.Failure. How could a Result be returned immediately, if the disk cache update is happening asynchronously? It can't. You would instead, perhaps, need a Result.Pending or something along those lines.

I felt your current implementation had contract obligations that I wasn't ready to upset. After all, you may already have clients that actually depend on the disk updates being synchronous. I would love to discuss this further, if you are open to contractual changes that might incorporate asynchronous disk cache updates as standard.

Instead, my strategy for this PR was to simply make it possible to have an alternate implementation of the Cache class. This would allow myself, or others, to create their own implementation that chooses a different disk write strategy.

What changed specifically?

  1. Your concrete Cache class was renamed to CacheImpl.
  2. A Cache interface was introduced, and CacheImpl changed to implement that.
  3. The Source enum was moved to be a top-level construct for other classes (such as my own implementation [yet to be written]) to use.
  4. References to CacheImpl.Source were changed to just be Source.
  5. I made one additional optimization, which I will call out in the code below.

Anything else?

I confirmed that your tests all still pass. I also made sure the sample app was still working.

@@ -30,16 +30,20 @@ object CacheBuilder {
}
}

fun <T : Any> Config<T>.build() = Cache(this)
fun <T : Any> Config<T>.build(): Cache<T> = CacheImpl(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.

The caller of build() should not need to know what specific implementation of Cache is used. So this was changed to just return the interface type.


class Cache<T : Any> internal constructor(private val config: Config<T>) : Fuse.Cacheable,
Fuse.Cacheable.Put<T>, Fuse.Cacheable.Get<T>, Fuse.DataConvertible<T> by config.convertible {
enum class Source {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved out of the concrete class, to the top level. This makes reusing of this enum more natural in alternative concrete implementations of Cache.

MEM,
DISK,
}
interface Cache<T : Any> : Fuse.Cacheable, Fuse.Cacheable.Put<T>, Fuse.Cacheable.Get<T>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the new interface definition.

interface Cache<T : Any> : Fuse.Cacheable, Fuse.Cacheable.Put<T>, Fuse.Cacheable.Get<T>,
Fuse.DataConvertible<T>

class CacheImpl<T : Any> internal constructor(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your concrete class now is of type Cache.

if (diskCache.get(safeKey) == null) {
val converted = convertToData(value as T)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This line was moved into the if condition, because the convertToData operation is potentially expensive. Users of your library supply the converter, so "we" can't make assumptions about how fast it is.

It seems better to defer the data conversion until we absolutely know we need to do it. Previously, the conversion would have run even if diskCache.get(safeKey) returned a non-null value.

Copy link
Owner

Choose a reason for hiding this comment

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

Ah, makes sense!

@eschlenz eschlenz marked this pull request as ready for review December 6, 2019 23:13
@codecov
Copy link

codecov bot commented Dec 6, 2019

Codecov Report

Merging #41 into master will decrease coverage by 0.54%.
The diff coverage is 87.5%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master      #41      +/-   ##
============================================
- Coverage     80.76%   80.21%   -0.55%     
+ Complexity       68       46      -22     
============================================
  Files            11       11              
  Lines           182      182              
  Branches         30       31       +1     
============================================
- Hits            147      146       -1     
- Misses           17       18       +1     
  Partials         18       18
Impacted Files Coverage Δ Complexity Δ
...c/main/java/com/github/kittinunf/fuse/core/Fuse.kt 50% <100%> (ø) 0 <0> (ø) ⬇️
...hub/kittinunf/fuse/core/scenario/ExpirableCache.kt 76.47% <100%> (ø) 12 <0> (ø) ⬇️
.../main/java/com/github/kittinunf/fuse/core/Cache.kt 81.15% <80%> (-1.45%) 0 <0> (-22)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 437d186...6dc6553. Read the comment docs.

@kittinunf kittinunf self-assigned this Dec 7, 2019
@kittinunf
Copy link
Owner

I don't see anything wrong with these changes! It makes it even more flexible .. 👍

@kittinunf kittinunf changed the title Refactor Cache to be an interface, thus allowing alternate implementa… ✨ Refactor Cache to be an interface Dec 9, 2019
@kittinunf kittinunf merged commit ad199d1 into kittinunf:master Dec 9, 2019
@eschlenz
Copy link
Contributor Author

eschlenz commented Dec 9, 2019

Thanks @kittinunf! Would it be possible to cut a new release with these changes?

@kittinunf
Copy link
Owner

Definitely! I will do that right away :)

@kittinunf
Copy link
Owner

kittinunf commented Dec 10, 2019

Done sir! ⏫ Please give it a try and report back, whether it works on your end ❤️

@eschlenz
Copy link
Contributor Author

Thank you!

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.

2 participants