GithubHelp home page GithubHelp logo

acornsgrow / hitnmiss Goto Github PK

View Code? Open in Web Editor NEW
8.0 91.0 4.0 48 KB

Ruby gem to support using the Repository pattern for read-through, write-behind caching using POROs

License: MIT License

Ruby 99.84% Shell 0.16%

hitnmiss's Introduction

Build Status Code Climate Test Coverage Issue Count

Hitnmiss

Hitnmiss is a Ruby gem that provides support for using the Repository pattern for read-through, write-behind caching in a thread-safe way. It is built heavily around using POROs (Plain Old Ruby Objects). This means it is intended to be used with all kinds of Ruby applications from plain Ruby command line tools, to framework (Rails, Sinatra, Rack, Hanami, etc.) based web apps, etc.

Having defined repositories for accessing your caches aids in a number of ways. First, it centralizes cache key generation. Secondly, it centralizes & standardizes access to the cache rather than having code spread across your app duplicating key generation and access. Third, it provides clean separation between the cache persistence layer and the business representation.

Installation

Add this line to your application's Gemfile:

gem 'hitnmiss'

And then execute:

$ bundle

Or install it yourself as:

$ gem install hitnmiss

Define/Mixin a Repository

Before you can use Hitnmiss to manage cache you first need to define a cache repository. This is done by defining a class for your repository and mixing the appropriate repository module in using include.

Below, are explanations of the Standard Repository and the Background Refresh Repository so that you can decide which one fits your needs as well as learn how to use them.

Standard Repository

The standard repository module, Hitnmiss::Repository, fits the most common caching use case, the "Expiration Model". The "Expiration Model" is a caching model where a value gets cached with an associated expiration and when that expiration is reached that value is no longer cached. This affects the app behavior by having it pay the caching cost when it tries to get a value and it has expired. The following is an example of creating a MostRecentPrice cache repository for the "Expiration Model".

class MostRecentPrice
  include Hitnmiss::Repository

  default_expiration 134 # expiration in seconds from when value cached
end

More often than not your caching use case will have a static/known expiration that you want to use for all values that get cached. This is handled for you by setting the default_expiration as seen in the example above. Note: Hitnmiss also supports being able to have different expirations for each cached value. You can learn more about this in the "Set Cache Source based Expiration" section.

Background Refresh Repository

Sometimes you don't have an expiration value and don't want cached values to disappear. In these scenarios you want something to update the cache for you based on some defined interval. When you use the Hitnmiss::BackgroundRefreshRepository module and set the refresh_interval as seen below, it prepares your repository to handle this scenario. This changes the behavior where the app never experiences the caching cost as it is continually managed for the app in the background based on the refresh_interval.

class MostRecentPrice
  include Hitnmiss::BackgroundRefreshRepository

  refresh_interval 60*5 # refresh interval in seconds
end

Once you have defined your Background Refresh Repository in order to get the background process to update your cache you have to kick it off using the background_refresh(*args, swallow_exceptions: []) method as seen in the example below. The optional keyword argument swallow_exceptions defaults to []. If enabled it will prevent the specified exceptions, raised in the fetch(*args) or stale?(*args) methods you defined, from killing the background thread, and prevent the exceptions from making their way up to the application. This is useful in scenarios where you want it to absorb say timeout exceptions, etc. and continue trucking along. Note: Any other exceptions not covered by the exceptions listed in the swallow_exceptions array will still be raised up into the application.

repository = MostRecentPrice.new
repository.background_refresh(store_id)

This model also has the added benefit that the priming of the cache in the background refresh process is non-blocking. This means if you use this model the consumer will not experience the priming of the cache like they would with the Standard Repository's Expiration Model.

Staleness Checking

The Background Refresh Repository model introduces a new concept, Staleness Checking. Staleness is checked during the background refresh process. The way it works is if the cache is identified to be stale, then it primes the cache in the background, if the cache is identified to NOT be stale, then it sleeps for another refresh_interval.

The stale checker, stale?(*args), defaults to an always stale value of true. This causes the background refresh process to prime the cache every refresh_interval.

If you want your cache implementation to be smarter and say validate a fingerprint or last modified value against the source, you can do it simply by overwriting the stale?(*args) method with your own staleness checking logic. The following is an example of this.

class MostRecentPrice
  include Hitnmiss::BackgroundRefreshRepository

  refresh_interval 60*5 # refresh interval in seconds

  def initialize
    @client = HTTPClient.new
  end

  def stale?(*args)
    hit_or_miss = get_from_cache(*args)
    if hit_or_miss.is_a?(Hitnmiss::Driver::Miss)
      return true
    elsif hit_or_miss.is_a?(Hitnmiss::Driver::Hit)
      url = "https://someresource.example.com/#{args[0]}/#{args[1]}/digest.json"
      res = @client.head(url)
      fingerprint = res.header['ETag'].first
      return false if fingerprint == hit_or_miss.fingerprint
      return true
    else
      raise Hitnmiss::Repository::UnsupportedDriverResponse.new("Driver '#{self.class.driver.inspect}' did not return an object of the support types (Hitnmiss::Driver::Hit, Hitnmiss::Driver::Miss)")
    end
  end
end

Set a Repositories Cache Driver

Hitnmiss defaults to the provided Hitnmiss::InMemoryDriver, but if an alternate driver is needed a new driver can be registered as seen below.

# config/hitnmiss.rb
Hitnmiss.register_driver(:my_driver, SomeAlternativeDriver.new)

Once you have registered the new driver you can tell Hitnmiss what driver to use for this particular repository. The following is an example of how one would accomplish setting the repository driver to the driver that was just registered.

class MostRecentPrice
  include Hitnmiss::Repository

  driver :my_driver
end

This works exactly the same with the Hitnmiss::BackgroundRefreshRepository.

Set a Logger

Hitnmiss defaults to not logging. However, if you would like to get detailed logging from Hitnmiss you can do so by passing your application controlled logger instance to the Hitnmiss repository. An example of this can be seen below.

class MostRecentPrice
  include Hitnmiss::Repository

  logger Logger.new(STDOUT)
end

Note: The above works exactly the same way for Hitnmiss::Repository and Hitnmiss::BackgroundRefreshRepository.

Use the File Driver

Hitnmiss ships with an alternate file based driver that can be used to write cache values to individual files in a folder.

class MostRecentPrice
  include Hitnmiss::Repository

  driver Hitnmiss::FileDriver.new("some_cache_folder")
end

Define Fetcher Methods

You may be asking yourself, "How does the cache value get set?" Well, the answer is one of two ways.

  • Fetching an individual cacheable entity
  • Fetching all of the repository's cacheable entities

Both of these scenarios are supported by defining the fetch(*args) method or the fetch_all(keyspace) method respectively in your cache repository class. See the following example.

class MostRecentPrice
  include Hitnmiss::Repository

  default_expiration 134

  private

  def fetch(*args)
    # - do whatever to get the value you want to cache
    # - construct a Hitnmiss::Entity with the value
    Hitnmiss::Entity.new('some value')
  end

  def fetch_all(keyspace)
    # - do whatever to get the values you want to cache
    # - construct a collection of arguments and Hitnmiss entities
    [
      { args: ['a', 'b'], entity: Hitnmiss::Entity.new('some value') },
      { args: ['x', 'y'], entity: Hitnmiss::Entity.new('other value') }
    ]
  end
end

The fetch(*args) method is responsible for obtaining the value that you want to cache by whatever means necessary and returning a Hitnmiss::Entity containing the value, and optionally an expiration, fingerprint, and last_modified. Note: The *args passed into the fetch(*args) method originate as the arguments that are passed into prime and get methods when they are called. For more context on prime and get please so Priming an Entity and Getting a Value respectively. Defining the fetch(*args) method is required if you want to be able to cache values or get cached values using the prime or get methods.

If you wish to support priming the cache for an entire repository using the prime_all method, you must define the fetch_all(keyspace) method on the repository class. This method must return a collection of hashes describing the args that would be used to get an entity and the corresponding Hitnmiss::Entity. See example above.

Set Cache Source based Expiration

In some cases you might need to set the expiration to be a different value for each cached value. This is generally needed when you get information back from a remote entity in the fetch(*args) method and you use it to compute the expiration. This for example could be via the Expiration header in an HTTP response. Once you have the expiration for that value you can set it by passing the expiration into the Hitnmiss::Entity constructor as seen below. Note: The expiration in the Hitnmiss::Entity will take precedence over the default_expiration.

class MostRecentPrice
  include Hitnmiss::Repository

  default_expiration 134

  private

  def fetch(*args)
    # - do whatever to get the value you want to cache
    # - construct a Hitnmiss::Entity with the value and optionally a
    #   result based expiration. If no result based expiration is
    #   provided it will use the default_expiration.
    Hitnmiss::Entity.new('some value', expiration: 235)
  end
end

Set Cache Source based Fingerprint

In some cases you might want to set the fingerprint for the cached value. Doing so provides more flexibility to Hitnmiss in terms of determining staleness of cache. A very common example of this would be if you are fetching something over HTTP from the remote and the remote includes the ETag header. If you take the value of the ETag header (a fingerprint) and you set it in the Hitnmiss::Entity it can be used later on by Hitnmiss to aid in identifying staleness. Once you have obtained the fingerprint by whatever means you can set it by passing the fingerprint into the Hitnmiss::Entity constructor as seen below.

class MostRecentPrice
  include Hitnmiss::Repository

  default_expiration 134

  private

  def fetch(*args)
    # - do whatever to get the value you want to cache
    # - do whatever to get the fingerprint of the value you want to cache
    # - construct a Hitnmiss::Entity with the value and optionally a
    #   fingerprint.
    Hitnmiss::Entity.new('some value',
                         fingerprint: "63478adce0dd0fbc82ef4bb1f1d64193")
  end
end

Set Cache Source based Last Modified

In some cases you might want to set the last_modified value for a cached value. Doing this provides more felxibility to Hitnmiss when trying to determine staleness of a cached item. A common example of this would be if you are fetching somthing over HTTP from a remote server and it includes the Last-Modified entity header in the response. If you took the value of the Last-Modified header and you set it in the Hitnmiss::Entity it can be used later on by Hitnmiss to aid in identifying staleness. Once you have obtained the Last-Modified value by whatever means you can set it by passing the last_modified option into the Hitnmiss::Entity constructor as seen below.

class MostRecentPrice
  include Hitnmiss::Repository

  default_expiration 134

  private

  def fetch(*args)
    # - do whatever to get the value you want to cache
    # - do whatever to get the last modified of the value you want to cache
    # - construct a Hitnmiss::Entity with the value and optionally a
    #   last_modified value.
    Hitnmiss::Entity.new('some value',
                         last_modified: "2016-04-15T13:00:00Z")
  end
end

Usage

The following is a light breakdown of the various pieces of Hitnmiss and how to get going with them after defining your cache repository.

Priming an entity

Once you have defined the fetch(*args) method you can construct an instance of your cache repository and use it. One way you might want to use it is to force the repository to cache a value. This can be done using the prime method as seen below.

repository = MostRecentPrice.new
repository.prime(current_user.id) # => cached_value

Priming the entire repository

You can use the prime_all method to prime the entire repository. Note: The repository class must define the fetch_all(keyspace) method for this to work. See the "Define Fetcher Methods" section above for details.

repository = MostRecentPrice.new
repository.prime_all # => [cached values, ...]

Getting a value

You can also use your cache repository to get a particular cached value by doing the following.

repository = MostRecentPrice.new
repository.get(current_user.id) # => cached_value

Deleting a cached value

You can delete a cached value by calling the delete method with the same arguments used to get it.

repository = MostRecentPrice.new
# cache some values ex: repository.prime(current_user.id)
repository.delete(current_user.id)

Clearing a repository

You can clear the entire repository of cached values by calling the clear method.

repository = MostRecentPrice.new
# cache some values ex: repository.prime(current_user.id)
repository.clear

Contributing

If you are interested in contributing to Hitnmiss. There is a guide (both code and general help) over in CONTRIBUTING.

License

The gem is available as open source under the terms of the MIT License.

hitnmiss's People

Contributors

drewdeponte avatar sdost avatar bbeck-acorns avatar brimil01 avatar slabounty avatar

Stargazers

Tyler Hanway avatar Ewout van Mansom avatar Craig Sheen avatar Yoichi Dan avatar Sam Davarnia avatar Nico Ritschel avatar Ryan Hedges avatar  avatar

Watchers

Chris Schepman avatar  avatar Oscar Godson avatar Michael Deol avatar Chris Bielinski avatar Russell Cloak avatar Marcin Seroczynski avatar Brandon Luth avatar Kamil Łęczycki avatar Mark Ronai avatar Carlos Marcano avatar James Menera avatar Robert Brown avatar Colin Bridges avatar Nate Smith avatar James Cloos avatar Leonardo avatar Tim Kendall avatar Chris Bolton avatar Ke Wang avatar Gilbert Zaldivar avatar Quinait Mathieu avatar Kyle Chong avatar Jeff avatar Tri Nguyen avatar Tim Kilcullen avatar Joseph Campbell avatar  avatar Warren Nisley avatar  avatar Ryan Hedges avatar Dave Packard avatar Briana (Evans) Roy avatar  avatar Eric Cook avatar Jimmy Amash avatar Ramesh Venkataramaiah avatar Kenneth Brandon avatar Michael Jones avatar Vishnu Vardan Reddy Onteddu avatar Amareesh Basanapalli avatar Joel Bond avatar Kristian Qaqi avatar Ben Veen avatar Brandon Friend avatar Matt Almeida avatar Akshit Gupta avatar Tim avatar Michael Miranovich avatar Mario avatar  avatar Richard Ornelas avatar  avatar Benito Sifuentes avatar Eric Yancey Dauenhauer avatar Daniel Hour avatar Ji Chung avatar Melanie Waters avatar Farid avatar Lisa avatar Caitlin Keck avatar Levi Johnston avatar Jennifer Pham avatar Augustin Calderon avatar Janel Buckingham avatar Kevin Wu avatar  avatar  avatar Jennifer Tran avatar Kyle Duong avatar Christian Godfirnon avatar Bryce Rich avatar Vinayak Nesarikar avatar Philip Lee avatar Clio Harper avatar Dave Primicias avatar Jey Starratt avatar Eric Kim avatar Pietro Sette avatar  avatar LeVan Smith avatar Soren Craig avatar  avatar Hila Qu avatar Kyle Marshall avatar  avatar Sai Jayanth Veeravalli avatar Laurene Rechert avatar Scott Recker avatar Tianxing Liu avatar Jeffrey 'JJ' Bassette avatar

hitnmiss's Issues

Move from class interface to object style interface

Currently we interact as follows:

SomeCacheRepository.fetch(*args)

Instead it would be better to interact as follows:

SomeCacheRepository.new.fetch(*args)

It makes Dependency Injection easier on a repository which makes testing easier and coupling clearer.

Figure out what we want to do, if anything, to support fetch() fails

The scenario is this.

The user has defined a fetch method say to get something from a remote HTTP endpoint and they attempt to get the URL and get a 404 or 500 or any other failure back.

We need to figure out if we would want that to just have them raise an exception and blow the app up so they know their fetching is failing (fail fast, fail load). Another option would be to provide some way for them to return a value indicating not to cache the result or maybe a provided exception that would be rescued by Hitnmiss. This would enable a fetch failure scenario to be swallowed and all the app to continue functioning without issue.

Thinking about it more we may actually want to allow them to do either of the above scenarios. If the remote source replied with an error saying the resource was temporarily unavailable we would want to some how continue through the process without blowing up the app. In the scenario where there is a hard failure that isn't temporary they might want to blow the app up.

Clearing a cache using redis scan or equiv in memcache is slow

The best alternative for this that I have come up with is to have Hitnmiss track keys along side the cached values that would get cached itself.

Then you could ask that object questions, like getting all the keys based on a specific wildcard pattern or something and it would be as efficient as it is to retrieve the cached collection of keys, unmarshal it, and perform the wildcard search on that data structure, and then simply calling delete on the keys.

disable caching for a test

It's take a little more work to get hit n miss to disable the caching in a test scenario. It might be nice to have something like Hitnmiss.disable_cache that doesn't store anything to the driver.

Make sure the driver boundary is set correctly

Specifically, I am wondering if we pass in a Hitnmiss::Entity when we cache a value and return only the cached value when we get a cached value from the driver.

Not sure which case it is but I think if we pass in a Hitnmiss::Entity we should probably get a Hitnmiss::Entity back, maybe?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.