hitnmiss 2.1.0

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