perobs 0.0.1

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.
@@ -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
+