hitnmiss 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hitnmiss"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hitnmiss/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hitnmiss"
8
+ spec.version = Hitnmiss::VERSION
9
+ spec.authors = ["Andrew De Ponte"]
10
+ spec.email = ["cyphactor@gmail.com"]
11
+
12
+ spec.summary = %q{Ruby read-through, write-behind caching using POROs}
13
+ spec.description = %q{Ruby gem to support using the Repository pattern for read-through, write-behind caching using POROs}
14
+ spec.homepage = "https://github.com/Acornsgrow/hitnmiss"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency "timecop", "~> 0.8"
26
+ spec.add_development_dependency "simplecov", "~> 0.11"
27
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
28
+ end
@@ -0,0 +1,22 @@
1
+ require "hitnmiss/version"
2
+ require "hitnmiss/errors"
3
+ require "hitnmiss/repository"
4
+ require "hitnmiss/background_refresh_repository"
5
+ require "hitnmiss/driver_registry"
6
+ require "hitnmiss/entity"
7
+ require "hitnmiss/driver"
8
+ require "hitnmiss/in_memory_driver"
9
+
10
+ module Hitnmiss
11
+ @driver_registry = DriverRegistry.new
12
+
13
+ def self.register_driver(name, driver)
14
+ @driver_registry.register(name, driver)
15
+ end
16
+
17
+ def self.driver(name)
18
+ @driver_registry.get(name)
19
+ end
20
+ end
21
+
22
+ Hitnmiss.register_driver(:in_memory, Hitnmiss::InMemoryDriver.new)
@@ -0,0 +1,78 @@
1
+ require 'hitnmiss/repository/cache_management'
2
+ require 'thread'
3
+
4
+ module Hitnmiss
5
+ module BackgroundRefreshRepository
6
+ class RefreshIntervalRequired < StandardError; end
7
+
8
+ def self.included(mod)
9
+ mod.include(Repository::CacheManagement)
10
+ mod.extend(ClassMethods)
11
+ mod.include(InstanceMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def refresh_interval(interval_in_seconds=nil)
16
+ if interval_in_seconds
17
+ @refresh_interval = interval_in_seconds
18
+ else
19
+ @refresh_interval
20
+ end
21
+ end
22
+ end
23
+
24
+ module InstanceMethods
25
+ def initialize
26
+ if self.class.refresh_interval.nil?
27
+ raise RefreshIntervalRequired, 'the refresh_interval must be set'
28
+ end
29
+ end
30
+
31
+ def stale?(*args)
32
+ true
33
+ end
34
+
35
+ def refresh(*args, swallow_exceptions: [])
36
+ if swallow_exceptions.empty?
37
+ if stale?(*args)
38
+ prime(*args)
39
+ end
40
+ else
41
+ begin
42
+ if stale?(*args)
43
+ prime(*args)
44
+ end
45
+ rescue *swallow_exceptions
46
+ end
47
+ end
48
+ end
49
+
50
+ def background_refresh(*args, swallow_exceptions: [])
51
+ @refresh_thread = Thread.new(self, args) do |repository, args|
52
+ while(true) do
53
+ refresh(*args, swallow_exceptions: swallow_exceptions)
54
+ sleep repository.class.refresh_interval
55
+ end
56
+ end
57
+ @refresh_thread.abort_on_exception = true
58
+ end
59
+
60
+ private
61
+
62
+ def strip_expiration(unstripped_entity)
63
+ if unstripped_entity.expiration
64
+ return Hitnmiss::Entity.new(unstripped_entity.value,
65
+ fingerprint: unstripped_entity.fingerprint,
66
+ last_modified: unstripped_entity.last_modified)
67
+ else
68
+ return unstripped_entity
69
+ end
70
+ end
71
+
72
+ def cache_entity(args, cacheable_entity)
73
+ entity = strip_expiration(cacheable_entity)
74
+ super(args, entity)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,39 @@
1
+ module Hitnmiss
2
+ module Driver
3
+ class Hit
4
+ attr_reader :value, :updated_at, :fingerprint, :last_modified
5
+
6
+ def initialize(value, updated_at: nil, fingerprint: nil,
7
+ last_modified: nil)
8
+ @value = value
9
+ @updated_at = updated_at
10
+ @fingerprint = fingerprint
11
+ @last_modified = last_modified
12
+ end
13
+ end
14
+
15
+ class Miss; end
16
+
17
+ module Interface
18
+ def set(key, entity)
19
+ raise Hitnmiss::Errors::NotImplemented
20
+ end
21
+
22
+ def get(key)
23
+ raise Hitnmiss::Errors::NotImplemented
24
+ end
25
+
26
+ def all(keyspace)
27
+ raise Hitnmiss::Errors::NotImplemented
28
+ end
29
+
30
+ def delete(key)
31
+ raise Hitnmiss::Errors::NotImplemented
32
+ end
33
+
34
+ def clear(keyspace)
35
+ raise Hitnmiss::Errors::NotImplemented
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module Hitnmiss
2
+ class DriverRegistry
3
+ def initialize
4
+ @registry = {}
5
+ end
6
+
7
+ def register(name, driver)
8
+ @registry[name] = driver
9
+ end
10
+
11
+ def get(name)
12
+ @registry.fetch(name) do |name|
13
+ raise Errors::UnregisteredDriver.new("#{name} is not a registered driver")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Hitnmiss
2
+ class Entity
3
+ attr_reader :value, :expiration, :fingerprint, :last_modified
4
+
5
+ def initialize(value, expiration: nil, fingerprint: nil, last_modified: nil)
6
+ @value = value
7
+ @expiration = expiration
8
+ @fingerprint = fingerprint
9
+ @last_modified = last_modified
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Hitnmiss
2
+ module Errors
3
+ class Error < StandardError; end
4
+
5
+ class NotImplemented < Error; end
6
+
7
+ class UnregisteredDriver < Error; end
8
+ end
9
+ end
@@ -0,0 +1,94 @@
1
+ module Hitnmiss
2
+ class InMemoryDriver
3
+ include Hitnmiss::Driver::Interface
4
+
5
+ def initialize
6
+ @mutex = Mutex.new
7
+ @cache = {}
8
+ end
9
+
10
+ def set(key, entity)
11
+ if entity.expiration
12
+ expiration = Time.now.to_i + entity.expiration
13
+ @mutex.synchronize do
14
+ @cache[key] = { 'value' => entity.value, 'expiration' => expiration }
15
+ @cache[key]['fingerprint'] = entity.fingerprint if entity.fingerprint
16
+ @cache[key]['updated_at'] = internal_timestamp
17
+ @cache[key]['last_modified'] = entity.last_modified if entity.last_modified
18
+ end
19
+ else
20
+ @mutex.synchronize do
21
+ @cache[key] = { 'value' => entity.value }
22
+ @cache[key]['fingerprint'] = entity.fingerprint if entity.fingerprint
23
+ @cache[key]['updated_at'] = internal_timestamp
24
+ @cache[key]['last_modified'] = entity.last_modified if entity.last_modified
25
+ end
26
+ end
27
+ end
28
+
29
+ def get(key)
30
+ cached_entity = nil
31
+ @mutex.synchronize do
32
+ cached_entity = @cache[key].dup if @cache[key]
33
+ end
34
+
35
+ return Hitnmiss::Driver::Miss.new if cached_entity.nil?
36
+
37
+ if cached_entity.has_key?('expiration')
38
+ if Time.now.to_i >= cached_entity['expiration']
39
+ return Hitnmiss::Driver::Miss.new
40
+ end
41
+ end
42
+
43
+ return Hitnmiss::Driver::Hit.new(cached_entity['value'],
44
+ build_hit_keyword_args(cached_entity))
45
+ end
46
+
47
+ def all(keyspace)
48
+ @mutex.synchronize do
49
+ matching_values = []
50
+ @cache.each do |key, entity|
51
+ matching_values << entity.fetch('value') if match_keyspace?(key, keyspace)
52
+ end
53
+ return matching_values
54
+ end
55
+ end
56
+
57
+ def delete(key)
58
+ @mutex.synchronize do
59
+ @cache.delete(key)
60
+ end
61
+ end
62
+
63
+ def clear(keyspace)
64
+ @mutex.synchronize do
65
+ @cache.delete_if { |key, _| match_keyspace?(key, keyspace) }
66
+ end
67
+ end
68
+
69
+ def match_keyspace?(key, keyspace)
70
+ regex = Regexp.new("^#{keyspace}\\" + Repository::KeyGeneration::KEY_COMPONENT_SEPARATOR)
71
+ return regex.match(key)
72
+ end
73
+
74
+ private
75
+
76
+ def internal_timestamp
77
+ Time.now.utc.iso8601
78
+ end
79
+
80
+ def build_hit_keyword_args(cached_entity)
81
+ options = {}
82
+ if cached_entity.has_key?('fingerprint')
83
+ options[:fingerprint] = cached_entity['fingerprint']
84
+ end
85
+ if cached_entity.has_key?('updated_at')
86
+ options[:updated_at] = Time.parse(cached_entity['updated_at'])
87
+ end
88
+ if cached_entity.has_key?('last_modified')
89
+ options[:last_modified] = cached_entity['last_modified']
90
+ end
91
+ return **options
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,41 @@
1
+ require 'hitnmiss/repository/cache_management'
2
+
3
+ module Hitnmiss
4
+ module Repository
5
+ def self.included(mod)
6
+ mod.include(CacheManagement)
7
+ mod.extend(ClassMethods)
8
+ mod.include(InstanceMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def default_expiration(expiration_in_seconds=nil)
13
+ if expiration_in_seconds
14
+ @default_expiration = expiration_in_seconds
15
+ else
16
+ @default_expiration
17
+ end
18
+ end
19
+ end
20
+
21
+ module InstanceMethods
22
+ private
23
+
24
+ def enrich_entity_expiration(unenriched_entity)
25
+ if unenriched_entity.expiration
26
+ return unenriched_entity
27
+ else
28
+ return Hitnmiss::Entity.new(unenriched_entity.value,
29
+ expiration: self.class.default_expiration,
30
+ fingerprint: unenriched_entity.fingerprint,
31
+ last_modified: unenriched_entity.last_modified)
32
+ end
33
+ end
34
+
35
+ def cache_entity(args, cacheable_entity)
36
+ entity = enrich_entity_expiration(cacheable_entity)
37
+ super(args, entity)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ require 'hitnmiss/repository/fetcher'
2
+ require 'hitnmiss/repository/driver_management'
3
+ require 'hitnmiss/repository/key_generation'
4
+
5
+ module Hitnmiss
6
+ module Repository
7
+ module CacheManagement
8
+ class UnsupportedDriverResponse < StandardError; end
9
+
10
+ def self.included(mod)
11
+ mod.include(Fetcher)
12
+ mod.extend(DriverManagement)
13
+ mod.include(KeyGeneration)
14
+ mod.include(InstanceMethods)
15
+ end
16
+
17
+ module InstanceMethods
18
+ def clear
19
+ Hitnmiss.driver(self.class.driver).clear(self.class.keyspace)
20
+ end
21
+
22
+ def delete(*args)
23
+ Hitnmiss.driver(self.class.driver).delete(generate_key(*args))
24
+ end
25
+
26
+ def all
27
+ Hitnmiss.driver(self.class.driver).all(self.class.keyspace)
28
+ end
29
+
30
+ def prime_all
31
+ cacheable_entities = fetch_all(self.class.keyspace)
32
+ return cacheable_entities.map do |cacheable_entity_hash|
33
+ args = cacheable_entity_hash.fetch(:args)
34
+ cacheable_entity = cacheable_entity_hash.fetch(:entity)
35
+ cache_entity(args, cacheable_entity)
36
+ cacheable_entity.value
37
+ end
38
+ end
39
+
40
+ def prime(*args)
41
+ cacheable_entity = fetch(*args)
42
+ cache_entity(args, cacheable_entity)
43
+ return cacheable_entity.value
44
+ end
45
+
46
+ def get(*args)
47
+ hit_or_miss = get_from_cache(*args)
48
+ if hit_or_miss.is_a?(Hitnmiss::Driver::Miss)
49
+ return prime(*args)
50
+ elsif hit_or_miss.is_a?(Hitnmiss::Driver::Hit)
51
+ return hit_or_miss.value
52
+ else
53
+ raise UnsupportedDriverResponse.new("Driver '#{self.class.driver.inspect}' did not return an object of the support types (Hitnmiss::Driver::Hit, Hitnmiss::Driver::Miss)")
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def get_from_cache(*args)
60
+ Hitnmiss.driver(self.class.driver).get(generate_key(*args))
61
+ end
62
+
63
+ def cache_entity(args, cacheable_entity)
64
+ Hitnmiss.driver(self.class.driver).set(generate_key(*args),
65
+ cacheable_entity)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end