perobs 2.5.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.warn "Cannot read object with ID #{id}: #{e.message}"
190
- return false
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 referenced to the given object ID.
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
+