hitnmiss 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|