skull_island 0.1.0 → 0.1.1
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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +39 -0
- data/.travis.yml +9 -2
- data/Gemfile +3 -1
- data/Gemfile.lock +127 -0
- data/README.md +348 -2
- data/Rakefile +13 -3
- data/bin/console +4 -3
- data/lib/core_extensions/string/transformations.rb +30 -0
- data/lib/skull_island/api_client.rb +36 -0
- data/lib/skull_island/api_client_base.rb +86 -0
- data/lib/skull_island/api_exception.rb +7 -0
- data/lib/skull_island/exceptions/api_client_not_configured.rb +9 -0
- data/lib/skull_island/exceptions/immutable_modification.rb +9 -0
- data/lib/skull_island/exceptions/invalid_arguments.rb +9 -0
- data/lib/skull_island/exceptions/invalid_cache_size.rb +9 -0
- data/lib/skull_island/exceptions/invalid_options.rb +9 -0
- data/lib/skull_island/exceptions/invalid_property.rb +9 -0
- data/lib/skull_island/exceptions/invalid_where_query.rb +9 -0
- data/lib/skull_island/exceptions/new_instance_with_id.rb +9 -0
- data/lib/skull_island/helpers/api_client.rb +64 -0
- data/lib/skull_island/helpers/resource.rb +178 -0
- data/lib/skull_island/helpers/resource_class.rb +74 -0
- data/lib/skull_island/lru_cache.rb +175 -0
- data/lib/skull_island/resource.rb +198 -0
- data/lib/skull_island/resource_collection.rb +193 -0
- data/lib/skull_island/resources/certificate.rb +36 -0
- data/lib/skull_island/resources/consumer.rb +20 -0
- data/lib/skull_island/resources/plugin.rb +144 -0
- data/lib/skull_island/resources/route.rb +83 -0
- data/lib/skull_island/resources/service.rb +94 -0
- data/lib/skull_island/resources/upstream.rb +129 -0
- data/lib/skull_island/resources/upstream_target.rb +86 -0
- data/lib/skull_island/rspec/fake_api_client.rb +63 -0
- data/lib/skull_island/rspec.rb +3 -0
- data/lib/skull_island/simple_api_client.rb +18 -0
- data/lib/skull_island/validations/api_client.rb +45 -0
- data/lib/skull_island/validations/resource.rb +24 -0
- data/lib/skull_island/version.rb +3 -1
- data/lib/skull_island.rb +47 -1
- data/skull_island.gemspec +16 -13
- metadata +66 -7
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkullIsland
|
4
|
+
# The API Client Base class
|
5
|
+
class APIClientBase
|
6
|
+
attr_reader :server, :base_uri
|
7
|
+
attr_accessor :username, :password
|
8
|
+
|
9
|
+
include Validations::APIClient
|
10
|
+
include Helpers::APIClient
|
11
|
+
|
12
|
+
def api_uri
|
13
|
+
@api_uri ||= URI.parse(server)
|
14
|
+
@api_uri.path = base_uri if base_uri
|
15
|
+
@api_uri
|
16
|
+
end
|
17
|
+
|
18
|
+
def authenticated?
|
19
|
+
raise Exceptions::APIClientNotConfigured unless configured?
|
20
|
+
|
21
|
+
@username && @password ? true : false
|
22
|
+
end
|
23
|
+
|
24
|
+
def configured?
|
25
|
+
@configured ? true : false
|
26
|
+
end
|
27
|
+
|
28
|
+
def json_headers
|
29
|
+
{ content_type: :json, accept: :json }
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(uri, data = nil)
|
33
|
+
client_action do |client|
|
34
|
+
if data
|
35
|
+
JSON.parse client[uri].get(json_headers.merge(params: data))
|
36
|
+
else
|
37
|
+
JSON.parse client[uri].get(json_headers)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def post(uri, data = nil)
|
43
|
+
client_action do |client|
|
44
|
+
if data
|
45
|
+
JSON.parse client[uri].post(json_escape(data.to_json), json_headers)
|
46
|
+
else
|
47
|
+
JSON.parse client[uri].post(nil, json_headers)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def patch(uri, data)
|
53
|
+
client_action do |client|
|
54
|
+
response = client[uri].patch(json_escape(data.to_json), json_headers)
|
55
|
+
if response && !response.empty?
|
56
|
+
JSON.parse(response)
|
57
|
+
else
|
58
|
+
true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def put(uri, data)
|
64
|
+
client_action do |client|
|
65
|
+
client[uri].put(json_escape(data.to_json), json_headers)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def client_action
|
72
|
+
raise Exceptions::APIClientNotConfigured unless configured?
|
73
|
+
|
74
|
+
yield connection
|
75
|
+
end
|
76
|
+
|
77
|
+
# Don't bother creating a connection until we need one
|
78
|
+
def connection
|
79
|
+
@connection ||= if authenticated?
|
80
|
+
RestClient::Resource.new(api_uri.to_s, @username, @password)
|
81
|
+
else
|
82
|
+
RestClient::Resource.new(api_uri.to_s)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkullIsland
|
4
|
+
module Helpers
|
5
|
+
# Simple helper methods for the API Client
|
6
|
+
module APIClient
|
7
|
+
def about_service
|
8
|
+
get '/'
|
9
|
+
end
|
10
|
+
|
11
|
+
def server_status
|
12
|
+
get '/status'
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache(key)
|
16
|
+
symbolized_key = key.to_sym
|
17
|
+
if !@cache.has?(symbolized_key) && block_given?
|
18
|
+
result = yield(self)
|
19
|
+
@cache.store(symbolized_key, result)
|
20
|
+
elsif !@cache.has?(symbolized_key)
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
@cache.retrieve(symbolized_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def invalidate_cache_for(key)
|
27
|
+
symbolized_key = key.to_sym
|
28
|
+
@cache.invalidate(symbolized_key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def lru_cache
|
32
|
+
@cache
|
33
|
+
end
|
34
|
+
|
35
|
+
# Substitute characters with their JSON-supported versions
|
36
|
+
# @return [String]
|
37
|
+
def json_escape(string)
|
38
|
+
json_escape = {
|
39
|
+
'&' => '\u0026',
|
40
|
+
'>' => '\u003e',
|
41
|
+
'<' => '\u003c',
|
42
|
+
'%' => '\u0025',
|
43
|
+
"\u2028" => '\u2028',
|
44
|
+
"\u2029" => '\u2029'
|
45
|
+
}
|
46
|
+
json_escape_regex = /[\u2028\u2029&><%]/u
|
47
|
+
|
48
|
+
string.to_s.gsub(json_escape_regex, json_escape)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Provides access to the "raw" underlying rest-client
|
52
|
+
# @return [RestClient::Resource]
|
53
|
+
def raw
|
54
|
+
connection
|
55
|
+
end
|
56
|
+
|
57
|
+
# The API Client version (uses Semantic Versioning)
|
58
|
+
# @return [String]
|
59
|
+
def version
|
60
|
+
VERSION
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkullIsland
|
4
|
+
module Helpers
|
5
|
+
# Simple helper methods for Resources
|
6
|
+
module Resource
|
7
|
+
def datetime_from_params(params, actual_key)
|
8
|
+
DateTime.new(
|
9
|
+
params["#{actual_key}(1i)"].to_i,
|
10
|
+
params["#{actual_key}(2i)"].to_i,
|
11
|
+
params["#{actual_key}(3i)"].to_i,
|
12
|
+
params["#{actual_key}(4i)"].to_i,
|
13
|
+
params["#{actual_key}(5i)"].to_i
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fresh?
|
18
|
+
!tainted?
|
19
|
+
end
|
20
|
+
|
21
|
+
def host_regex
|
22
|
+
/^(([\w]|[\w][\w\-]*[\w])\.)*([\w]|[\w][\w\-]*[\w])$/
|
23
|
+
end
|
24
|
+
|
25
|
+
def id_property
|
26
|
+
self.class.properties.select { |_, opts| opts[:id_property] }.keys.first || 'id'
|
27
|
+
end
|
28
|
+
|
29
|
+
def id
|
30
|
+
@entity[id_property.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
def immutable?
|
34
|
+
self.class.immutable?
|
35
|
+
end
|
36
|
+
|
37
|
+
# ActiveRecord ActiveModel::Name compatibility method
|
38
|
+
def model_name
|
39
|
+
self.class
|
40
|
+
end
|
41
|
+
|
42
|
+
def new?
|
43
|
+
!@entity.key?(id_property.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
# ActiveRecord ActiveModel::Model compatibility method
|
47
|
+
def persisted?
|
48
|
+
!new?
|
49
|
+
end
|
50
|
+
|
51
|
+
def postprocess_created_at(value)
|
52
|
+
Time.at(value).utc.to_datetime
|
53
|
+
end
|
54
|
+
|
55
|
+
def postprocess_updated_at(value)
|
56
|
+
Time.at(value).utc.to_datetime
|
57
|
+
end
|
58
|
+
|
59
|
+
def properties
|
60
|
+
self.class.properties
|
61
|
+
end
|
62
|
+
|
63
|
+
def required_properties
|
64
|
+
properties.select { |_key, value| value[:required] }
|
65
|
+
end
|
66
|
+
|
67
|
+
def tainted?
|
68
|
+
@tainted ? true : false
|
69
|
+
end
|
70
|
+
|
71
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
72
|
+
def to_key
|
73
|
+
new? ? [] : [id]
|
74
|
+
end
|
75
|
+
|
76
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
77
|
+
def to_model
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# ActiveRecord ActiveModel::Conversion compatibility method
|
82
|
+
def to_param
|
83
|
+
new? ? nil : id.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def destroy
|
87
|
+
raise Exceptions::ImmutableModification if immutable?
|
88
|
+
|
89
|
+
unless new?
|
90
|
+
@api_client.delete(relative_uri.to_s)
|
91
|
+
@api_client.invalidate_cache_for(relative_uri.to_s)
|
92
|
+
@lazy = false
|
93
|
+
@tainted = true
|
94
|
+
@entity.delete('id')
|
95
|
+
end
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def reload
|
100
|
+
if new?
|
101
|
+
# Can't reload a new resource
|
102
|
+
false
|
103
|
+
else
|
104
|
+
@api_client.invalidate_cache_for(relative_uri.to_s)
|
105
|
+
entity_data = @api_client.cache(relative_uri.to_s) do |client|
|
106
|
+
client.get(relative_uri.to_s)
|
107
|
+
end
|
108
|
+
@entity = entity_data
|
109
|
+
@lazy = false
|
110
|
+
@tainted = false
|
111
|
+
true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def save
|
116
|
+
saveable_data = prune_for_save(@entity)
|
117
|
+
validate_required_properties(saveable_data)
|
118
|
+
|
119
|
+
if new?
|
120
|
+
@entity = @api_client.post(save_uri.to_s, saveable_data)
|
121
|
+
@lazy = true
|
122
|
+
else
|
123
|
+
@api_client.invalidate_cache_for(relative_uri.to_s)
|
124
|
+
@entity = @api_client.put(relative_uri, saveable_data)
|
125
|
+
end
|
126
|
+
@tainted = false
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def save_uri
|
131
|
+
self.class.relative_uri
|
132
|
+
end
|
133
|
+
|
134
|
+
# ActiveRecord ActiveModel compatibility method
|
135
|
+
def update(params)
|
136
|
+
new_params = {}
|
137
|
+
# need to convert multi-part datetime params
|
138
|
+
params.each do |key, value|
|
139
|
+
if /([^(]+)\(1i/.match?(key)
|
140
|
+
actual_key = key.match(/([^(]+)\(/)[1]
|
141
|
+
new_params[actual_key] = datetime_from_params(params, actual_key)
|
142
|
+
else
|
143
|
+
new_params[key] = value
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
new_params.each do |key, value|
|
148
|
+
setter_key = "#{key}=".to_sym
|
149
|
+
raise Exceptions::InvalidProperty unless respond_to?(setter_key)
|
150
|
+
|
151
|
+
send(setter_key, value)
|
152
|
+
end
|
153
|
+
save
|
154
|
+
end
|
155
|
+
|
156
|
+
def <=>(other)
|
157
|
+
if id < other.id
|
158
|
+
-1
|
159
|
+
elsif id > other.id
|
160
|
+
1
|
161
|
+
elsif id == other.id
|
162
|
+
0
|
163
|
+
else
|
164
|
+
raise Exceptions::InvalidArguments
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def prune_for_save(data)
|
169
|
+
data.reject do |k, v|
|
170
|
+
k.to_sym == id_property ||
|
171
|
+
!properties[k.to_sym] ||
|
172
|
+
properties[k.to_sym][:read_only] ||
|
173
|
+
v.nil?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkullIsland
|
4
|
+
module Helpers
|
5
|
+
# Simple helper class methods for Resource
|
6
|
+
module ResourceClass
|
7
|
+
# Determine a list of names to use to access a resource entity attribute
|
8
|
+
# @param original_name [String,Symbol] the name of the underlying attribute
|
9
|
+
# @param opts [Hash] property options as defined in a {Resource} subclass
|
10
|
+
# @return [Array<Symbol>] the list of names
|
11
|
+
def determine_getter_names(original_name, opts)
|
12
|
+
names = []
|
13
|
+
names << (opts[:type] == :boolean ? "#{original_name}?" : original_name)
|
14
|
+
if opts[:as]
|
15
|
+
Array(opts[:as]).each do |new_name|
|
16
|
+
names << (opts[:type] == :boolean ? "#{new_name}?" : new_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
names.map(&:to_sym).uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determine a list of names to use to set a resource entity attribute
|
23
|
+
# @param original_name [String,Symbol] the name of the underlying attribute
|
24
|
+
# @param opts [Hash] property options as defined in a {Resource} subclass
|
25
|
+
# @return [Array<Symbol>] the list of names
|
26
|
+
def determine_setter_names(original_name, opts)
|
27
|
+
names = ["#{original_name}="]
|
28
|
+
names.concat(Array(opts[:as]).map { |new_name| "#{new_name}=" }) if opts[:as]
|
29
|
+
names.map(&:to_sym).uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
# Produce a more human-readable representation of {#i18n_key}
|
33
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
34
|
+
# @return [String]
|
35
|
+
def human
|
36
|
+
i18n_key.humanize
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if a resource class is immutable
|
40
|
+
def immutable?
|
41
|
+
@immutable ||= false
|
42
|
+
end
|
43
|
+
|
44
|
+
# A mock internationalization key based on the class name
|
45
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
46
|
+
# @return [String]
|
47
|
+
def i18n_key
|
48
|
+
name.split('::').last.to_underscore
|
49
|
+
end
|
50
|
+
|
51
|
+
alias singular_route_key i18n_key
|
52
|
+
|
53
|
+
# A symbolized version of {#i18n_key}
|
54
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
55
|
+
# @return [Symbol]
|
56
|
+
def param_key
|
57
|
+
i18n_key.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
# All the properties defined for this Resource class
|
61
|
+
# @return [Hash{Symbol => Hash}]
|
62
|
+
def properties
|
63
|
+
@properties ||= {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# A route key for building URLs
|
67
|
+
# @note ActiveRecord ActiveModel::Name compatibility method
|
68
|
+
# @return [String]
|
69
|
+
def route_key
|
70
|
+
i18n_key.en.plural
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkullIsland
|
4
|
+
# A very simple Least Recently Used (LRU) cache implementation. Stores data
|
5
|
+
# in a Hash, uses a dedicated Array for storing and sorting keys (and
|
6
|
+
# implementing the LRU algorithm), and doesn't bother storing access
|
7
|
+
# information for cache data. It stores hit and miss counts for the
|
8
|
+
# entire cache (not for individual keys). It also uses three mutexes for
|
9
|
+
# thread-safety: a write lock, a read lock, and a metadata lock.
|
10
|
+
class LRUCache
|
11
|
+
attr_reader :max_size, :keys
|
12
|
+
|
13
|
+
# @raise [Exceptions::InvalidCacheSize] if the max_size isn't an Integer
|
14
|
+
def initialize(max_size = 100)
|
15
|
+
raise Exceptions::InvalidCacheSize unless max_size.is_a?(Integer)
|
16
|
+
|
17
|
+
@max_size = max_size
|
18
|
+
@hits = 0
|
19
|
+
@misses = 0
|
20
|
+
@keys = []
|
21
|
+
@data = {}
|
22
|
+
@read_mutex = Mutex.new
|
23
|
+
@write_mutex = Mutex.new
|
24
|
+
@meta_mutex = Mutex.new
|
25
|
+
end
|
26
|
+
|
27
|
+
# Does the cache contain the requested item?
|
28
|
+
# This doesn't count against cache misses
|
29
|
+
# @param key [Symbol] the index of the potentially cached object
|
30
|
+
def has?(key)
|
31
|
+
@meta_mutex.synchronize { @keys.include?(key) }
|
32
|
+
end
|
33
|
+
|
34
|
+
alias has_key? has?
|
35
|
+
alias include? has?
|
36
|
+
|
37
|
+
# The number of items in the cache
|
38
|
+
# @return [Fixnum] key count
|
39
|
+
def size
|
40
|
+
@meta_mutex.synchronize { @keys.size }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Convert the contents of the cache to a Hash
|
44
|
+
# @return [Hash] the cached data
|
45
|
+
def to_hash
|
46
|
+
@read_mutex.synchronize { @data.dup }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return a raw Array of the cache data without its keys.
|
50
|
+
# Not particularly useful but it may be useful in the future.
|
51
|
+
# @return [Array] just the cached values
|
52
|
+
def values
|
53
|
+
@read_mutex.synchronize { @data.values }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Allow iterating over the cached items, represented as key+value pairs
|
57
|
+
def each(&block)
|
58
|
+
to_hash.each(&block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Invalidate a cached item by its index / key. Returns `nil` if the object
|
62
|
+
# doesn't exist.
|
63
|
+
# @param key [Symbol] the cached object's index
|
64
|
+
def invalidate(key)
|
65
|
+
invalidate_key(key)
|
66
|
+
@write_mutex.synchronize { @data.delete(key) }
|
67
|
+
end
|
68
|
+
|
69
|
+
alias delete invalidate
|
70
|
+
|
71
|
+
# Remove all items from the cache without clearing statistics
|
72
|
+
# @return [Boolean] was the truncate operation successful?
|
73
|
+
def truncate
|
74
|
+
@read_mutex.synchronize do
|
75
|
+
@write_mutex.synchronize do
|
76
|
+
@meta_mutex.synchronize { @keys = [] }
|
77
|
+
@data = {}
|
78
|
+
end
|
79
|
+
@data.empty?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Similar to {#truncate} (in fact, it calls it) but it also clears the
|
84
|
+
# statistical metadata.
|
85
|
+
# @return [Boolean] was the flush operation successful?
|
86
|
+
def flush
|
87
|
+
if truncate
|
88
|
+
@meta_mutex.synchronize do
|
89
|
+
@hits = 0
|
90
|
+
@misses = 0
|
91
|
+
end
|
92
|
+
true
|
93
|
+
else
|
94
|
+
false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Provides a hash of the current metadata for the cache. It provides the
|
99
|
+
# current cache size (`:size`),the number of cache hits (`:hits`), and
|
100
|
+
# the number of cache misses (`:misses`).
|
101
|
+
# @return [Hash] cache statistics
|
102
|
+
def statistics
|
103
|
+
{
|
104
|
+
size: size,
|
105
|
+
hits: @meta_mutex.synchronize { @hits },
|
106
|
+
misses: @meta_mutex.synchronize { @misses }
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Store some data (`value`) indexed by a `key`. If an object exists with
|
111
|
+
# the same key, and the value is different, it will be overwritten.
|
112
|
+
# Storing a value causes its key to be moved to the end of the keys array
|
113
|
+
# (meaning it is the __most recently used__ item), and this happens on
|
114
|
+
# #store regardless of whether or not the key previously existed.
|
115
|
+
# This behavior is relied upon by {#retrieve} to allow reorganization of
|
116
|
+
# the keys without necessarily modifying the data it indexes.
|
117
|
+
# Uses recursion for overwriting existing items.
|
118
|
+
#
|
119
|
+
# @param key [Symbol] the index to use for referencing this cached item
|
120
|
+
# @param value [Object] the data to cache
|
121
|
+
def store(key, value)
|
122
|
+
if has?(key)
|
123
|
+
if @read_mutex.synchronize { @data[key] == value }
|
124
|
+
invalidate_key(key)
|
125
|
+
@meta_mutex.synchronize { @keys << key }
|
126
|
+
value
|
127
|
+
else
|
128
|
+
invalidate(key)
|
129
|
+
store(key, value)
|
130
|
+
end
|
131
|
+
else
|
132
|
+
invalidate(@keys.first) until size < @max_size
|
133
|
+
|
134
|
+
@write_mutex.synchronize do
|
135
|
+
@meta_mutex.synchronize { @keys << key }
|
136
|
+
@data[key] = value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
alias []= store
|
142
|
+
|
143
|
+
# Retrieve an item from the cache. Returns `nil` if the item does not
|
144
|
+
# exist. Relies on {#store} returning the stored value to ensure the LRU
|
145
|
+
# algorithm is maintained safely.
|
146
|
+
# @param key [Symbol] the index to retrieve
|
147
|
+
def retrieve(key)
|
148
|
+
if has?(key)
|
149
|
+
@meta_mutex.synchronize { @hits += 1 }
|
150
|
+
# Looks dumb, but it actually only reorganizes the keys Array
|
151
|
+
store(key, @read_mutex.synchronize { @data[key] })
|
152
|
+
else
|
153
|
+
@meta_mutex.synchronize { @misses += 1 }
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
alias [] retrieve
|
159
|
+
|
160
|
+
def marshal_dump
|
161
|
+
[@max_size, @hits, @misses, @keys, @data]
|
162
|
+
end
|
163
|
+
|
164
|
+
def marshal_load(array)
|
165
|
+
@max_size, @hits, @misses, @keys, @data = array
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
# Invalidate just the key of a cached item. Dangerous if used incorrectly.
|
171
|
+
def invalidate_key(key)
|
172
|
+
@meta_mutex.synchronize { @keys.delete(key) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|