rffdb 0.0.5
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/LICENSE +22 -0
- data/lib/rffdb/cache_provider.rb +14 -0
- data/lib/rffdb/cache_providers/lru_cache.rb +162 -0
- data/lib/rffdb/cache_providers/rr_cache.rb +35 -0
- data/lib/rffdb/document.rb +225 -0
- data/lib/rffdb/document_collection.rb +72 -0
- data/lib/rffdb/exception.rb +5 -0
- data/lib/rffdb/exceptions/cache_exceptions.rb +9 -0
- data/lib/rffdb/exceptions/document_exceptions.rb +22 -0
- data/lib/rffdb/storage_engine.rb +123 -0
- data/lib/rffdb/storage_engines/json_engine.rb +52 -0
- data/lib/rffdb/storage_engines/yaml_engine.rb +50 -0
- data/lib/rffdb/version.rb +3 -0
- data/lib/rffdb.rb +11 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bc32e7e6da39ca78200a564f07ababf89cb3749f
|
4
|
+
data.tar.gz: a9978b86ec538b44b238e10e32fa85bb4bc83136
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8220e8ef033aa49a605148edf591334d6449e569ca38a3a823b2b6019759d077e390a3be3e1d6e5c718b22e79eeecf9a3c0b42443fb057c16748f40978b2fd22
|
7
|
+
data.tar.gz: 84ccd13638927c947da052720ffda3fcd50869a0a267308f5c9a6e466deebbb8a908929b31bba261a839f60b1bdd9048878f981b7d19e360dc0cd888b7409dfb
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jonathan Gnagy <jonathan.gnagy@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
# Generic Cache Provider definition. Any subclass *must* implement or inherit the methods defined here (if any).
|
3
|
+
class CacheProvider
|
4
|
+
# Used for pulling data from the cache
|
5
|
+
def [](key)
|
6
|
+
nil
|
7
|
+
end
|
8
|
+
|
9
|
+
# Used for storing data in the cache
|
10
|
+
def []=(key, value)
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
module CacheProviders
|
3
|
+
# A very simple Least Recently Used (LRU) cache implementation. Stores data in a Hash,
|
4
|
+
# uses a dedicated Array for storing and sorting keys (and implementing the LRU algorithm),
|
5
|
+
# and doesn't bother storing access information for cache data. It stores hit and miss counts
|
6
|
+
# for the entire cache (not for individual keys). It also uses three mutexes for thread-safety:
|
7
|
+
# a write lock, a read lock, and a metadata lock.
|
8
|
+
class LRUCache < CacheProvider
|
9
|
+
attr_reader :max_size, :keys
|
10
|
+
|
11
|
+
# @raise [Exceptions::InvalidCacheSize] if the max_size isn't an Integer
|
12
|
+
def initialize(max_size = 100)
|
13
|
+
raise Exceptions::InvalidCacheSize unless max_size.kind_of?(Integer)
|
14
|
+
|
15
|
+
@max_size = max_size
|
16
|
+
@hits = 0
|
17
|
+
@misses = 0
|
18
|
+
@keys = []
|
19
|
+
@data = {}
|
20
|
+
@read_mutex = Mutex.new
|
21
|
+
@write_mutex = Mutex.new
|
22
|
+
@meta_mutex = Mutex.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# Does the cache contain the requested item?
|
26
|
+
# @param key [Symbol] the index of the potentially cached object
|
27
|
+
def has?(key)
|
28
|
+
@meta_mutex.synchronize { @keys.include?(key) }
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :has_key?, :has?
|
32
|
+
alias_method :include?, :has?
|
33
|
+
|
34
|
+
# The number of items in the cache
|
35
|
+
# @return [Fixnum] key count
|
36
|
+
def size
|
37
|
+
@meta_mutex.synchronize { @keys.size }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert the contents of the cache to a Hash
|
41
|
+
# @return [Hash] the cached data
|
42
|
+
def to_hash
|
43
|
+
@read_mutex.synchronize { @data.dup }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return a raw Array of the cache data without its keys.
|
47
|
+
# Not particularly useful but it may be useful in the future.
|
48
|
+
# @return [Array] just the cached values
|
49
|
+
def values
|
50
|
+
@read_mutex.synchronize { @data.values }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Allow iterating over the cached items, represented as key+value pairs
|
54
|
+
def each(&block)
|
55
|
+
to_hash.each(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Invalidate a cached item by its index / key. Returns `nil` if the object doesn't exist.
|
59
|
+
# @param key [Symbol] the cached object's index
|
60
|
+
def invalidate(key)
|
61
|
+
invalidate_key(key)
|
62
|
+
@write_mutex.synchronize { @data.delete(key) }
|
63
|
+
end
|
64
|
+
|
65
|
+
alias_method :delete, :invalidate
|
66
|
+
|
67
|
+
# Remove all items from the cache without clearing statistics
|
68
|
+
# @return [Boolean] was the truncate operation successful?
|
69
|
+
def truncate
|
70
|
+
@read_mutex.synchronize do
|
71
|
+
@write_mutex.synchronize do
|
72
|
+
@meta_mutex.synchronize { @keys = [] }
|
73
|
+
@data = {}
|
74
|
+
end
|
75
|
+
@data.empty?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Similar to {#truncate} (in fact, it calls it) but it also clears the statistical metadata.
|
80
|
+
# @return [Boolean] was the flush operation successful?
|
81
|
+
def flush
|
82
|
+
if truncate
|
83
|
+
@meta_mutex.synchronize { @hits, @misses = 0, 0 }
|
84
|
+
true
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Provides a hash of the current metadata for the cache. It provides the current cache size (`:size`),
|
91
|
+
# the number of cache hits (`:hits`), and the number of cache misses (`:misses`).
|
92
|
+
# @return [Hash] cache statistics
|
93
|
+
def statistics
|
94
|
+
{size: size, hits: @meta_mutex.synchronize { @hits }, misses: @meta_mutex.synchronize { @misses }}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Store some data (`value`) indexed by a `key`. If an object exists with the same key, and the
|
98
|
+
# value is different, it will be overwritten. Storing a value causes its key to be moved to the end
|
99
|
+
# of the keys array (meaning it is the __most recently used__ item), and this happens on #store regardless
|
100
|
+
# of whether or not the key previously existed. This behavior is relied upon by {#retrieve} to allow
|
101
|
+
# reorganization of the keys without necessarily modifying the data it indexes. Uses recursion for overwriting
|
102
|
+
# existing items.
|
103
|
+
#
|
104
|
+
# @param key [Symbol] the index to use for referencing this cached item
|
105
|
+
# @param value [Object] the data to cache
|
106
|
+
def store(key, value)
|
107
|
+
if has?(key)
|
108
|
+
if @read_mutex.synchronize { @data[key] == value }
|
109
|
+
invalidate_key(key)
|
110
|
+
@meta_mutex.synchronize { @keys << key }
|
111
|
+
value
|
112
|
+
else
|
113
|
+
invalidate(key)
|
114
|
+
store(key, value)
|
115
|
+
end
|
116
|
+
else
|
117
|
+
if size >= @max_size
|
118
|
+
invalidate(@keys.first) until size < @max_size
|
119
|
+
end
|
120
|
+
|
121
|
+
@write_mutex.synchronize do
|
122
|
+
@meta_mutex.synchronize { @keys << key }
|
123
|
+
@data[key] = value
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
alias_method :[]=, :store
|
129
|
+
|
130
|
+
# Retrieve an item from the cache. Returns `nil` if the item doesn't exist. Relies on {#store} returning the
|
131
|
+
# stored value to ensure the LRU algorithm is maintained safely.
|
132
|
+
# @param key [Symbol] the index to retrieve
|
133
|
+
def retrieve(key)
|
134
|
+
if has?(key)
|
135
|
+
@meta_mutex.synchronize { @hits += 1 }
|
136
|
+
# Looks dumb, as it stores the value again, but it actually only reorganizes the keys Array
|
137
|
+
store(key, @read_mutex.synchronize { @data[key] })
|
138
|
+
else
|
139
|
+
@meta_mutex.synchronize { @misses += 1 }
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
alias_method :[], :retrieve
|
145
|
+
|
146
|
+
def marshal_dump
|
147
|
+
[@max_size, @hits, @misses, @keys, @data]
|
148
|
+
end
|
149
|
+
|
150
|
+
def marshal_load(array)
|
151
|
+
@max_size, @hits, @misses, @keys, @data = array
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Invalidate just the key of a cached item. Dangerous if used incorrectly.
|
157
|
+
def invalidate_key(key)
|
158
|
+
@meta_mutex.synchronize { @keys.delete(key) }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
module CacheProviders
|
3
|
+
# A simple Random Replacement (RR) cache implementation. Stores data in a Hash,
|
4
|
+
# uses a dedicated Array for storing keys (and implementing the RR algorithm),
|
5
|
+
# and doesn't bother storing access information for cache data. It stores hit and miss counts
|
6
|
+
# for the entire cache (not for individual keys). It also uses three mutexes for thread-safety:
|
7
|
+
# a write lock, a read lock, and a metadata lock. The RRCache borrows nearly all its functionality
|
8
|
+
# from the {LRUCache}, only overwriting the storage (and therefore the revocation) method.
|
9
|
+
class RRCache < LRUCache
|
10
|
+
# Store some data (`value`) indexed by a `key`. If an object exists with the same key, and the
|
11
|
+
# value is different, it will be overwritten. Storing a new item when the cache is full causes
|
12
|
+
# the keys Array a random entry to be evicted via a shuffling of the keys. Keys are stored in
|
13
|
+
# the order in which they were inserted (not shuffled).
|
14
|
+
#
|
15
|
+
# @param key [Symbol] the index to use for referencing this cached item
|
16
|
+
# @param value [Object] the data to cache
|
17
|
+
def store(key, value)
|
18
|
+
if has?(key)
|
19
|
+
super(key, value)
|
20
|
+
else
|
21
|
+
if size >= @max_size
|
22
|
+
invalidate(@keys.shuffle.first) until size < @max_size
|
23
|
+
end
|
24
|
+
|
25
|
+
@write_mutex.synchronize do
|
26
|
+
@meta_mutex.synchronize { @keys << key }
|
27
|
+
@data[key] = value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :[]=, :store
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
class Document
|
3
|
+
|
4
|
+
# @raise [Exceptions::NoSuchDocument] when attempting to retrieve a non-existing document by id
|
5
|
+
def initialize(existing_id = false, lazy = true)
|
6
|
+
if existing_id
|
7
|
+
@document_id = existing_id
|
8
|
+
raise Exceptions::NoSuchDocument unless File.exists?(file_path)
|
9
|
+
if lazy
|
10
|
+
@lazy = true
|
11
|
+
else
|
12
|
+
reload(true)
|
13
|
+
@lazy = false
|
14
|
+
end
|
15
|
+
@saved = true
|
16
|
+
else
|
17
|
+
@document_id = storage.next_id(self.class)
|
18
|
+
@data = {}
|
19
|
+
# relative to database root
|
20
|
+
@saved = false
|
21
|
+
end
|
22
|
+
@read_lock = Mutex.new
|
23
|
+
@write_lock = Mutex.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Overrides the Object#id method to deliver an id derived from this document's storage engine
|
27
|
+
# @return [Fixnum] the object id from the storage engine
|
28
|
+
def id
|
29
|
+
@document_id
|
30
|
+
end
|
31
|
+
|
32
|
+
# The location of the flat-file
|
33
|
+
# @return [String] the path to the flat-file used to store this document (may not exist yet)
|
34
|
+
def file_path
|
35
|
+
storage.file_path(self.class, @document_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Commit the document to storage
|
39
|
+
# @return [Boolean]
|
40
|
+
def commit
|
41
|
+
@read_lock.synchronize do
|
42
|
+
@write_lock.synchronize do
|
43
|
+
storage.store(self.class, @document_id, @data.dup) unless @saved
|
44
|
+
@saved = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :save, :commit
|
50
|
+
|
51
|
+
# Has this documented been committed to storage?
|
52
|
+
# @return [Boolean]
|
53
|
+
def committed?
|
54
|
+
return @saved
|
55
|
+
end
|
56
|
+
|
57
|
+
# Retrieve the stored data from disk, never using cache. Allows forcing to overwrite uncommitted changes.
|
58
|
+
# @raise [Exceptions::PendingChanges] if attempting to reload with uncommitted changes (and if `force` is false)
|
59
|
+
def reload(force = false)
|
60
|
+
if committed? or force
|
61
|
+
@read_lock.synchronize do
|
62
|
+
@write_lock.synchronize do
|
63
|
+
@data = storage.retrieve(self.class, @document_id, false)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
raise Exceptions::PendingChanges
|
68
|
+
end
|
69
|
+
@read_lock.synchronize do
|
70
|
+
@write_lock.synchronize { @saved = true }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Overwrites the document's data, either from disk or from cache. Useful for lazy-loading and not
|
75
|
+
# typically used directly. Since data might have been pulled from cache, this can lead to bizarre
|
76
|
+
# things if not used carefully and things rely on #committed? or @saved.
|
77
|
+
def refresh
|
78
|
+
@write_lock.synchronize do
|
79
|
+
@data = storage.retrieve(self.class, @document_id)
|
80
|
+
@saved = true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Currently an alias for #new, but used as a wrapper in case more work needs to be done
|
85
|
+
# before pulling a document from the storage engine (such as sanitizing input, etc)
|
86
|
+
def self.load(id)
|
87
|
+
return self.new(id)
|
88
|
+
end
|
89
|
+
|
90
|
+
self.singleton_class.send(:alias_method, :get, :load)
|
91
|
+
|
92
|
+
# This DSL method is used to define the schema for a document. It sets up all data access for the class,
|
93
|
+
# and allows specifying strict checks on that schema during its use, such as validations, class types, regexp
|
94
|
+
# formatting, etc.
|
95
|
+
#
|
96
|
+
# @param name [Symbol] the unique name of the attribute
|
97
|
+
# @option options [Class] :class (Object) the expected object class for this attribute
|
98
|
+
# @option options [Regexp] :format a regular expression for the required format of the attribute (for any :class that supports #.to_s)
|
99
|
+
# @option options [Array, Symbol] :validate either a symbol or array of symbols referencing the instance method(s) to use to validate this attribute
|
100
|
+
def self.attribute(name, options = {})
|
101
|
+
@structure ||= {}
|
102
|
+
@structure[name.to_sym] = {}
|
103
|
+
# setup the schema
|
104
|
+
@structure[name.to_sym][:class] = options.has_key?(:class) ? options[:class] : Object
|
105
|
+
@structure[name.to_sym][:format] = options.has_key?(:format) ? options[:format] : nil
|
106
|
+
@structure[name.to_sym][:validations] = options.has_key?(:validate) ? [*options[:validate]] : []
|
107
|
+
end
|
108
|
+
|
109
|
+
# This DSL method is used to setup the backend {StorageEngine} class and optionally the {CacheProvider}
|
110
|
+
# for this Document type.
|
111
|
+
#
|
112
|
+
# @param storage_engine [Class] the {StorageEngine} child class to use
|
113
|
+
# @option cache_opts [Class] :cache_provider (CacheProviders::LRUCache) the {CacheProvider} child class for caching
|
114
|
+
# @option cache_opts [Fixnum] :cache_size the cache size, in terms of the number of objects stored
|
115
|
+
# @raise [Exceptions::InvalidEngine] if the specified {StorageEngine} does not exist
|
116
|
+
# @raise [Exceptions::InvalidCacheProvider] if a cache_provider is specified and it isn't a type of {CacheProvider}
|
117
|
+
def self.engine(storage_engine, cache_opts = {})
|
118
|
+
raise Exceptions::InvalidEngine unless storage_engine.instance_of? Class and storage_engine.ancestors.include?(StorageEngine)
|
119
|
+
@engine = storage_engine
|
120
|
+
if cache_opts.has_key?(:cache_provider)
|
121
|
+
# Make sure the cache provider specified is valid
|
122
|
+
unless cache_opts[:cache_provider].instance_of? Class and cache_opts[:cache_provider].ancestors.include?(CacheProvider)
|
123
|
+
raise Exceptions::InvalidCacheProvider
|
124
|
+
end
|
125
|
+
@engine.cache_provider(self, cache_opts[:cache_provider])
|
126
|
+
end
|
127
|
+
if cache_opts.has_key?(:cache_size)
|
128
|
+
@engine.cache_size(self, cache_opts[:cache_size])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [StorageEngine] a reference to the storage engine singleton of this document class
|
133
|
+
def self.storage
|
134
|
+
@engine ||= StorageEngines::YamlEngine
|
135
|
+
@engine
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [StorageEngine] a reference to the storage engine singleton of this document class
|
139
|
+
def storage
|
140
|
+
self.class.send(:storage)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [Hash] a copy of the schema information for this class
|
144
|
+
def self.structure
|
145
|
+
@structure ||= {}
|
146
|
+
@structure.dup
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Hash] a copy of the schema information for this class
|
150
|
+
def structure
|
151
|
+
self.class.send(:structure)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Sets the maximum number of entries the cache instance for this document will hold.
|
155
|
+
# Note, this clears the current contents of the cache.
|
156
|
+
# @param size [Fixnum] the maximum size of this class' cache instance
|
157
|
+
def self.cache_size(size)
|
158
|
+
storage.cache_size(self, size)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Allow direct access to the cache instance of this document class
|
162
|
+
# @return [CacheProvider] this class' cache instance
|
163
|
+
def self.cache
|
164
|
+
storage.cache(self)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return all available instances of this type
|
168
|
+
# @return [DocumentCollection] all documents of this type
|
169
|
+
def self.all
|
170
|
+
DocumentCollection.new(storage.all(self).collect {|doc_id| load(doc_id)}, self)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Query for Documents based on an attribute
|
174
|
+
# @see DocumentCollection#where
|
175
|
+
def self.where(attribute, value, comparison_method = "==")
|
176
|
+
all.where(attribute, value, comparison_method)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Uses the defined schema to setup getter and setter methods. Runs validations,
|
180
|
+
# format checking, and type checking on setting methods.
|
181
|
+
# @raise [Exceptions::FailedValidation] if validation of an attribute fails while setting
|
182
|
+
# @raise [Exceptions::InvalidInput] if, while setting, an attribute fails to conform to the type or format defined in the schema
|
183
|
+
def method_missing(method, *args, &block)
|
184
|
+
setter = method.to_s.match(/.*=$/) ? true : false
|
185
|
+
key = setter ? method.to_s.match(/(.*)=$/)[1].to_sym : method.to_s.to_sym
|
186
|
+
|
187
|
+
if structure.has_key?(key) and setter
|
188
|
+
if args.last.kind_of? structure[key][:class] and (structure[key][:format].nil? or args.last.to_s.match structure[key][:format])
|
189
|
+
valid = true
|
190
|
+
structure[key][:validations].each do |validation|
|
191
|
+
valid = self.send(validation.to_sym, args.last)
|
192
|
+
raise Exceptions::FailedValidation unless valid
|
193
|
+
end
|
194
|
+
refresh if @read_lock.synchronize { @lazy } and @read_lock.synchronize { committed? } # here is where the lazy-loading happens
|
195
|
+
@read_lock.synchronize do
|
196
|
+
@write_lock.synchronize do
|
197
|
+
@data[key.to_s] = args.last if valid
|
198
|
+
end
|
199
|
+
end
|
200
|
+
else
|
201
|
+
raise Exceptions::InvalidInput
|
202
|
+
end
|
203
|
+
@saved = false
|
204
|
+
elsif structure.has_key?(key)
|
205
|
+
refresh if @read_lock.synchronize { @lazy } and @read_lock.synchronize { committed? } # here is where the lazy-loading happens
|
206
|
+
@read_lock.synchronize do
|
207
|
+
@data[key.to_s]
|
208
|
+
end
|
209
|
+
else
|
210
|
+
super
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def respond_to?(method)
|
215
|
+
key = method.to_s.match(/.*=$/) ? method.to_s.match(/(.*)=$/)[1].to_sym : method.to_s.to_sym
|
216
|
+
|
217
|
+
if structure.has_key?(key)
|
218
|
+
true
|
219
|
+
else
|
220
|
+
super
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
class DocumentCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
# @return [Class] this is a collection of this {Document} subclass
|
6
|
+
attr_reader :type
|
7
|
+
|
8
|
+
# @param list [#to_a] the list of Documents to reference
|
9
|
+
# @param type [Class] the type of Document this collection references
|
10
|
+
def initialize(list, type = Document)
|
11
|
+
@list = list.to_a
|
12
|
+
@type = type
|
13
|
+
end
|
14
|
+
|
15
|
+
# Iterates over the list of Document instances
|
16
|
+
def each(&block)
|
17
|
+
@list.each(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the number of Document instances in the collection
|
21
|
+
# @return [Fixnum]
|
22
|
+
def size
|
23
|
+
@list.size
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the first item in the collection
|
27
|
+
# @return [Document] the first item in the collection
|
28
|
+
def first
|
29
|
+
@list.first
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return the last item in the collection
|
33
|
+
# @return [Document] the last item in the collection
|
34
|
+
def last
|
35
|
+
@list.last
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the collection item at the specified index
|
39
|
+
# @return [Document,DocumentCollection] the item at the requested index
|
40
|
+
def [](index)
|
41
|
+
if index.kind_of?(Range)
|
42
|
+
self.class.new(@list[index], @type)
|
43
|
+
else
|
44
|
+
@list[index]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Allow complex sorting like an Array
|
49
|
+
# @return [DocumentCollection] sorted collection
|
50
|
+
def sort(&block)
|
51
|
+
self.class.new(super(&block), @type)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Horribly inefficient way to allow querying Documents by their attributes.
|
55
|
+
# This method can be chained for multiple / more specific queries.
|
56
|
+
#
|
57
|
+
# @param attribute [Symbol] the attribute to query
|
58
|
+
# @param value [Object] the value to compare against
|
59
|
+
# @param comparison_method [String,Symbol] the method to use for comparison - allowed options are "'==', '>', '>=', '<', '<=', and 'match'"
|
60
|
+
# @raise [Exceptions::InvalidWhereQuery] if not the right kind of comparison
|
61
|
+
# @return [DocumentCollection]
|
62
|
+
def where(attribute, value, comparison_method = '==')
|
63
|
+
unless [:'==', :'>', :'>=', :'<', :'<=', :match].include?(comparison_method.to_sym)
|
64
|
+
raise Exceptions::InvalidWhereQuery
|
65
|
+
end
|
66
|
+
self.class.new(
|
67
|
+
@list.collect {|item| item if item.send(attribute).send(comparison_method.to_sym, value) }.compact,
|
68
|
+
@type
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
module Exceptions
|
3
|
+
class FailedValidation < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
class InvalidEngine < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidInput < Exception
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidWhereQuery < Exception
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoSuchDocument < Exception
|
16
|
+
end
|
17
|
+
|
18
|
+
class PendingChanges < Exception
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
# Generic Storage Engine definition. Any subclass *must* implement or inherit the methods defined here (if any).
|
3
|
+
class StorageEngine
|
4
|
+
# Read locking by document type
|
5
|
+
# @param type [Document] implements the equivalent of table-level read-locking on this {Document} type
|
6
|
+
def self.read_lock(type, &block)
|
7
|
+
@read_mutexes ||= {}
|
8
|
+
@read_mutexes[type] ||= Mutex.new
|
9
|
+
@read_mutexes[type].synchronize(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Write locking by document type, with implicit read locking
|
13
|
+
# @param type [Document] implements the equivalent of table-level write-locking on this {Document} type
|
14
|
+
def self.write_lock(type, &block)
|
15
|
+
@write_mutexes ||= {}
|
16
|
+
@write_mutexes[type] ||= Mutex.new
|
17
|
+
@write_mutexes[type].synchronize do
|
18
|
+
read_lock(type, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Store data
|
23
|
+
# @param type [Document] type of {Document} to store
|
24
|
+
# @param object_id [Object] unique identifier for the data to store (usually an Integer)
|
25
|
+
# @param data [Object] data to be stored
|
26
|
+
def self.store(type, object_id, data)
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Retrieve some stored data
|
31
|
+
# @param type [Document] type of {Document} to retrieve
|
32
|
+
# @param object_id [Object] unique identifier for the stored data (usually an Integer)
|
33
|
+
# @param use_caching [Boolean] attempt to pull the data from cache (or not)
|
34
|
+
def self.retrieve(type, object_id, use_caching = true)
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
# Flush all changes to disk (usually done automatically)
|
39
|
+
def self.flush
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
# The full path to a stored (or would-be stored) {Document}
|
44
|
+
# @param type [Document] the document type
|
45
|
+
# @param object_id [Object] unique identifier for the document (usually an Integer)
|
46
|
+
def self.file_path(type, object_id)
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return all known instances of a {Document}
|
51
|
+
# @param type [Document] the document type
|
52
|
+
# @return [Array]
|
53
|
+
def self.all(type)
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Determine the next unique identifier available for a {Document} type
|
58
|
+
# @param type [Document] the document type
|
59
|
+
# @return [Fixnum]
|
60
|
+
def self.next_id(type)
|
61
|
+
last_id = all(type)[-1]
|
62
|
+
next_key = last_id.nil? ? 1 : (last_id + 1)
|
63
|
+
if @highest_known_key and @highest_known_key >= next_key
|
64
|
+
write_lock(type) { @highest_known_key += 1 }
|
65
|
+
else
|
66
|
+
write_lock(type) { @highest_known_key = next_key }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set the cache provider to use for a document type
|
71
|
+
# This completely flushes all cache.
|
72
|
+
# @param document_type [Document] the document type
|
73
|
+
# @param cache_provider_class [CacheProvider] the type of {CacheProvider} to use
|
74
|
+
def self.cache_provider(document_type, cache_provider_class)
|
75
|
+
unless cache_provider_class.instance_of? Class and cache_provider_class.ancestors.include?(CacheProvider)
|
76
|
+
raise Exceptions::InvalidCacheProvider
|
77
|
+
end
|
78
|
+
@caches ||= {}
|
79
|
+
@caches[document_type] = cache_provider_class.new
|
80
|
+
end
|
81
|
+
|
82
|
+
# Set the maximum size of a cache, based on {Document} type
|
83
|
+
# @param type [Document] the document type
|
84
|
+
# @param size [Fixnum] the maximum size of the cache
|
85
|
+
def self.cache_size(type, size)
|
86
|
+
@caches ||= {}
|
87
|
+
if @caches.has_key?(type)
|
88
|
+
@caches[type] = @caches[type].class.new(size)
|
89
|
+
else
|
90
|
+
@caches[type] = CacheProviders::LRUCache.new(size)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Attempt to retrieve an item from the {Document} type's cache instance
|
95
|
+
# @param type [Document] the document type
|
96
|
+
# @param object_id [Object] unique identifier for the document (usually an Integer)
|
97
|
+
def self.cache_lookup(type, object_id)
|
98
|
+
@caches ||= {}
|
99
|
+
@caches[type] ||= CacheProviders::LRUCache.new
|
100
|
+
@caches[type][object_id.to_s]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Store some data in the cache for the {Document} type
|
104
|
+
# @param type [Document] the document type
|
105
|
+
# @param object_id [Object] unique identifier for the document (usually an Integer)
|
106
|
+
# @param data [Object] data to be stored
|
107
|
+
# @return [Boolean]
|
108
|
+
def self.cache_store(type, object_id, data)
|
109
|
+
@caches ||= {}
|
110
|
+
@caches[type] ||= CacheProviders::LRUCache.new
|
111
|
+
@caches[type][object_id.to_s] = data
|
112
|
+
return true
|
113
|
+
end
|
114
|
+
|
115
|
+
# Allow access to the cache instance directly (kind of dangerous but helpful for troubleshooting)
|
116
|
+
# @param type [Document] the document type
|
117
|
+
# @return [CacheProvider]
|
118
|
+
def self.cache(type)
|
119
|
+
@caches ||= {}
|
120
|
+
@caches[type] ||= CacheProviders::LRUCache.new
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
module StorageEngines
|
3
|
+
class JsonEngine < StorageEngine
|
4
|
+
# TODO add support for sharding since directories will fill up quickly
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
def self.store(type, object_id, data)
|
8
|
+
path = file_path(type, object_id)
|
9
|
+
write_lock(type) do
|
10
|
+
FileUtils.mkdir_p(File.dirname(path))
|
11
|
+
File.open(path, "w") do |file|
|
12
|
+
file.puts JSON.dump(data)
|
13
|
+
end
|
14
|
+
cache_store(type, object_id, data)
|
15
|
+
end
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.retrieve(type, object_id, use_caching = true)
|
20
|
+
result = nil
|
21
|
+
begin
|
22
|
+
result = cache_lookup(type, object_id) if use_caching
|
23
|
+
unless result
|
24
|
+
read_lock(type) do
|
25
|
+
file = File.open(file_path(type, object_id), "r")
|
26
|
+
result = JSON.load(file)
|
27
|
+
file.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
cache_store(type, object_id, result)
|
31
|
+
rescue => e
|
32
|
+
puts e.message
|
33
|
+
end
|
34
|
+
return result.dup # Return a duplicate to support caching
|
35
|
+
end
|
36
|
+
|
37
|
+
# Lazily grab all document ids in use
|
38
|
+
def self.all(type)
|
39
|
+
directory_glob = read_lock(type) { Dir.glob(File.join(File.dirname(file_path(type, 0)), "*.json")) }
|
40
|
+
if directory_glob and !directory_glob.empty?
|
41
|
+
directory_glob.map {|doc| Integer(File.basename(doc, ".json"))}.sort
|
42
|
+
else
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.file_path(type, object_id)
|
48
|
+
File.join(type.to_s.gsub('::', "__"), 'documents', object_id.to_s + ".json")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module RubyFFDB
|
2
|
+
module StorageEngines
|
3
|
+
class YamlEngine < StorageEngine
|
4
|
+
# TODO add support for sharding since directories will fill up quickly
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
def self.store(type, object_id, data)
|
8
|
+
path = file_path(type, object_id)
|
9
|
+
write_lock(type) do
|
10
|
+
FileUtils.mkdir_p(File.dirname(path))
|
11
|
+
File.open(path, "w") do |file|
|
12
|
+
file.puts YAML.dump(data)
|
13
|
+
end
|
14
|
+
cache_store(type, object_id, data)
|
15
|
+
end
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.retrieve(type, object_id, use_caching = true)
|
20
|
+
result = nil
|
21
|
+
begin
|
22
|
+
result = cache_lookup(type, object_id) if use_caching
|
23
|
+
read_lock(type) do
|
24
|
+
result ||= YAML.load_file(file_path(type, object_id))
|
25
|
+
end
|
26
|
+
write_lock(type) do
|
27
|
+
cache_store(type, object_id, result)
|
28
|
+
end
|
29
|
+
rescue => e
|
30
|
+
puts e.message
|
31
|
+
end
|
32
|
+
return result.dup # Return a duplicate to support caching
|
33
|
+
end
|
34
|
+
|
35
|
+
# Lazily grab all document ids in use
|
36
|
+
def self.all(type)
|
37
|
+
directory_glob = read_lock(type) { Dir.glob(File.join(File.dirname(file_path(type, 0)), "*.yml")) }
|
38
|
+
if directory_glob and !directory_glob.empty?
|
39
|
+
directory_glob.map {|doc| Integer(File.basename(doc, ".yml"))}.sort
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.file_path(type, object_id)
|
46
|
+
File.join(type.to_s.gsub('::', "__"), 'documents', object_id.to_s + ".yml")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/rffdb.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'rffdb/version'
|
3
|
+
require 'rffdb/exception'
|
4
|
+
require 'rffdb/exceptions/cache_exceptions'
|
5
|
+
require 'rffdb/exceptions/document_exceptions'
|
6
|
+
require 'rffdb/cache_provider'
|
7
|
+
require 'rffdb/cache_providers/lru_cache'
|
8
|
+
require 'rffdb/storage_engine'
|
9
|
+
require 'rffdb/storage_engines/yaml_engine'
|
10
|
+
require 'rffdb/document'
|
11
|
+
require 'rffdb/document_collection'
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rffdb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Gnagy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A demonstration gem
|
14
|
+
email: jonathan.gnagy@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE
|
20
|
+
- lib/rffdb.rb
|
21
|
+
- lib/rffdb/cache_provider.rb
|
22
|
+
- lib/rffdb/cache_providers/lru_cache.rb
|
23
|
+
- lib/rffdb/cache_providers/rr_cache.rb
|
24
|
+
- lib/rffdb/document.rb
|
25
|
+
- lib/rffdb/document_collection.rb
|
26
|
+
- lib/rffdb/exception.rb
|
27
|
+
- lib/rffdb/exceptions/cache_exceptions.rb
|
28
|
+
- lib/rffdb/exceptions/document_exceptions.rb
|
29
|
+
- lib/rffdb/storage_engine.rb
|
30
|
+
- lib/rffdb/storage_engines/json_engine.rb
|
31
|
+
- lib/rffdb/storage_engines/yaml_engine.rb
|
32
|
+
- lib/rffdb/version.rb
|
33
|
+
homepage: https://rubygems.org/gems/rffdb
|
34
|
+
licenses:
|
35
|
+
- MIT
|
36
|
+
metadata: {}
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - "~>"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 2.2.0
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: Ruby FlatFile DB
|
57
|
+
test_files: []
|
58
|
+
has_rdoc:
|