perobs 2.5.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/perobs/Array.rb +1 -1
- data/lib/perobs/BTree.rb +233 -0
- data/lib/perobs/BTreeDB.rb +2 -0
- data/lib/perobs/BTreeNode.rb +706 -0
- data/lib/perobs/BTreeNodeCache.rb +107 -0
- data/lib/perobs/BTreeNodeLink.rb +141 -0
- data/lib/perobs/EquiBlobsFile.rb +570 -0
- data/lib/perobs/FlatFile.rb +179 -78
- data/lib/perobs/FlatFileBlobHeader.rb +92 -17
- data/lib/perobs/FlatFileDB.rb +16 -7
- data/lib/perobs/LockFile.rb +181 -0
- data/lib/perobs/Object.rb +2 -1
- data/lib/perobs/SpaceTree.rb +181 -0
- data/lib/perobs/SpaceTreeNode.rb +672 -0
- data/lib/perobs/SpaceTreeNodeCache.rb +76 -0
- data/lib/perobs/SpaceTreeNodeLink.rb +103 -0
- data/lib/perobs/Store.rb +27 -13
- data/lib/perobs/version.rb +1 -1
- data/test/BTree_spec.rb +128 -0
- data/test/EquiBlobsFile_spec.rb +199 -0
- data/test/FlatFileDB_spec.rb +63 -9
- data/test/LockFile_spec.rb +133 -0
- data/test/SpaceTree_spec.rb +245 -0
- data/test/Store_spec.rb +3 -0
- data/test/spec_helper.rb +13 -0
- metadata +21 -13
- data/lib/perobs/FixedSizeBlobFile.rb +0 -193
- data/lib/perobs/FreeSpaceManager.rb +0 -204
- data/lib/perobs/IndexTree.rb +0 -145
- data/lib/perobs/IndexTreeNode.rb +0 -316
- data/test/FixedSizeBlobFile_spec.rb +0 -91
- data/test/FreeSpaceManager_spec.rb +0 -91
- data/test/IndexTree_spec.rb +0 -118
data/lib/perobs/FlatFileDB.rb
CHANGED
@@ -70,14 +70,14 @@ module PEROBS
|
|
70
70
|
def open
|
71
71
|
@flat_file = FlatFile.new(@db_dir)
|
72
72
|
@flat_file.open
|
73
|
-
PEROBS.log.info "FlatFile opened"
|
73
|
+
PEROBS.log.info "FlatFile '#{@db_dir}' opened"
|
74
74
|
end
|
75
75
|
|
76
76
|
# Close the FlatFileDB.
|
77
77
|
def close
|
78
78
|
@flat_file.close
|
79
79
|
@flat_file = nil
|
80
|
-
PEROBS.log.info "FlatFile closed"
|
80
|
+
PEROBS.log.info "FlatFile '#{@db_dir}' closed"
|
81
81
|
end
|
82
82
|
|
83
83
|
# Delete the entire database. The database is no longer usable after this
|
@@ -127,7 +127,6 @@ module PEROBS
|
|
127
127
|
# Store the given object into the cluster files.
|
128
128
|
# @param obj [Hash] Object as defined by PEROBS::ObjectBase
|
129
129
|
def put_object(obj, id)
|
130
|
-
@flat_file.delete_obj_by_id(id)
|
131
130
|
@flat_file.write_obj_by_id(id, serialize(obj))
|
132
131
|
end
|
133
132
|
|
@@ -172,6 +171,7 @@ module PEROBS
|
|
172
171
|
# Basic consistency check.
|
173
172
|
# @param repair [TrueClass/FalseClass] True if found errors should be
|
174
173
|
# repaired.
|
174
|
+
# @return number of errors found
|
175
175
|
def check_db(repair = false)
|
176
176
|
@flat_file.check(repair)
|
177
177
|
end
|
@@ -185,10 +185,18 @@ module PEROBS
|
|
185
185
|
def check(id, repair)
|
186
186
|
begin
|
187
187
|
return get_object(id) != nil
|
188
|
-
rescue => e
|
189
|
-
PEROBS.log.
|
190
|
-
|
188
|
+
rescue PEROBS::FatalError => e
|
189
|
+
PEROBS.log.error "Cannot read object with ID #{id}: #{e.message}"
|
190
|
+
if repair
|
191
|
+
begin
|
192
|
+
PEROBS.log.error "Discarding broken object with ID #{id}"
|
193
|
+
@flat_file.delete_obj_by_id(id)
|
194
|
+
rescue PEROBS::FatalError
|
195
|
+
end
|
196
|
+
end
|
191
197
|
end
|
198
|
+
|
199
|
+
return false
|
192
200
|
end
|
193
201
|
|
194
202
|
# Store the given serialized object into the cluster files. This method is
|
@@ -196,7 +204,6 @@ module PEROBS
|
|
196
204
|
# @param raw [String] Serialized Object as defined by PEROBS::ObjectBase
|
197
205
|
# @param id [Fixnum or Bignum] Object ID
|
198
206
|
def put_raw_object(raw, id)
|
199
|
-
@flat_file.delete_obj_(id)
|
200
207
|
@flat_file.write_obj_by_id(id, raw)
|
201
208
|
end
|
202
209
|
|
@@ -235,6 +242,8 @@ module PEROBS
|
|
235
242
|
# well.
|
236
243
|
if version < VERSION
|
237
244
|
write_version_file(version_file)
|
245
|
+
PEROBS.log.warn "Update of FlatFileDB '#{@db_dir}' from version " +
|
246
|
+
"#{version} to version #{VERSION} completed"
|
238
247
|
end
|
239
248
|
end
|
240
249
|
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = LockFile.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2017 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
|
+
|
30
|
+
module PEROBS
|
31
|
+
|
32
|
+
# This class implements a file based lock. It can only be taken by one
|
33
|
+
# process at a time. It support configurable lock lifetime, maximum retries
|
34
|
+
# and pause between retries.
|
35
|
+
class LockFile
|
36
|
+
|
37
|
+
# Create a new lock for the given file.
|
38
|
+
# @param file_name [String] file name of the lock file
|
39
|
+
# @param options [Hash] See case statement
|
40
|
+
def initialize(file_name, options = {})
|
41
|
+
@file_name = file_name
|
42
|
+
# The handle of the lock file
|
43
|
+
@file = nil
|
44
|
+
# The maximum duration after which a lock file is considered a left-over
|
45
|
+
# from a dead or malefunctioning process.
|
46
|
+
@timeout_secs = 60 * 60
|
47
|
+
# The maximum number of times we try to get the lock.
|
48
|
+
@max_retries = 5
|
49
|
+
# The time we wait between retries
|
50
|
+
@pause_secs = 1
|
51
|
+
|
52
|
+
options.each do |name, value|
|
53
|
+
case name
|
54
|
+
when :timeout_secs
|
55
|
+
@timeout_secs = value
|
56
|
+
when :max_retries
|
57
|
+
@max_retries = value
|
58
|
+
when :pause_secs
|
59
|
+
@pause_secs = value
|
60
|
+
else
|
61
|
+
PEROBS.log.fatal "Unknown option #{name}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Attempt to take the lock.
|
67
|
+
# @return [Boolean] true if lock was taken, false otherwise
|
68
|
+
def lock
|
69
|
+
retries = @max_retries
|
70
|
+
while retries > 0
|
71
|
+
begin
|
72
|
+
@file = File.open(@file_name, File::RDWR | File::CREAT, 0644)
|
73
|
+
|
74
|
+
if @file.flock(File::LOCK_EX | File::LOCK_NB)
|
75
|
+
# We have taken the lock. Write the PID into the file and leave it
|
76
|
+
# open.
|
77
|
+
@file.write($$)
|
78
|
+
@file.flush
|
79
|
+
@file.truncate(@file.pos)
|
80
|
+
PEROBS.log.debug "Lock file #{@file_name} has been taken for " +
|
81
|
+
"process #{$$}"
|
82
|
+
|
83
|
+
return true
|
84
|
+
else
|
85
|
+
# We did not manage to take the lock file.
|
86
|
+
if @file.mtime <= Time.now - @timeout_secs
|
87
|
+
pid = @file.read.to_i
|
88
|
+
PEROBS.log.info "Old lock file found for PID #{pid}. " +
|
89
|
+
"Removing lock."
|
90
|
+
if is_running?(pid)
|
91
|
+
send_signal('TERM', pid)
|
92
|
+
# Give the process 3 seconds to terminate gracefully.
|
93
|
+
sleep 3
|
94
|
+
# Then send a SIGKILL to ensure it's gone.
|
95
|
+
send_signal('KILL', pid) if is_running?(pid)
|
96
|
+
end
|
97
|
+
@file.close
|
98
|
+
File.delete(@file_name) if File.exist?(@file_name)
|
99
|
+
else
|
100
|
+
PEROBS.log.debug "Lock file #{@file_name} is taken. Trying " +
|
101
|
+
"to get it #{retries} more times."
|
102
|
+
end
|
103
|
+
end
|
104
|
+
rescue => e
|
105
|
+
PEROBS.log.error "Cannot take lock file #{@file_name}: #{e.message}"
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
|
109
|
+
retries -= 1
|
110
|
+
sleep(@pause_secs)
|
111
|
+
end
|
112
|
+
|
113
|
+
PEROBS.log.info "Failed to get lock file #{@file_name} due to timeout"
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if the lock has been taken.
|
118
|
+
# @return [Boolean] true if taken, false otherweise.
|
119
|
+
def is_locked?
|
120
|
+
File.exist?(@file_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Release the lock again.
|
124
|
+
def unlock
|
125
|
+
unless @file
|
126
|
+
PEROBS.log.error "There is no current lock to release"
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
begin
|
131
|
+
@file.flock(File::LOCK_UN)
|
132
|
+
@file.close
|
133
|
+
forced_unlock
|
134
|
+
PEROBS.log.debug "Lock file #{@file_name} for PID #{$$} has been " +
|
135
|
+
"released"
|
136
|
+
rescue => e
|
137
|
+
PEROBS.log.error "Releasing of lock file #{@file_name} failed: " +
|
138
|
+
e.message
|
139
|
+
return false
|
140
|
+
end
|
141
|
+
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Erase the lock file. It's essentially a forced unlock method.
|
146
|
+
def forced_unlock
|
147
|
+
@file = nil
|
148
|
+
if File.exist?(@file_name)
|
149
|
+
begin
|
150
|
+
File.delete(@file_name)
|
151
|
+
PEROBS.log.debug "Lock file #{@file_name} has been deleted."
|
152
|
+
rescue IOError => e
|
153
|
+
PEROBS.log.error "Cannot delete lock file #{@file_name}: " +
|
154
|
+
e.message
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def send_signal(name, pid)
|
162
|
+
begin
|
163
|
+
Process.kill(name, pid)
|
164
|
+
rescue => e
|
165
|
+
PEROBS.log.info "Process kill error: #{e.message}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def is_running?(pid)
|
170
|
+
begin
|
171
|
+
Process.getpgid(pid)
|
172
|
+
true
|
173
|
+
rescue Errno::ESRCH
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
data/lib/perobs/Object.rb
CHANGED
@@ -151,7 +151,7 @@ module PEROBS
|
|
151
151
|
end
|
152
152
|
|
153
153
|
# This method should only be used during store repair operations. It will
|
154
|
-
# delete all
|
154
|
+
# delete all references to the given object ID.
|
155
155
|
# @param id [Fixnum/Bignum] targeted object ID
|
156
156
|
def _delete_reference_to_id(id)
|
157
157
|
_all_attributes.each do |attr|
|
@@ -161,6 +161,7 @@ module PEROBS
|
|
161
161
|
instance_variable_set(ivar, nil)
|
162
162
|
end
|
163
163
|
end
|
164
|
+
mark_as_modified
|
164
165
|
end
|
165
166
|
|
166
167
|
# Restore the persistent data from a single data structure.
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = SpaceTree.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016, 2017 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/EquiBlobsFile'
|
30
|
+
require 'perobs/SpaceTreeNodeCache'
|
31
|
+
require 'perobs/SpaceTreeNode'
|
32
|
+
require 'perobs/FlatFile'
|
33
|
+
|
34
|
+
module PEROBS
|
35
|
+
|
36
|
+
# The SpaceTree keeps a complete list of all empty spaces in the FlatFile.
|
37
|
+
# Spaces are stored with size and address. The Tree is Tenary Tree. The
|
38
|
+
# nodes can link to other nodes with smaller spaces, same spaces and bigger
|
39
|
+
# spaces. The advantage of the ternary tree is that all nodes have equal
|
40
|
+
# size which drastically simplifies the backing store operation.
|
41
|
+
class SpaceTree
|
42
|
+
|
43
|
+
attr_reader :nodes
|
44
|
+
|
45
|
+
# Manage the free spaces tree in the specified directory
|
46
|
+
# @param dir [String] directory path of an existing directory
|
47
|
+
def initialize(dir)
|
48
|
+
@dir = dir
|
49
|
+
|
50
|
+
# This EquiBlobsFile contains the nodes of the SpaceTree.
|
51
|
+
@nodes = EquiBlobsFile.new(@dir, 'database_spaces',
|
52
|
+
SpaceTreeNode::NODE_BYTES, 1)
|
53
|
+
|
54
|
+
@node_cache = SpaceTreeNodeCache.new(128)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Open the SpaceTree file.
|
58
|
+
def open
|
59
|
+
@nodes.open
|
60
|
+
@node_cache.clear
|
61
|
+
@root = SpaceTreeNode.new(self, nil, @nodes.total_entries == 0 ?
|
62
|
+
nil : @nodes.first_entry)
|
63
|
+
@node_cache.insert(@root)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Close the SpaceTree file.
|
67
|
+
def close
|
68
|
+
@nodes.close
|
69
|
+
@root = nil
|
70
|
+
@node_cache.clear
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_root(node)
|
74
|
+
@root = node
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Erase the SpaceTree file. This method cannot be called while the file is
|
79
|
+
# open.
|
80
|
+
def erase
|
81
|
+
@nodes.erase
|
82
|
+
end
|
83
|
+
|
84
|
+
# Add a new space with a given address and size.
|
85
|
+
# @param address [Integer] Starting address of the space
|
86
|
+
# @param size [Integer] size of the space in bytes
|
87
|
+
def add_space(address, size)
|
88
|
+
if size <= 0
|
89
|
+
PEROBS.log.fatal "Size (#{size}) must be larger than 0."
|
90
|
+
end
|
91
|
+
@root.add_space(address, size)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get a space that has at least the requested size.
|
95
|
+
# @param size [Integer] Required size in bytes
|
96
|
+
# @return [Array] Touple with address and actual size of the space.
|
97
|
+
def get_space(size)
|
98
|
+
if size <= 0
|
99
|
+
PEROBS.log.fatal "Size (#{size}) must be larger than 0."
|
100
|
+
end
|
101
|
+
|
102
|
+
if (address_size = @root.find_matching_space(size))
|
103
|
+
# First we try to find an exact match.
|
104
|
+
return address_size
|
105
|
+
elsif (address_size = @root.find_equal_or_larger_space(size))
|
106
|
+
return address_size
|
107
|
+
else
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Delete the node at the given address in the SpaceTree file.
|
113
|
+
# @param address [Integer] address in file
|
114
|
+
def delete_node(address)
|
115
|
+
@node_cache.delete(address)
|
116
|
+
@nodes.delete_blob(address)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Clear all pools and forget any registered spaces.
|
120
|
+
def clear
|
121
|
+
@nodes.clear
|
122
|
+
@node_cache.clear
|
123
|
+
@root = SpaceTreeNode.new(self)
|
124
|
+
@node_cache.insert(@root)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Create a new SpaceTreeNode.
|
128
|
+
# @param parent [SpaceTreeNode] parent node
|
129
|
+
# @param blob_address [Integer] address of the free space
|
130
|
+
# @param size [Integer] size of the free space
|
131
|
+
def new_node(parent, blob_address, size)
|
132
|
+
node = SpaceTreeNode.new(self, parent, nil, blob_address, size)
|
133
|
+
@node_cache.insert(node)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return the SpaceTreeNode that matches the given node address. If a blob
|
137
|
+
# address and size are given, a new node is created instead of being read
|
138
|
+
# from the file.
|
139
|
+
# @param node_address [Integer] Address of the node in the SpaceTree file
|
140
|
+
# @return [SpaceTreeNode]
|
141
|
+
def get_node(node_address)
|
142
|
+
if (node = @node_cache.get(node_address))
|
143
|
+
return node
|
144
|
+
end
|
145
|
+
|
146
|
+
@node_cache.insert(SpaceTreeNode.new(self, nil, node_address))
|
147
|
+
end
|
148
|
+
|
149
|
+
# Check if there is a space in the free space lists that matches the
|
150
|
+
# address and the size.
|
151
|
+
# @param [Integer] address Address of the space
|
152
|
+
# @param [Integer] size Length of the space in bytes
|
153
|
+
# @return [Boolean] True if space is found, false otherwise
|
154
|
+
def has_space?(address, size)
|
155
|
+
@root.has_space?(address, size)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Check if the index is OK and matches the flat_file data (if given).
|
159
|
+
# @param flat_file [FlatFile] Flat file to compare with
|
160
|
+
# @return True if space list matches, flase otherwise
|
161
|
+
def check(flat_file = nil)
|
162
|
+
@nodes.check
|
163
|
+
@root.check(flat_file)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Complete internal tree data structure as textual tree.
|
167
|
+
# @return [String]
|
168
|
+
def to_s
|
169
|
+
@root.to_tree_s
|
170
|
+
end
|
171
|
+
|
172
|
+
# Convert the tree into an Array of [address, size] touples.
|
173
|
+
# @return [Array]
|
174
|
+
def to_a
|
175
|
+
@root.to_a
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|