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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +9 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +69 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +56 -0
- data/DEVELOPMENT.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +397 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/hitnmiss.gemspec +28 -0
- data/lib/hitnmiss.rb +22 -0
- data/lib/hitnmiss/background_refresh_repository.rb +78 -0
- data/lib/hitnmiss/driver.rb +39 -0
- data/lib/hitnmiss/driver_registry.rb +17 -0
- data/lib/hitnmiss/entity.rb +12 -0
- data/lib/hitnmiss/errors.rb +9 -0
- data/lib/hitnmiss/in_memory_driver.rb +94 -0
- data/lib/hitnmiss/repository.rb +41 -0
- data/lib/hitnmiss/repository/cache_management.rb +70 -0
- data/lib/hitnmiss/repository/driver_management.rb +17 -0
- data/lib/hitnmiss/repository/fetcher.rb +15 -0
- data/lib/hitnmiss/repository/key_generation.rb +31 -0
- data/lib/hitnmiss/version.rb +3 -0
- metadata +157 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/hitnmiss.gemspec
ADDED
@@ -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
|
data/lib/hitnmiss.rb
ADDED
@@ -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,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
|