perobs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,153 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = HashedBlocksDB.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2015 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 'time'
29
+ require 'json'
30
+ require 'json/add/core'
31
+ require 'json/add/struct'
32
+ require 'yaml'
33
+ require 'fileutils'
34
+
35
+ require 'perobs/DataBase'
36
+ require 'perobs/BlockDB'
37
+
38
+ module PEROBS
39
+
40
+ # This class provides a filesytem based database store for objects.
41
+ class HashedBlocksDB < DataBase
42
+
43
+ @@Extensions = {
44
+ :marshal => '.mshl',
45
+ :json => '.json',
46
+ :yaml => '.yml'
47
+ }
48
+
49
+ # Create a new FileSystemDB object. This will create a DB with the given
50
+ # name. A database will live in a directory of that name.
51
+ # @param db_name [String] name of the DB directory
52
+ # @param options [Hash] options to customize the behavior. Currently only
53
+ # the following options are supported:
54
+ # :serializer : Can be :marshal, :json, :yaml
55
+ # :dir_nibbles : The number of nibbles to use for directory names.
56
+ # Meaningful values are 1, 2, and 3. The larger the
57
+ # number the more back-end files are used. Each
58
+ # nibble provides 16 times more directories.
59
+ # :block_size : The size of the blocks inside the storage files in
60
+ # bytes. This should roughly correspond to the size
61
+ # of the smallest serialized objects you want to
62
+ # store in quantities. It also should be an fraction
63
+ # of 4096, the native storage system block size.
64
+ def initialize(db_name, options = {})
65
+ super(options[:serializer] || :json)
66
+ @db_dir = db_name
67
+ @dir_nibbles = options[:dir_nibbles] || 2
68
+ @block_size = options[:block_size] || 256
69
+
70
+ # Create the database directory if it doesn't exist yet.
71
+ ensure_dir_exists(@db_dir)
72
+ end
73
+
74
+ # Return true if the object with given ID exists
75
+ # @param id [Fixnum or Bignum]
76
+ def include?(id)
77
+ !BlockDB.new(directory(id), @block_size).find(id).nil?
78
+ end
79
+
80
+ # Store the given object into the cluster files.
81
+ # @param obj [Hash] Object as defined by PEROBS::ObjectBase
82
+ def put_object(obj, id)
83
+ BlockDB.new(directory(id), @block_size).write_object(id, serialize(obj))
84
+ end
85
+
86
+ # Load the given object from the filesystem.
87
+ # @param id [Fixnum or Bignum] object ID
88
+ # @return [Hash] Object as defined by PEROBS::ObjectBase
89
+ def get_object(id)
90
+ deserialize(BlockDB.new(directory(id), @block_size).read_object(id))
91
+ end
92
+
93
+ # This method must be called to initiate the marking process.
94
+ def clear_marks
95
+ Dir.glob(File.join(@db_dir, '*')) do |dir|
96
+ BlockDB.new(dir, @block_size).clear_marks
97
+ end
98
+ end
99
+
100
+ # Permanently delete all objects that have not been marked. Those are
101
+ # orphaned and are no longer referenced by any actively used object.
102
+ def delete_unmarked_objects
103
+ Dir.glob(File.join(@db_dir, '*')) do |dir|
104
+ BlockDB.new(dir, @block_size).delete_unmarked_entries
105
+ end
106
+ end
107
+
108
+ # Mark an object.
109
+ # @param id [Fixnum or Bignum] ID of the object to mark
110
+ def mark(id)
111
+ BlockDB.new(directory(id), @block_size).mark(id)
112
+ end
113
+
114
+ # Check if the object is marked.
115
+ # @param id [Fixnum or Bignum] ID of the object to check
116
+ def is_marked?(id)
117
+ BlockDB.new(directory(id), @block_size).is_marked?(id)
118
+ end
119
+
120
+ # Check if the stored object is syntactically correct.
121
+ # @param id [Fixnum/Bignum] Object ID
122
+ # @param repair [TrueClass/FalseClass] True if an repair attempt should be
123
+ # made.
124
+ # @return [TrueClass/FalseClass] True if the object is OK, otherwise
125
+ # false.
126
+ def check(id, repair)
127
+ begin
128
+ get_object(id)
129
+ rescue => e
130
+ $stderr.puts "Cannot read object with ID #{id}: #{e.message}"
131
+ return false
132
+ end
133
+
134
+ true
135
+ end
136
+
137
+ private
138
+
139
+ # Determine the file name to store the object. The object ID determines
140
+ # the directory and file name inside the store.
141
+ # @param id [Fixnum or Bignum] ID of the object
142
+ def directory(id)
143
+ hex_id = "%016X" % id
144
+ dir = hex_id[0..(@dir_nibbles - 1)]
145
+ ensure_dir_exists(dir_name = File.join(@db_dir, dir))
146
+
147
+ dir_name
148
+ end
149
+
150
+ end
151
+
152
+ end
153
+
@@ -0,0 +1,189 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = Object.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2015 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 'time'
29
+
30
+ require 'perobs/ObjectBase'
31
+
32
+ module PEROBS
33
+
34
+ # The PEROBS::Object class is the base class for user-defined objects to be
35
+ # stored in the Store. It provides all the plumbing to define the class
36
+ # attributes and to transparently load and store the instances of the class
37
+ # in the database. You can use instance variables like normal instance
38
+ # variables unless they refer to other PEROBS objects. In these cases you
39
+ # must use the accessor methods for these instance variables. You must use
40
+ # accessor methods for any read and write operation to instance variables
41
+ # that hold or should hold PEROBS objects.
42
+ class Object < ObjectBase
43
+
44
+ # Modify the Metaclass of PEROBS::Object to add the attribute method and
45
+ # instance variables to store the default values of the attributes.
46
+ class << self
47
+
48
+ attr_reader :attributes
49
+
50
+ # This method can be used to define instance variable for
51
+ # PEROBS::Object derived classes.
52
+ # @param attributes [Symbol] Name of the instance variable
53
+ def po_attr(*attributes)
54
+ attributes.each do |attr_name|
55
+ unless attr_name.is_a?(Symbol)
56
+ raise ArgumentError, "name must be a symbol but is a " +
57
+ "#{attr_name.class}"
58
+ end
59
+
60
+ # Create the attribute reader method with name of attr_name.
61
+ define_method(attr_name.to_s) do
62
+ _get(attr_name)
63
+ end
64
+ # Create the attribute writer method with name of attr_name.
65
+ define_method(attr_name.to_s + '=') do |val|
66
+ _set(attr_name, val)
67
+ end
68
+
69
+ # Store a list of the attribute names
70
+ @attributes ||= []
71
+ @attributes << attr_name unless @attributes.include?(attr_name)
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ attr_reader :attributes
78
+
79
+ # Create a new PEROBS::Object object.
80
+ def initialize(store)
81
+ super
82
+ end
83
+
84
+ # Initialize the specified attribute _attr_ with the value _val_ unless
85
+ # the attribute has been initialized already. Use this method in the class
86
+ # constructor to avoid overwriting values that have been set when the
87
+ # object was reconstructed from the store.
88
+ # @param attr [Symbol] Name of the attribute
89
+ # @param val [Any] Value to be set
90
+ # @return [true|false] True if the value was initialized, otherwise false.
91
+ def init_attr(attr, val)
92
+ if self.class.attributes.include?(attr)
93
+ _set(attr, val)
94
+ return true
95
+ end
96
+
97
+ false
98
+ end
99
+
100
+ # Return a list of all object IDs that the attributes of this instance are
101
+ # referencing.
102
+ # @return [Array of Fixnum or Bignum] IDs of referenced objects
103
+ def _referenced_object_ids
104
+ ids = []
105
+ self.class.attributes.each do |attr|
106
+ value = instance_variable_get(('@' + attr.to_s).to_sym)
107
+ ids << value.id if value && value.is_a?(POReference)
108
+ end
109
+ ids
110
+ end
111
+
112
+ # This method should only be used during store repair operations. It will
113
+ # delete all referenced to the given object ID.
114
+ # @param id [Fixnum/Bignum] targeted object ID
115
+ def _delete_reference_to_id(id)
116
+ self.class.attributes.each do |attr|
117
+ ivar = ('@' + attr.to_s).to_sym
118
+ value = instance_variable_get(ivar)
119
+ if value && value.is_a?(POReference) && value.id == id
120
+ instance_variable_set(ivar, nil)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Restore the persistent data from a single data structure.
126
+ # This is a library internal method. Do not use outside of this library.
127
+ # @param data [Hash] attribute values hashed by their name
128
+ # @private
129
+ def _deserialize(data)
130
+ # Initialize all attributes with the provided values.
131
+ data.each do |attr_name, value|
132
+ instance_variable_set(('@' + attr_name).to_sym, value)
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ # Return a single data structure that holds all persistent data for this
139
+ # class.
140
+ def _serialize
141
+ attributes = {}
142
+ self.class.attributes.each do |attr|
143
+ ivar = ('@' + attr.to_s).to_sym
144
+ if (value = instance_variable_get(ivar)).is_a?(ObjectBase)
145
+ raise ArgumentError, "The instance variable #{ivar} contains a " +
146
+ "reference to a PEROBS::ObjectBase object! " +
147
+ "This is not allowed. You must use the " +
148
+ "accessor method to assign a reference to " +
149
+ "another PEROBS object."
150
+ end
151
+ attributes[attr.to_s] = value
152
+ end
153
+ attributes
154
+ end
155
+
156
+ def _set(attr, val)
157
+ ivar = ('@' + attr.to_s).to_sym
158
+ if val.is_a?(ObjectBase)
159
+ # References to other PEROBS::Objects must be handled somewhat
160
+ # special.
161
+ if @store != val.store
162
+ raise ArgumentError, 'The referenced object is not part of this store'
163
+ end
164
+ # To release the object from the Ruby object list later, we store the
165
+ # PEROBS::Store ID of the referenced object instead of the actual
166
+ # reference.
167
+ instance_variable_set(ivar, POReference.new(val._id))
168
+ else
169
+ instance_variable_set(ivar, val)
170
+ end
171
+ # Let the store know that we have a modified object.
172
+ @store.cache.cache_write(self)
173
+
174
+ val
175
+ end
176
+
177
+ def _get(attr)
178
+ value = instance_variable_get(('@' + attr.to_s).to_sym)
179
+ if value.is_a?(POReference)
180
+ @store.object_by_id(value.id)
181
+ else
182
+ value
183
+ end
184
+ end
185
+
186
+ end
187
+
188
+ end
189
+
@@ -0,0 +1,159 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = ObjectBase.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2015 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
+ module PEROBS
29
+
30
+ # This class is used to replace a direct reference to another Ruby object by
31
+ # the Store ID. This makes object disposable by the Ruby garbage collector
32
+ # since it's no longer referenced once it has been evicted from the
33
+ # PEROBS::Store cache.
34
+ class POReference < Struct.new(:id)
35
+ end
36
+
37
+ # Base class for all persistent objects. It provides the functionality
38
+ # common to all classes of persistent objects.
39
+ class ObjectBase
40
+
41
+ attr_reader :_id, :store
42
+
43
+ # Create a new PEROBS::ObjectBase object.
44
+ def initialize(store)
45
+ @store = store
46
+ @_id = @store.db.new_id
47
+ @_stash_map = nil
48
+
49
+ # Let the store know that we have a modified object.
50
+ @store.cache.cache_write(self)
51
+ end
52
+
53
+ # Two objects are considered equal if their object IDs are the same.
54
+ def ==(obj)
55
+ obj && @_id == obj._id
56
+ end
57
+
58
+ # Write the object into the backing store database.
59
+ def _sync
60
+ # Reset the stash map to ensure that it's reset before the next
61
+ # transaction is being started.
62
+ @_stash_map = nil
63
+
64
+ db_obj = {
65
+ 'class' => self.class.to_s,
66
+ 'data' => _serialize
67
+ }
68
+ @store.db.put_object(db_obj, @_id)
69
+ end
70
+
71
+ # Read an raw object with the specified ID from the backing store and
72
+ # instantiate a new object of the specific type.
73
+ def ObjectBase.read(store, id)
74
+ # Read the object from database.
75
+ db_obj = store.db.get_object(id)
76
+
77
+ # Call the constructor of the specified class.
78
+ obj = Object.const_get(db_obj['class']).new(store)
79
+ # The object gets created with a new ID by default. We need to restore
80
+ # the old one.
81
+ obj._change_id(id)
82
+ obj._deserialize(db_obj['data'])
83
+
84
+ obj
85
+ end
86
+
87
+ # Restore the object state from the storage back-end.
88
+ # @param level [Fixnum] the transaction nesting level
89
+ def _restore(level)
90
+ # Find the most recently stored state of this object. This could be on
91
+ # any previous stash level or in the regular object DB. If the object
92
+ # was created during the transaction, there is not previous state to
93
+ # restore to.
94
+ id = nil
95
+ if @_stash_map
96
+ (level - 1).downto(0) do |lvl|
97
+ if @_stash_map[lvl]
98
+ id = @_stash_map[lvl]
99
+ break
100
+ end
101
+ end
102
+ end
103
+ unless id
104
+ if @store.db.include?(@_id)
105
+ id = @_id
106
+ end
107
+ end
108
+ if id
109
+ db_obj = store.db.get_object(id)
110
+ _deserialize(db_obj['data'])
111
+ end
112
+ end
113
+
114
+ # Save the object state for this transaction level to the storage
115
+ # back-end. The object gets a new ID that is stored in @_stash_map to map
116
+ # the stash ID back to the original data.
117
+ def _stash(level)
118
+ db_obj = {
119
+ 'class' => self.class.to_s,
120
+ 'data' => _serialize
121
+ }
122
+ @_stash_map = [] unless @_stash_map
123
+ # Get a new ID to store this version of the object.
124
+ @_stash_map[level] = stash_id = @store.db.new_id
125
+ @store.db.put_object(db_obj, stash_id)
126
+ end
127
+
128
+ # Library internal method. Do not use outside of this library.
129
+ # @private
130
+ def _change_id(id)
131
+ # Unregister the object with the old ID from the write cache to prevent
132
+ # cache corruption. The objects are index by ID in the cache.
133
+ store.cache.unwrite(self)
134
+ @_id = id
135
+ end
136
+
137
+ private
138
+
139
+ def _dereferenced(v)
140
+ v.is_a?(POReference) ? @store.object_by_id(v.id) : v
141
+ end
142
+
143
+ def _referenced(obj)
144
+ if obj.is_a?(ObjectBase)
145
+ # The obj is a reference to another persistent object. Store the ID
146
+ # of that object in a POReference object.
147
+ if @store != obj.store
148
+ raise ArgumentError, 'The referenced object is not part of this store'
149
+ end
150
+ POReference.new(obj._id)
151
+ else
152
+ obj
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+