perobs 2.3.1 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/perobs/Array.rb +17 -4
- data/lib/perobs/BTreeBlob.rb +22 -23
- data/lib/perobs/BTreeDB.rb +11 -12
- data/lib/perobs/Cache.rb +5 -4
- data/lib/perobs/DataBase.rb +19 -7
- data/lib/perobs/DynamoDB.rb +1 -1
- data/lib/perobs/FixedSizeBlobFile.rb +189 -0
- data/lib/perobs/FlatFile.rb +513 -0
- data/lib/perobs/FlatFileDB.rb +249 -0
- data/lib/perobs/FreeSpaceManager.rb +204 -0
- data/lib/perobs/Hash.rb +17 -4
- data/lib/perobs/IndexTree.rb +164 -0
- data/lib/perobs/IndexTreeNode.rb +296 -0
- data/lib/perobs/Log.rb +125 -0
- data/lib/perobs/Object.rb +10 -11
- data/lib/perobs/ObjectBase.rb +18 -5
- data/lib/perobs/StackFile.rb +137 -0
- data/lib/perobs/Store.rb +85 -19
- data/lib/perobs/TreeDB.rb +276 -0
- data/lib/perobs/version.rb +1 -1
- data/test/Array_spec.rb +11 -2
- data/test/FixedSizeBlobFile_spec.rb +91 -0
- data/test/FlatFileDB_spec.rb +56 -0
- data/test/FreeSpaceManager_spec.rb +91 -0
- data/test/Hash_spec.rb +11 -2
- data/test/IndexTree_spec.rb +118 -0
- data/test/Object_spec.rb +29 -17
- data/test/StackFile_spec.rb +113 -0
- data/test/Store_spec.rb +37 -3
- metadata +22 -3
@@ -0,0 +1,249 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = FlatFileDB.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015, 2016 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 'fileutils'
|
29
|
+
require 'zlib'
|
30
|
+
|
31
|
+
require 'perobs/Log'
|
32
|
+
require 'perobs/DataBase'
|
33
|
+
require 'perobs/FlatFile'
|
34
|
+
|
35
|
+
module PEROBS
|
36
|
+
|
37
|
+
# The FlatFileDB is a storage backend that uses a single flat file to store
|
38
|
+
# the value blobs.
|
39
|
+
class FlatFileDB < DataBase
|
40
|
+
|
41
|
+
# This version number increases whenever the on-disk format changes in a
|
42
|
+
# way that requires conversion actions after an update.
|
43
|
+
VERSION = 1
|
44
|
+
|
45
|
+
attr_reader :max_blob_size
|
46
|
+
|
47
|
+
# Create a new FlatFileDB object.
|
48
|
+
# @param db_name [String] name of the DB directory
|
49
|
+
# @param options [Hash] options to customize the behavior. Currently only
|
50
|
+
# the following options are supported:
|
51
|
+
# :serializer : Can be :marshal, :json, :yaml
|
52
|
+
def initialize(db_name, options = {})
|
53
|
+
super(options[:serializer] || :json)
|
54
|
+
|
55
|
+
@db_dir = db_name
|
56
|
+
# Create the database directory if it doesn't exist yet.
|
57
|
+
ensure_dir_exists(@db_dir)
|
58
|
+
PEROBS.log.open(File.join(@db_dir, 'log'))
|
59
|
+
check_version
|
60
|
+
|
61
|
+
# Read the existing DB config.
|
62
|
+
@config = get_hash('config')
|
63
|
+
check_option('serializer')
|
64
|
+
|
65
|
+
put_hash('config', @config)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Open the FlatFileDB for transactions.
|
69
|
+
def open
|
70
|
+
@flat_file = FlatFile.new(@db_dir)
|
71
|
+
@flat_file.open
|
72
|
+
PEROBS.log.info "FlatFile opened"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Close the FlatFileDB.
|
76
|
+
def close
|
77
|
+
@flat_file.close
|
78
|
+
@flat_file = nil
|
79
|
+
PEROBS.log.info "FlatFile closed"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Delete the entire database. The database is no longer usable after this
|
83
|
+
# method was called.
|
84
|
+
def delete_database
|
85
|
+
FileUtils.rm_rf(@db_dir)
|
86
|
+
end
|
87
|
+
|
88
|
+
def FlatFileDB::delete_db(db_name)
|
89
|
+
FileUtils.rm_rf(db_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return true if the object with given ID exists
|
93
|
+
# @param id [Fixnum or Bignum]
|
94
|
+
def include?(id)
|
95
|
+
!@flat_file.find_obj_addr_by_id(id).nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
# Store a simple Hash as a JSON encoded file into the DB directory.
|
99
|
+
# @param name [String] Name of the hash. Will be used as file name.
|
100
|
+
# @param hash [Hash] A Hash that maps String objects to strings or
|
101
|
+
# numbers.
|
102
|
+
def put_hash(name, hash)
|
103
|
+
file_name = File.join(@db_dir, name + '.json')
|
104
|
+
begin
|
105
|
+
File.write(file_name, hash.to_json)
|
106
|
+
rescue => e
|
107
|
+
PEROBS.log.fatal "Cannot write hash file '#{file_name}': #{e.message}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Load the Hash with the given name.
|
112
|
+
# @param name [String] Name of the hash.
|
113
|
+
# @return [Hash] A Hash that maps String objects to strings or numbers.
|
114
|
+
def get_hash(name)
|
115
|
+
file_name = File.join(@db_dir, name + '.json')
|
116
|
+
return ::Hash.new unless File.exist?(file_name)
|
117
|
+
|
118
|
+
begin
|
119
|
+
json = File.read(file_name)
|
120
|
+
rescue => e
|
121
|
+
PEROBS.log.fatal "Cannot read hash file '#{file_name}': #{e.message}"
|
122
|
+
end
|
123
|
+
JSON.parse(json, :create_additions => true)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Store the given object into the cluster files.
|
127
|
+
# @param obj [Hash] Object as defined by PEROBS::ObjectBase
|
128
|
+
def put_object(obj, id)
|
129
|
+
@flat_file.delete_obj_by_id(id)
|
130
|
+
@flat_file.write_obj_by_id(id, serialize(obj))
|
131
|
+
end
|
132
|
+
|
133
|
+
# Load the given object from the filesystem.
|
134
|
+
# @param id [Fixnum or Bignum] object ID
|
135
|
+
# @return [Hash] Object as defined by PEROBS::ObjectBase or nil if ID does
|
136
|
+
# not exist
|
137
|
+
def get_object(id)
|
138
|
+
if (raw_obj = @flat_file.read_obj_by_id(id))
|
139
|
+
return deserialize(raw_obj)
|
140
|
+
else
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# This method must be called to initiate the marking process.
|
146
|
+
def clear_marks
|
147
|
+
t = Time.now
|
148
|
+
PEROBS.log.info "Clearing all marks"
|
149
|
+
@flat_file.clear_all_marks
|
150
|
+
PEROBS.log.info "All marks cleared in #{Time.now - t} seconds"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Permanently delete all objects that have not been marked. Those are
|
154
|
+
# orphaned and are no longer referenced by any actively used object.
|
155
|
+
# @return [Array] List of IDs that have been removed from the DB.
|
156
|
+
def delete_unmarked_objects
|
157
|
+
t = Time.now
|
158
|
+
PEROBS.log.info "Deleting unmarked objects"
|
159
|
+
retval = @flat_file.delete_unmarked_objects
|
160
|
+
PEROBS.log.info "Unmarked objects deleted in #{Time.now - t} seconds"
|
161
|
+
retval
|
162
|
+
end
|
163
|
+
|
164
|
+
# Mark an object.
|
165
|
+
# @param id [Fixnum or Bignum] ID of the object to mark
|
166
|
+
def mark(id)
|
167
|
+
@flat_file.mark_obj_by_id(id)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check if the object is marked.
|
171
|
+
# @param id [Fixnum or Bignum] ID of the object to check
|
172
|
+
# @param ignore_errors [Boolean] If set to true no errors will be raised
|
173
|
+
# for non-existing objects.
|
174
|
+
def is_marked?(id, ignore_errors = false)
|
175
|
+
@flat_file.is_marked_by_id?(id)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Basic consistency check.
|
179
|
+
# @param repair [TrueClass/FalseClass] True if found errors should be
|
180
|
+
# repaired.
|
181
|
+
def check_db(repair = false)
|
182
|
+
t = Time.now
|
183
|
+
PEROBS.log.info "check_db started"
|
184
|
+
@flat_file.check(repair)
|
185
|
+
PEROBS.log.info "check_db completed in #{Time.now - t} seconds"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Check if the stored object is syntactically correct.
|
189
|
+
# @param id [Fixnum/Bignum] Object ID
|
190
|
+
# @param repair [TrueClass/FalseClass] True if an repair attempt should be
|
191
|
+
# made.
|
192
|
+
# @return [TrueClass/FalseClass] True if the object is OK, otherwise
|
193
|
+
# false.
|
194
|
+
def check(id, repair)
|
195
|
+
begin
|
196
|
+
get_object(id)
|
197
|
+
rescue => e
|
198
|
+
PEROBS.log.warn "Cannot read object with ID #{id}: #{e.message}"
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
# Store the given serialized object into the cluster files. This method is
|
206
|
+
# for internal use only!
|
207
|
+
# @param raw [String] Serialized Object as defined by PEROBS::ObjectBase
|
208
|
+
# @param id [Fixnum or Bignum] Object ID
|
209
|
+
def put_raw_object(raw, id)
|
210
|
+
@flat_file.delete_obj_(id)
|
211
|
+
@flat_file.write_obj_by_id(id, raw)
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def check_version
|
217
|
+
version_file = File.join(@db_dir, 'version')
|
218
|
+
version = VERSION
|
219
|
+
|
220
|
+
if File.exist?(version_file)
|
221
|
+
begin
|
222
|
+
version = File.read(version_file).to_i
|
223
|
+
rescue => e
|
224
|
+
PEROBS.log.fatal "Cannot read version number file " +
|
225
|
+
"'#{version_file}': " + e.message
|
226
|
+
end
|
227
|
+
else
|
228
|
+
write_version_file(version_file)
|
229
|
+
end
|
230
|
+
|
231
|
+
if version > VERSION
|
232
|
+
PEROBS.log.fatal "Cannot downgrade the FlatFile database from " +
|
233
|
+
"version #{version} to version #{VERSION}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def write_version_file(version_file)
|
238
|
+
begin
|
239
|
+
File.write(version_file, "#{VERSION}")
|
240
|
+
rescue => e
|
241
|
+
PEROBS.log.fatal "Cannot write version number file " +
|
242
|
+
"'#{version_file}': " + e.message
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = FreeSpaceManager.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 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/Log'
|
29
|
+
require 'perobs/StackFile'
|
30
|
+
|
31
|
+
module PEROBS
|
32
|
+
|
33
|
+
# The FreeSpaceManager keeps a list of the free spaces in the FlatFile. Each
|
34
|
+
# space is stored with address and size. The data is persisted in the file
|
35
|
+
# system. Internally the free spaces are stored in different pools. Each
|
36
|
+
# pool holds spaces that are at least of a given size and not as big as the
|
37
|
+
# next pool up. Pool entry minimum sizes increase by a factor of 2 from
|
38
|
+
# pool to pool.
|
39
|
+
class FreeSpaceManager
|
40
|
+
|
41
|
+
# Create a new FreeSpaceManager object in the specified directory.
|
42
|
+
# @param dir [String] directory path
|
43
|
+
def initialize(dir)
|
44
|
+
@dir = dir
|
45
|
+
@pools = []
|
46
|
+
end
|
47
|
+
|
48
|
+
# Open the pool files.
|
49
|
+
def open
|
50
|
+
Dir.glob(File.join(@dir, 'free_list_*.stack')).each do |file|
|
51
|
+
basename = File.basename(file)
|
52
|
+
# Cut out the pool index from the file name.
|
53
|
+
index = basename[10..-7].to_i
|
54
|
+
@pools[index] = StackFile.new(@dir, basename[0..-7], 2 * 8)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Close all pool files.
|
59
|
+
def close
|
60
|
+
@pools = []
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add a new space with a given address and size.
|
64
|
+
# @param address [Integer] Starting address of the space
|
65
|
+
# @param size [Integer] size of the space in bytes
|
66
|
+
def add_space(address, size)
|
67
|
+
if size <= 0
|
68
|
+
PEROBS.log.fatal "Size (#{size}) must be larger than 0."
|
69
|
+
end
|
70
|
+
pool_index = msb(size)
|
71
|
+
new_pool(pool_index) unless @pools[pool_index]
|
72
|
+
push_pool(pool_index, [ address, size ].pack('QQ'))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get a space that has at least the requested size.
|
76
|
+
# @param size [Integer] Required size in bytes
|
77
|
+
# @return [Array] Touple with address and actual size of the space.
|
78
|
+
def get_space(size)
|
79
|
+
if size <= 0
|
80
|
+
PEROBS.log.fatal "Size (#{size}) must be larger than 0."
|
81
|
+
end
|
82
|
+
# When we search for a free space we need to search the pool that
|
83
|
+
# corresponds to (size - 1) * 2. It is the pool that has the spaces that
|
84
|
+
# are at least as big as size.
|
85
|
+
pool_index = size == 1 ? 0 : msb(size - 1) + 1
|
86
|
+
unless @pools[pool_index]
|
87
|
+
return nil
|
88
|
+
else
|
89
|
+
return nil unless (entry = pop_pool(pool_index))
|
90
|
+
sp_address, sp_size = entry.unpack('QQ')
|
91
|
+
if sp_size < size
|
92
|
+
PEROBS.log.fatal "Space at address #{sp_address} is too small. " +
|
93
|
+
"Must be at least #{size} bytes but is only #{sp_size} bytes."
|
94
|
+
end
|
95
|
+
[ sp_address, sp_size ]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Clear all pools and forget any registered spaces.
|
100
|
+
def clear
|
101
|
+
@pools.each do |pool|
|
102
|
+
if pool
|
103
|
+
pool.open
|
104
|
+
pool.clear
|
105
|
+
pool.close
|
106
|
+
end
|
107
|
+
end
|
108
|
+
close
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check if there is a space in the free space lists that matches the
|
112
|
+
# address and the size.
|
113
|
+
# @param [Integer] address Address of the space
|
114
|
+
# @param [Integer] size Length of the space in bytes
|
115
|
+
# @return [Boolean] True if space is found, false otherwise
|
116
|
+
def has_space?(address, size)
|
117
|
+
unless (pool = @pools[msb(size)])
|
118
|
+
return false
|
119
|
+
end
|
120
|
+
|
121
|
+
pool.open
|
122
|
+
pool.each do |entry|
|
123
|
+
sp_address, sp_size = entry.unpack('QQ')
|
124
|
+
if address == sp_address
|
125
|
+
if size != sp_size
|
126
|
+
PEROBS.log.fatal "FreeSpaceManager has space with different " +
|
127
|
+
"size"
|
128
|
+
end
|
129
|
+
pool.close
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
pool.close
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
def check(flat_file)
|
139
|
+
@pools.each do |pool|
|
140
|
+
next unless pool
|
141
|
+
|
142
|
+
pool.open
|
143
|
+
pool.each do |entry|
|
144
|
+
address, size = entry.unpack('QQ')
|
145
|
+
unless flat_file.has_space?(address, size)
|
146
|
+
PEROBS.log.error "FreeSpaceManager has space that isn't " +
|
147
|
+
"available in the FlatFile."
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
pool.close
|
152
|
+
end
|
153
|
+
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
157
|
+
def inspect
|
158
|
+
'[' + @pools.map do |p|
|
159
|
+
if p
|
160
|
+
p.open
|
161
|
+
r = p.to_ary.map { |bs| bs.unpack('QQ')}.inspect
|
162
|
+
p.close
|
163
|
+
r
|
164
|
+
else
|
165
|
+
'nil'
|
166
|
+
end
|
167
|
+
end.join(', ') + ']'
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def new_pool(index)
|
173
|
+
# The file name pattern for the pool files.
|
174
|
+
filename = "free_list_#{index}"
|
175
|
+
@pools[index] = sf = StackFile.new(@dir, filename, 2 * 8)
|
176
|
+
end
|
177
|
+
|
178
|
+
def push_pool(index, value)
|
179
|
+
pool = @pools[index]
|
180
|
+
pool.open
|
181
|
+
pool.push(value)
|
182
|
+
pool.close
|
183
|
+
end
|
184
|
+
|
185
|
+
def pop_pool(index)
|
186
|
+
pool = @pools[index]
|
187
|
+
pool.open
|
188
|
+
value = pool.pop
|
189
|
+
pool.close
|
190
|
+
|
191
|
+
value
|
192
|
+
end
|
193
|
+
|
194
|
+
def msb(i)
|
195
|
+
unless i > 0
|
196
|
+
PEROBS.log.fatal "i must be larger than 0"
|
197
|
+
end
|
198
|
+
i.to_s(2).length - 1
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
data/lib/perobs/Hash.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
#
|
3
3
|
# = Hash.rb -- Persistent Ruby Object Store
|
4
4
|
#
|
5
|
-
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
5
|
+
# Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
|
6
6
|
#
|
7
7
|
# MIT License
|
8
8
|
#
|
@@ -25,6 +25,7 @@
|
|
25
25
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
26
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
27
|
|
28
|
+
require 'perobs/Log'
|
28
29
|
require 'perobs/ObjectBase'
|
29
30
|
|
30
31
|
module PEROBS
|
@@ -48,7 +49,7 @@ module PEROBS
|
|
48
49
|
:==, :[], :assoc, :compare_by_identity, :compare_by_identity?, :default,
|
49
50
|
:default_proc, :each, :each_key, :each_pair, :each_value, :empty?,
|
50
51
|
:eql?, :fetch, :flatten, :has_key?, :has_value?, :hash, :include?,
|
51
|
-
:
|
52
|
+
:invert, :key, :key?, :keys, :length, :member?, :merge,
|
52
53
|
:pretty_print, :pretty_print_cycle, :rassoc, :reject, :select, :size,
|
53
54
|
:to_a, :to_h, :to_hash, :to_s, :value?, :values, :values_at
|
54
55
|
] + Enumerable.instance_methods).uniq.each do |method_sym|
|
@@ -104,6 +105,7 @@ module PEROBS
|
|
104
105
|
@data.delete_if do |k, v|
|
105
106
|
v && v.respond_to?(:is_poxreference?) && v.id == id
|
106
107
|
end
|
108
|
+
@store.cache.cache_write(self)
|
107
109
|
end
|
108
110
|
|
109
111
|
# Restore the persistent data from a single data structure.
|
@@ -117,6 +119,17 @@ module PEROBS
|
|
117
119
|
@data
|
118
120
|
end
|
119
121
|
|
122
|
+
# Textual dump for debugging purposes
|
123
|
+
# @return [String]
|
124
|
+
def inspect
|
125
|
+
"<#{self.class}:#{@_id}>\n{\n" +
|
126
|
+
@data.map do |k, v|
|
127
|
+
" #{k.inspect} => " + (v.respond_to?(:is_poxreference?) ?
|
128
|
+
"<PEROBS::ObjectBase:#{v._id}>" : v.inspect)
|
129
|
+
end.join(",\n") +
|
130
|
+
"\n}\n"
|
131
|
+
end
|
132
|
+
|
120
133
|
private
|
121
134
|
|
122
135
|
def _serialize
|
@@ -130,9 +143,9 @@ module PEROBS
|
|
130
143
|
# objects should not be used directly. The library only exposes them
|
131
144
|
# via POXReference proxy objects.
|
132
145
|
if v.is_a?(ObjectBase)
|
133
|
-
|
146
|
+
PEROBS.log.fatal 'A PEROBS::ObjectBase object escaped! ' +
|
134
147
|
"It is stored in a PEROBS::Hash with key #{k.inspect}. " +
|
135
|
-
'Have you used self() instead of myself() to' +
|
148
|
+
'Have you used self() instead of myself() to ' +
|
136
149
|
"get the reference of this PEROBS object?\n" +
|
137
150
|
v.inspect
|
138
151
|
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = IndexTree.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 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/FixedSizeBlobFile'
|
29
|
+
require 'perobs/IndexTreeNode'
|
30
|
+
|
31
|
+
module PEROBS
|
32
|
+
|
33
|
+
# The IndexTree maps the object ID to the address in the FlatFile. The
|
34
|
+
# search in the tree is much faster than the linear search in the FlatFile.
|
35
|
+
class IndexTree
|
36
|
+
|
37
|
+
# Determines how many levels of the IndexTree will be kept in memory to
|
38
|
+
# accerlerate the access. A number of 7 will keep up to 21845 entries in
|
39
|
+
# the cache but will accelerate the access to the FlatFile address.
|
40
|
+
MAX_CACHED_LEVEL = 7
|
41
|
+
|
42
|
+
attr_reader :nodes, :ids
|
43
|
+
|
44
|
+
def initialize(db_dir)
|
45
|
+
# Directory path used to store the files.
|
46
|
+
@db_dir = db_dir
|
47
|
+
|
48
|
+
# This FixedSizeBlobFile contains the nodes of the IndexTree.
|
49
|
+
@nodes = FixedSizeBlobFile.new(db_dir, 'database_index',
|
50
|
+
IndexTreeNode::NODE_BYTES)
|
51
|
+
|
52
|
+
# The node sequence usually only reveals a partial match with the
|
53
|
+
# requested ID. So, the leaves of the tree point to the object_id_index
|
54
|
+
# file which contains the full object ID and the address of the
|
55
|
+
# corresponding object in the FlatFile.
|
56
|
+
@ids = FixedSizeBlobFile.new(db_dir, 'object_id_index', 2 * 8)
|
57
|
+
|
58
|
+
# The first MAX_CACHED_LEVEL levels of nodes will be cached in memory to
|
59
|
+
# improve access times.
|
60
|
+
@node_cache = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Open the tree files.
|
64
|
+
def open
|
65
|
+
@nodes.open
|
66
|
+
@ids.open
|
67
|
+
@root = IndexTreeNode.new(self, 0, 0)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Close the tree files.
|
71
|
+
def close
|
72
|
+
@ids.close
|
73
|
+
@nodes.close
|
74
|
+
end
|
75
|
+
|
76
|
+
# Flush out all unwritten data
|
77
|
+
def sync
|
78
|
+
@ids.sync
|
79
|
+
@nodes.sync
|
80
|
+
end
|
81
|
+
|
82
|
+
# Delete all data from the tree.
|
83
|
+
def clear
|
84
|
+
@nodes.clear
|
85
|
+
@ids.clear
|
86
|
+
@node_cache = {}
|
87
|
+
@root = IndexTreeNode.new(self, 0, 0)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Return an IndexTreeNode object that corresponds to the given address.
|
91
|
+
# @param nibble [Fixnum] Index of the nibble the node should correspond to
|
92
|
+
# @param address [Integer] Address of the node in @nodes or nil
|
93
|
+
def get_node(nibble, address = nil)
|
94
|
+
# Generate a mask for the least significant bits up to and including the
|
95
|
+
# nibble.
|
96
|
+
mask = (2 ** ((1 + nibble) * 4)) - 1
|
97
|
+
if address && (node = @node_cache[address & mask])
|
98
|
+
# We have an address and have found the node in the node cache.
|
99
|
+
return node
|
100
|
+
else
|
101
|
+
# We don't have a IndexTreeNode object yet for this node. Create it
|
102
|
+
# with the data from the 'database_index' file.
|
103
|
+
node = IndexTreeNode.new(self, nibble, address)
|
104
|
+
# Add the node to the node cache if it's up to MAX_CACHED_LEVEL levels
|
105
|
+
# down from the root.
|
106
|
+
@node_cache[address & mask] = node if nibble <= MAX_CACHED_LEVEL
|
107
|
+
return node
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Delete a node from the tree that corresponds to the address.
|
112
|
+
# @param nibble [Fixnum] The corresponding nibble for the node
|
113
|
+
# @param address [Integer] The address of the node in @nodes
|
114
|
+
def delete_node(nibble, address)
|
115
|
+
# First delete the node from the node cache.
|
116
|
+
mask = (2 ** ((1 + nibble) * 4)) - 1
|
117
|
+
@node_cache.delete(address & mask)
|
118
|
+
# Then delete it from the 'database_index' file.
|
119
|
+
@nodes.delete_blob(address)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Store a ID/value touple into the tree. The value can later be retrieved
|
123
|
+
# by the ID again. IDs are always unique in the tree. If the ID already
|
124
|
+
# exists in the tree, the value will be overwritten.
|
125
|
+
# @param id [Integer] ID or key
|
126
|
+
# @param value [Integer] value to store
|
127
|
+
def put_value(id, value)
|
128
|
+
#MAX_CACHED_LEVEL.downto(0) do |i|
|
129
|
+
# mask = (2 ** ((1 + i) * 4)) - 1
|
130
|
+
# if (node = @node_cache[value & mask])
|
131
|
+
# return node.put_value(id, value)
|
132
|
+
# end
|
133
|
+
#end
|
134
|
+
@root.put_value(id, value)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Retrieve the value that was stored with the given ID.
|
138
|
+
# @param id [Integer] ID of the value to retrieve
|
139
|
+
# @return [Fixnum] value
|
140
|
+
def get_value(id)
|
141
|
+
@root.get_value(id)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Delete the value with the given ID.
|
145
|
+
# @param [Integer] id
|
146
|
+
def delete_value(id)
|
147
|
+
@root.delete_value(id)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check if the index is OK and matches the flat_file data.
|
151
|
+
def check(flat_file)
|
152
|
+
@root.check(flat_file)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Convert the tree into a human readable form.
|
156
|
+
# @return [String]
|
157
|
+
def inspect
|
158
|
+
@root.inspect
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|