perobs 2.5.0 → 3.0.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/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
|
+
|