perobs 0.0.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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +113 -0
- data/Rakefile +22 -0
- data/lib/perobs/Array.rb +173 -0
- data/lib/perobs/BlockDB.rb +242 -0
- data/lib/perobs/Cache.rb +201 -0
- data/lib/perobs/DataBase.rb +115 -0
- data/lib/perobs/FileSystemDB.rb +171 -0
- data/lib/perobs/Hash.rb +175 -0
- data/lib/perobs/HashedBlocksDB.rb +153 -0
- data/lib/perobs/Object.rb +189 -0
- data/lib/perobs/ObjectBase.rb +159 -0
- data/lib/perobs/Store.rb +290 -0
- data/lib/perobs/version.rb +4 -0
- data/lib/perobs.rb +29 -0
- data/perobs.gemspec +23 -0
- data/spec/Array_spec.rb +94 -0
- data/spec/FileSystemDB_spec.rb +107 -0
- data/spec/Hash_spec.rb +96 -0
- data/spec/Object_spec.rb +108 -0
- data/spec/Store_spec.rb +412 -0
- data/spec/perobs_spec.rb +155 -0
- data/tasks/changelog.rake +169 -0
- data/tasks/gem.rake +50 -0
- data/tasks/rdoc.rake +14 -0
- data/tasks/test.rake +7 -0
- metadata +121 -0
data/lib/perobs/Cache.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = Cache.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/Store'
|
29
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
# The Cache provides two functions for the PEROBS Store. It keeps some
|
33
|
+
# amount of objects in memory to substantially reduce read access latencies.
|
34
|
+
# It # also stores a list of objects that haven't been synced to the
|
35
|
+
# permanent store yet to accelerate object writes.
|
36
|
+
class Cache
|
37
|
+
|
38
|
+
# Create a new Cache object.
|
39
|
+
# @param bits [Fixnum] Number of bits for the cache index. This parameter
|
40
|
+
# heavilty affects the performance and memory consumption of the
|
41
|
+
# cache.
|
42
|
+
def initialize(bits = 16)
|
43
|
+
@bits = bits
|
44
|
+
# This mask is used to access the _bits_ least significant bits of the
|
45
|
+
# object ID.
|
46
|
+
@mask = 2 ** bits - 1
|
47
|
+
# Initialize the read and write cache
|
48
|
+
reset
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add an PEROBS::Object to the read cache.
|
52
|
+
# @param obj [PEROBS::ObjectBase]
|
53
|
+
def cache_read(obj)
|
54
|
+
@reads[index(obj)] = obj
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add a PEROBS::Object to the write cache.
|
58
|
+
# @param obj [PEROBS::ObjectBase]
|
59
|
+
def cache_write(obj)
|
60
|
+
if @transaction_stack.empty?
|
61
|
+
idx = index(obj)
|
62
|
+
if (old_obj = @writes[idx]) && old_obj._id != obj._id
|
63
|
+
# There is another old object using this cache slot. Before we can
|
64
|
+
# re-use the slot, we need to sync it to the permanent storage.
|
65
|
+
old_obj._sync
|
66
|
+
end
|
67
|
+
@writes[idx] = obj
|
68
|
+
else
|
69
|
+
# When a transaction is active, we don't have a write cache. The read
|
70
|
+
# cache is used to speed up access to recently used objects.
|
71
|
+
cache_read(obj)
|
72
|
+
# Push the reference of the modified object into the write buffer for
|
73
|
+
# this transaction level.
|
74
|
+
unless @transaction_stack.last.include?(obj)
|
75
|
+
@transaction_stack.last << obj
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Remove an object from the write cache. This will prevent a modified
|
81
|
+
# object from being written to the back-end store.
|
82
|
+
def unwrite(obj)
|
83
|
+
if @transaction_stack.empty?
|
84
|
+
idx = index(obj)
|
85
|
+
if (old_obj = @writes[idx]).nil? || old_obj._id != obj._id
|
86
|
+
raise RuntimeError, "Object to unwrite is not in cache"
|
87
|
+
end
|
88
|
+
@writes[idx] = nil
|
89
|
+
else
|
90
|
+
unless @transaction_stack.last.include?(obj)
|
91
|
+
raise RuntimeError, 'unwrite failed'
|
92
|
+
end
|
93
|
+
@transaction_stack.last.delete(obj)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the PEROBS::Object with the specified ID or nil if not found.
|
98
|
+
# @param id [Fixnum or Bignum] ID of the cached PEROBS::ObjectBase
|
99
|
+
def object_by_id(id)
|
100
|
+
idx = id & @mask
|
101
|
+
# The index is just a hash. We still need to check if the object IDs are
|
102
|
+
# actually the same before we can return the object.
|
103
|
+
if (obj = @writes[idx]) && obj._id == id
|
104
|
+
# The object was in the write cache.
|
105
|
+
return obj
|
106
|
+
elsif (obj = @reads[idx]) && obj._id == id
|
107
|
+
# The object was in the read cache.
|
108
|
+
return obj
|
109
|
+
end
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
# Flush all pending writes to the persistant storage back-end.
|
115
|
+
def flush
|
116
|
+
@writes.each { |w| w._sync if w }
|
117
|
+
@writes = ::Array.new(2 ** @bits)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true if the Cache is currently handling a transaction, false
|
121
|
+
# otherwise.
|
122
|
+
# @return [true/false]
|
123
|
+
def in_transaction?
|
124
|
+
!@transaction_stack.empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
# Tell the cache to start a new transaction. If no other transaction is
|
128
|
+
# active, the write cached is flushed before the transaction is started.
|
129
|
+
def begin_transaction
|
130
|
+
if @transaction_stack.empty?
|
131
|
+
# This is the top-level transaction. Flush the write buffer to save
|
132
|
+
# the current state of all objects.
|
133
|
+
flush
|
134
|
+
else
|
135
|
+
@transaction_stack.last.each do |o|
|
136
|
+
o._stash(@transaction_stack.length - 1)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# Push a transaction buffer onto the transaction stack. This buffer will
|
140
|
+
# hold a reference to all objects modified during this transaction.
|
141
|
+
@transaction_stack.push(::Array.new)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Tell the cache to end the currently active transaction. All write
|
145
|
+
# operations of the current transaction will be synced to the storage
|
146
|
+
# back-end.
|
147
|
+
def end_transaction
|
148
|
+
case @transaction_stack.length
|
149
|
+
when 0
|
150
|
+
raise RuntimeError, 'No ongoing transaction to end'
|
151
|
+
when 1
|
152
|
+
# All transactions completed successfully. Write all modified objects
|
153
|
+
# into the backend storage.
|
154
|
+
@transaction_stack.pop.each { |o| o._sync }
|
155
|
+
else
|
156
|
+
# A nested transaction completed successfully. We add the list of
|
157
|
+
# modified objects to the list of the enclosing transaction.
|
158
|
+
transactions = @transaction_stack.pop
|
159
|
+
# Merge the two lists
|
160
|
+
@transaction_stack.push(@transaction_stack.pop + transactions)
|
161
|
+
# Ensure that each object is only included once in the list.
|
162
|
+
@transaction_stack.last.uniq!
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Tell the cache to abort the currently active transaction. All modified
|
167
|
+
# objects will be restored from the storage back-end to their state before
|
168
|
+
# the transaction started.
|
169
|
+
def abort_transaction
|
170
|
+
if @transaction_stack.empty?
|
171
|
+
raise RuntimeError, 'No ongoing transaction to abort'
|
172
|
+
end
|
173
|
+
@transaction_stack.pop.each { |o| o._restore(@transaction_stack.length) }
|
174
|
+
end
|
175
|
+
|
176
|
+
# Clear all cached entries. You must call flush before calling this
|
177
|
+
# method. Otherwise unwritten objects will be lost.
|
178
|
+
def reset
|
179
|
+
# The read and write caches are Arrays. We use the _bits_ least
|
180
|
+
# significant bits of the PEROBS::ObjectBase ID to select the index in
|
181
|
+
# the read or write cache Arrays.
|
182
|
+
@reads = ::Array.new(2 ** @bits)
|
183
|
+
@writes = ::Array.new(2 ** @bits)
|
184
|
+
@transaction_stack = []
|
185
|
+
end
|
186
|
+
|
187
|
+
# Don't include the cache buffers in output of other objects that
|
188
|
+
# reference Cache.
|
189
|
+
def inspect
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def index(obj)
|
195
|
+
obj._id & @mask
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = DataBase.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'time'
|
29
|
+
require 'json'
|
30
|
+
require 'json/add/core'
|
31
|
+
require 'json/add/struct'
|
32
|
+
require 'yaml'
|
33
|
+
require 'fileutils'
|
34
|
+
|
35
|
+
require 'perobs/ObjectBase'
|
36
|
+
|
37
|
+
module PEROBS
|
38
|
+
|
39
|
+
# Base class for all storage back-ends.
|
40
|
+
class DataBase
|
41
|
+
|
42
|
+
def initialize(serializer = :json)
|
43
|
+
@serializer = serializer
|
44
|
+
end
|
45
|
+
|
46
|
+
# Serialize the given object using the object serializer.
|
47
|
+
# @param obj [ObjectBase] Object to serialize
|
48
|
+
# @return [String] Serialized version
|
49
|
+
def serialize(obj)
|
50
|
+
begin
|
51
|
+
case @serializer
|
52
|
+
when :marshal
|
53
|
+
Marshal.dump(obj)
|
54
|
+
when :json
|
55
|
+
obj.to_json
|
56
|
+
when :yaml
|
57
|
+
YAML.dump(obj)
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
raise RuntimeError,
|
61
|
+
"Cannot serialize object as #{@serializer}: #{e.message}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# De-serialize the given String into a Ruby object.
|
66
|
+
# @param raw [String]
|
67
|
+
# @return [Hash] Deserialized version
|
68
|
+
def deserialize(raw)
|
69
|
+
begin
|
70
|
+
case @serializer
|
71
|
+
when :marshal
|
72
|
+
Marshal.load(raw)
|
73
|
+
when :json
|
74
|
+
JSON.parse(raw, :create_additions => true)
|
75
|
+
when :yaml
|
76
|
+
YAML.load(raw)
|
77
|
+
end
|
78
|
+
rescue => e
|
79
|
+
raise RuntimeError,
|
80
|
+
"Cannot de-serialize object with #{@serializer} parser: " +
|
81
|
+
e.message
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generate a new unique ID. It uses random numbers between 0 and 2**64 -
|
86
|
+
# 1. Deriving classes can overwrite this method. They must implement a
|
87
|
+
# include? method.
|
88
|
+
# @return [Fixnum or Bignum]
|
89
|
+
def new_id
|
90
|
+
begin
|
91
|
+
# Generate a random number. It's recommended to not store more than
|
92
|
+
# 2**62 objects in the same store.
|
93
|
+
id = rand(2**64)
|
94
|
+
# Ensure that we don't have already another object with this ID.
|
95
|
+
end while include?(id)
|
96
|
+
|
97
|
+
id
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Ensure that we have a directory to store the DB items.
|
103
|
+
def ensure_dir_exists(dir)
|
104
|
+
unless Dir.exists?(dir)
|
105
|
+
begin
|
106
|
+
Dir.mkdir(dir)
|
107
|
+
rescue IOError => e
|
108
|
+
raise IOError, "Cannote create DB directory '#{dir}': #{e.message}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = FileSystemDB.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'time'
|
29
|
+
require 'json'
|
30
|
+
require 'json/add/core'
|
31
|
+
require 'json/add/struct'
|
32
|
+
require 'yaml'
|
33
|
+
require 'fileutils'
|
34
|
+
|
35
|
+
require 'perobs/DataBase'
|
36
|
+
require 'perobs/ObjectBase'
|
37
|
+
|
38
|
+
module PEROBS
|
39
|
+
|
40
|
+
# This class provides a filesytem based database store for objects.
|
41
|
+
class FileSystemDB < DataBase
|
42
|
+
|
43
|
+
@@Extensions = {
|
44
|
+
:marshal => '.mshl',
|
45
|
+
:json => '.json',
|
46
|
+
:yaml => '.yml'
|
47
|
+
}
|
48
|
+
|
49
|
+
# Create a new FileSystemDB object. This will create a DB with the given
|
50
|
+
# name. A database will live in a directory of that name.
|
51
|
+
# @param db_name [String] name of the DB directory
|
52
|
+
# @param options [Hash] options to customize the behavior. Currently only
|
53
|
+
# the following option is supported:
|
54
|
+
# :serializer : Can be :marshal, :json, :yaml
|
55
|
+
def initialize(db_name, options = {})
|
56
|
+
super(options[:serializer] || :json)
|
57
|
+
@db_dir = db_name
|
58
|
+
|
59
|
+
# Create the database directory if it doesn't exist yet.
|
60
|
+
ensure_dir_exists(@db_dir)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return true if the object with given ID exists
|
64
|
+
# @param id [Fixnum or Bignum]
|
65
|
+
def include?(id)
|
66
|
+
File.exists?(object_file_name(id))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Store the given object into the filesystem.
|
70
|
+
# @param obj [Hash] Object as defined by PEROBS::ObjectBase
|
71
|
+
def put_object(obj, id)
|
72
|
+
File.write(object_file_name(id), serialize(obj))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Load the given object from the filesystem.
|
76
|
+
# @param id [Fixnum or Bignum] object ID
|
77
|
+
# @return [Hash] Object as defined by PEROBS::ObjectBase
|
78
|
+
def get_object(id)
|
79
|
+
begin
|
80
|
+
raw = File.read(file_name = object_file_name(id))
|
81
|
+
rescue => e
|
82
|
+
raise RuntimeError, "Error in #{file_name}: #{e.message}"
|
83
|
+
end
|
84
|
+
deserialize(raw)
|
85
|
+
end
|
86
|
+
|
87
|
+
# This method must be called to initiate the marking process.
|
88
|
+
def clear_marks
|
89
|
+
@mark_start = Time.now
|
90
|
+
# The filesystem stores access times with second granularity. We need to
|
91
|
+
# wait 1 sec. to ensure that all marks are noticeable.
|
92
|
+
sleep(1)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Permanently delete all objects that have not been marked. Those are
|
96
|
+
# orphaned and are no longer referenced by any actively used object.
|
97
|
+
def delete_unmarked_objects
|
98
|
+
Dir.glob(File.join(@db_dir, '*')) do |dir|
|
99
|
+
next unless Dir.exists?(dir)
|
100
|
+
|
101
|
+
Dir.glob(File.join(dir, '*')) do |file|
|
102
|
+
if File.atime(file) <= @mark_start
|
103
|
+
File.delete(file)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Mark an object.
|
110
|
+
# @param id [Fixnum or Bignum] ID of the object to mark
|
111
|
+
def mark(id)
|
112
|
+
FileUtils.touch(object_file_name(id))
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if the object is marked.
|
116
|
+
# @param id [Fixnum or Bignum] ID of the object to check
|
117
|
+
def is_marked?(id)
|
118
|
+
File.atime(object_file_name(id)) > @mark_start
|
119
|
+
end
|
120
|
+
|
121
|
+
# Check if the stored object is syntactically correct.
|
122
|
+
# @param id [Fixnum/Bignum] Object ID
|
123
|
+
# @param repair [TrueClass/FalseClass] True if an repair attempt should be
|
124
|
+
# made.
|
125
|
+
# @return [TrueClass/FalseClass] True if the object is OK, otherwise
|
126
|
+
# false.
|
127
|
+
def check(id, repair)
|
128
|
+
file_name = object_file_name(id)
|
129
|
+
unless File.exists?(file_name)
|
130
|
+
$stderr.puts "Object file for ID #{id} does not exist"
|
131
|
+
return false
|
132
|
+
end
|
133
|
+
|
134
|
+
begin
|
135
|
+
get_object(id)
|
136
|
+
rescue => e
|
137
|
+
$stderr.puts "Cannot read object file #{file_name}: #{e.message}"
|
138
|
+
return false
|
139
|
+
end
|
140
|
+
|
141
|
+
true
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Ensure that we have a directory to store the DB items.
|
147
|
+
def ensure_dir_exists(dir)
|
148
|
+
unless Dir.exists?(dir)
|
149
|
+
begin
|
150
|
+
Dir.mkdir(dir)
|
151
|
+
rescue IOError => e
|
152
|
+
raise IOError, "Cannote create DB directory '#{dir}': #{e.message}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Determine the file name to store the object. The object ID determines
|
158
|
+
# the directory and file name inside the store.
|
159
|
+
# @param id [Fixnum or Bignum] ID of the object
|
160
|
+
def object_file_name(id)
|
161
|
+
hex_id = "%016X" % id
|
162
|
+
dir = hex_id[0..1]
|
163
|
+
ensure_dir_exists(File.join(@db_dir, dir))
|
164
|
+
|
165
|
+
File.join(@db_dir, dir, hex_id + @@Extensions[@serializer])
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
data/lib/perobs/Hash.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = Hash.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/ObjectBase'
|
29
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
# A Hash that is transparently persisted in the back-end storage. It is very
|
33
|
+
# similar to the Ruby built-in Hash class but has some additional
|
34
|
+
# limitations. The hash key must always be a String.
|
35
|
+
class Hash < ObjectBase
|
36
|
+
|
37
|
+
# Create a new PersistentHash object.
|
38
|
+
# @param store [Store] The Store this hash is stored in
|
39
|
+
# @param default [Any] The default value that is returned when no value is
|
40
|
+
# stored for a specific key.
|
41
|
+
def initialize(store, default = nil)
|
42
|
+
super(store)
|
43
|
+
@default = nil
|
44
|
+
@data = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieves the value object corresponding to the
|
48
|
+
# key object. If not found, returns the default value.
|
49
|
+
def [](key)
|
50
|
+
#unless key.is_a?(String)
|
51
|
+
# raise ArgumentError, 'The Hash key must be of type String'
|
52
|
+
#end
|
53
|
+
_dereferenced(@data.include?(key) ? @data[key] : @default)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Associates the value given by value with the key given by key.
|
57
|
+
# @param key [String] The key
|
58
|
+
# @param value [Any] The value to store
|
59
|
+
def []=(key, value)
|
60
|
+
#unless key.is_a?(String)
|
61
|
+
# raise ArgumentError, 'The Hash key must be of type String'
|
62
|
+
#end
|
63
|
+
@data[key] = _referenced(value)
|
64
|
+
@store.cache.cache_write(self)
|
65
|
+
|
66
|
+
value
|
67
|
+
end
|
68
|
+
|
69
|
+
# Equivalent to Hash::clear
|
70
|
+
def clear
|
71
|
+
@store.cache.cache_write(self)
|
72
|
+
@data.clear
|
73
|
+
end
|
74
|
+
|
75
|
+
# Equivalent to Hash::delete
|
76
|
+
def delete(key)
|
77
|
+
@store.cache.cache_write(self)
|
78
|
+
@data.delete(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Equivalent to Hash::delete_if
|
82
|
+
def delete_if
|
83
|
+
@store.cache.cache_write(self)
|
84
|
+
@data.delete_if do |k, v|
|
85
|
+
yield(k, _dereferenced(v))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Equivalent to Hash::each
|
90
|
+
def each
|
91
|
+
@data.each do |k, v|
|
92
|
+
yield(k, _dereferenced(v))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Equivalent to Hash::each_key
|
97
|
+
def each_key
|
98
|
+
@data.each_key { |k| yield(k) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Equivalent to Hash::each_value
|
102
|
+
def each_value
|
103
|
+
@data.each_value do |v|
|
104
|
+
yield(_dereferenced(v))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Equivalent to Hash::empty?
|
109
|
+
def emtpy?
|
110
|
+
@data.empty?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Equivalent to Hash::has_key?
|
114
|
+
def has_key?(key)
|
115
|
+
@data.has_key?(key)
|
116
|
+
end
|
117
|
+
alias include? has_key?
|
118
|
+
alias key? has_key?
|
119
|
+
alias member? has_key?
|
120
|
+
|
121
|
+
# Equivalent to Hash::keys
|
122
|
+
def keys
|
123
|
+
@data.keys
|
124
|
+
end
|
125
|
+
|
126
|
+
# Equivalent to Hash::length
|
127
|
+
def length
|
128
|
+
@data.length
|
129
|
+
end
|
130
|
+
alias size length
|
131
|
+
|
132
|
+
# Equivalent to Hash::map
|
133
|
+
def map
|
134
|
+
@data.map do |k, v|
|
135
|
+
yield(k, _dereferenced(v))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Equivalent to Hash::values
|
140
|
+
def values
|
141
|
+
@data.values.map { |v| _dereferenced(v) }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Return a list of all object IDs of all persistend objects that this Hash
|
145
|
+
# is referencing.
|
146
|
+
# @return [Array of Fixnum or Bignum] IDs of referenced objects
|
147
|
+
def _referenced_object_ids
|
148
|
+
@data.each_value.select { |v| v && v.is_a?(POReference) }.map { |o| o.id }
|
149
|
+
end
|
150
|
+
|
151
|
+
# This method should only be used during store repair operations. It will
|
152
|
+
# delete all referenced to the given object ID.
|
153
|
+
# @param id [Fixnum/Bignum] targeted object ID
|
154
|
+
def _delete_reference_to_id(id)
|
155
|
+
@data.delete_if { |k, v| v && v.is_a?(POReference) && v.id == id }
|
156
|
+
end
|
157
|
+
|
158
|
+
# Restore the persistent data from a single data structure.
|
159
|
+
# This is a library internal method. Do not use outside of this library.
|
160
|
+
# @param data [Hash] the actual Hash object
|
161
|
+
# @private
|
162
|
+
def _deserialize(data)
|
163
|
+
@data = data
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def _serialize
|
169
|
+
@data
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|