perobs 2.3.1 → 2.4.0
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/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
|
+
|